xref: /openbmc/phosphor-dbus-monitor/src/pdmgen.py (revision b97bfff73e44e53571f8a00b80ec947496930d7b)
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        super(PropertyWatch, self).__init__(**kw)
414
415
416class Callback(HasPropertyIndex):
417    '''Interface and common logic for callbacks.'''
418
419    def __init__(self, *a, **kw):
420        super(Callback, self).__init__(**kw)
421
422
423class ConditionCallback(ConfigEntry, Renderer):
424    '''Handle the journal callback config file directive.'''
425
426    def __init__(self, *a, **kw):
427        self.condition = kw.pop('condition')
428        self.instance = kw.pop('instance')
429        super(ConditionCallback, self).__init__(**kw)
430
431    def factory(self, objs):
432        '''Create a graph instance for this callback.'''
433
434        args = {
435            'configfile': self.configfile,
436            'members': [self.instance],
437            'class': 'callbackgroup',
438            'callbackgroup': 'callback',
439            'name': [self.instance]
440        }
441
442        entry = CallbackGraphEntry(**args)
443        add_unique(entry, objs, config=self.configfile)
444
445        super(ConditionCallback, self).factory(objs)
446
447    def setup(self, objs):
448        '''Resolve condition and graph entry.'''
449
450        self.graph = get_index(
451            objs,
452            'callbackgroup',
453            [self.instance],
454            config=self.configfile)
455
456        self.condition = get_index(
457            objs,
458            'condition',
459            self.name,
460            config=self.configfile)
461
462        super(ConditionCallback, self).setup(objs)
463
464    def construct(self, loader, indent):
465        return self.render(
466            loader,
467            'conditional.mako.cpp',
468            c=self,
469            indent=indent)
470
471
472class Condition(HasPropertyIndex):
473    '''Interface and common logic for conditions.'''
474
475    def __init__(self, *a, **kw):
476        self.callback = kw.pop('callback')
477        super(Condition, self).__init__(**kw)
478
479    def factory(self, objs):
480        '''Create a callback instance for this conditional.'''
481
482        args = {
483            'configfile': self.configfile,
484            'condition': self.name,
485            'class': 'callback',
486            'callback': 'conditional',
487            'instance': self.callback,
488            'name': self.name,
489        }
490
491        callback = ConditionCallback(**args)
492        add_unique(callback, objs, config=self.configfile)
493        callback.factory(objs)
494
495        super(Condition, self).factory(objs)
496
497
498class CountCondition(Condition, Renderer):
499    '''Handle the count condition config file directive.'''
500
501    def __init__(self, *a, **kw):
502        self.countop = kw.pop('countop')
503        self.countbound = kw.pop('countbound')
504        self.op = kw.pop('op')
505        self.bound = kw.pop('bound')
506        super(CountCondition, self).__init__(**kw)
507
508    def construct(self, loader, indent):
509        return self.render(
510            loader,
511            'count.mako.cpp',
512            c=self,
513            indent=indent)
514
515
516class Journal(Callback, Renderer):
517    '''Handle the journal callback config file directive.'''
518
519    def __init__(self, *a, **kw):
520        self.severity = kw.pop('severity')
521        self.message = kw.pop('message')
522        super(Journal, self).__init__(**kw)
523
524    def construct(self, loader, indent):
525        return self.render(
526            loader,
527            'journal.mako.cpp',
528            c=self,
529            indent=indent)
530
531
532class CallbackGraphEntry(Group):
533    '''An entry in a traversal list for groups of callbacks.'''
534
535    def __init__(self, *a, **kw):
536        super(CallbackGraphEntry, self).__init__(**kw)
537
538    def setup(self, objs):
539        '''Resolve group members.'''
540
541        def map_member(x):
542            return get_index(
543                objs, 'callback', x, config=self.configfile)
544
545        self.members = map(
546            map_member,
547            self.members)
548
549        super(CallbackGraphEntry, self).setup(objs)
550
551
552class GroupOfCallbacks(ConfigEntry, Renderer):
553    '''Handle the callback group config file directive.'''
554
555    def __init__(self, *a, **kw):
556        self.members = kw.pop('members')
557        super(GroupOfCallbacks, self).__init__(**kw)
558
559    def factory(self, objs):
560        '''Create a graph instance for this group of callbacks.'''
561
562        args = {
563            'configfile': self.configfile,
564            'members': self.members,
565            'class': 'callbackgroup',
566            'callbackgroup': 'callback',
567            'name': self.members
568        }
569
570        entry = CallbackGraphEntry(**args)
571        add_unique(entry, objs, config=self.configfile)
572
573        super(GroupOfCallbacks, self).factory(objs)
574
575    def setup(self, objs):
576        '''Resolve graph entry.'''
577
578        self.graph = get_index(
579            objs, 'callbackgroup', self.members, config=self.configfile)
580
581        super(GroupOfCallbacks, self).setup(objs)
582
583    def construct(self, loader, indent):
584        return self.render(
585            loader,
586            'callbackgroup.mako.cpp',
587            c=self,
588            indent=indent)
589
590
591class Everything(Renderer):
592    '''Parse/render entry point.'''
593
594    @staticmethod
595    def classmap(cls, sub=None):
596        '''Map render item class and subclass entries to the appropriate
597        handler methods.'''
598
599        class_map = {
600            'path': {
601                'element': Path,
602            },
603            'pathgroup': {
604                'path': GroupOfPaths,
605            },
606            'propertygroup': {
607                'property': GroupOfProperties,
608            },
609            'property': {
610                'element': Property,
611            },
612            'watch': {
613                'property': PropertyWatch,
614            },
615            'instance': {
616                'element': Instance,
617            },
618            'callback': {
619                'journal': Journal,
620                'group': GroupOfCallbacks,
621            },
622            'condition': {
623                'count': CountCondition,
624            },
625        }
626
627        if cls not in class_map:
628            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
629        if sub not in class_map[cls]:
630            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
631                cls, sub))
632
633        return class_map[cls][sub]
634
635    @staticmethod
636    def load_one_yaml(path, fd, objs):
637        '''Parse a single YAML file.  Parsing occurs in three phases.
638        In the first phase a factory method associated with each
639        configuration file directive is invoked.  These factory
640        methods generate more factory methods.  In the second
641        phase the factory methods created in the first phase
642        are invoked.  In the last phase a callback is invoked on
643        each object created in phase two.  Typically the callback
644        resolves references to other configuration file directives.'''
645
646        factory_objs = {}
647        for x in yaml.safe_load(fd.read()) or {}:
648
649            # Create factory object for this config file directive.
650            cls = x['class']
651            sub = x.get(cls)
652            if cls == 'group':
653                cls = '{0}group'.format(sub)
654
655            factory = Everything.classmap(cls, sub)
656            obj = factory(configfile=path, **x)
657
658            # For a given class of directive, validate the file
659            # doesn't have any duplicate names (duplicates are
660            # ok across config files).
661            if exists(factory_objs, obj.cls, obj.name, config=path):
662                raise NotUniqueError(path, cls, obj.name)
663
664            factory_objs.setdefault(cls, []).append(obj)
665            objs.setdefault(cls, []).append(obj)
666
667        for cls, items in factory_objs.items():
668            for obj in items:
669                # Add objects for template consumption.
670                obj.factory(objs)
671
672    @staticmethod
673    def load(args):
674        '''Aggregate all the YAML in the input directory
675        into a single aggregate.'''
676
677        objs = {}
678        yaml_files = filter(
679            lambda x: x.endswith('.yaml'),
680            os.listdir(args.inputdir))
681
682        yaml_files.sort()
683
684        for x in yaml_files:
685            path = os.path.join(args.inputdir, x)
686            with open(path, 'r') as fd:
687                Everything.load_one_yaml(path, fd, objs)
688
689        # Configuration file directives reference each other via
690        # the name attribute; however, when rendered the reference
691        # is just an array index.
692        #
693        # At this point all objects have been created but references
694        # have not been resolved to array indicies.  Instruct objects
695        # to do that now.
696        for cls, items in objs.items():
697            for obj in items:
698                obj.setup(objs)
699
700        return Everything(**objs)
701
702    def __init__(self, *a, **kw):
703        self.pathmeta = kw.pop('path', [])
704        self.paths = kw.pop('pathname', [])
705        self.meta = kw.pop('meta', [])
706        self.pathgroups = kw.pop('pathgroup', [])
707        self.interfaces = kw.pop('interface', [])
708        self.properties = kw.pop('property', [])
709        self.propertynames = kw.pop('propertyname', [])
710        self.propertygroups = kw.pop('propertygroup', [])
711        self.instances = kw.pop('instance', [])
712        self.instancegroups = kw.pop('instancegroup', [])
713        self.watches = kw.pop('watch', [])
714        self.callbacks = kw.pop('callback', [])
715        self.callbackgroups = kw.pop('callbackgroup', [])
716        self.conditions = kw.pop('condition', [])
717
718        super(Everything, self).__init__(**kw)
719
720    def generate_cpp(self, loader):
721        '''Render the template with the provided data.'''
722        with open(args.output, 'w') as fd:
723            fd.write(
724                self.render(
725                    loader,
726                    args.template,
727                    meta=self.meta,
728                    properties=self.properties,
729                    propertynames=self.propertynames,
730                    interfaces=self.interfaces,
731                    paths=self.paths,
732                    pathmeta=self.pathmeta,
733                    pathgroups=self.pathgroups,
734                    propertygroups=self.propertygroups,
735                    instances=self.instances,
736                    watches=self.watches,
737                    instancegroups=self.instancegroups,
738                    callbacks=self.callbacks,
739                    callbackgroups=self.callbackgroups,
740                    conditions=self.conditions,
741                    indent=Indent()))
742
743if __name__ == '__main__':
744    script_dir = os.path.dirname(os.path.realpath(__file__))
745    valid_commands = {
746        'generate-cpp': 'generate_cpp',
747    }
748
749    parser = ArgumentParser(
750        description='Phosphor DBus Monitor (PDM) YAML '
751        'scanner and code generator.')
752
753    parser.add_argument(
754        "-o", "--out", dest="output",
755        default='generated.cpp',
756        help="Generated output file name and path.")
757    parser.add_argument(
758        '-t', '--template', dest='template',
759        default='generated.mako.hpp',
760        help='The top level template to render.')
761    parser.add_argument(
762        '-p', '--template-path', dest='template_search',
763        default=script_dir,
764        help='The space delimited mako template search path.')
765    parser.add_argument(
766        '-d', '--dir', dest='inputdir',
767        default=os.path.join(script_dir, 'example'),
768        help='Location of files to process.')
769    parser.add_argument(
770        'command', metavar='COMMAND', type=str,
771        choices=valid_commands.keys(),
772        help='%s.' % " | ".join(valid_commands.keys()))
773
774    args = parser.parse_args()
775
776    if sys.version_info < (3, 0):
777        lookup = mako.lookup.TemplateLookup(
778            directories=args.template_search.split(),
779            disable_unicode=True)
780    else:
781        lookup = mako.lookup.TemplateLookup(
782            directories=args.template_search.split())
783    try:
784        function = getattr(
785            Everything.load(args),
786            valid_commands[args.command])
787        function(lookup)
788    except InvalidConfigError as e:
789        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
790        raise
791