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