xref: /openbmc/phosphor-dbus-monitor/src/pdmgen.py (revision b604480a43a25d0645b944fb16c1268d36782d36)
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
25
26
27class InvalidConfigError(BaseException):
28    '''General purpose config file parsing error.'''
29
30    def __init__(self, path, msg):
31        '''Display configuration file with the syntax
32        error and the error message.'''
33
34        self.config = path
35        self.msg = msg
36
37
38class NotUniqueError(InvalidConfigError):
39    '''Within a config file names must be unique.
40    Display the config file with the duplicate and
41    the duplicate itself.'''
42
43    def __init__(self, path, cls, *names):
44        fmt = 'Duplicate {0}: "{1}"'
45        super(NotUniqueError, self).__init__(
46            path, fmt.format(cls, ' '.join(names)))
47
48
49def get_index(objs, cls, name, config=None):
50    '''Items are usually rendered as C++ arrays and as
51    such are stored in python lists.  Given an item name
52    its class, and an optional config file filter, find
53    the item index.'''
54
55    for i, x in enumerate(objs.get(cls, [])):
56        if config and x.configfile != config:
57            continue
58        if x.name != name:
59            continue
60
61        return i
62    raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
63
64
65def exists(objs, cls, name, config=None):
66    '''Check to see if an item already exists in a list given
67    the item name.'''
68
69    try:
70        get_index(objs, cls, name, config)
71    except:
72        return False
73
74    return True
75
76
77def add_unique(obj, *a, **kw):
78    '''Add an item to one or more lists unless already present,
79    with an option to constrain the search to a specific config file.'''
80
81    for container in a:
82        if not exists(container, obj.cls, obj.name, config=kw.get('config')):
83            container.setdefault(obj.cls, []).append(obj)
84
85
86class Indent(object):
87    '''Help templates be depth agnostic.'''
88
89    def __init__(self, depth=0):
90        self.depth = depth
91
92    def __add__(self, depth):
93        return Indent(self.depth + depth)
94
95    def __call__(self, depth):
96        '''Render an indent at the current depth plus depth.'''
97        return 4*' '*(depth + self.depth)
98
99
100class ConfigEntry(NamedElement):
101    '''Base interface for rendered items.'''
102
103    def __init__(self, *a, **kw):
104        '''Pop the configfile/class/subclass keywords.'''
105
106        self.configfile = kw.pop('configfile')
107        self.cls = kw.pop('class')
108        self.subclass = kw.pop(self.cls)
109        super(ConfigEntry, self).__init__(**kw)
110
111    def factory(self, objs):
112        ''' Optional factory interface for subclasses to add
113        additional items to be rendered.'''
114
115        pass
116
117    def setup(self, objs):
118        ''' Optional setup interface for subclasses, invoked
119        after all factory methods have been run.'''
120
121        pass
122
123
124class Path(ConfigEntry):
125    '''Path/metadata association.'''
126
127    def __init__(self, *a, **kw):
128        super(Path, self).__init__(**kw)
129
130    def factory(self, objs):
131        '''Create path and metadata elements.'''
132
133        args = {
134            'class': 'pathname',
135            'pathname': 'element',
136            'name': self.name['path']
137        }
138        add_unique(ConfigEntry(
139            configfile=self.configfile, **args), objs)
140
141        args = {
142            'class': 'meta',
143            'meta': 'element',
144            'name': self.name['meta']
145        }
146        add_unique(ConfigEntry(
147            configfile=self.configfile, **args), objs)
148
149        super(Path, self).factory(objs)
150
151    def setup(self, objs):
152        '''Resolve path and metadata names to indicies.'''
153
154        self.path = get_index(
155            objs, 'pathname', self.name['path'])
156        self.meta = get_index(
157            objs, 'meta', self.name['meta'])
158
159        super(Path, self).setup(objs)
160
161
162class Group(ConfigEntry):
163    '''Pop the members keyword for groups.'''
164
165    def __init__(self, *a, **kw):
166        self.members = kw.pop('members')
167        super(Group, self).__init__(**kw)
168
169
170class ImplicitGroup(Group):
171    '''Provide a factory method for groups whose members are
172    not explicitly declared in the config files.'''
173
174    def __init__(self, *a, **kw):
175        super(ImplicitGroup, self).__init__(**kw)
176
177    def factory(self, objs):
178        '''Create group members.'''
179
180        factory = Everything.classmap(self.subclass, 'element')
181        for m in self.members:
182            args = {
183                'class': self.subclass,
184                self.subclass: 'element',
185                'name': m
186            }
187
188            obj = factory(configfile=self.configfile, **args)
189            add_unique(obj, objs)
190            obj.factory(objs)
191
192        super(ImplicitGroup, self).factory(objs)
193
194
195class GroupOfPaths(ImplicitGroup):
196    '''Path group config file directive.'''
197
198    def __init__(self, *a, **kw):
199        super(GroupOfPaths, self).__init__(**kw)
200
201    def setup(self, objs):
202        '''Resolve group members.'''
203
204        def map_member(x):
205            path = get_index(
206                objs, 'pathname', x['path'])
207            meta = get_index(
208                objs, 'meta', x['meta'])
209            return (path, meta)
210
211        self.members = map(
212            map_member,
213            self.members)
214
215        super(GroupOfPaths, self).setup(objs)
216
217
218class Everything(Renderer):
219    '''Parse/render entry point.'''
220
221    @staticmethod
222    def classmap(cls, sub=None):
223        '''Map render item class and subclass entries to the appropriate
224        handler methods.'''
225
226        class_map = {
227            'path': {
228                'element': Path,
229            },
230            'pathgroup': {
231                'path': GroupOfPaths,
232            },
233        }
234
235        if cls not in class_map:
236            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
237        if sub not in class_map[cls]:
238            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
239                cls, sub))
240
241        return class_map[cls][sub]
242
243    @staticmethod
244    def load_one_yaml(path, fd, objs):
245        '''Parse a single YAML file.  Parsing occurs in three phases.
246        In the first phase a factory method associated with each
247        configuration file directive is invoked.  These factory
248        methods generate more factory methods.  In the second
249        phase the factory methods created in the first phase
250        are invoked.  In the last phase a callback is invoked on
251        each object created in phase two.  Typically the callback
252        resolves references to other configuration file directives.'''
253
254        factory_objs = {}
255        for x in yaml.safe_load(fd.read()) or {}:
256
257            # Create factory object for this config file directive.
258            cls = x['class']
259            sub = x.get(cls)
260            if cls == 'group':
261                cls = '{0}group'.format(sub)
262
263            factory = Everything.classmap(cls, sub)
264            obj = factory(configfile=path, **x)
265
266            # For a given class of directive, validate the file
267            # doesn't have any duplicate names (duplicates are
268            # ok across config files).
269            if exists(factory_objs, obj.cls, obj.name, config=path):
270                raise NotUniqueError(path, cls, obj.name)
271
272            factory_objs.setdefault(cls, []).append(obj)
273            objs.setdefault(cls, []).append(obj)
274
275        for cls, items in factory_objs.items():
276            for obj in items:
277                # Add objects for template consumption.
278                obj.factory(objs)
279
280    @staticmethod
281    def load(args):
282        '''Aggregate all the YAML in the input directory
283        into a single aggregate.'''
284
285        objs = {}
286        yaml_files = filter(
287            lambda x: x.endswith('.yaml'),
288            os.listdir(args.inputdir))
289
290        yaml_files.sort()
291
292        for x in yaml_files:
293            path = os.path.join(args.inputdir, x)
294            with open(path, 'r') as fd:
295                Everything.load_one_yaml(path, fd, objs)
296
297        # Configuration file directives reference each other via
298        # the name attribute; however, when rendered the reference
299        # is just an array index.
300        #
301        # At this point all objects have been created but references
302        # have not been resolved to array indicies.  Instruct objects
303        # to do that now.
304        for cls, items in objs.items():
305            for obj in items:
306                obj.setup(objs)
307
308        return Everything(**objs)
309
310    def __init__(self, *a, **kw):
311        self.pathmeta = kw.pop('path', [])
312        self.paths = kw.pop('pathname', [])
313        self.meta = kw.pop('meta', [])
314        self.pathgroups = kw.pop('pathgroup', [])
315
316        super(Everything, self).__init__(**kw)
317
318    def generate_cpp(self, loader):
319        '''Render the template with the provided data.'''
320        with open(args.output, 'w') as fd:
321            fd.write(
322                self.render(
323                    loader,
324                    args.template,
325                    meta=self.meta,
326                    paths=self.paths,
327                    pathmeta=self.pathmeta,
328                    pathgroups=self.pathgroups,
329                    indent=Indent()))
330
331if __name__ == '__main__':
332    script_dir = os.path.dirname(os.path.realpath(__file__))
333    valid_commands = {
334        'generate-cpp': 'generate_cpp',
335    }
336
337    parser = ArgumentParser(
338        description='Phosphor DBus Monitor (PDM) YAML '
339        'scanner and code generator.')
340
341    parser.add_argument(
342        "-o", "--out", dest="output",
343        default='generated.cpp',
344        help="Generated output file name and path.")
345    parser.add_argument(
346        '-t', '--template', dest='template',
347        default='generated.mako.hpp',
348        help='The top level template to render.')
349    parser.add_argument(
350        '-p', '--template-path', dest='template_search',
351        default=script_dir,
352        help='The space delimited mako template search path.')
353    parser.add_argument(
354        '-d', '--dir', dest='inputdir',
355        default=os.path.join(script_dir, 'example'),
356        help='Location of files to process.')
357    parser.add_argument(
358        'command', metavar='COMMAND', type=str,
359        choices=valid_commands.keys(),
360        help='%s.' % " | ".join(valid_commands.keys()))
361
362    args = parser.parse_args()
363
364    if sys.version_info < (3, 0):
365        lookup = mako.lookup.TemplateLookup(
366            directories=args.template_search.split(),
367            disable_unicode=True)
368    else:
369        lookup = mako.lookup.TemplateLookup(
370            directories=args.template_search.split())
371    try:
372        function = getattr(
373            Everything.load(args),
374            valid_commands[args.command])
375        function(lookup)
376    except InvalidConfigError as e:
377        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
378        raise
379