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