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