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