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