xref: /openbmc/phosphor-dbus-monitor/src/pdmgen.py (revision 13fd8722e616dd424cd585187fb0ef65f6316023)
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 Everything(Renderer):
417    '''Parse/render entry point.'''
418
419    @staticmethod
420    def classmap(cls, sub=None):
421        '''Map render item class and subclass entries to the appropriate
422        handler methods.'''
423
424        class_map = {
425            'path': {
426                'element': Path,
427            },
428            'pathgroup': {
429                'path': GroupOfPaths,
430            },
431            'propertygroup': {
432                'property': GroupOfProperties,
433            },
434            'property': {
435                'element': Property,
436            },
437            'watch': {
438                'property': PropertyWatch,
439            },
440            'instance': {
441                'element': Instance,
442            },
443        }
444
445        if cls not in class_map:
446            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
447        if sub not in class_map[cls]:
448            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
449                cls, sub))
450
451        return class_map[cls][sub]
452
453    @staticmethod
454    def load_one_yaml(path, fd, objs):
455        '''Parse a single YAML file.  Parsing occurs in three phases.
456        In the first phase a factory method associated with each
457        configuration file directive is invoked.  These factory
458        methods generate more factory methods.  In the second
459        phase the factory methods created in the first phase
460        are invoked.  In the last phase a callback is invoked on
461        each object created in phase two.  Typically the callback
462        resolves references to other configuration file directives.'''
463
464        factory_objs = {}
465        for x in yaml.safe_load(fd.read()) or {}:
466
467            # Create factory object for this config file directive.
468            cls = x['class']
469            sub = x.get(cls)
470            if cls == 'group':
471                cls = '{0}group'.format(sub)
472
473            factory = Everything.classmap(cls, sub)
474            obj = factory(configfile=path, **x)
475
476            # For a given class of directive, validate the file
477            # doesn't have any duplicate names (duplicates are
478            # ok across config files).
479            if exists(factory_objs, obj.cls, obj.name, config=path):
480                raise NotUniqueError(path, cls, obj.name)
481
482            factory_objs.setdefault(cls, []).append(obj)
483            objs.setdefault(cls, []).append(obj)
484
485        for cls, items in factory_objs.items():
486            for obj in items:
487                # Add objects for template consumption.
488                obj.factory(objs)
489
490    @staticmethod
491    def load(args):
492        '''Aggregate all the YAML in the input directory
493        into a single aggregate.'''
494
495        objs = {}
496        yaml_files = filter(
497            lambda x: x.endswith('.yaml'),
498            os.listdir(args.inputdir))
499
500        yaml_files.sort()
501
502        for x in yaml_files:
503            path = os.path.join(args.inputdir, x)
504            with open(path, 'r') as fd:
505                Everything.load_one_yaml(path, fd, objs)
506
507        # Configuration file directives reference each other via
508        # the name attribute; however, when rendered the reference
509        # is just an array index.
510        #
511        # At this point all objects have been created but references
512        # have not been resolved to array indicies.  Instruct objects
513        # to do that now.
514        for cls, items in objs.items():
515            for obj in items:
516                obj.setup(objs)
517
518        return Everything(**objs)
519
520    def __init__(self, *a, **kw):
521        self.pathmeta = kw.pop('path', [])
522        self.paths = kw.pop('pathname', [])
523        self.meta = kw.pop('meta', [])
524        self.pathgroups = kw.pop('pathgroup', [])
525        self.interfaces = kw.pop('interface', [])
526        self.properties = kw.pop('property', [])
527        self.propertynames = kw.pop('propertyname', [])
528        self.propertygroups = kw.pop('propertygroup', [])
529        self.instances = kw.pop('instance', [])
530        self.instancegroups = kw.pop('instancegroup', [])
531        self.watches = kw.pop('watch', [])
532
533        super(Everything, self).__init__(**kw)
534
535    def generate_cpp(self, loader):
536        '''Render the template with the provided data.'''
537        with open(args.output, 'w') as fd:
538            fd.write(
539                self.render(
540                    loader,
541                    args.template,
542                    meta=self.meta,
543                    properties=self.properties,
544                    propertynames=self.propertynames,
545                    interfaces=self.interfaces,
546                    paths=self.paths,
547                    pathmeta=self.pathmeta,
548                    pathgroups=self.pathgroups,
549                    propertygroups=self.propertygroups,
550                    instances=self.instances,
551                    watches=self.watches,
552                    instancegroups=self.instancegroups,
553                    indent=Indent()))
554
555if __name__ == '__main__':
556    script_dir = os.path.dirname(os.path.realpath(__file__))
557    valid_commands = {
558        'generate-cpp': 'generate_cpp',
559    }
560
561    parser = ArgumentParser(
562        description='Phosphor DBus Monitor (PDM) YAML '
563        'scanner and code generator.')
564
565    parser.add_argument(
566        "-o", "--out", dest="output",
567        default='generated.cpp',
568        help="Generated output file name and path.")
569    parser.add_argument(
570        '-t', '--template', dest='template',
571        default='generated.mako.hpp',
572        help='The top level template to render.')
573    parser.add_argument(
574        '-p', '--template-path', dest='template_search',
575        default=script_dir,
576        help='The space delimited mako template search path.')
577    parser.add_argument(
578        '-d', '--dir', dest='inputdir',
579        default=os.path.join(script_dir, 'example'),
580        help='Location of files to process.')
581    parser.add_argument(
582        'command', metavar='COMMAND', type=str,
583        choices=valid_commands.keys(),
584        help='%s.' % " | ".join(valid_commands.keys()))
585
586    args = parser.parse_args()
587
588    if sys.version_info < (3, 0):
589        lookup = mako.lookup.TemplateLookup(
590            directories=args.template_search.split(),
591            disable_unicode=True)
592    else:
593        lookup = mako.lookup.TemplateLookup(
594            directories=args.template_search.split())
595    try:
596        function = getattr(
597            Everything.load(args),
598            valid_commands[args.command])
599        function(lookup)
600    except InvalidConfigError as e:
601        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
602        raise
603