1#!/usr/bin/env python3
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 list(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.items()}
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').items():
288            key_o = TrivialArgument(
289                value=path,
290                type='string',
291                decorators=[Literal('string')])
292            value_i = []
293
294            for interface, properties, in interfaces.items():
295                key_i = TrivialArgument(value=interface, type='string')
296                value_p = []
297                if properties:
298                    for prop, value in properties.items():
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 = [x for x in os.listdir(events_dir) if
532                    x.endswith('.yaml')]
533
534            for x in yaml_files:
535                with open(os.path.join(events_dir, x), 'r') as fd:
536                    for e in yaml.safe_load(fd.read()).get('events', {}):
537                        events.append(e)
538
539        interfaces, interface_composite = Everything.get_interfaces(
540            args.ifacesdir)
541        extra_interfaces, extra_interface_composite = \
542            Everything.get_interfaces(
543                os.path.join(args.inputdir, 'extra_interfaces.d'))
544        interface_composite.update(extra_interface_composite)
545        interface_composite = InterfaceComposite(interface_composite)
546        # Update busname if configured differenly than the default
547        busname = args.busname
548
549        return Everything(
550            *events,
551            interfaces=interfaces + extra_interfaces,
552            interface_composite=interface_composite)
553
554    @staticmethod
555    def get_interfaces(targetdir):
556        '''Scan the interfaces directory for interfaces that PIM can create.'''
557
558        yaml_files = []
559        interfaces = []
560        interface_composite = {}
561
562        if targetdir and os.path.exists(targetdir):
563            for directory, _, files in os.walk(targetdir):
564                if not files:
565                    continue
566
567                yaml_files += [os.path.relpath(
568                        os.path.join(directory, f),
569                        targetdir) for f in [f for f in files if
570                                f.endswith('.interface.yaml')]]
571
572        for y in yaml_files:
573            # parse only phosphor dbus related interface files
574            if not y.startswith('xyz'):
575                continue
576            with open(os.path.join(targetdir, y)) as fd:
577                i = y.replace('.interface.yaml', '').replace(os.sep, '.')
578
579                # PIM can't create interfaces with methods.
580                parsed = yaml.safe_load(fd.read())
581                if parsed.get('methods', None):
582                    continue
583                # Cereal can't understand the type sdbusplus::object_path. This
584                # type is a wrapper around std::string. Ignore interfaces having
585                # a property of this type for now. The only interface that has a
586                # property of this type now is xyz.openbmc_project.Association,
587                # which is an unused interface. No inventory objects implement
588                # this interface.
589                # TODO via openbmc/openbmc#2123 : figure out how to make Cereal
590                # understand sdbusplus::object_path.
591                properties = parsed.get('properties', None)
592                if properties:
593                    if any('path' in p['type'] for p in properties):
594                        continue
595                interface_composite[i] = properties
596                interfaces.append(i)
597
598        return interfaces, interface_composite
599
600    def __init__(self, *a, **kw):
601        self.interfaces = \
602            [Interface(x) for x in kw.pop('interfaces', [])]
603        self.interface_composite = \
604            kw.pop('interface_composite', {})
605        self.events = [
606            self.class_map[x['type']](**x) for x in a]
607        super(Everything, self).__init__(**kw)
608
609    def generate_cpp(self, loader):
610        '''Render the template with the provided events and interfaces.'''
611        with open(os.path.join(
612                args.outputdir,
613                'generated.cpp'), 'w') as fd:
614            fd.write(
615                self.render(
616                    loader,
617                    'generated.mako.cpp',
618                    events=self.events,
619                    interfaces=self.interfaces,
620                    indent=Indent()))
621
622    def generate_serialization(self, loader):
623        with open(os.path.join(
624                args.outputdir,
625                'gen_serialization.hpp'), 'w') as fd:
626            fd.write(
627                self.render(
628                    loader,
629                    'gen_serialization.mako.hpp',
630                    interfaces=self.interfaces,
631                    interface_composite=self.interface_composite))
632
633
634if __name__ == '__main__':
635    script_dir = os.path.dirname(os.path.realpath(__file__))
636    valid_commands = {
637        'generate-cpp': 'generate_cpp',
638        'generate-serialization': 'generate_serialization',
639    }
640
641    parser = argparse.ArgumentParser(
642        description='Phosphor Inventory Manager (PIM) YAML '
643        'scanner and code generator.')
644    parser.add_argument(
645        '-o', '--output-dir', dest='outputdir',
646        default='.', help='Output directory.')
647    parser.add_argument(
648        '-i', '--interfaces-dir', dest='ifacesdir',
649        help='Location of interfaces to be supported.')
650    parser.add_argument(
651        '-d', '--dir', dest='inputdir',
652        default=os.path.join(script_dir, 'example'),
653        help='Location of files to process.')
654    parser.add_argument(
655        '-b', '--bus-name', dest='busname',
656        default='xyz.openbmc_project.Inventory.Manager',
657        help='Inventory manager busname.')
658    parser.add_argument(
659        'command', metavar='COMMAND', type=str,
660        choices=list(valid_commands.keys()),
661        help='%s.' % " | ".join(list(valid_commands.keys())))
662
663    args = parser.parse_args()
664
665    if sys.version_info < (3, 0):
666        lookup = mako.lookup.TemplateLookup(
667            directories=[script_dir],
668            disable_unicode=True)
669    else:
670        lookup = mako.lookup.TemplateLookup(
671            directories=[script_dir])
672
673    function = getattr(
674        Everything.load(args),
675        valid_commands[args.command])
676    function(lookup)
677
678
679# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
680