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