1#!/usr/bin/env python3
2
3"""Phosphor Inventory Manager YAML parser and code generator.
4
5The parser workflow is broken down as follows:
6  1 - Import YAML files as native python type(s) instance(s).
7  2 - Create an instance of the Everything class from the
8        native python type instance(s) with the Everything.load
9        method.
10  3 - The Everything class constructor orchestrates conversion of the
11        native python type(s) instances(s) to render helper types.
12        Each render helper type constructor imports its attributes
13        from the native python type(s) instances(s).
14  4 - Present the converted YAML to the command processing method
15        requested by the script user.
16"""
17
18import argparse
19import os
20import sys
21
22import mako.lookup
23import sdbusplus.property
24import yaml
25from sdbusplus.namedelement import NamedElement
26from sdbusplus.renderer import Renderer
27
28# Global busname for use within classes where necessary
29busname = "xyz.openbmc_project.Inventory.Manager"
30
31
32def cppTypeName(yaml_type):
33    """Convert yaml types to cpp types."""
34    return sdbusplus.property.Property(type=yaml_type).cppTypeName
35
36
37class InterfaceComposite(object):
38    """Compose interface properties."""
39
40    def __init__(self, dict):
41        self.dict = dict
42
43    def interfaces(self):
44        return list(self.dict.keys())
45
46    def names(self, interface):
47        names = []
48        if self.dict[interface]:
49            names = [
50                NamedElement(name=x["name"]) for x in self.dict[interface]
51            ]
52        return names
53
54
55class Interface(list):
56    """Provide various interface transformations."""
57
58    def __init__(self, iface):
59        super(Interface, self).__init__(iface.split("."))
60
61    def namespace(self):
62        """Represent as an sdbusplus namespace."""
63        return "::".join(["sdbusplus"] + self[:-1] + ["server", self[-1]])
64
65    def header(self):
66        """Represent as an sdbusplus server binding header."""
67        return os.sep.join(self + ["server.hpp"])
68
69    def __str__(self):
70        return ".".join(self)
71
72
73class Indent(object):
74    """Help templates be depth agnostic."""
75
76    def __init__(self, depth=0):
77        self.depth = depth
78
79    def __add__(self, depth):
80        return Indent(self.depth + depth)
81
82    def __call__(self, depth):
83        """Render an indent at the current depth plus depth."""
84        return 4 * " " * (depth + self.depth)
85
86
87class Template(NamedElement):
88    """Associate a template name with its namespace."""
89
90    def __init__(self, **kw):
91        self.namespace = kw.pop("namespace", [])
92        super(Template, self).__init__(**kw)
93
94    def qualified(self):
95        return "::".join(self.namespace + [self.name])
96
97
98class FixBool(object):
99    """Un-capitalize booleans."""
100
101    def __call__(self, arg):
102        return "{0}".format(arg.lower())
103
104
105class Quote(object):
106    """Decorate an argument by quoting it."""
107
108    def __call__(self, arg):
109        return '"{0}"'.format(arg)
110
111
112class Cast(object):
113    """Decorate an argument by casting it."""
114
115    def __init__(self, cast, target):
116        """cast is the cast type (static, const, etc...).
117        target is the cast target type."""
118        self.cast = cast
119        self.target = target
120
121    def __call__(self, arg):
122        return "{0}_cast<{1}>({2})".format(self.cast, self.target, arg)
123
124
125class Literal(object):
126    """Decorate an argument with a literal operator."""
127
128    integer_types = [
129        "int8",
130        "int16",
131        "int32",
132        "int64",
133        "uint8",
134        "uint16",
135        "uint32",
136        "uint64",
137    ]
138
139    def __init__(self, type):
140        self.type = type
141
142    def __call__(self, arg):
143        if "uint" in self.type:
144            arg = "{0}ull".format(arg)
145        elif "int" in self.type:
146            arg = "{0}ll".format(arg)
147
148        if self.type in self.integer_types:
149            return Cast("static", "{0}_t".format(self.type))(arg)
150
151        if self.type == "string":
152            return "{0}s".format(arg)
153
154        return arg
155
156
157class Argument(NamedElement, Renderer):
158    """Define argument type inteface."""
159
160    def __init__(self, **kw):
161        self.type = kw.pop("type", None)
162        super(Argument, self).__init__(**kw)
163
164    def argument(self, loader, indent):
165        raise NotImplementedError
166
167
168class TrivialArgument(Argument):
169    """Non-array type arguments."""
170
171    def __init__(self, **kw):
172        self.value = kw.pop("value")
173        self.decorators = kw.pop("decorators", [])
174        if kw.get("type", None) == "string":
175            self.decorators.insert(0, Quote())
176        if kw.get("type", None) == "boolean":
177            self.decorators.insert(0, FixBool())
178
179        super(TrivialArgument, self).__init__(**kw)
180
181    def argument(self, loader, indent):
182        a = str(self.value)
183        for d in self.decorators:
184            a = d(a)
185
186        return a
187
188
189class InitializerList(Argument):
190    """Initializer list arguments."""
191
192    def __init__(self, **kw):
193        self.values = kw.pop("values")
194        super(InitializerList, self).__init__(**kw)
195
196    def argument(self, loader, indent):
197        return self.render(
198            loader, "argument.mako.cpp", arg=self, indent=indent
199        )
200
201
202class DbusSignature(Argument):
203    """DBus signature arguments."""
204
205    def __init__(self, **kw):
206        self.sig = {x: y for x, y in kw.items()}
207        kw.clear()
208        super(DbusSignature, self).__init__(**kw)
209
210    def argument(self, loader, indent):
211        return self.render(
212            loader, "signature.mako.cpp", signature=self, indent=indent
213        )
214
215
216class MethodCall(Argument):
217    """Render syntatically correct c++ method calls."""
218
219    def __init__(self, **kw):
220        self.namespace = kw.pop("namespace", [])
221        self.templates = kw.pop("templates", [])
222        self.args = kw.pop("args", [])
223        super(MethodCall, self).__init__(**kw)
224
225    def call(self, loader, indent):
226        return self.render(
227            loader, "method.mako.cpp", method=self, indent=indent
228        )
229
230    def argument(self, loader, indent):
231        return self.call(loader, indent)
232
233
234class Vector(MethodCall):
235    """Convenience type for vectors."""
236
237    def __init__(self, **kw):
238        kw["name"] = "vector"
239        kw["namespace"] = ["std"]
240        kw["args"] = [InitializerList(values=kw.pop("args"))]
241        super(Vector, self).__init__(**kw)
242
243
244class Filter(MethodCall):
245    """Convenience type for filters"""
246
247    def __init__(self, **kw):
248        kw["name"] = "make_filter"
249        super(Filter, self).__init__(**kw)
250
251
252class Action(MethodCall):
253    """Convenience type for actions"""
254
255    def __init__(self, **kw):
256        kw["name"] = "make_action"
257        super(Action, self).__init__(**kw)
258
259
260class PathCondition(MethodCall):
261    """Convenience type for path conditions"""
262
263    def __init__(self, **kw):
264        kw["name"] = "make_path_condition"
265        super(PathCondition, self).__init__(**kw)
266
267
268class GetProperty(MethodCall):
269    """Convenience type for getting inventory properties"""
270
271    def __init__(self, **kw):
272        kw["name"] = "make_get_property"
273        super(GetProperty, self).__init__(**kw)
274
275
276class CreateObjects(MethodCall):
277    """Assemble a createObjects functor."""
278
279    def __init__(self, **kw):
280        objs = []
281
282        for path, interfaces in kw.pop("objs").items():
283            key_o = TrivialArgument(
284                value=path, type="string", decorators=[Literal("string")]
285            )
286            value_i = []
287
288            for (
289                interface,
290                properties,
291            ) in interfaces.items():
292                key_i = TrivialArgument(value=interface, type="string")
293                value_p = []
294                if properties:
295                    for prop, value in properties.items():
296                        key_p = TrivialArgument(value=prop, type="string")
297                        value_v = TrivialArgument(
298                            decorators=[Literal(value.get("type", None))],
299                            **value
300                        )
301                        value_p.append(
302                            InitializerList(values=[key_p, value_v])
303                        )
304
305                value_p = InitializerList(values=value_p)
306                value_i.append(InitializerList(values=[key_i, value_p]))
307
308            value_i = InitializerList(values=value_i)
309            objs.append(InitializerList(values=[key_o, value_i]))
310
311        kw["args"] = [InitializerList(values=objs)]
312        kw["namespace"] = ["functor"]
313        super(CreateObjects, self).__init__(**kw)
314
315
316class DestroyObjects(MethodCall):
317    """Assemble a destroyObject functor."""
318
319    def __init__(self, **kw):
320        values = [{"value": x, "type": "string"} for x in kw.pop("paths")]
321        conditions = [
322            Event.functor_map[x["name"]](**x) for x in kw.pop("conditions", [])
323        ]
324        conditions = [PathCondition(args=[x]) for x in conditions]
325        args = [InitializerList(values=[TrivialArgument(**x) for x in values])]
326        args.append(InitializerList(values=conditions))
327        kw["args"] = args
328        kw["namespace"] = ["functor"]
329        super(DestroyObjects, self).__init__(**kw)
330
331
332class SetProperty(MethodCall):
333    """Assemble a setProperty functor."""
334
335    def __init__(self, **kw):
336        args = []
337
338        value = kw.pop("value")
339        prop = kw.pop("property")
340        iface = kw.pop("interface")
341        iface = Interface(iface)
342        namespace = iface.namespace().split("::")[:-1]
343        name = iface[-1]
344        t = Template(namespace=namespace, name=iface[-1])
345
346        member = "&%s" % "::".join(
347            namespace + [name, NamedElement(name=prop).camelCase]
348        )
349        member_type = cppTypeName(value["type"])
350        member_cast = "{0} ({1}::*)({0})".format(member_type, t.qualified())
351
352        paths = [{"value": x, "type": "string"} for x in kw.pop("paths")]
353        args.append(
354            InitializerList(values=[TrivialArgument(**x) for x in paths])
355        )
356
357        conditions = [
358            Event.functor_map[x["name"]](**x) for x in kw.pop("conditions", [])
359        ]
360        conditions = [PathCondition(args=[x]) for x in conditions]
361
362        args.append(InitializerList(values=conditions))
363        args.append(TrivialArgument(value=str(iface), type="string"))
364        args.append(
365            TrivialArgument(
366                value=member, decorators=[Cast("static", member_cast)]
367            )
368        )
369        args.append(TrivialArgument(**value))
370
371        kw["templates"] = [Template(name=name, namespace=namespace)]
372        kw["args"] = args
373        kw["namespace"] = ["functor"]
374        super(SetProperty, self).__init__(**kw)
375
376
377class PropertyChanged(MethodCall):
378    """Assemble a propertyChanged functor."""
379
380    def __init__(self, **kw):
381        args = []
382        args.append(TrivialArgument(value=kw.pop("interface"), type="string"))
383        args.append(TrivialArgument(value=kw.pop("property"), type="string"))
384        args.append(
385            TrivialArgument(
386                decorators=[Literal(kw["value"].get("type", None))],
387                **kw.pop("value")
388            )
389        )
390        kw["args"] = args
391        kw["namespace"] = ["functor"]
392        super(PropertyChanged, self).__init__(**kw)
393
394
395class PropertyIs(MethodCall):
396    """Assemble a propertyIs functor."""
397
398    def __init__(self, **kw):
399        args = []
400        path = kw.pop("path", None)
401        if not path:
402            path = TrivialArgument(value="nullptr")
403        else:
404            path = TrivialArgument(value=path, type="string")
405
406        args.append(path)
407        iface = TrivialArgument(value=kw.pop("interface"), type="string")
408        args.append(iface)
409        prop = TrivialArgument(value=kw.pop("property"), type="string")
410        args.append(prop)
411        args.append(
412            TrivialArgument(
413                decorators=[Literal(kw["value"].get("type", None))],
414                **kw.pop("value")
415            )
416        )
417
418        service = kw.pop("service", None)
419        if service:
420            args.append(TrivialArgument(value=service, type="string"))
421
422        dbusMember = kw.pop("dbusMember", None)
423        if dbusMember:
424            # Inventory manager's service name is required
425            if not service or service != busname:
426                args.append(TrivialArgument(value=busname, type="string"))
427
428            gpArgs = []
429            gpArgs.append(path)
430            gpArgs.append(iface)
431            # Prepend '&' and append 'getPropertyByName' function on dbusMember
432            gpArgs.append(
433                TrivialArgument(value="&" + dbusMember + "::getPropertyByName")
434            )
435            gpArgs.append(prop)
436            fArg = MethodCall(
437                name="getProperty",
438                namespace=["functor"],
439                templates=[Template(name=dbusMember, namespace=[])],
440                args=gpArgs,
441            )
442
443            # Append getProperty functor
444            args.append(
445                GetProperty(
446                    templates=[
447                        Template(
448                            name=dbusMember + "::PropertiesVariant",
449                            namespace=[],
450                        )
451                    ],
452                    args=[fArg],
453                )
454            )
455
456        kw["args"] = args
457        kw["namespace"] = ["functor"]
458        super(PropertyIs, self).__init__(**kw)
459
460
461class Event(MethodCall):
462    """Assemble an inventory manager event."""
463
464    functor_map = {
465        "destroyObjects": DestroyObjects,
466        "createObjects": CreateObjects,
467        "propertyChangedTo": PropertyChanged,
468        "propertyIs": PropertyIs,
469        "setProperty": SetProperty,
470    }
471
472    def __init__(self, **kw):
473        self.summary = kw.pop("name")
474
475        filters = [
476            self.functor_map[x["name"]](**x) for x in kw.pop("filters", [])
477        ]
478        filters = [Filter(args=[x]) for x in filters]
479        filters = Vector(
480            templates=[Template(name="Filter", namespace=[])], args=filters
481        )
482
483        event = MethodCall(
484            name="make_shared",
485            namespace=["std"],
486            templates=[
487                Template(
488                    name=kw.pop("event"),
489                    namespace=kw.pop("event_namespace", []),
490                )
491            ],
492            args=kw.pop("event_args", []) + [filters],
493        )
494
495        events = Vector(
496            templates=[Template(name="EventBasePtr", namespace=[])],
497            args=[event],
498        )
499
500        action_type = Template(name="Action", namespace=[])
501        action_args = [
502            self.functor_map[x["name"]](**x) for x in kw.pop("actions", [])
503        ]
504        action_args = [Action(args=[x]) for x in action_args]
505        actions = Vector(templates=[action_type], args=action_args)
506
507        kw["name"] = "make_tuple"
508        kw["namespace"] = ["std"]
509        kw["args"] = [events, actions]
510        super(Event, self).__init__(**kw)
511
512
513class MatchEvent(Event):
514    """Associate one or more dbus signal match signatures with
515    a filter."""
516
517    def __init__(self, **kw):
518        kw["event"] = "DbusSignal"
519        kw["event_namespace"] = []
520        kw["event_args"] = [
521            DbusSignature(**x) for x in kw.pop("signatures", [])
522        ]
523
524        super(MatchEvent, self).__init__(**kw)
525
526
527class StartupEvent(Event):
528    """Assemble a startup event."""
529
530    def __init__(self, **kw):
531        kw["event"] = "StartupEvent"
532        kw["event_namespace"] = []
533        super(StartupEvent, self).__init__(**kw)
534
535
536class Everything(Renderer):
537    """Parse/render entry point."""
538
539    class_map = {
540        "match": MatchEvent,
541        "startup": StartupEvent,
542    }
543
544    @staticmethod
545    def load(args):
546        # Aggregate all the event YAML in the events.d directory
547        # into a single list of events.
548
549        events = []
550        events_dir = os.path.join(args.inputdir, "events.d")
551
552        if os.path.exists(events_dir):
553            yaml_files = [
554                x for x in os.listdir(events_dir) if x.endswith(".yaml")
555            ]
556
557            for x in yaml_files:
558                with open(os.path.join(events_dir, x), "r") as fd:
559                    for e in yaml.safe_load(fd.read()).get("events", {}):
560                        events.append(e)
561
562        interfaces, interface_composite = Everything.get_interfaces(
563            args.ifacesdir
564        )
565        (
566            extra_interfaces,
567            extra_interface_composite,
568        ) = Everything.get_interfaces(
569            os.path.join(args.inputdir, "extra_interfaces.d")
570        )
571        interface_composite.update(extra_interface_composite)
572        interface_composite = InterfaceComposite(interface_composite)
573        # Update busname if configured differenly than the default
574        global busname
575        busname = args.busname
576
577        return Everything(
578            *events,
579            interfaces=interfaces + extra_interfaces,
580            interface_composite=interface_composite
581        )
582
583    @staticmethod
584    def get_interfaces(targetdir):
585        """Scan the interfaces directory for interfaces that PIM can create."""
586
587        yaml_files = []
588        interfaces = []
589        interface_composite = {}
590
591        if targetdir and os.path.exists(targetdir):
592            for directory, _, files in os.walk(targetdir):
593                if not files:
594                    continue
595
596                yaml_files += [
597                    os.path.relpath(os.path.join(directory, f), targetdir)
598                    for f in [
599                        f for f in files if f.endswith(".interface.yaml")
600                    ]
601                ]
602
603        for y in yaml_files:
604            # parse only phosphor dbus related interface files
605            if not (
606                y.startswith("xyz")
607                or y.startswith("com/ibm/ipzvpd")
608                or y.startswith("com/ibm/Control/Host")
609            ):
610                continue
611            with open(os.path.join(targetdir, y)) as fd:
612                i = y.replace(".interface.yaml", "").replace(os.sep, ".")
613
614                # PIM can't create interfaces with methods.
615                parsed = yaml.safe_load(fd.read())
616                if parsed.get("methods", None):
617                    continue
618                # Cereal can't understand the type sdbusplus::object_path. This
619                # type is a wrapper around std::string. Ignore interfaces
620                # having a property of this type for now. The only interface
621                # that has a property of this type now is
622                # xyz.openbmc_project.Association, which is an unused
623                # interface. No inventory objects implement this interface.
624                # TODO via openbmc/openbmc#2123 : figure out how to make Cereal
625                # understand sdbusplus::object_path.
626                properties = parsed.get("properties", None)
627                if properties:
628                    if any("path" in p["type"] for p in properties):
629                        continue
630                interface_composite[i] = properties
631                interfaces.append(i)
632
633        return interfaces, interface_composite
634
635    def __init__(self, *a, **kw):
636        self.interfaces = [Interface(x) for x in kw.pop("interfaces", [])]
637        self.interface_composite = kw.pop("interface_composite", {})
638        self.events = [self.class_map[x["type"]](**x) for x in a]
639        super(Everything, self).__init__(**kw)
640
641    def generate_cpp(self, loader):
642        """Render the template with the provided events and interfaces."""
643        with open(os.path.join(args.outputdir, "generated.cpp"), "w") as fd:
644            fd.write(
645                self.render(
646                    loader,
647                    "generated.mako.cpp",
648                    events=self.events,
649                    interfaces=self.interfaces,
650                    indent=Indent(),
651                )
652            )
653
654    def generate_serialization(self, loader):
655        with open(
656            os.path.join(args.outputdir, "gen_serialization.hpp"), "w"
657        ) as fd:
658            fd.write(
659                self.render(
660                    loader,
661                    "gen_serialization.mako.hpp",
662                    interfaces=self.interfaces,
663                    interface_composite=self.interface_composite,
664                )
665            )
666
667
668if __name__ == "__main__":
669    script_dir = os.path.dirname(os.path.realpath(__file__))
670    valid_commands = {
671        "generate-cpp": "generate_cpp",
672        "generate-serialization": "generate_serialization",
673    }
674
675    parser = argparse.ArgumentParser(
676        description=(
677            "Phosphor Inventory Manager (PIM) YAML scanner and code generator."
678        )
679    )
680    parser.add_argument(
681        "-o",
682        "--output-dir",
683        dest="outputdir",
684        default=".",
685        help="Output directory.",
686    )
687    parser.add_argument(
688        "-i",
689        "--interfaces-dir",
690        dest="ifacesdir",
691        help="Location of interfaces to be supported.",
692    )
693    parser.add_argument(
694        "-d",
695        "--dir",
696        dest="inputdir",
697        default=os.path.join(script_dir, "example"),
698        help="Location of files to process.",
699    )
700    parser.add_argument(
701        "-b",
702        "--bus-name",
703        dest="busname",
704        default="xyz.openbmc_project.Inventory.Manager",
705        help="Inventory manager busname.",
706    )
707    parser.add_argument(
708        "command",
709        metavar="COMMAND",
710        type=str,
711        choices=list(valid_commands.keys()),
712        help="%s." % " | ".join(list(valid_commands.keys())),
713    )
714
715    args = parser.parse_args()
716
717    if sys.version_info < (3, 0):
718        lookup = mako.lookup.TemplateLookup(
719            directories=[script_dir], disable_unicode=True
720        )
721    else:
722        lookup = mako.lookup.TemplateLookup(directories=[script_dir])
723
724    function = getattr(Everything.load(args), valid_commands[args.command])
725    function(lookup)
726
727
728# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
729