1#!/usr/bin/env python
2
3'''Phosphor Inventory Manager 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 sys
19import os
20import argparse
21import subprocess
22import yaml
23import mako.lookup
24import sdbusplus.property
25from sdbusplus.namedelement import NamedElement
26from sdbusplus.renderer import Renderer
27
28
29# Global busname for use within classes where necessary
30busname = "xyz.openbmc_project.Inventory.Manager"
31
32
33def cppTypeName(yaml_type):
34    ''' Convert yaml types to cpp types.'''
35    return sdbusplus.property.Property(type=yaml_type).cppTypeName
36
37
38class InterfaceComposite(object):
39    '''Compose interface properties.'''
40
41    def __init__(self, dict):
42        self.dict = dict
43
44    def properties(self, interface):
45        return self.dict[interface]
46
47    def interfaces(self):
48        return self.dict.keys()
49
50    def names(self, interface):
51        names = []
52        if self.dict[interface]:
53            names = [x["name"] for x in self.dict[interface]]
54        return names
55
56
57class Interface(list):
58    '''Provide various interface transformations.'''
59
60    def __init__(self, iface):
61        super(Interface, self).__init__(iface.split('.'))
62
63    def namespace(self):
64        '''Represent as an sdbusplus namespace.'''
65        return '::'.join(['sdbusplus'] + self[:-1] + ['server', self[-1]])
66
67    def header(self):
68        '''Represent as an sdbusplus server binding header.'''
69        return os.sep.join(self + ['server.hpp'])
70
71    def __str__(self):
72        return '.'.join(self)
73
74
75class Indent(object):
76    '''Help templates be depth agnostic.'''
77
78    def __init__(self, depth=0):
79        self.depth = depth
80
81    def __add__(self, depth):
82        return Indent(self.depth + depth)
83
84    def __call__(self, depth):
85        '''Render an indent at the current depth plus depth.'''
86        return 4*' '*(depth + self.depth)
87
88
89class Template(NamedElement):
90    '''Associate a template name with its namespace.'''
91
92    def __init__(self, **kw):
93        self.namespace = kw.pop('namespace', [])
94        super(Template, self).__init__(**kw)
95
96    def qualified(self):
97        return '::'.join(self.namespace + [self.name])
98
99
100class FixBool(object):
101    '''Un-capitalize booleans.'''
102
103    def __call__(self, arg):
104        return '{0}'.format(arg.lower())
105
106
107class Quote(object):
108    '''Decorate an argument by quoting it.'''
109
110    def __call__(self, arg):
111        return '"{0}"'.format(arg)
112
113
114class Cast(object):
115    '''Decorate an argument by casting it.'''
116
117    def __init__(self, cast, target):
118        '''cast is the cast type (static, const, etc...).
119           target is the cast target type.'''
120        self.cast = cast
121        self.target = target
122
123    def __call__(self, arg):
124        return '{0}_cast<{1}>({2})'.format(self.cast, self.target, arg)
125
126
127class Literal(object):
128    '''Decorate an argument with a literal operator.'''
129
130    integer_types = [
131        'int8',
132        'int16',
133        'int32',
134        'int64',
135        'uint8',
136        'uint16',
137        'uint32',
138        'uint64'
139    ]
140
141    def __init__(self, type):
142        self.type = type
143
144    def __call__(self, arg):
145        if 'uint' in self.type:
146            arg = '{0}ull'.format(arg)
147        elif 'int' in self.type:
148            arg = '{0}ll'.format(arg)
149
150        if self.type in self.integer_types:
151            return Cast('static', '{0}_t'.format(self.type))(arg)
152
153        if self.type == 'string':
154            return '{0}s'.format(arg)
155
156        return arg
157
158
159class Argument(NamedElement, Renderer):
160    '''Define argument type inteface.'''
161
162    def __init__(self, **kw):
163        self.type = kw.pop('type', None)
164        super(Argument, self).__init__(**kw)
165
166    def argument(self, loader, indent):
167        raise NotImplementedError
168
169
170class TrivialArgument(Argument):
171    '''Non-array type arguments.'''
172
173    def __init__(self, **kw):
174        self.value = kw.pop('value')
175        self.decorators = kw.pop('decorators', [])
176        if kw.get('type', None) == 'string':
177            self.decorators.insert(0, Quote())
178        if kw.get('type', None) == 'boolean':
179            self.decorators.insert(0, FixBool())
180
181        super(TrivialArgument, self).__init__(**kw)
182
183    def argument(self, loader, indent):
184        a = str(self.value)
185        for d in self.decorators:
186            a = d(a)
187
188        return a
189
190
191class InitializerList(Argument):
192    '''Initializer list arguments.'''
193
194    def __init__(self, **kw):
195        self.values = kw.pop('values')
196        super(InitializerList, self).__init__(**kw)
197
198    def argument(self, loader, indent):
199        return self.render(
200            loader,
201            'argument.mako.cpp',
202            arg=self,
203            indent=indent)
204
205
206class DbusSignature(Argument):
207    '''DBus signature arguments.'''
208
209    def __init__(self, **kw):
210        self.sig = {x: y for x, y in kw.iteritems()}
211        kw.clear()
212        super(DbusSignature, self).__init__(**kw)
213
214    def argument(self, loader, indent):
215        return self.render(
216            loader,
217            'signature.mako.cpp',
218            signature=self,
219            indent=indent)
220
221
222class MethodCall(Argument):
223    '''Render syntatically correct c++ method calls.'''
224
225    def __init__(self, **kw):
226        self.namespace = kw.pop('namespace', [])
227        self.templates = kw.pop('templates', [])
228        self.args = kw.pop('args', [])
229        super(MethodCall, self).__init__(**kw)
230
231    def call(self, loader, indent):
232        return self.render(
233            loader,
234            'method.mako.cpp',
235            method=self,
236            indent=indent)
237
238    def argument(self, loader, indent):
239        return self.call(loader, indent)
240
241
242class Vector(MethodCall):
243    '''Convenience type for vectors.'''
244
245    def __init__(self, **kw):
246        kw['name'] = 'vector'
247        kw['namespace'] = ['std']
248        kw['args'] = [InitializerList(values=kw.pop('args'))]
249        super(Vector, self).__init__(**kw)
250
251
252class Filter(MethodCall):
253    '''Convenience type for filters'''
254
255    def __init__(self, **kw):
256        kw['name'] = 'make_filter'
257        super(Filter, self).__init__(**kw)
258
259
260class Action(MethodCall):
261    '''Convenience type for actions'''
262
263    def __init__(self, **kw):
264        kw['name'] = 'make_action'
265        super(Action, self).__init__(**kw)
266
267
268class PathCondition(MethodCall):
269    '''Convenience type for path conditions'''
270
271    def __init__(self, **kw):
272        kw['name'] = 'make_path_condition'
273        super(PathCondition, self).__init__(**kw)
274
275
276class GetProperty(MethodCall):
277    '''Convenience type for getting inventory properties'''
278
279    def __init__(self, **kw):
280        kw['name'] = 'make_get_property'
281        super(GetProperty, self).__init__(**kw)
282
283
284class CreateObjects(MethodCall):
285    '''Assemble a createObjects functor.'''
286
287    def __init__(self, **kw):
288        objs = []
289
290        for path, interfaces in kw.pop('objs').iteritems():
291            key_o = TrivialArgument(
292                value=path,
293                type='string',
294                decorators=[Literal('string')])
295            value_i = []
296
297            for interface, properties, in interfaces.iteritems():
298                key_i = TrivialArgument(value=interface, type='string')
299                value_p = []
300                if properties:
301                    for prop, value in properties.iteritems():
302                        key_p = TrivialArgument(value=prop, type='string')
303                        value_v = TrivialArgument(
304                            decorators=[Literal(value.get('type', None))],
305                            **value)
306                        value_p.append(InitializerList(values=[key_p, value_v]))
307
308                value_p = InitializerList(values=value_p)
309                value_i.append(InitializerList(values=[key_i, value_p]))
310
311            value_i = InitializerList(values=value_i)
312            objs.append(InitializerList(values=[key_o, value_i]))
313
314        kw['args'] = [InitializerList(values=objs)]
315        kw['namespace'] = ['functor']
316        super(CreateObjects, self).__init__(**kw)
317
318
319class DestroyObjects(MethodCall):
320    '''Assemble a destroyObject functor.'''
321
322    def __init__(self, **kw):
323        values = [{'value': x, 'type': 'string'} for x in kw.pop('paths')]
324        conditions = [
325            Event.functor_map[
326                x['name']](**x) for x in kw.pop('conditions', [])]
327        conditions = [PathCondition(args=[x]) for x in conditions]
328        args = [InitializerList(
329            values=[TrivialArgument(**x) for x in values])]
330        args.append(InitializerList(values=conditions))
331        kw['args'] = args
332        kw['namespace'] = ['functor']
333        super(DestroyObjects, self).__init__(**kw)
334
335
336class SetProperty(MethodCall):
337    '''Assemble a setProperty functor.'''
338
339    def __init__(self, **kw):
340        args = []
341
342        value = kw.pop('value')
343        prop = kw.pop('property')
344        iface = kw.pop('interface')
345        iface = Interface(iface)
346        namespace = iface.namespace().split('::')[:-1]
347        name = iface[-1]
348        t = Template(namespace=namespace, name=iface[-1])
349
350        member = '&%s' % '::'.join(
351            namespace + [name, NamedElement(name=prop).camelCase])
352        member_type = cppTypeName(value['type'])
353        member_cast = '{0} ({1}::*)({0})'.format(member_type, t.qualified())
354
355        paths = [{'value': x, 'type': 'string'} for x in kw.pop('paths')]
356        args.append(InitializerList(
357            values=[TrivialArgument(**x) for x in paths]))
358
359        conditions = [
360            Event.functor_map[
361                x['name']](**x) for x in kw.pop('conditions', [])]
362        conditions = [PathCondition(args=[x]) for x in conditions]
363
364        args.append(InitializerList(values=conditions))
365        args.append(TrivialArgument(value=str(iface), type='string'))
366        args.append(TrivialArgument(
367            value=member, decorators=[Cast('static', member_cast)]))
368        args.append(TrivialArgument(**value))
369
370        kw['templates'] = [Template(name=name, namespace=namespace)]
371        kw['args'] = args
372        kw['namespace'] = ['functor']
373        super(SetProperty, self).__init__(**kw)
374
375
376class PropertyChanged(MethodCall):
377    '''Assemble a propertyChanged functor.'''
378
379    def __init__(self, **kw):
380        args = []
381        args.append(TrivialArgument(value=kw.pop('interface'), type='string'))
382        args.append(TrivialArgument(value=kw.pop('property'), type='string'))
383        args.append(TrivialArgument(
384            decorators=[
385                Literal(kw['value'].get('type', None))], **kw.pop('value')))
386        kw['args'] = args
387        kw['namespace'] = ['functor']
388        super(PropertyChanged, self).__init__(**kw)
389
390
391class PropertyIs(MethodCall):
392    '''Assemble a propertyIs functor.'''
393
394    def __init__(self, **kw):
395        args = []
396        path = kw.pop('path', None)
397        if not path:
398            path = TrivialArgument(value='nullptr')
399        else:
400            path = TrivialArgument(value=path, type='string')
401
402        args.append(path)
403        iface = TrivialArgument(value=kw.pop('interface'), type='string')
404        args.append(iface)
405        prop = TrivialArgument(value=kw.pop('property'), type='string')
406        args.append(prop)
407        args.append(TrivialArgument(
408            decorators=[
409                Literal(kw['value'].get('type', None))], **kw.pop('value')))
410
411        service = kw.pop('service', None)
412        if service:
413            args.append(TrivialArgument(value=service, type='string'))
414
415        dbusMember = kw.pop('dbusMember', None)
416        if dbusMember:
417            # Inventory manager's service name is required
418            if not service or service != busname:
419                args.append(TrivialArgument(value=busname, type='string'))
420
421            gpArgs = []
422            gpArgs.append(path)
423            gpArgs.append(iface)
424            # Prepend '&' and append 'getPropertyByName' function on dbusMember
425            gpArgs.append(TrivialArgument(
426                value='&'+dbusMember+'::getPropertyByName'))
427            gpArgs.append(prop)
428            fArg = MethodCall(
429                name='getProperty',
430                namespace=['functor'],
431                templates=[Template(
432                    name=dbusMember,
433                    namespace=[])],
434                args=gpArgs)
435
436            # Append getProperty functor
437            args.append(GetProperty(
438                templates=[Template(
439                    name=dbusMember+'::PropertiesVariant',
440                    namespace=[])],
441                    args=[fArg]))
442
443        kw['args'] = args
444        kw['namespace'] = ['functor']
445        super(PropertyIs, self).__init__(**kw)
446
447
448class Event(MethodCall):
449    '''Assemble an inventory manager event.'''
450
451    functor_map = {
452        'destroyObjects': DestroyObjects,
453        'createObjects': CreateObjects,
454        'propertyChangedTo': PropertyChanged,
455        'propertyIs': PropertyIs,
456        'setProperty': SetProperty,
457    }
458
459    def __init__(self, **kw):
460        self.summary = kw.pop('name')
461
462        filters = [
463            self.functor_map[x['name']](**x) for x in kw.pop('filters', [])]
464        filters = [Filter(args=[x]) for x in filters]
465        filters = Vector(
466            templates=[Template(name='Filter', namespace=[])],
467            args=filters)
468
469        event = MethodCall(
470            name='make_shared',
471            namespace=['std'],
472            templates=[Template(
473                name=kw.pop('event'),
474                namespace=kw.pop('event_namespace', []))],
475            args=kw.pop('event_args', []) + [filters])
476
477        events = Vector(
478            templates=[Template(name='EventBasePtr', namespace=[])],
479            args=[event])
480
481        action_type = Template(name='Action', namespace=[])
482        action_args = [
483            self.functor_map[x['name']](**x) for x in kw.pop('actions', [])]
484        action_args = [Action(args=[x]) for x in action_args]
485        actions = Vector(
486            templates=[action_type],
487            args=action_args)
488
489        kw['name'] = 'make_tuple'
490        kw['namespace'] = ['std']
491        kw['args'] = [events, actions]
492        super(Event, self).__init__(**kw)
493
494
495class MatchEvent(Event):
496    '''Associate one or more dbus signal match signatures with
497    a filter.'''
498
499    def __init__(self, **kw):
500        kw['event'] = 'DbusSignal'
501        kw['event_namespace'] = []
502        kw['event_args'] = [
503            DbusSignature(**x) for x in kw.pop('signatures', [])]
504
505        super(MatchEvent, self).__init__(**kw)
506
507
508class StartupEvent(Event):
509    '''Assemble a startup event.'''
510
511    def __init__(self, **kw):
512        kw['event'] = 'StartupEvent'
513        kw['event_namespace'] = []
514        super(StartupEvent, self).__init__(**kw)
515
516
517class Everything(Renderer):
518    '''Parse/render entry point.'''
519
520    class_map = {
521        'match': MatchEvent,
522        'startup': StartupEvent,
523    }
524
525    @staticmethod
526    def load(args):
527        # Aggregate all the event YAML in the events.d directory
528        # into a single list of events.
529
530        events = []
531        events_dir = os.path.join(args.inputdir, 'events.d')
532
533        if os.path.exists(events_dir):
534            yaml_files = filter(
535                lambda x: x.endswith('.yaml'),
536                os.listdir(events_dir))
537
538            for x in yaml_files:
539                with open(os.path.join(events_dir, x), 'r') as fd:
540                    for e in yaml.safe_load(fd.read()).get('events', {}):
541                        events.append(e)
542
543        interfaces, interface_composite = Everything.get_interfaces(
544            args.ifacesdir)
545        extra_interfaces, extra_interface_composite = \
546            Everything.get_interfaces(
547                os.path.join(args.inputdir, 'extra_interfaces.d'))
548        interface_composite.update(extra_interface_composite)
549        interface_composite = InterfaceComposite(interface_composite)
550        # Update busname if configured differenly than the default
551        busname = args.busname
552
553        return Everything(
554            *events,
555            interfaces=interfaces + extra_interfaces,
556            interface_composite=interface_composite)
557
558    @staticmethod
559    def get_interfaces(targetdir):
560        '''Scan the interfaces directory for interfaces that PIM can create.'''
561
562        yaml_files = []
563        interfaces = []
564        interface_composite = {}
565
566        if targetdir and os.path.exists(targetdir):
567            for directory, _, files in os.walk(targetdir):
568                if not files:
569                    continue
570
571                yaml_files += map(
572                    lambda f: os.path.relpath(
573                        os.path.join(directory, f),
574                        targetdir),
575                    filter(lambda f: f.endswith('.interface.yaml'), files))
576
577        for y in yaml_files:
578            # parse only phosphor dbus related interface files
579            if not y.startswith('xyz'):
580                continue
581            with open(os.path.join(targetdir, y)) as fd:
582                i = y.replace('.interface.yaml', '').replace(os.sep, '.')
583
584                # PIM can't create interfaces with methods.
585                parsed = yaml.safe_load(fd.read())
586                if parsed.get('methods', None):
587                    continue
588                # Cereal can't understand the type sdbusplus::object_path. This
589                # type is a wrapper around std::string. Ignore interfaces having
590                # a property of this type for now. The only interface that has a
591                # property of this type now is xyz.openbmc_project.Association,
592                # which is an unused interface. No inventory objects implement
593                # this interface.
594                # TODO via openbmc/openbmc#2123 : figure out how to make Cereal
595                # understand sdbusplus::object_path.
596                properties = parsed.get('properties', None)
597                if properties:
598                    if any('path' in p['type'] for p in properties):
599                        continue
600                interface_composite[i] = properties
601                interfaces.append(i)
602
603        return interfaces, interface_composite
604
605    def __init__(self, *a, **kw):
606        self.interfaces = \
607            [Interface(x) for x in kw.pop('interfaces', [])]
608        self.interface_composite = \
609            kw.pop('interface_composite', {})
610        self.events = [
611            self.class_map[x['type']](**x) for x in a]
612        super(Everything, self).__init__(**kw)
613
614    def generate_cpp(self, loader):
615        '''Render the template with the provided events and interfaces.'''
616        with open(os.path.join(
617                args.outputdir,
618                'generated.cpp'), 'w') as fd:
619            fd.write(
620                self.render(
621                    loader,
622                    'generated.mako.cpp',
623                    events=self.events,
624                    interfaces=self.interfaces,
625                    indent=Indent()))
626
627    def generate_serialization(self, loader):
628        with open(os.path.join(
629                args.outputdir,
630                'gen_serialization.hpp'), 'w') as fd:
631            fd.write(
632                self.render(
633                    loader,
634                    'gen_serialization.mako.hpp',
635                    interfaces=self.interfaces,
636                    interface_composite=self.interface_composite))
637
638
639if __name__ == '__main__':
640    script_dir = os.path.dirname(os.path.realpath(__file__))
641    valid_commands = {
642        'generate-cpp': 'generate_cpp',
643        'generate-serialization': 'generate_serialization',
644    }
645
646    parser = argparse.ArgumentParser(
647        description='Phosphor Inventory Manager (PIM) YAML '
648        'scanner and code generator.')
649    parser.add_argument(
650        '-o', '--output-dir', dest='outputdir',
651        default='.', help='Output directory.')
652    parser.add_argument(
653        '-i', '--interfaces-dir', dest='ifacesdir',
654        help='Location of interfaces to be supported.')
655    parser.add_argument(
656        '-d', '--dir', dest='inputdir',
657        default=os.path.join(script_dir, 'example'),
658        help='Location of files to process.')
659    parser.add_argument(
660        '-b', '--bus-name', dest='busname',
661        default='xyz.openbmc_project.Inventory.Manager',
662        help='Inventory manager busname.')
663    parser.add_argument(
664        'command', metavar='COMMAND', type=str,
665        choices=valid_commands.keys(),
666        help='%s.' % " | ".join(valid_commands.keys()))
667
668    args = parser.parse_args()
669
670    if sys.version_info < (3, 0):
671        lookup = mako.lookup.TemplateLookup(
672            directories=[script_dir],
673            disable_unicode=True)
674    else:
675        lookup = mako.lookup.TemplateLookup(
676            directories=[script_dir])
677
678    function = getattr(
679        Everything.load(args),
680        valid_commands[args.command])
681    function(lookup)
682
683
684# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
685