xref: /openbmc/phosphor-dbus-monitor/src/pdmgen.py (revision fccdc39fdf8f97ef4bf3de3b8ddcae28fef4bc8e)
1#!/usr/bin/env python
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 Indent(object):
88    '''Help templates be depth agnostic.'''
89
90    def __init__(self, depth=0):
91        self.depth = depth
92
93    def __add__(self, depth):
94        return Indent(self.depth + depth)
95
96    def __call__(self, depth):
97        '''Render an indent at the current depth plus depth.'''
98        return 4*' '*(depth + self.depth)
99
100
101class ConfigEntry(NamedElement):
102    '''Base interface for rendered items.'''
103
104    def __init__(self, *a, **kw):
105        '''Pop the configfile/class/subclass keywords.'''
106
107        self.configfile = kw.pop('configfile')
108        self.cls = kw.pop('class')
109        self.subclass = kw.pop(self.cls)
110        super(ConfigEntry, self).__init__(**kw)
111
112    def factory(self, objs):
113        ''' Optional factory interface for subclasses to add
114        additional items to be rendered.'''
115
116        pass
117
118    def setup(self, objs):
119        ''' Optional setup interface for subclasses, invoked
120        after all factory methods have been run.'''
121
122        pass
123
124
125class Path(ConfigEntry):
126    '''Path/metadata association.'''
127
128    def __init__(self, *a, **kw):
129        super(Path, self).__init__(**kw)
130
131    def factory(self, objs):
132        '''Create path and metadata elements.'''
133
134        args = {
135            'class': 'pathname',
136            'pathname': 'element',
137            'name': self.name['path']
138        }
139        add_unique(ConfigEntry(
140            configfile=self.configfile, **args), objs)
141
142        args = {
143            'class': 'meta',
144            'meta': 'element',
145            'name': self.name['meta']
146        }
147        add_unique(ConfigEntry(
148            configfile=self.configfile, **args), objs)
149
150        super(Path, self).factory(objs)
151
152    def setup(self, objs):
153        '''Resolve path and metadata names to indicies.'''
154
155        self.path = get_index(
156            objs, 'pathname', self.name['path'])
157        self.meta = get_index(
158            objs, 'meta', self.name['meta'])
159
160        super(Path, self).setup(objs)
161
162
163class Property(ConfigEntry):
164    '''Property/interface/metadata association.'''
165
166    def __init__(self, *a, **kw):
167        super(Property, self).__init__(**kw)
168
169    def factory(self, objs):
170        '''Create interface, property name and metadata elements.'''
171
172        args = {
173            'class': 'interface',
174            'interface': 'element',
175            'name': self.name['interface']
176        }
177        add_unique(ConfigEntry(
178            configfile=self.configfile, **args), objs)
179
180        args = {
181            'class': 'propertyname',
182            'propertyname': 'element',
183            'name': self.name['property']
184        }
185        add_unique(ConfigEntry(
186            configfile=self.configfile, **args), objs)
187
188        args = {
189            'class': 'meta',
190            'meta': 'element',
191            'name': self.name['meta']
192        }
193        add_unique(ConfigEntry(
194            configfile=self.configfile, **args), objs)
195
196        super(Property, self).factory(objs)
197
198    def setup(self, objs):
199        '''Resolve interface, property and metadata to indicies.'''
200
201        self.interface = get_index(
202            objs, 'interface', self.name['interface'])
203        self.prop = get_index(
204            objs, 'propertyname', self.name['property'])
205        self.meta = get_index(
206            objs, 'meta', self.name['meta'])
207
208        super(Property, self).setup(objs)
209
210
211class Instance(ConfigEntry):
212    '''Property/Path association.'''
213
214    def __init__(self, *a, **kw):
215        super(Instance, self).__init__(**kw)
216
217    def setup(self, objs):
218        '''Resolve elements to indicies.'''
219
220        self.interface = get_index(
221            objs, 'interface', self.name['property']['interface'])
222        self.prop = get_index(
223            objs, 'propertyname', self.name['property']['property'])
224        self.propmeta = get_index(
225            objs, 'meta', self.name['property']['meta'])
226        self.path = get_index(
227            objs, 'pathname', self.name['path']['path'])
228        self.pathmeta = get_index(
229            objs, 'meta', self.name['path']['meta'])
230
231        super(Instance, self).setup(objs)
232
233
234class Group(ConfigEntry):
235    '''Pop the members keyword for groups.'''
236
237    def __init__(self, *a, **kw):
238        self.members = kw.pop('members')
239        super(Group, self).__init__(**kw)
240
241
242class ImplicitGroup(Group):
243    '''Provide a factory method for groups whose members are
244    not explicitly declared in the config files.'''
245
246    def __init__(self, *a, **kw):
247        super(ImplicitGroup, self).__init__(**kw)
248
249    def factory(self, objs):
250        '''Create group members.'''
251
252        factory = Everything.classmap(self.subclass, 'element')
253        for m in self.members:
254            args = {
255                'class': self.subclass,
256                self.subclass: 'element',
257                'name': m
258            }
259
260            obj = factory(configfile=self.configfile, **args)
261            add_unique(obj, objs)
262            obj.factory(objs)
263
264        super(ImplicitGroup, self).factory(objs)
265
266
267class GroupOfPaths(ImplicitGroup):
268    '''Path group config file directive.'''
269
270    def __init__(self, *a, **kw):
271        super(GroupOfPaths, self).__init__(**kw)
272
273    def setup(self, objs):
274        '''Resolve group members.'''
275
276        def map_member(x):
277            path = get_index(
278                objs, 'pathname', x['path'])
279            meta = get_index(
280                objs, 'meta', x['meta'])
281            return (path, meta)
282
283        self.members = map(
284            map_member,
285            self.members)
286
287        super(GroupOfPaths, self).setup(objs)
288
289
290class GroupOfProperties(ImplicitGroup):
291    '''Property group config file directive.'''
292
293    def __init__(self, *a, **kw):
294        self.datatype = sdbusplus.property.Property(
295            name=kw.get('name'),
296            type=kw.pop('type')).cppTypeName
297
298        super(GroupOfProperties, self).__init__(**kw)
299
300    def setup(self, objs):
301        '''Resolve group members.'''
302
303        def map_member(x):
304            iface = get_index(
305                objs, 'interface', x['interface'])
306            prop = get_index(
307                objs, 'propertyname', x['property'])
308            meta = get_index(
309                objs, 'meta', x['meta'])
310
311            return (iface, prop, meta)
312
313        self.members = map(
314            map_member,
315            self.members)
316
317        super(GroupOfProperties, self).setup(objs)
318
319
320class GroupOfInstances(ImplicitGroup):
321    '''A group of property instances.'''
322
323    def __init__(self, *a, **kw):
324        super(GroupOfInstances, self).__init__(**kw)
325
326    def setup(self, objs):
327        '''Resolve group members.'''
328
329        def map_member(x):
330            path = get_index(objs, 'pathname', x['path']['path'])
331            pathmeta = get_index(objs, 'meta', x['path']['meta'])
332            interface = get_index(
333                objs, 'interface', x['property']['interface'])
334            prop = get_index(objs, 'propertyname', x['property']['property'])
335            propmeta = get_index(objs, 'meta', x['property']['meta'])
336            instance = get_index(objs, 'instance', x)
337
338            return (path, pathmeta, interface, prop, propmeta, instance)
339
340        self.members = map(
341            map_member,
342            self.members)
343
344        super(GroupOfInstances, self).setup(objs)
345
346
347class HasPropertyIndex(ConfigEntry):
348    '''Handle config file directives that require an index to be
349    constructed.'''
350
351    def __init__(self, *a, **kw):
352        self.paths = kw.pop('paths')
353        self.properties = kw.pop('properties')
354        super(HasPropertyIndex, self).__init__(**kw)
355
356    def factory(self, objs):
357        '''Create a group of instances for this index.'''
358
359        members = []
360        path_group = get_index(
361            objs, 'pathgroup', self.paths, config=self.configfile)
362        property_group = get_index(
363            objs, 'propertygroup', self.properties, config=self.configfile)
364
365        for path in objs['pathgroup'][path_group].members:
366            for prop in objs['propertygroup'][property_group].members:
367                member = {
368                    'path': path,
369                    'property': prop,
370                }
371                members.append(member)
372
373        args = {
374            'members': members,
375            'class': 'instancegroup',
376            'instancegroup': 'instance',
377            'name': '{0} {1}'.format(self.paths, self.properties)
378        }
379
380        group = GroupOfInstances(configfile=self.configfile, **args)
381        add_unique(group, objs, config=self.configfile)
382        group.factory(objs)
383
384        super(HasPropertyIndex, self).factory(objs)
385
386    def setup(self, objs):
387        '''Resolve path, property, and instance groups.'''
388
389        self.instances = get_index(
390            objs,
391            'instancegroup',
392            '{0} {1}'.format(self.paths, self.properties),
393            config=self.configfile)
394        self.paths = get_index(
395            objs,
396            'pathgroup',
397            self.paths,
398            config=self.configfile)
399        self.properties = get_index(
400            objs,
401            'propertygroup',
402            self.properties,
403            config=self.configfile)
404        self.datatype = objs['propertygroup'][self.properties].datatype
405
406        super(HasPropertyIndex, self).setup(objs)
407
408
409class PropertyWatch(HasPropertyIndex):
410    '''Handle the property watch config file directive.'''
411
412    def __init__(self, *a, **kw):
413        self.callback = kw.pop('callback', None)
414        super(PropertyWatch, self).__init__(**kw)
415
416    def setup(self, objs):
417        '''Resolve optional callback.'''
418
419        if self.callback:
420            self.callback = get_index(
421                objs,
422                'callback',
423                self.callback,
424                config=self.configfile)
425
426        super(PropertyWatch, self).setup(objs)
427
428
429class Callback(HasPropertyIndex):
430    '''Interface and common logic for callbacks.'''
431
432    def __init__(self, *a, **kw):
433        super(Callback, self).__init__(**kw)
434
435
436class ConditionCallback(ConfigEntry, Renderer):
437    '''Handle the journal callback config file directive.'''
438
439    def __init__(self, *a, **kw):
440        self.condition = kw.pop('condition')
441        self.instance = kw.pop('instance')
442        super(ConditionCallback, self).__init__(**kw)
443
444    def factory(self, objs):
445        '''Create a graph instance for this callback.'''
446
447        args = {
448            'configfile': self.configfile,
449            'members': [self.instance],
450            'class': 'callbackgroup',
451            'callbackgroup': 'callback',
452            'name': [self.instance]
453        }
454
455        entry = CallbackGraphEntry(**args)
456        add_unique(entry, objs, config=self.configfile)
457
458        super(ConditionCallback, self).factory(objs)
459
460    def setup(self, objs):
461        '''Resolve condition and graph entry.'''
462
463        self.graph = get_index(
464            objs,
465            'callbackgroup',
466            [self.instance],
467            config=self.configfile)
468
469        self.condition = get_index(
470            objs,
471            'condition',
472            self.name,
473            config=self.configfile)
474
475        super(ConditionCallback, self).setup(objs)
476
477    def construct(self, loader, indent):
478        return self.render(
479            loader,
480            'conditional.mako.cpp',
481            c=self,
482            indent=indent)
483
484
485class Condition(HasPropertyIndex):
486    '''Interface and common logic for conditions.'''
487
488    def __init__(self, *a, **kw):
489        self.callback = kw.pop('callback')
490        super(Condition, self).__init__(**kw)
491
492    def factory(self, objs):
493        '''Create a callback instance for this conditional.'''
494
495        args = {
496            'configfile': self.configfile,
497            'condition': self.name,
498            'class': 'callback',
499            'callback': 'conditional',
500            'instance': self.callback,
501            'name': self.name,
502        }
503
504        callback = ConditionCallback(**args)
505        add_unique(callback, objs, config=self.configfile)
506        callback.factory(objs)
507
508        super(Condition, self).factory(objs)
509
510
511class CountCondition(Condition, Renderer):
512    '''Handle the count condition config file directive.'''
513
514    def __init__(self, *a, **kw):
515        self.countop = kw.pop('countop')
516        self.countbound = kw.pop('countbound')
517        self.op = kw.pop('op')
518        self.bound = kw.pop('bound')
519        super(CountCondition, self).__init__(**kw)
520
521    def construct(self, loader, indent):
522        return self.render(
523            loader,
524            'count.mako.cpp',
525            c=self,
526            indent=indent)
527
528
529class Journal(Callback, Renderer):
530    '''Handle the journal callback config file directive.'''
531
532    def __init__(self, *a, **kw):
533        self.severity = kw.pop('severity')
534        self.message = kw.pop('message')
535        super(Journal, self).__init__(**kw)
536
537    def construct(self, loader, indent):
538        return self.render(
539            loader,
540            'journal.mako.cpp',
541            c=self,
542            indent=indent)
543
544
545class CallbackGraphEntry(Group):
546    '''An entry in a traversal list for groups of callbacks.'''
547
548    def __init__(self, *a, **kw):
549        super(CallbackGraphEntry, self).__init__(**kw)
550
551    def setup(self, objs):
552        '''Resolve group members.'''
553
554        def map_member(x):
555            return get_index(
556                objs, 'callback', x, config=self.configfile)
557
558        self.members = map(
559            map_member,
560            self.members)
561
562        super(CallbackGraphEntry, self).setup(objs)
563
564
565class GroupOfCallbacks(ConfigEntry, Renderer):
566    '''Handle the callback group config file directive.'''
567
568    def __init__(self, *a, **kw):
569        self.members = kw.pop('members')
570        super(GroupOfCallbacks, self).__init__(**kw)
571
572    def factory(self, objs):
573        '''Create a graph instance for this group of callbacks.'''
574
575        args = {
576            'configfile': self.configfile,
577            'members': self.members,
578            'class': 'callbackgroup',
579            'callbackgroup': 'callback',
580            'name': self.members
581        }
582
583        entry = CallbackGraphEntry(**args)
584        add_unique(entry, objs, config=self.configfile)
585
586        super(GroupOfCallbacks, self).factory(objs)
587
588    def setup(self, objs):
589        '''Resolve graph entry.'''
590
591        self.graph = get_index(
592            objs, 'callbackgroup', self.members, config=self.configfile)
593
594        super(GroupOfCallbacks, self).setup(objs)
595
596    def construct(self, loader, indent):
597        return self.render(
598            loader,
599            'callbackgroup.mako.cpp',
600            c=self,
601            indent=indent)
602
603
604class Everything(Renderer):
605    '''Parse/render entry point.'''
606
607    @staticmethod
608    def classmap(cls, sub=None):
609        '''Map render item class and subclass entries to the appropriate
610        handler methods.'''
611
612        class_map = {
613            'path': {
614                'element': Path,
615            },
616            'pathgroup': {
617                'path': GroupOfPaths,
618            },
619            'propertygroup': {
620                'property': GroupOfProperties,
621            },
622            'property': {
623                'element': Property,
624            },
625            'watch': {
626                'property': PropertyWatch,
627            },
628            'instance': {
629                'element': Instance,
630            },
631            'callback': {
632                'journal': Journal,
633                'group': GroupOfCallbacks,
634            },
635            'condition': {
636                'count': CountCondition,
637            },
638        }
639
640        if cls not in class_map:
641            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
642        if sub not in class_map[cls]:
643            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
644                cls, sub))
645
646        return class_map[cls][sub]
647
648    @staticmethod
649    def load_one_yaml(path, fd, objs):
650        '''Parse a single YAML file.  Parsing occurs in three phases.
651        In the first phase a factory method associated with each
652        configuration file directive is invoked.  These factory
653        methods generate more factory methods.  In the second
654        phase the factory methods created in the first phase
655        are invoked.  In the last phase a callback is invoked on
656        each object created in phase two.  Typically the callback
657        resolves references to other configuration file directives.'''
658
659        factory_objs = {}
660        for x in yaml.safe_load(fd.read()) or {}:
661
662            # Create factory object for this config file directive.
663            cls = x['class']
664            sub = x.get(cls)
665            if cls == 'group':
666                cls = '{0}group'.format(sub)
667
668            factory = Everything.classmap(cls, sub)
669            obj = factory(configfile=path, **x)
670
671            # For a given class of directive, validate the file
672            # doesn't have any duplicate names (duplicates are
673            # ok across config files).
674            if exists(factory_objs, obj.cls, obj.name, config=path):
675                raise NotUniqueError(path, cls, obj.name)
676
677            factory_objs.setdefault(cls, []).append(obj)
678            objs.setdefault(cls, []).append(obj)
679
680        for cls, items in factory_objs.items():
681            for obj in items:
682                # Add objects for template consumption.
683                obj.factory(objs)
684
685    @staticmethod
686    def load(args):
687        '''Aggregate all the YAML in the input directory
688        into a single aggregate.'''
689
690        objs = {}
691        yaml_files = filter(
692            lambda x: x.endswith('.yaml'),
693            os.listdir(args.inputdir))
694
695        yaml_files.sort()
696
697        for x in yaml_files:
698            path = os.path.join(args.inputdir, x)
699            with open(path, 'r') as fd:
700                Everything.load_one_yaml(path, fd, objs)
701
702        # Configuration file directives reference each other via
703        # the name attribute; however, when rendered the reference
704        # is just an array index.
705        #
706        # At this point all objects have been created but references
707        # have not been resolved to array indicies.  Instruct objects
708        # to do that now.
709        for cls, items in objs.items():
710            for obj in items:
711                obj.setup(objs)
712
713        return Everything(**objs)
714
715    def __init__(self, *a, **kw):
716        self.pathmeta = kw.pop('path', [])
717        self.paths = kw.pop('pathname', [])
718        self.meta = kw.pop('meta', [])
719        self.pathgroups = kw.pop('pathgroup', [])
720        self.interfaces = kw.pop('interface', [])
721        self.properties = kw.pop('property', [])
722        self.propertynames = kw.pop('propertyname', [])
723        self.propertygroups = kw.pop('propertygroup', [])
724        self.instances = kw.pop('instance', [])
725        self.instancegroups = kw.pop('instancegroup', [])
726        self.watches = kw.pop('watch', [])
727        self.callbacks = kw.pop('callback', [])
728        self.callbackgroups = kw.pop('callbackgroup', [])
729        self.conditions = kw.pop('condition', [])
730
731        super(Everything, self).__init__(**kw)
732
733    def generate_cpp(self, loader):
734        '''Render the template with the provided data.'''
735        with open(args.output, 'w') as fd:
736            fd.write(
737                self.render(
738                    loader,
739                    args.template,
740                    meta=self.meta,
741                    properties=self.properties,
742                    propertynames=self.propertynames,
743                    interfaces=self.interfaces,
744                    paths=self.paths,
745                    pathmeta=self.pathmeta,
746                    pathgroups=self.pathgroups,
747                    propertygroups=self.propertygroups,
748                    instances=self.instances,
749                    watches=self.watches,
750                    instancegroups=self.instancegroups,
751                    callbacks=self.callbacks,
752                    callbackgroups=self.callbackgroups,
753                    conditions=self.conditions,
754                    indent=Indent()))
755
756if __name__ == '__main__':
757    script_dir = os.path.dirname(os.path.realpath(__file__))
758    valid_commands = {
759        'generate-cpp': 'generate_cpp',
760    }
761
762    parser = ArgumentParser(
763        description='Phosphor DBus Monitor (PDM) YAML '
764        'scanner and code generator.')
765
766    parser.add_argument(
767        "-o", "--out", dest="output",
768        default='generated.cpp',
769        help="Generated output file name and path.")
770    parser.add_argument(
771        '-t', '--template', dest='template',
772        default='generated.mako.hpp',
773        help='The top level template to render.')
774    parser.add_argument(
775        '-p', '--template-path', dest='template_search',
776        default=script_dir,
777        help='The space delimited mako template search path.')
778    parser.add_argument(
779        '-d', '--dir', dest='inputdir',
780        default=os.path.join(script_dir, 'example'),
781        help='Location of files to process.')
782    parser.add_argument(
783        'command', metavar='COMMAND', type=str,
784        choices=valid_commands.keys(),
785        help='%s.' % " | ".join(valid_commands.keys()))
786
787    args = parser.parse_args()
788
789    if sys.version_info < (3, 0):
790        lookup = mako.lookup.TemplateLookup(
791            directories=args.template_search.split(),
792            disable_unicode=True)
793    else:
794        lookup = mako.lookup.TemplateLookup(
795            directories=args.template_search.split())
796    try:
797        function = getattr(
798            Everything.load(args),
799            valid_commands[args.command])
800        function(lookup)
801    except InvalidConfigError as e:
802        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
803        raise
804