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