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