xref: /openbmc/phosphor-dbus-monitor/src/pdmgen.py (revision c9e173f84effdfbdda9f0d5e8650644572a2d95e)
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 Group(ConfigEntry):
212    '''Pop the members keyword for groups.'''
213
214    def __init__(self, *a, **kw):
215        self.members = kw.pop('members')
216        super(Group, self).__init__(**kw)
217
218
219class ImplicitGroup(Group):
220    '''Provide a factory method for groups whose members are
221    not explicitly declared in the config files.'''
222
223    def __init__(self, *a, **kw):
224        super(ImplicitGroup, self).__init__(**kw)
225
226    def factory(self, objs):
227        '''Create group members.'''
228
229        factory = Everything.classmap(self.subclass, 'element')
230        for m in self.members:
231            args = {
232                'class': self.subclass,
233                self.subclass: 'element',
234                'name': m
235            }
236
237            obj = factory(configfile=self.configfile, **args)
238            add_unique(obj, objs)
239            obj.factory(objs)
240
241        super(ImplicitGroup, self).factory(objs)
242
243
244class GroupOfPaths(ImplicitGroup):
245    '''Path group config file directive.'''
246
247    def __init__(self, *a, **kw):
248        super(GroupOfPaths, self).__init__(**kw)
249
250    def setup(self, objs):
251        '''Resolve group members.'''
252
253        def map_member(x):
254            path = get_index(
255                objs, 'pathname', x['path'])
256            meta = get_index(
257                objs, 'meta', x['meta'])
258            return (path, meta)
259
260        self.members = map(
261            map_member,
262            self.members)
263
264        super(GroupOfPaths, self).setup(objs)
265
266
267class GroupOfProperties(ImplicitGroup):
268    '''Property group config file directive.'''
269
270    def __init__(self, *a, **kw):
271        self.datatype = sdbusplus.property.Property(
272            name=kw.get('name'),
273            type=kw.pop('type')).cppTypeName
274
275        super(GroupOfProperties, self).__init__(**kw)
276
277    def setup(self, objs):
278        '''Resolve group members.'''
279
280        def map_member(x):
281            iface = get_index(
282                objs, 'interface', x['interface'])
283            prop = get_index(
284                objs, 'propertyname', x['property'])
285            meta = get_index(
286                objs, 'meta', x['meta'])
287
288            return (iface, prop, meta)
289
290        self.members = map(
291            map_member,
292            self.members)
293
294        super(GroupOfProperties, self).setup(objs)
295
296
297class Everything(Renderer):
298    '''Parse/render entry point.'''
299
300    @staticmethod
301    def classmap(cls, sub=None):
302        '''Map render item class and subclass entries to the appropriate
303        handler methods.'''
304
305        class_map = {
306            'path': {
307                'element': Path,
308            },
309            'pathgroup': {
310                'path': GroupOfPaths,
311            },
312            'propertygroup': {
313                'property': GroupOfProperties,
314            },
315            'property': {
316                'element': Property,
317            },
318        }
319
320        if cls not in class_map:
321            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
322        if sub not in class_map[cls]:
323            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
324                cls, sub))
325
326        return class_map[cls][sub]
327
328    @staticmethod
329    def load_one_yaml(path, fd, objs):
330        '''Parse a single YAML file.  Parsing occurs in three phases.
331        In the first phase a factory method associated with each
332        configuration file directive is invoked.  These factory
333        methods generate more factory methods.  In the second
334        phase the factory methods created in the first phase
335        are invoked.  In the last phase a callback is invoked on
336        each object created in phase two.  Typically the callback
337        resolves references to other configuration file directives.'''
338
339        factory_objs = {}
340        for x in yaml.safe_load(fd.read()) or {}:
341
342            # Create factory object for this config file directive.
343            cls = x['class']
344            sub = x.get(cls)
345            if cls == 'group':
346                cls = '{0}group'.format(sub)
347
348            factory = Everything.classmap(cls, sub)
349            obj = factory(configfile=path, **x)
350
351            # For a given class of directive, validate the file
352            # doesn't have any duplicate names (duplicates are
353            # ok across config files).
354            if exists(factory_objs, obj.cls, obj.name, config=path):
355                raise NotUniqueError(path, cls, obj.name)
356
357            factory_objs.setdefault(cls, []).append(obj)
358            objs.setdefault(cls, []).append(obj)
359
360        for cls, items in factory_objs.items():
361            for obj in items:
362                # Add objects for template consumption.
363                obj.factory(objs)
364
365    @staticmethod
366    def load(args):
367        '''Aggregate all the YAML in the input directory
368        into a single aggregate.'''
369
370        objs = {}
371        yaml_files = filter(
372            lambda x: x.endswith('.yaml'),
373            os.listdir(args.inputdir))
374
375        yaml_files.sort()
376
377        for x in yaml_files:
378            path = os.path.join(args.inputdir, x)
379            with open(path, 'r') as fd:
380                Everything.load_one_yaml(path, fd, objs)
381
382        # Configuration file directives reference each other via
383        # the name attribute; however, when rendered the reference
384        # is just an array index.
385        #
386        # At this point all objects have been created but references
387        # have not been resolved to array indicies.  Instruct objects
388        # to do that now.
389        for cls, items in objs.items():
390            for obj in items:
391                obj.setup(objs)
392
393        return Everything(**objs)
394
395    def __init__(self, *a, **kw):
396        self.pathmeta = kw.pop('path', [])
397        self.paths = kw.pop('pathname', [])
398        self.meta = kw.pop('meta', [])
399        self.pathgroups = kw.pop('pathgroup', [])
400        self.interfaces = kw.pop('interface', [])
401        self.properties = kw.pop('property', [])
402        self.propertynames = kw.pop('propertyname', [])
403        self.propertygroups = kw.pop('propertygroup', [])
404
405        super(Everything, self).__init__(**kw)
406
407    def generate_cpp(self, loader):
408        '''Render the template with the provided data.'''
409        with open(args.output, 'w') as fd:
410            fd.write(
411                self.render(
412                    loader,
413                    args.template,
414                    meta=self.meta,
415                    properties=self.properties,
416                    propertynames=self.propertynames,
417                    interfaces=self.interfaces,
418                    paths=self.paths,
419                    pathmeta=self.pathmeta,
420                    pathgroups=self.pathgroups,
421                    propertygroups=self.propertygroups,
422                    indent=Indent()))
423
424if __name__ == '__main__':
425    script_dir = os.path.dirname(os.path.realpath(__file__))
426    valid_commands = {
427        'generate-cpp': 'generate_cpp',
428    }
429
430    parser = ArgumentParser(
431        description='Phosphor DBus Monitor (PDM) YAML '
432        'scanner and code generator.')
433
434    parser.add_argument(
435        "-o", "--out", dest="output",
436        default='generated.cpp',
437        help="Generated output file name and path.")
438    parser.add_argument(
439        '-t', '--template', dest='template',
440        default='generated.mako.hpp',
441        help='The top level template to render.')
442    parser.add_argument(
443        '-p', '--template-path', dest='template_search',
444        default=script_dir,
445        help='The space delimited mako template search path.')
446    parser.add_argument(
447        '-d', '--dir', dest='inputdir',
448        default=os.path.join(script_dir, 'example'),
449        help='Location of files to process.')
450    parser.add_argument(
451        'command', metavar='COMMAND', type=str,
452        choices=valid_commands.keys(),
453        help='%s.' % " | ".join(valid_commands.keys()))
454
455    args = parser.parse_args()
456
457    if sys.version_info < (3, 0):
458        lookup = mako.lookup.TemplateLookup(
459            directories=args.template_search.split(),
460            disable_unicode=True)
461    else:
462        lookup = mako.lookup.TemplateLookup(
463            directories=args.template_search.split())
464    try:
465        function = getattr(
466            Everything.load(args),
467            valid_commands[args.command])
468        function(lookup)
469    except InvalidConfigError as e:
470        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
471        raise
472