1#!/usr/bin/env python3
2
3"""Phosphor DBus Monitor 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 os
19import sys
20import yaml
21import mako.lookup
22from argparse import ArgumentParser
23from sdbusplus.renderer import Renderer
24from sdbusplus.namedelement import NamedElement
25import sdbusplus.property
26
27
28class InvalidConfigError(Exception):
29    """General purpose config file parsing error."""
30
31    def __init__(self, path, msg):
32        """Display configuration file with the syntax
33        error and the error message."""
34
35        self.config = path
36        self.msg = msg
37
38
39class NotUniqueError(InvalidConfigError):
40    """Within a config file names must be unique.
41    Display the config file with the duplicate and
42    the duplicate itself."""
43
44    def __init__(self, path, cls, *names):
45        fmt = 'Duplicate {0}: "{1}"'
46        super(NotUniqueError, self).__init__(
47            path, fmt.format(cls, " ".join(names))
48        )
49
50
51def get_index(objs, cls, name, config=None):
52    """Items are usually rendered as C++ arrays and as
53    such are stored in python lists.  Given an item name
54    its class, and an optional config file filter, find
55    the item index."""
56
57    for i, x in enumerate(objs.get(cls, [])):
58        if config and x.configfile != config:
59            continue
60        if x.name != name:
61            continue
62
63        return i
64    raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
65
66
67def exists(objs, cls, name, config=None):
68    """Check to see if an item already exists in a list given
69    the item name."""
70
71    try:
72        get_index(objs, cls, name, config)
73    except Exception:
74        return False
75
76    return True
77
78
79def add_unique(obj, *a, **kw):
80    """Add an item to one or more lists unless already present,
81    with an option to constrain the search to a specific config file."""
82
83    for container in a:
84        if not exists(container, obj.cls, obj.name, config=kw.get("config")):
85            container.setdefault(obj.cls, []).append(obj)
86
87
88class Cast(object):
89    """Decorate an argument by casting it."""
90
91    def __init__(self, cast, target):
92        """cast is the cast type (static, const, etc...).
93        target is the cast target type."""
94        self.cast = cast
95        self.target = target
96
97    def __call__(self, arg):
98        return "{0}_cast<{1}>({2})".format(self.cast, self.target, arg)
99
100
101class Literal(object):
102    """Decorate an argument with a literal operator."""
103
104    integer_types = ["int16", "int32", "int64", "uint16", "uint32", "uint64"]
105
106    def __init__(self, type):
107        self.type = type
108
109    def __call__(self, arg):
110        if "uint" in self.type:
111            arg = "{0}ull".format(arg)
112        elif "int" in self.type:
113            arg = "{0}ll".format(arg)
114
115        if self.type in self.integer_types:
116            return Cast("static", "{0}_t".format(self.type))(arg)
117        elif self.type == "byte":
118            return Cast("static", "uint8_t")(arg)
119        elif self.type == "double":
120            return Cast("static", "double")(arg)
121
122        if self.type == "string":
123            return "{0}s".format(arg)
124
125        return arg
126
127
128class FixBool(object):
129    """Un-capitalize booleans."""
130
131    def __call__(self, arg):
132        return "{0}".format(arg.lower())
133
134
135class Quote(object):
136    """Decorate an argument by quoting it."""
137
138    def __call__(self, arg):
139        return '"{0}"'.format(arg)
140
141
142class Argument(NamedElement, Renderer):
143    """Define argument type interface."""
144
145    def __init__(self, **kw):
146        self.type = kw.pop("type", None)
147        super(Argument, self).__init__(**kw)
148
149    def argument(self, loader, indent):
150        raise NotImplementedError
151
152
153class TrivialArgument(Argument):
154    """Non-array type arguments."""
155
156    def __init__(self, **kw):
157        self.value = kw.pop("value")
158        self.decorators = kw.pop("decorators", [])
159        if kw.get("type", None):
160            self.decorators.insert(0, Literal(kw["type"]))
161        if kw.get("type", None) == "string":
162            self.decorators.insert(0, Quote())
163        if kw.get("type", None) == "boolean":
164            self.decorators.insert(0, FixBool())
165
166        super(TrivialArgument, self).__init__(**kw)
167
168    def argument(self, loader, indent):
169        a = str(self.value)
170        for d in self.decorators:
171            a = d(a)
172
173        return a
174
175
176class Metadata(Argument):
177    """Metadata type arguments."""
178
179    def __init__(self, **kw):
180        self.value = kw.pop("value")
181        self.decorators = kw.pop("decorators", [])
182        if kw.get("type", None) == "string":
183            self.decorators.insert(0, Quote())
184
185        super(Metadata, self).__init__(**kw)
186
187    def argument(self, loader, indent):
188        a = str(self.value)
189        for d in self.decorators:
190            a = d(a)
191
192        return a
193
194
195class OpArgument(Argument):
196    """Operation type arguments."""
197
198    def __init__(self, **kw):
199        self.op = kw.pop("op")
200        self.bound = kw.pop("bound")
201        self.decorators = kw.pop("decorators", [])
202        if kw.get("type", None):
203            self.decorators.insert(0, Literal(kw["type"]))
204        if kw.get("type", None) == "string":
205            self.decorators.insert(0, Quote())
206        if kw.get("type", None) == "boolean":
207            self.decorators.insert(0, FixBool())
208
209        super(OpArgument, self).__init__(**kw)
210
211    def argument(self, loader, indent):
212        a = str(self.bound)
213        for d in self.decorators:
214            a = d(a)
215
216        return a
217
218
219class Indent(object):
220    """Help templates be depth agnostic."""
221
222    def __init__(self, depth=0):
223        self.depth = depth
224
225    def __add__(self, depth):
226        return Indent(self.depth + depth)
227
228    def __call__(self, depth):
229        """Render an indent at the current depth plus depth."""
230        return 4 * " " * (depth + self.depth)
231
232
233class ConfigEntry(NamedElement):
234    """Base interface for rendered items."""
235
236    def __init__(self, *a, **kw):
237        """Pop the configfile/class/subclass keywords."""
238
239        self.configfile = kw.pop("configfile")
240        self.cls = kw.pop("class")
241        self.subclass = kw.pop(self.cls)
242
243        # TODO: NamedElement requires 'name' to be a string, but in many cases
244        #       this script treats 'name' as a dict.  Save the property off and
245        #       insert it after ConfigEntry does its own thing to avoid
246        #       exceptions.  This should be refactored throughout the whole
247        #       script to not overload 'name' as a dict.
248        name_save = kw.pop("name")
249        super(ConfigEntry, self).__init__(**kw)
250        self.name = name_save
251
252    def factory(self, objs):
253        """Optional factory interface for subclasses to add
254        additional items to be rendered."""
255
256        pass
257
258    def setup(self, objs):
259        """Optional setup interface for subclasses, invoked
260        after all factory methods have been run."""
261
262        pass
263
264
265class Path(ConfigEntry):
266    """Path/metadata association."""
267
268    def __init__(self, *a, **kw):
269        super(Path, self).__init__(**kw)
270
271        if self.name["meta"].upper() != self.name["meta"]:
272            raise InvalidConfigError(
273                self.configfile,
274                'Metadata tag "{0}" must be upper case.'.format(
275                    self.name["meta"]
276                ),
277            )
278
279    def factory(self, objs):
280        """Create path and metadata elements."""
281
282        args = {
283            "class": "pathname",
284            "pathname": "element",
285            "name": self.name["path"],
286        }
287        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
288
289        args = {"class": "meta", "meta": "element", "name": self.name["meta"]}
290        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
291
292        super(Path, self).factory(objs)
293
294    def setup(self, objs):
295        """Resolve path and metadata names to indices."""
296
297        self.path = get_index(objs, "pathname", self.name["path"])
298        self.meta = get_index(objs, "meta", self.name["meta"])
299
300        super(Path, self).setup(objs)
301
302
303class Property(ConfigEntry):
304    """Property/interface/metadata association."""
305
306    def __init__(self, *a, **kw):
307        super(Property, self).__init__(**kw)
308
309        if self.name["meta"].upper() != self.name["meta"]:
310            raise InvalidConfigError(
311                self.configfile,
312                'Metadata tag "{0}" must be upper case.'.format(
313                    self.name["meta"]
314                ),
315            )
316
317    def factory(self, objs):
318        """Create interface, property name and metadata elements."""
319
320        args = {
321            "class": "interface",
322            "interface": "element",
323            "name": self.name["interface"],
324        }
325        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
326
327        args = {
328            "class": "propertyname",
329            "propertyname": "element",
330            "name": self.name["property"],
331        }
332        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
333
334        args = {"class": "meta", "meta": "element", "name": self.name["meta"]}
335        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
336
337        super(Property, self).factory(objs)
338
339    def setup(self, objs):
340        """Resolve interface, property and metadata to indices."""
341
342        self.interface = get_index(objs, "interface", self.name["interface"])
343        self.prop = get_index(objs, "propertyname", self.name["property"])
344        self.meta = get_index(objs, "meta", self.name["meta"])
345
346        super(Property, self).setup(objs)
347
348
349class Instance(ConfigEntry):
350    """Property/Path association."""
351
352    def __init__(self, *a, **kw):
353        super(Instance, self).__init__(**kw)
354
355    def setup(self, objs):
356        """Resolve elements to indices."""
357
358        self.interface = get_index(
359            objs, "interface", self.name["property"]["interface"]
360        )
361        self.prop = get_index(
362            objs, "propertyname", self.name["property"]["property"]
363        )
364        self.propmeta = get_index(objs, "meta", self.name["property"]["meta"])
365        self.path = get_index(objs, "pathname", self.name["path"]["path"])
366        self.pathmeta = get_index(objs, "meta", self.name["path"]["meta"])
367
368        super(Instance, self).setup(objs)
369
370
371class PathInstance(ConfigEntry):
372    """Path association."""
373
374    def __init__(self, *a, **kw):
375        super(PathInstance, self).__init__(**kw)
376
377    def setup(self, objs):
378        """Resolve elements to indices."""
379        self.path = self.name["path"]["path"]
380        self.pathmeta = self.name["path"]["meta"]
381        super(PathInstance, self).setup(objs)
382
383
384class Group(ConfigEntry):
385    """Pop the members keyword for groups."""
386
387    def __init__(self, *a, **kw):
388        self.members = kw.pop("members")
389        super(Group, self).__init__(**kw)
390
391
392class ImplicitGroup(Group):
393    """Provide a factory method for groups whose members are
394    not explicitly declared in the config files."""
395
396    def __init__(self, *a, **kw):
397        super(ImplicitGroup, self).__init__(**kw)
398
399    def factory(self, objs):
400        """Create group members."""
401
402        factory = Everything.classmap(self.subclass, "element")
403        for m in self.members:
404            args = {
405                "class": self.subclass,
406                self.subclass: "element",
407                "name": m,
408            }
409
410            obj = factory(configfile=self.configfile, **args)
411            add_unique(obj, objs)
412            obj.factory(objs)
413
414        super(ImplicitGroup, self).factory(objs)
415
416
417class GroupOfPaths(ImplicitGroup):
418    """Path group config file directive."""
419
420    def __init__(self, *a, **kw):
421        super(GroupOfPaths, self).__init__(**kw)
422
423    def setup(self, objs):
424        """Resolve group members."""
425
426        def map_member(x):
427            path = get_index(objs, "pathname", x["path"])
428            meta = get_index(objs, "meta", x["meta"])
429            return (path, meta)
430
431        self.members = map(map_member, self.members)
432
433        super(GroupOfPaths, self).setup(objs)
434
435
436class GroupOfProperties(ImplicitGroup):
437    """Property group config file directive."""
438
439    def __init__(self, *a, **kw):
440        self.type = kw.pop("type")
441        self.datatype = sdbusplus.property.Property(
442            name=kw.get("name"), type=self.type
443        ).cppTypeName
444
445        super(GroupOfProperties, self).__init__(**kw)
446
447    def setup(self, objs):
448        """Resolve group members."""
449
450        def map_member(x):
451            iface = get_index(objs, "interface", x["interface"])
452            prop = get_index(objs, "propertyname", x["property"])
453            meta = get_index(objs, "meta", x["meta"])
454
455            return (iface, prop, meta)
456
457        self.members = map(map_member, self.members)
458
459        super(GroupOfProperties, self).setup(objs)
460
461
462class GroupOfInstances(ImplicitGroup):
463    """A group of property instances."""
464
465    def __init__(self, *a, **kw):
466        super(GroupOfInstances, self).__init__(**kw)
467
468    def setup(self, objs):
469        """Resolve group members."""
470
471        def map_member(x):
472            path = get_index(objs, "pathname", x["path"]["path"])
473            pathmeta = get_index(objs, "meta", x["path"]["meta"])
474            interface = get_index(
475                objs, "interface", x["property"]["interface"]
476            )
477            prop = get_index(objs, "propertyname", x["property"]["property"])
478            propmeta = get_index(objs, "meta", x["property"]["meta"])
479            instance = get_index(objs, "instance", x)
480
481            return (path, pathmeta, interface, prop, propmeta, instance)
482
483        self.members = map(map_member, self.members)
484
485        super(GroupOfInstances, self).setup(objs)
486
487
488class GroupOfPathInstances(ImplicitGroup):
489    """A group of path instances."""
490
491    def __init__(self, *a, **kw):
492        super(GroupOfPathInstances, self).__init__(**kw)
493
494    def setup(self, objs):
495        """Resolve group members."""
496
497        def map_member(x):
498            path = get_index(objs, "pathname", x["path"]["path"])
499            pathmeta = get_index(objs, "meta", x["path"]["meta"])
500            pathinstance = get_index(objs, "pathinstance", x)
501            return (path, pathmeta, pathinstance)
502
503        self.members = map(map_member, self.members)
504
505        super(GroupOfPathInstances, self).setup(objs)
506
507
508class HasPropertyIndex(ConfigEntry):
509    """Handle config file directives that require an index to be
510    constructed."""
511
512    def __init__(self, *a, **kw):
513        self.paths = kw.pop("paths")
514        self.properties = kw.pop("properties")
515        super(HasPropertyIndex, self).__init__(**kw)
516
517    def factory(self, objs):
518        """Create a group of instances for this index."""
519
520        members = []
521        path_group = get_index(
522            objs, "pathgroup", self.paths, config=self.configfile
523        )
524        property_group = get_index(
525            objs, "propertygroup", self.properties, config=self.configfile
526        )
527
528        for path in objs["pathgroup"][path_group].members:
529            for prop in objs["propertygroup"][property_group].members:
530                member = {
531                    "path": path,
532                    "property": prop,
533                }
534                members.append(member)
535
536        args = {
537            "members": members,
538            "class": "instancegroup",
539            "instancegroup": "instance",
540            "name": "{0} {1}".format(self.paths, self.properties),
541        }
542
543        group = GroupOfInstances(configfile=self.configfile, **args)
544        add_unique(group, objs, config=self.configfile)
545        group.factory(objs)
546
547        super(HasPropertyIndex, self).factory(objs)
548
549    def setup(self, objs):
550        """Resolve path, property, and instance groups."""
551
552        self.instances = get_index(
553            objs,
554            "instancegroup",
555            "{0} {1}".format(self.paths, self.properties),
556            config=self.configfile,
557        )
558        self.paths = get_index(
559            objs, "pathgroup", self.paths, config=self.configfile
560        )
561        self.properties = get_index(
562            objs, "propertygroup", self.properties, config=self.configfile
563        )
564        self.datatype = objs["propertygroup"][self.properties].datatype
565        self.type = objs["propertygroup"][self.properties].type
566
567        super(HasPropertyIndex, self).setup(objs)
568
569
570class HasPathIndex(ConfigEntry):
571    """Handle config file directives that require an index to be
572    constructed."""
573
574    def __init__(self, *a, **kw):
575        self.paths = kw.pop("paths")
576        super(HasPathIndex, self).__init__(**kw)
577
578    def factory(self, objs):
579        """Create a group of instances for this index."""
580
581        members = []
582        path_group = get_index(
583            objs, "pathgroup", self.paths, config=self.configfile
584        )
585
586        for path in objs["pathgroup"][path_group].members:
587            member = {
588                "path": path,
589            }
590            members.append(member)
591
592        args = {
593            "members": members,
594            "class": "pathinstancegroup",
595            "pathinstancegroup": "pathinstance",
596            "name": "{0}".format(self.paths),
597        }
598
599        group = GroupOfPathInstances(configfile=self.configfile, **args)
600        add_unique(group, objs, config=self.configfile)
601        group.factory(objs)
602
603        super(HasPathIndex, self).factory(objs)
604
605    def setup(self, objs):
606        """Resolve path and instance groups."""
607
608        self.pathinstances = get_index(
609            objs,
610            "pathinstancegroup",
611            "{0}".format(self.paths),
612            config=self.configfile,
613        )
614        self.paths = get_index(
615            objs, "pathgroup", self.paths, config=self.configfile
616        )
617        super(HasPathIndex, self).setup(objs)
618
619
620class GroupOfFilters(ConfigEntry):
621    """Handle config file directives that require an index for filters."""
622
623    def __init__(self, *a, **kw):
624        # Pop filters data for adding to the available filters array
625        self.type = kw.pop("type")
626        self.datatype = kw.pop("datatype", None)
627        self.filters = kw.pop("filters", None)
628
629        super(GroupOfFilters, self).__init__(**kw)
630
631    def factory(self, objs):
632        """Modify filters to add the property value type and
633        make them of operation argument type."""
634        if self.filters:
635            # 'type' used within OpArgument to generate filter
636            # argument values so add to each filter
637            for f in self.filters:
638                f["type"] = self.type
639            self.filters = [OpArgument(**x) for x in self.filters]
640
641        super(GroupOfFilters, self).factory(objs)
642
643
644class PropertyWatch(HasPropertyIndex):
645    """Handle the property watch config file directive."""
646
647    def __init__(self, *a, **kw):
648        # Pop optional filters for the properties being watched
649        self.filters = kw.pop("filters", None)
650        self.callback = kw.pop("callback", None)
651        self.ignore_start_callback = kw.pop("ignore_start_callback", False)
652        self.ignore_start_callback = (
653            "true" if self.ignore_start_callback else "false"
654        )
655        super(PropertyWatch, self).__init__(**kw)
656
657    def factory(self, objs):
658        """Create any filters for this property watch."""
659
660        if self.filters:
661            # Get the datatype(i.e. "int64_t") of the properties in this watch
662            # (Made available after all `super` classes init'd)
663            datatype = objs["propertygroup"][
664                get_index(
665                    objs,
666                    "propertygroup",
667                    self.properties,
668                    config=self.configfile,
669                )
670            ].datatype
671            # Get the type(i.e. "int64") of the properties in this watch
672            # (Made available after all `super` classes init'd)
673            type = objs["propertygroup"][
674                get_index(
675                    objs,
676                    "propertygroup",
677                    self.properties,
678                    config=self.configfile,
679                )
680            ].type
681            # Construct the data needed to make the filters for
682            # this watch available.
683            # *Note: 'class', 'subclass', 'name' are required for
684            # storing the filter data(i.e. 'type', 'datatype', & 'filters')
685            args = {
686                "type": type,
687                "datatype": datatype,
688                "filters": self.filters,
689                "class": "filtersgroup",
690                "filtersgroup": "filters",
691                "name": self.name,
692            }
693            # Init GroupOfFilters class with this watch's filters' arguments
694            group = GroupOfFilters(configfile=self.configfile, **args)
695            # Store this group of filters so it can be indexed later
696            add_unique(group, objs, config=self.configfile)
697            group.factory(objs)
698
699        super(PropertyWatch, self).factory(objs)
700
701    def setup(self, objs):
702        """Resolve optional filters and callback."""
703
704        if self.filters:
705            # Watch has filters, provide array index to access them
706            self.filters = get_index(
707                objs, "filtersgroup", self.name, config=self.configfile
708            )
709
710        if self.callback:
711            self.callback = get_index(
712                objs, "callback", self.callback, config=self.configfile
713            )
714
715        super(PropertyWatch, self).setup(objs)
716
717
718class PathWatch(HasPathIndex):
719    """Handle the path watch config file directive."""
720
721    def __init__(self, *a, **kw):
722        self.pathcallback = kw.pop("pathcallback", None)
723        super(PathWatch, self).__init__(**kw)
724
725    def setup(self, objs):
726        """Resolve optional callback."""
727        if self.pathcallback:
728            self.pathcallback = get_index(
729                objs, "pathcallback", self.pathcallback, config=self.configfile
730            )
731        super(PathWatch, self).setup(objs)
732
733
734class Callback(HasPropertyIndex):
735    """Interface and common logic for callbacks."""
736
737    def __init__(self, *a, **kw):
738        super(Callback, self).__init__(**kw)
739
740
741class PathCallback(HasPathIndex):
742    """Interface and common logic for callbacks."""
743
744    def __init__(self, *a, **kw):
745        super(PathCallback, self).__init__(**kw)
746
747
748class ConditionCallback(ConfigEntry, Renderer):
749    """Handle the journal callback config file directive."""
750
751    def __init__(self, *a, **kw):
752        self.condition = kw.pop("condition")
753        self.instance = kw.pop("instance")
754        self.defer = kw.pop("defer", None)
755        super(ConditionCallback, self).__init__(**kw)
756
757    def factory(self, objs):
758        """Create a graph instance for this callback."""
759
760        args = {
761            "configfile": self.configfile,
762            "members": [self.instance],
763            "class": "callbackgroup",
764            "callbackgroup": "callback",
765            "name": [self.instance],
766        }
767
768        entry = CallbackGraphEntry(**args)
769        add_unique(entry, objs, config=self.configfile)
770
771        super(ConditionCallback, self).factory(objs)
772
773    def setup(self, objs):
774        """Resolve condition and graph entry."""
775
776        self.graph = get_index(
777            objs, "callbackgroup", [self.instance], config=self.configfile
778        )
779
780        self.condition = get_index(
781            objs, "condition", self.name, config=self.configfile
782        )
783
784        super(ConditionCallback, self).setup(objs)
785
786    def construct(self, loader, indent):
787        return self.render(
788            loader, "conditional.mako.cpp", c=self, indent=indent
789        )
790
791
792class Condition(HasPropertyIndex):
793    """Interface and common logic for conditions."""
794
795    def __init__(self, *a, **kw):
796        self.callback = kw.pop("callback")
797        self.defer = kw.pop("defer", None)
798        super(Condition, self).__init__(**kw)
799
800    def factory(self, objs):
801        """Create a callback instance for this conditional."""
802
803        args = {
804            "configfile": self.configfile,
805            "condition": self.name,
806            "class": "callback",
807            "callback": "conditional",
808            "instance": self.callback,
809            "name": self.name,
810            "defer": self.defer,
811        }
812
813        callback = ConditionCallback(**args)
814        add_unique(callback, objs, config=self.configfile)
815        callback.factory(objs)
816
817        super(Condition, self).factory(objs)
818
819
820class CountCondition(Condition, Renderer):
821    """Handle the count condition config file directive."""
822
823    def __init__(self, *a, **kw):
824        self.countop = kw.pop("countop")
825        self.countbound = kw.pop("countbound")
826        self.op = kw.pop("op")
827        self.bound = kw.pop("bound")
828        self.oneshot = TrivialArgument(
829            type="boolean", value=kw.pop("oneshot", False)
830        )
831        super(CountCondition, self).__init__(**kw)
832
833    def setup(self, objs):
834        """Resolve type."""
835
836        super(CountCondition, self).setup(objs)
837        self.bound = TrivialArgument(type=self.type, value=self.bound)
838
839    def construct(self, loader, indent):
840        return self.render(loader, "count.mako.cpp", c=self, indent=indent)
841
842
843class MedianCondition(Condition, Renderer):
844    """Handle the median condition config file directive."""
845
846    def __init__(self, *a, **kw):
847        self.op = kw.pop("op")
848        self.bound = kw.pop("bound")
849        self.oneshot = TrivialArgument(
850            type="boolean", value=kw.pop("oneshot", False)
851        )
852        super(MedianCondition, self).__init__(**kw)
853
854    def setup(self, objs):
855        """Resolve type."""
856
857        super(MedianCondition, self).setup(objs)
858        self.bound = TrivialArgument(type=self.type, value=self.bound)
859
860    def construct(self, loader, indent):
861        return self.render(loader, "median.mako.cpp", c=self, indent=indent)
862
863
864class Journal(Callback, Renderer):
865    """Handle the journal callback config file directive."""
866
867    def __init__(self, *a, **kw):
868        self.severity = kw.pop("severity")
869        self.message = kw.pop("message")
870        super(Journal, self).__init__(**kw)
871
872    def construct(self, loader, indent):
873        return self.render(loader, "journal.mako.cpp", c=self, indent=indent)
874
875
876class Elog(Callback, Renderer):
877    """Handle the elog callback config file directive."""
878
879    def __init__(self, *a, **kw):
880        self.error = kw.pop("error")
881        self.metadata = [Metadata(**x) for x in kw.pop("metadata", {})]
882        super(Elog, self).__init__(**kw)
883
884    def construct(self, loader, indent):
885        with open(args.gen_errors, "a") as fd:
886            fd.write(self.render(loader, "errors.mako.hpp", c=self))
887        return self.render(loader, "elog.mako.cpp", c=self, indent=indent)
888
889
890class Event(Callback, Renderer):
891    """Handle the event callback config file directive."""
892
893    def __init__(self, *a, **kw):
894        self.eventName = kw.pop("eventName")
895        self.eventMessage = kw.pop("eventMessage")
896        super(Event, self).__init__(**kw)
897
898    def construct(self, loader, indent):
899        return self.render(loader, "event.mako.cpp", c=self, indent=indent)
900
901
902class EventPath(PathCallback, Renderer):
903    """Handle the event path callback config file directive."""
904
905    def __init__(self, *a, **kw):
906        self.eventType = kw.pop("eventType")
907        super(EventPath, self).__init__(**kw)
908
909    def construct(self, loader, indent):
910        return self.render(loader, "eventpath.mako.cpp", c=self, indent=indent)
911
912
913class ElogWithMetadata(Callback, Renderer):
914    """Handle the elog_with_metadata callback config file directive."""
915
916    def __init__(self, *a, **kw):
917        self.error = kw.pop("error")
918        self.metadata = kw.pop("metadata")
919        super(ElogWithMetadata, self).__init__(**kw)
920
921    def construct(self, loader, indent):
922        with open(args.gen_errors, "a") as fd:
923            fd.write(self.render(loader, "errors.mako.hpp", c=self))
924        return self.render(
925            loader, "elog_with_metadata.mako.cpp", c=self, indent=indent
926        )
927
928
929class ResolveCallout(Callback, Renderer):
930    """Handle the 'resolve callout' callback config file directive."""
931
932    def __init__(self, *a, **kw):
933        self.callout = kw.pop("callout")
934        super(ResolveCallout, self).__init__(**kw)
935
936    def construct(self, loader, indent):
937        return self.render(
938            loader, "resolve_errors.mako.cpp", c=self, indent=indent
939        )
940
941
942class Method(ConfigEntry, Renderer):
943    """Handle the method callback config file directive."""
944
945    def __init__(self, *a, **kw):
946        self.service = kw.pop("service")
947        self.path = kw.pop("path")
948        self.interface = kw.pop("interface")
949        self.method = kw.pop("method")
950        self.args = [TrivialArgument(**x) for x in kw.pop("args", {})]
951        super(Method, self).__init__(**kw)
952
953    def factory(self, objs):
954        args = {
955            "class": "interface",
956            "interface": "element",
957            "name": self.service,
958        }
959        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
960
961        args = {"class": "pathname", "pathname": "element", "name": self.path}
962        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
963
964        args = {
965            "class": "interface",
966            "interface": "element",
967            "name": self.interface,
968        }
969        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
970
971        args = {
972            "class": "propertyname",
973            "propertyname": "element",
974            "name": self.method,
975        }
976        add_unique(ConfigEntry(configfile=self.configfile, **args), objs)
977
978        super(Method, self).factory(objs)
979
980    def setup(self, objs):
981        """Resolve elements."""
982
983        self.service = get_index(objs, "interface", self.service)
984
985        self.path = get_index(objs, "pathname", self.path)
986
987        self.interface = get_index(objs, "interface", self.interface)
988
989        self.method = get_index(objs, "propertyname", self.method)
990
991        super(Method, self).setup(objs)
992
993    def construct(self, loader, indent):
994        return self.render(loader, "method.mako.cpp", c=self, indent=indent)
995
996
997class CallbackGraphEntry(Group):
998    """An entry in a traversal list for groups of callbacks."""
999
1000    def __init__(self, *a, **kw):
1001        super(CallbackGraphEntry, self).__init__(**kw)
1002
1003    def setup(self, objs):
1004        """Resolve group members."""
1005
1006        def map_member(x):
1007            return get_index(objs, "callback", x, config=self.configfile)
1008
1009        self.members = map(map_member, self.members)
1010
1011        super(CallbackGraphEntry, self).setup(objs)
1012
1013
1014class PathCallbackGraphEntry(Group):
1015    """An entry in a traversal list for groups of callbacks."""
1016
1017    def __init__(self, *a, **kw):
1018        super(PathCallbackGraphEntry, self).__init__(**kw)
1019
1020    def setup(self, objs):
1021        """Resolve group members."""
1022
1023        def map_member(x):
1024            return get_index(objs, "pathcallback", x, config=self.configfile)
1025
1026        self.members = map(map_member, self.members)
1027
1028        super(PathCallbackGraphEntry, self).setup(objs)
1029
1030
1031class GroupOfCallbacks(ConfigEntry, Renderer):
1032    """Handle the callback group config file directive."""
1033
1034    def __init__(self, *a, **kw):
1035        self.members = kw.pop("members")
1036        super(GroupOfCallbacks, self).__init__(**kw)
1037
1038    def factory(self, objs):
1039        """Create a graph instance for this group of callbacks."""
1040
1041        args = {
1042            "configfile": self.configfile,
1043            "members": self.members,
1044            "class": "callbackgroup",
1045            "callbackgroup": "callback",
1046            "name": self.members,
1047        }
1048
1049        entry = CallbackGraphEntry(**args)
1050        add_unique(entry, objs, config=self.configfile)
1051
1052        super(GroupOfCallbacks, self).factory(objs)
1053
1054    def setup(self, objs):
1055        """Resolve graph entry."""
1056
1057        self.graph = get_index(
1058            objs, "callbackgroup", self.members, config=self.configfile
1059        )
1060
1061        super(GroupOfCallbacks, self).setup(objs)
1062
1063    def construct(self, loader, indent):
1064        return self.render(
1065            loader, "callbackgroup.mako.cpp", c=self, indent=indent
1066        )
1067
1068
1069class GroupOfPathCallbacks(ConfigEntry, Renderer):
1070    """Handle the callback group config file directive."""
1071
1072    def __init__(self, *a, **kw):
1073        self.members = kw.pop("members")
1074        super(GroupOfPathCallbacks, self).__init__(**kw)
1075
1076    def factory(self, objs):
1077        """Create a graph instance for this group of callbacks."""
1078
1079        args = {
1080            "configfile": self.configfile,
1081            "members": self.members,
1082            "class": "pathcallbackgroup",
1083            "pathcallbackgroup": "pathcallback",
1084            "name": self.members,
1085        }
1086
1087        entry = PathCallbackGraphEntry(**args)
1088        add_unique(entry, objs, config=self.configfile)
1089        super(GroupOfPathCallbacks, self).factory(objs)
1090
1091    def setup(self, objs):
1092        """Resolve graph entry."""
1093
1094        self.graph = get_index(
1095            objs, "callbackpathgroup", self.members, config=self.configfile
1096        )
1097
1098        super(GroupOfPathCallbacks, self).setup(objs)
1099
1100    def construct(self, loader, indent):
1101        return self.render(
1102            loader, "callbackpathgroup.mako.cpp", c=self, indent=indent
1103        )
1104
1105
1106class Everything(Renderer):
1107    """Parse/render entry point."""
1108
1109    @staticmethod
1110    def classmap(cls, sub=None):
1111        """Map render item class and subclass entries to the appropriate
1112        handler methods."""
1113        class_map = {
1114            "path": {
1115                "element": Path,
1116            },
1117            "pathgroup": {
1118                "path": GroupOfPaths,
1119            },
1120            "propertygroup": {
1121                "property": GroupOfProperties,
1122            },
1123            "property": {
1124                "element": Property,
1125            },
1126            "watch": {
1127                "property": PropertyWatch,
1128            },
1129            "pathwatch": {
1130                "path": PathWatch,
1131            },
1132            "instance": {
1133                "element": Instance,
1134            },
1135            "pathinstance": {
1136                "element": PathInstance,
1137            },
1138            "callback": {
1139                "journal": Journal,
1140                "elog": Elog,
1141                "elog_with_metadata": ElogWithMetadata,
1142                "event": Event,
1143                "group": GroupOfCallbacks,
1144                "method": Method,
1145                "resolve callout": ResolveCallout,
1146            },
1147            "pathcallback": {
1148                "eventpath": EventPath,
1149                "grouppath": GroupOfPathCallbacks,
1150            },
1151            "condition": {
1152                "count": CountCondition,
1153                "median": MedianCondition,
1154            },
1155        }
1156
1157        if cls not in class_map:
1158            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
1159        if sub not in class_map[cls]:
1160            raise NotImplementedError(
1161                'Unknown {0} type: "{1}"'.format(cls, sub)
1162            )
1163
1164        return class_map[cls][sub]
1165
1166    @staticmethod
1167    def load_one_yaml(path, fd, objs):
1168        """Parse a single YAML file.  Parsing occurs in three phases.
1169        In the first phase a factory method associated with each
1170        configuration file directive is invoked.  These factory
1171        methods generate more factory methods.  In the second
1172        phase the factory methods created in the first phase
1173        are invoked.  In the last phase a callback is invoked on
1174        each object created in phase two.  Typically the callback
1175        resolves references to other configuration file directives."""
1176
1177        factory_objs = {}
1178        for x in yaml.safe_load(fd.read()) or {}:
1179            # Create factory object for this config file directive.
1180            cls = x["class"]
1181            sub = x.get(cls)
1182            if cls == "group":
1183                cls = "{0}group".format(sub)
1184
1185            factory = Everything.classmap(cls, sub)
1186            obj = factory(configfile=path, **x)
1187
1188            # For a given class of directive, validate the file
1189            # doesn't have any duplicate names (duplicates are
1190            # ok across config files).
1191            if exists(factory_objs, obj.cls, obj.name, config=path):
1192                raise NotUniqueError(path, cls, obj.name)
1193
1194            factory_objs.setdefault(cls, []).append(obj)
1195            objs.setdefault(cls, []).append(obj)
1196
1197        for cls, items in factory_objs.items():
1198            for obj in items:
1199                # Add objects for template consumption.
1200                obj.factory(objs)
1201
1202    @staticmethod
1203    def load(args):
1204        """Aggregate all the YAML in the input directory
1205        into a single aggregate."""
1206
1207        objs = {}
1208        yaml_files = filter(
1209            lambda x: x.endswith(".yaml"), os.listdir(args.inputdir)
1210        )
1211
1212        for x in sorted(yaml_files):
1213            path = os.path.join(args.inputdir, x)
1214            with open(path, "r") as fd:
1215                Everything.load_one_yaml(path, fd, objs)
1216
1217        # Configuration file directives reference each other via
1218        # the name attribute; however, when rendered the reference
1219        # is just an array index.
1220        #
1221        # At this point all objects have been created but references
1222        # have not been resolved to array indices.  Instruct objects
1223        # to do that now.
1224        for cls, items in objs.items():
1225            for obj in items:
1226                obj.setup(objs)
1227
1228        return Everything(**objs)
1229
1230    def __init__(self, *a, **kw):
1231        self.pathmeta = kw.pop("path", [])
1232        self.paths = kw.pop("pathname", [])
1233        self.meta = kw.pop("meta", [])
1234        self.pathgroups = kw.pop("pathgroup", [])
1235        self.interfaces = kw.pop("interface", [])
1236        self.properties = kw.pop("property", [])
1237        self.propertynames = kw.pop("propertyname", [])
1238        self.propertygroups = kw.pop("propertygroup", [])
1239        self.instances = kw.pop("instance", [])
1240        self.pathinstances = kw.pop("pathinstance", [])
1241        self.instancegroups = kw.pop("instancegroup", [])
1242        self.pathinstancegroups = kw.pop("pathinstancegroup", [])
1243        self.watches = kw.pop("watch", [])
1244        self.pathwatches = kw.pop("pathwatch", [])
1245        self.callbacks = kw.pop("callback", [])
1246        self.pathcallbacks = kw.pop("pathcallback", [])
1247        self.callbackgroups = kw.pop("callbackgroup", [])
1248        self.pathcallbackgroups = kw.pop("pathcallbackgroup", [])
1249        self.conditions = kw.pop("condition", [])
1250        self.filters = kw.pop("filtersgroup", [])
1251
1252        super(Everything, self).__init__(**kw)
1253
1254    def generate_cpp(self, loader):
1255        """Render the template with the provided data."""
1256        # errors.hpp is used by generated.hpp to included any error.hpp files
1257        open(args.gen_errors, "w+")
1258
1259        with open(args.output, "w") as fd:
1260            fd.write(
1261                self.render(
1262                    loader,
1263                    args.template,
1264                    meta=self.meta,
1265                    properties=self.properties,
1266                    propertynames=self.propertynames,
1267                    interfaces=self.interfaces,
1268                    paths=self.paths,
1269                    pathmeta=self.pathmeta,
1270                    pathgroups=self.pathgroups,
1271                    propertygroups=self.propertygroups,
1272                    instances=self.instances,
1273                    pathinstances=self.pathinstances,
1274                    watches=self.watches,
1275                    pathwatches=self.pathwatches,
1276                    instancegroups=self.instancegroups,
1277                    pathinstancegroups=self.pathinstancegroups,
1278                    callbacks=self.callbacks,
1279                    pathcallbacks=self.pathcallbacks,
1280                    callbackgroups=self.callbackgroups,
1281                    pathcallbackgroups=self.pathcallbackgroups,
1282                    conditions=self.conditions,
1283                    filters=self.filters,
1284                    indent=Indent(),
1285                )
1286            )
1287
1288
1289if __name__ == "__main__":
1290    script_dir = os.path.dirname(os.path.realpath(__file__))
1291    valid_commands = {
1292        "generate-cpp": "generate_cpp",
1293    }
1294
1295    parser = ArgumentParser(
1296        description=(
1297            "Phosphor DBus Monitor (PDM) YAML scanner and code generator."
1298        )
1299    )
1300
1301    parser.add_argument(
1302        "-o",
1303        "--out",
1304        dest="output",
1305        default="generated.cpp",
1306        help="Generated output file name and path.",
1307    )
1308    parser.add_argument(
1309        "-t",
1310        "--template",
1311        dest="template",
1312        default="generated.mako.hpp",
1313        help="The top level template to render.",
1314    )
1315    parser.add_argument(
1316        "-e",
1317        "--errors",
1318        dest="gen_errors",
1319        default="errors.hpp",
1320        help="Generated errors.hpp output filename.",
1321    )
1322    parser.add_argument(
1323        "-p",
1324        "--template-path",
1325        dest="template_search",
1326        default=script_dir,
1327        help="The space delimited mako template search path.",
1328    )
1329    parser.add_argument(
1330        "-d",
1331        "--dir",
1332        dest="inputdir",
1333        default=os.path.join(script_dir, "example"),
1334        help="Location of files to process.",
1335    )
1336    parser.add_argument(
1337        "command",
1338        metavar="COMMAND",
1339        type=str,
1340        choices=valid_commands.keys(),
1341        help="%s." % " | ".join(valid_commands.keys()),
1342    )
1343
1344    args = parser.parse_args()
1345
1346    if sys.version_info < (3, 0):
1347        lookup = mako.lookup.TemplateLookup(
1348            directories=args.template_search.split(), disable_unicode=True
1349        )
1350    else:
1351        lookup = mako.lookup.TemplateLookup(
1352            directories=args.template_search.split()
1353        )
1354    try:
1355        function = getattr(Everything.load(args), valid_commands[args.command])
1356        function(lookup)
1357    except InvalidConfigError as e:
1358        sys.stdout.write("{0}: {1}\n\n".format(e.config, e.msg))
1359        raise
1360