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