#!/usr/bin/env python '''Phosphor Inventory Manager YAML parser and code generator. The parser workflow is broken down as follows: 1 - Import YAML files as native python type(s) instance(s). 2 - Create an instance of the Everything class from the native python type instance(s) with the Everything.load method. 3 - The Everything class constructor orchestrates conversion of the native python type(s) instances(s) to render helper types. Each render helper type constructor imports its attributes from the native python type(s) instances(s). 4 - Present the converted YAML to the command processing method requested by the script user. ''' import sys import os import argparse import subprocess import yaml import mako.lookup import sdbusplus.property from sdbusplus.namedelement import NamedElement from sdbusplus.renderer import Renderer # Global busname for use within classes where necessary busname = "xyz.openbmc_project.Inventory.Manager" def cppTypeName(yaml_type): ''' Convert yaml types to cpp types.''' return sdbusplus.property.Property(type=yaml_type).cppTypeName class InterfaceComposite(object): '''Compose interface properties.''' def __init__(self, dict): self.dict = dict def interfaces(self): return self.dict.keys() def names(self, interface): names = [] if self.dict[interface]: names = [NamedElement(name=x["name"]) for x in self.dict[interface]] return names class Interface(list): '''Provide various interface transformations.''' def __init__(self, iface): super(Interface, self).__init__(iface.split('.')) def namespace(self): '''Represent as an sdbusplus namespace.''' return '::'.join(['sdbusplus'] + self[:-1] + ['server', self[-1]]) def header(self): '''Represent as an sdbusplus server binding header.''' return os.sep.join(self + ['server.hpp']) def __str__(self): return '.'.join(self) class Indent(object): '''Help templates be depth agnostic.''' def __init__(self, depth=0): self.depth = depth def __add__(self, depth): return Indent(self.depth + depth) def __call__(self, depth): '''Render an indent at the current depth plus depth.''' return 4*' '*(depth + self.depth) class Template(NamedElement): '''Associate a template name with its namespace.''' def __init__(self, **kw): self.namespace = kw.pop('namespace', []) super(Template, self).__init__(**kw) def qualified(self): return '::'.join(self.namespace + [self.name]) class FixBool(object): '''Un-capitalize booleans.''' def __call__(self, arg): return '{0}'.format(arg.lower()) class Quote(object): '''Decorate an argument by quoting it.''' def __call__(self, arg): return '"{0}"'.format(arg) class Cast(object): '''Decorate an argument by casting it.''' def __init__(self, cast, target): '''cast is the cast type (static, const, etc...). target is the cast target type.''' self.cast = cast self.target = target def __call__(self, arg): return '{0}_cast<{1}>({2})'.format(self.cast, self.target, arg) class Literal(object): '''Decorate an argument with a literal operator.''' integer_types = [ 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64' ] def __init__(self, type): self.type = type def __call__(self, arg): if 'uint' in self.type: arg = '{0}ull'.format(arg) elif 'int' in self.type: arg = '{0}ll'.format(arg) if self.type in self.integer_types: return Cast('static', '{0}_t'.format(self.type))(arg) if self.type == 'string': return '{0}s'.format(arg) return arg class Argument(NamedElement, Renderer): '''Define argument type inteface.''' def __init__(self, **kw): self.type = kw.pop('type', None) super(Argument, self).__init__(**kw) def argument(self, loader, indent): raise NotImplementedError class TrivialArgument(Argument): '''Non-array type arguments.''' def __init__(self, **kw): self.value = kw.pop('value') self.decorators = kw.pop('decorators', []) if kw.get('type', None) == 'string': self.decorators.insert(0, Quote()) if kw.get('type', None) == 'boolean': self.decorators.insert(0, FixBool()) super(TrivialArgument, self).__init__(**kw) def argument(self, loader, indent): a = str(self.value) for d in self.decorators: a = d(a) return a class InitializerList(Argument): '''Initializer list arguments.''' def __init__(self, **kw): self.values = kw.pop('values') super(InitializerList, self).__init__(**kw) def argument(self, loader, indent): return self.render( loader, 'argument.mako.cpp', arg=self, indent=indent) class DbusSignature(Argument): '''DBus signature arguments.''' def __init__(self, **kw): self.sig = {x: y for x, y in kw.iteritems()} kw.clear() super(DbusSignature, self).__init__(**kw) def argument(self, loader, indent): return self.render( loader, 'signature.mako.cpp', signature=self, indent=indent) class MethodCall(Argument): '''Render syntatically correct c++ method calls.''' def __init__(self, **kw): self.namespace = kw.pop('namespace', []) self.templates = kw.pop('templates', []) self.args = kw.pop('args', []) super(MethodCall, self).__init__(**kw) def call(self, loader, indent): return self.render( loader, 'method.mako.cpp', method=self, indent=indent) def argument(self, loader, indent): return self.call(loader, indent) class Vector(MethodCall): '''Convenience type for vectors.''' def __init__(self, **kw): kw['name'] = 'vector' kw['namespace'] = ['std'] kw['args'] = [InitializerList(values=kw.pop('args'))] super(Vector, self).__init__(**kw) class Filter(MethodCall): '''Convenience type for filters''' def __init__(self, **kw): kw['name'] = 'make_filter' super(Filter, self).__init__(**kw) class Action(MethodCall): '''Convenience type for actions''' def __init__(self, **kw): kw['name'] = 'make_action' super(Action, self).__init__(**kw) class PathCondition(MethodCall): '''Convenience type for path conditions''' def __init__(self, **kw): kw['name'] = 'make_path_condition' super(PathCondition, self).__init__(**kw) class GetProperty(MethodCall): '''Convenience type for getting inventory properties''' def __init__(self, **kw): kw['name'] = 'make_get_property' super(GetProperty, self).__init__(**kw) class CreateObjects(MethodCall): '''Assemble a createObjects functor.''' def __init__(self, **kw): objs = [] for path, interfaces in kw.pop('objs').iteritems(): key_o = TrivialArgument( value=path, type='string', decorators=[Literal('string')]) value_i = [] for interface, properties, in interfaces.iteritems(): key_i = TrivialArgument(value=interface, type='string') value_p = [] if properties: for prop, value in properties.iteritems(): key_p = TrivialArgument(value=prop, type='string') value_v = TrivialArgument( decorators=[Literal(value.get('type', None))], **value) value_p.append(InitializerList(values=[key_p, value_v])) value_p = InitializerList(values=value_p) value_i.append(InitializerList(values=[key_i, value_p])) value_i = InitializerList(values=value_i) objs.append(InitializerList(values=[key_o, value_i])) kw['args'] = [InitializerList(values=objs)] kw['namespace'] = ['functor'] super(CreateObjects, self).__init__(**kw) class DestroyObjects(MethodCall): '''Assemble a destroyObject functor.''' def __init__(self, **kw): values = [{'value': x, 'type': 'string'} for x in kw.pop('paths')] conditions = [ Event.functor_map[ x['name']](**x) for x in kw.pop('conditions', [])] conditions = [PathCondition(args=[x]) for x in conditions] args = [InitializerList( values=[TrivialArgument(**x) for x in values])] args.append(InitializerList(values=conditions)) kw['args'] = args kw['namespace'] = ['functor'] super(DestroyObjects, self).__init__(**kw) class SetProperty(MethodCall): '''Assemble a setProperty functor.''' def __init__(self, **kw): args = [] value = kw.pop('value') prop = kw.pop('property') iface = kw.pop('interface') iface = Interface(iface) namespace = iface.namespace().split('::')[:-1] name = iface[-1] t = Template(namespace=namespace, name=iface[-1]) member = '&%s' % '::'.join( namespace + [name, NamedElement(name=prop).camelCase]) member_type = cppTypeName(value['type']) member_cast = '{0} ({1}::*)({0})'.format(member_type, t.qualified()) paths = [{'value': x, 'type': 'string'} for x in kw.pop('paths')] args.append(InitializerList( values=[TrivialArgument(**x) for x in paths])) conditions = [ Event.functor_map[ x['name']](**x) for x in kw.pop('conditions', [])] conditions = [PathCondition(args=[x]) for x in conditions] args.append(InitializerList(values=conditions)) args.append(TrivialArgument(value=str(iface), type='string')) args.append(TrivialArgument( value=member, decorators=[Cast('static', member_cast)])) args.append(TrivialArgument(**value)) kw['templates'] = [Template(name=name, namespace=namespace)] kw['args'] = args kw['namespace'] = ['functor'] super(SetProperty, self).__init__(**kw) class PropertyChanged(MethodCall): '''Assemble a propertyChanged functor.''' def __init__(self, **kw): args = [] args.append(TrivialArgument(value=kw.pop('interface'), type='string')) args.append(TrivialArgument(value=kw.pop('property'), type='string')) args.append(TrivialArgument( decorators=[ Literal(kw['value'].get('type', None))], **kw.pop('value'))) kw['args'] = args kw['namespace'] = ['functor'] super(PropertyChanged, self).__init__(**kw) class PropertyIs(MethodCall): '''Assemble a propertyIs functor.''' def __init__(self, **kw): args = [] path = kw.pop('path', None) if not path: path = TrivialArgument(value='nullptr') else: path = TrivialArgument(value=path, type='string') args.append(path) iface = TrivialArgument(value=kw.pop('interface'), type='string') args.append(iface) prop = TrivialArgument(value=kw.pop('property'), type='string') args.append(prop) args.append(TrivialArgument( decorators=[ Literal(kw['value'].get('type', None))], **kw.pop('value'))) service = kw.pop('service', None) if service: args.append(TrivialArgument(value=service, type='string')) dbusMember = kw.pop('dbusMember', None) if dbusMember: # Inventory manager's service name is required if not service or service != busname: args.append(TrivialArgument(value=busname, type='string')) gpArgs = [] gpArgs.append(path) gpArgs.append(iface) # Prepend '&' and append 'getPropertyByName' function on dbusMember gpArgs.append(TrivialArgument( value='&'+dbusMember+'::getPropertyByName')) gpArgs.append(prop) fArg = MethodCall( name='getProperty', namespace=['functor'], templates=[Template( name=dbusMember, namespace=[])], args=gpArgs) # Append getProperty functor args.append(GetProperty( templates=[Template( name=dbusMember+'::PropertiesVariant', namespace=[])], args=[fArg])) kw['args'] = args kw['namespace'] = ['functor'] super(PropertyIs, self).__init__(**kw) class Event(MethodCall): '''Assemble an inventory manager event.''' functor_map = { 'destroyObjects': DestroyObjects, 'createObjects': CreateObjects, 'propertyChangedTo': PropertyChanged, 'propertyIs': PropertyIs, 'setProperty': SetProperty, } def __init__(self, **kw): self.summary = kw.pop('name') filters = [ self.functor_map[x['name']](**x) for x in kw.pop('filters', [])] filters = [Filter(args=[x]) for x in filters] filters = Vector( templates=[Template(name='Filter', namespace=[])], args=filters) event = MethodCall( name='make_shared', namespace=['std'], templates=[Template( name=kw.pop('event'), namespace=kw.pop('event_namespace', []))], args=kw.pop('event_args', []) + [filters]) events = Vector( templates=[Template(name='EventBasePtr', namespace=[])], args=[event]) action_type = Template(name='Action', namespace=[]) action_args = [ self.functor_map[x['name']](**x) for x in kw.pop('actions', [])] action_args = [Action(args=[x]) for x in action_args] actions = Vector( templates=[action_type], args=action_args) kw['name'] = 'make_tuple' kw['namespace'] = ['std'] kw['args'] = [events, actions] super(Event, self).__init__(**kw) class MatchEvent(Event): '''Associate one or more dbus signal match signatures with a filter.''' def __init__(self, **kw): kw['event'] = 'DbusSignal' kw['event_namespace'] = [] kw['event_args'] = [ DbusSignature(**x) for x in kw.pop('signatures', [])] super(MatchEvent, self).__init__(**kw) class StartupEvent(Event): '''Assemble a startup event.''' def __init__(self, **kw): kw['event'] = 'StartupEvent' kw['event_namespace'] = [] super(StartupEvent, self).__init__(**kw) class Everything(Renderer): '''Parse/render entry point.''' class_map = { 'match': MatchEvent, 'startup': StartupEvent, } @staticmethod def load(args): # Aggregate all the event YAML in the events.d directory # into a single list of events. events = [] events_dir = os.path.join(args.inputdir, 'events.d') if os.path.exists(events_dir): yaml_files = filter( lambda x: x.endswith('.yaml'), os.listdir(events_dir)) for x in yaml_files: with open(os.path.join(events_dir, x), 'r') as fd: for e in yaml.safe_load(fd.read()).get('events', {}): events.append(e) interfaces, interface_composite = Everything.get_interfaces( args.ifacesdir) extra_interfaces, extra_interface_composite = \ Everything.get_interfaces( os.path.join(args.inputdir, 'extra_interfaces.d')) interface_composite.update(extra_interface_composite) interface_composite = InterfaceComposite(interface_composite) # Update busname if configured differenly than the default busname = args.busname return Everything( *events, interfaces=interfaces + extra_interfaces, interface_composite=interface_composite) @staticmethod def get_interfaces(targetdir): '''Scan the interfaces directory for interfaces that PIM can create.''' yaml_files = [] interfaces = [] interface_composite = {} if targetdir and os.path.exists(targetdir): for directory, _, files in os.walk(targetdir): if not files: continue yaml_files += map( lambda f: os.path.relpath( os.path.join(directory, f), targetdir), filter(lambda f: f.endswith('.interface.yaml'), files)) for y in yaml_files: # parse only phosphor dbus related interface files if not y.startswith('xyz'): continue with open(os.path.join(targetdir, y)) as fd: i = y.replace('.interface.yaml', '').replace(os.sep, '.') # PIM can't create interfaces with methods. parsed = yaml.safe_load(fd.read()) if parsed.get('methods', None): continue # Cereal can't understand the type sdbusplus::object_path. This # type is a wrapper around std::string. Ignore interfaces having # a property of this type for now. The only interface that has a # property of this type now is xyz.openbmc_project.Association, # which is an unused interface. No inventory objects implement # this interface. # TODO via openbmc/openbmc#2123 : figure out how to make Cereal # understand sdbusplus::object_path. properties = parsed.get('properties', None) if properties: if any('path' in p['type'] for p in properties): continue interface_composite[i] = properties interfaces.append(i) return interfaces, interface_composite def __init__(self, *a, **kw): self.interfaces = \ [Interface(x) for x in kw.pop('interfaces', [])] self.interface_composite = \ kw.pop('interface_composite', {}) self.events = [ self.class_map[x['type']](**x) for x in a] super(Everything, self).__init__(**kw) def generate_cpp(self, loader): '''Render the template with the provided events and interfaces.''' with open(os.path.join( args.outputdir, 'generated.cpp'), 'w') as fd: fd.write( self.render( loader, 'generated.mako.cpp', events=self.events, interfaces=self.interfaces, indent=Indent())) def generate_serialization(self, loader): with open(os.path.join( args.outputdir, 'gen_serialization.hpp'), 'w') as fd: fd.write( self.render( loader, 'gen_serialization.mako.hpp', interfaces=self.interfaces, interface_composite=self.interface_composite)) if __name__ == '__main__': script_dir = os.path.dirname(os.path.realpath(__file__)) valid_commands = { 'generate-cpp': 'generate_cpp', 'generate-serialization': 'generate_serialization', } parser = argparse.ArgumentParser( description='Phosphor Inventory Manager (PIM) YAML ' 'scanner and code generator.') parser.add_argument( '-o', '--output-dir', dest='outputdir', default='.', help='Output directory.') parser.add_argument( '-i', '--interfaces-dir', dest='ifacesdir', help='Location of interfaces to be supported.') parser.add_argument( '-d', '--dir', dest='inputdir', default=os.path.join(script_dir, 'example'), help='Location of files to process.') parser.add_argument( '-b', '--bus-name', dest='busname', default='xyz.openbmc_project.Inventory.Manager', help='Inventory manager busname.') parser.add_argument( 'command', metavar='COMMAND', type=str, choices=valid_commands.keys(), help='%s.' % " | ".join(valid_commands.keys())) args = parser.parse_args() if sys.version_info < (3, 0): lookup = mako.lookup.TemplateLookup( directories=[script_dir], disable_unicode=True) else: lookup = mako.lookup.TemplateLookup( directories=[script_dir]) function = getattr( Everything.load(args), valid_commands[args.command]) function(lookup) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4