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 argparse 19import os 20import sys 21 22import mako.lookup 23import sdbusplus.property 24import yaml 25from sdbusplus.namedelement import NamedElement 26from sdbusplus.renderer import Renderer 27 28# Global busname for use within classes where necessary 29busname = "xyz.openbmc_project.Inventory.Manager" 30 31 32def cppTypeName(yaml_type): 33 """Convert yaml types to cpp types.""" 34 return sdbusplus.property.Property(type=yaml_type).cppTypeName 35 36 37class InterfaceComposite(object): 38 """Compose interface properties.""" 39 40 def __init__(self, dict): 41 self.dict = dict 42 43 def interfaces(self): 44 return list(self.dict.keys()) 45 46 def names(self, interface): 47 names = [] 48 if self.dict[interface]: 49 names = [ 50 NamedElement(name=x["name"]) for x in self.dict[interface] 51 ] 52 return names 53 54 55class Interface(list): 56 """Provide various interface transformations.""" 57 58 def __init__(self, iface): 59 super(Interface, self).__init__(iface.split(".")) 60 61 def namespace(self): 62 """Represent as an sdbusplus namespace.""" 63 return "::".join(["sdbusplus"] + self[:-1] + ["server", self[-1]]) 64 65 def header(self): 66 """Represent as an sdbusplus server binding header.""" 67 return os.sep.join(self + ["server.hpp"]) 68 69 def __str__(self): 70 return ".".join(self) 71 72 73class Indent(object): 74 """Help templates be depth agnostic.""" 75 76 def __init__(self, depth=0): 77 self.depth = depth 78 79 def __add__(self, depth): 80 return Indent(self.depth + depth) 81 82 def __call__(self, depth): 83 """Render an indent at the current depth plus depth.""" 84 return 4 * " " * (depth + self.depth) 85 86 87class Template(NamedElement): 88 """Associate a template name with its namespace.""" 89 90 def __init__(self, **kw): 91 self.namespace = kw.pop("namespace", []) 92 super(Template, self).__init__(**kw) 93 94 def qualified(self): 95 return "::".join(self.namespace + [self.name]) 96 97 98class FixBool(object): 99 """Un-capitalize booleans.""" 100 101 def __call__(self, arg): 102 return "{0}".format(arg.lower()) 103 104 105class Quote(object): 106 """Decorate an argument by quoting it.""" 107 108 def __call__(self, arg): 109 return '"{0}"'.format(arg) 110 111 112class Cast(object): 113 """Decorate an argument by casting it.""" 114 115 def __init__(self, cast, target): 116 """cast is the cast type (static, const, etc...). 117 target is the cast target type.""" 118 self.cast = cast 119 self.target = target 120 121 def __call__(self, arg): 122 return "{0}_cast<{1}>({2})".format(self.cast, self.target, arg) 123 124 125class Literal(object): 126 """Decorate an argument with a literal operator.""" 127 128 integer_types = [ 129 "int8", 130 "int16", 131 "int32", 132 "int64", 133 "uint8", 134 "uint16", 135 "uint32", 136 "uint64", 137 ] 138 139 def __init__(self, type): 140 self.type = type 141 142 def __call__(self, arg): 143 if "uint" in self.type: 144 arg = "{0}ull".format(arg) 145 elif "int" in self.type: 146 arg = "{0}ll".format(arg) 147 148 if self.type in self.integer_types: 149 return Cast("static", "{0}_t".format(self.type))(arg) 150 151 if self.type == "string": 152 return "{0}s".format(arg) 153 154 return arg 155 156 157class Argument(NamedElement, Renderer): 158 """Define argument type inteface.""" 159 160 def __init__(self, **kw): 161 self.type = kw.pop("type", None) 162 super(Argument, self).__init__(**kw) 163 164 def argument(self, loader, indent): 165 raise NotImplementedError 166 167 168class TrivialArgument(Argument): 169 """Non-array type arguments.""" 170 171 def __init__(self, **kw): 172 self.value = kw.pop("value") 173 self.decorators = kw.pop("decorators", []) 174 if kw.get("type", None) == "string": 175 self.decorators.insert(0, Quote()) 176 if kw.get("type", None) == "boolean": 177 self.decorators.insert(0, FixBool()) 178 179 super(TrivialArgument, self).__init__(**kw) 180 181 def argument(self, loader, indent): 182 a = str(self.value) 183 for d in self.decorators: 184 a = d(a) 185 186 return a 187 188 189class InitializerList(Argument): 190 """Initializer list arguments.""" 191 192 def __init__(self, **kw): 193 self.values = kw.pop("values") 194 super(InitializerList, self).__init__(**kw) 195 196 def argument(self, loader, indent): 197 return self.render( 198 loader, "argument.mako.cpp", arg=self, indent=indent 199 ) 200 201 202class DbusSignature(Argument): 203 """DBus signature arguments.""" 204 205 def __init__(self, **kw): 206 self.sig = {x: y for x, y in kw.items()} 207 kw.clear() 208 super(DbusSignature, self).__init__(**kw) 209 210 def argument(self, loader, indent): 211 return self.render( 212 loader, "signature.mako.cpp", signature=self, indent=indent 213 ) 214 215 216class MethodCall(Argument): 217 """Render syntatically correct c++ method calls.""" 218 219 def __init__(self, **kw): 220 self.namespace = kw.pop("namespace", []) 221 self.templates = kw.pop("templates", []) 222 self.args = kw.pop("args", []) 223 super(MethodCall, self).__init__(**kw) 224 225 def call(self, loader, indent): 226 return self.render( 227 loader, "method.mako.cpp", method=self, indent=indent 228 ) 229 230 def argument(self, loader, indent): 231 return self.call(loader, indent) 232 233 234class Vector(MethodCall): 235 """Convenience type for vectors.""" 236 237 def __init__(self, **kw): 238 kw["name"] = "vector" 239 kw["namespace"] = ["std"] 240 kw["args"] = [InitializerList(values=kw.pop("args"))] 241 super(Vector, self).__init__(**kw) 242 243 244class Filter(MethodCall): 245 """Convenience type for filters""" 246 247 def __init__(self, **kw): 248 kw["name"] = "make_filter" 249 super(Filter, self).__init__(**kw) 250 251 252class Action(MethodCall): 253 """Convenience type for actions""" 254 255 def __init__(self, **kw): 256 kw["name"] = "make_action" 257 super(Action, self).__init__(**kw) 258 259 260class PathCondition(MethodCall): 261 """Convenience type for path conditions""" 262 263 def __init__(self, **kw): 264 kw["name"] = "make_path_condition" 265 super(PathCondition, self).__init__(**kw) 266 267 268class GetProperty(MethodCall): 269 """Convenience type for getting inventory properties""" 270 271 def __init__(self, **kw): 272 kw["name"] = "make_get_property" 273 super(GetProperty, self).__init__(**kw) 274 275 276class CreateObjects(MethodCall): 277 """Assemble a createObjects functor.""" 278 279 def __init__(self, **kw): 280 objs = [] 281 282 for path, interfaces in kw.pop("objs").items(): 283 key_o = TrivialArgument( 284 value=path, type="string", decorators=[Literal("string")] 285 ) 286 value_i = [] 287 288 for ( 289 interface, 290 properties, 291 ) in interfaces.items(): 292 key_i = TrivialArgument(value=interface, type="string") 293 value_p = [] 294 if properties: 295 for prop, value in properties.items(): 296 key_p = TrivialArgument(value=prop, type="string") 297 value_v = TrivialArgument( 298 decorators=[Literal(value.get("type", None))], 299 **value 300 ) 301 value_p.append( 302 InitializerList(values=[key_p, value_v]) 303 ) 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[x["name"]](**x) for x in kw.pop("conditions", []) 323 ] 324 conditions = [PathCondition(args=[x]) for x in conditions] 325 args = [InitializerList(values=[TrivialArgument(**x) for x in values])] 326 args.append(InitializerList(values=conditions)) 327 kw["args"] = args 328 kw["namespace"] = ["functor"] 329 super(DestroyObjects, self).__init__(**kw) 330 331 332class SetProperty(MethodCall): 333 """Assemble a setProperty functor.""" 334 335 def __init__(self, **kw): 336 args = [] 337 338 value = kw.pop("value") 339 prop = kw.pop("property") 340 iface = kw.pop("interface") 341 iface = Interface(iface) 342 namespace = iface.namespace().split("::")[:-1] 343 name = iface[-1] 344 t = Template(namespace=namespace, name=iface[-1]) 345 346 member = "&%s" % "::".join( 347 namespace + [name, NamedElement(name=prop).camelCase] 348 ) 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( 354 InitializerList(values=[TrivialArgument(**x) for x in paths]) 355 ) 356 357 conditions = [ 358 Event.functor_map[x["name"]](**x) for x in kw.pop("conditions", []) 359 ] 360 conditions = [PathCondition(args=[x]) for x in conditions] 361 362 args.append(InitializerList(values=conditions)) 363 args.append(TrivialArgument(value=str(iface), type="string")) 364 args.append( 365 TrivialArgument( 366 value=member, decorators=[Cast("static", member_cast)] 367 ) 368 ) 369 args.append(TrivialArgument(**value)) 370 371 kw["templates"] = [Template(name=name, namespace=namespace)] 372 kw["args"] = args 373 kw["namespace"] = ["functor"] 374 super(SetProperty, self).__init__(**kw) 375 376 377class PropertyChanged(MethodCall): 378 """Assemble a propertyChanged functor.""" 379 380 def __init__(self, **kw): 381 args = [] 382 args.append(TrivialArgument(value=kw.pop("interface"), type="string")) 383 args.append(TrivialArgument(value=kw.pop("property"), type="string")) 384 args.append( 385 TrivialArgument( 386 decorators=[Literal(kw["value"].get("type", None))], 387 **kw.pop("value") 388 ) 389 ) 390 kw["args"] = args 391 kw["namespace"] = ["functor"] 392 super(PropertyChanged, self).__init__(**kw) 393 394 395class PropertyIs(MethodCall): 396 """Assemble a propertyIs functor.""" 397 398 def __init__(self, **kw): 399 args = [] 400 path = kw.pop("path", None) 401 if not path: 402 path = TrivialArgument(value="nullptr") 403 else: 404 path = TrivialArgument(value=path, type="string") 405 406 args.append(path) 407 iface = TrivialArgument(value=kw.pop("interface"), type="string") 408 args.append(iface) 409 prop = TrivialArgument(value=kw.pop("property"), type="string") 410 args.append(prop) 411 args.append( 412 TrivialArgument( 413 decorators=[Literal(kw["value"].get("type", None))], 414 **kw.pop("value") 415 ) 416 ) 417 418 service = kw.pop("service", None) 419 if service: 420 args.append(TrivialArgument(value=service, type="string")) 421 422 dbusMember = kw.pop("dbusMember", None) 423 if dbusMember: 424 # Inventory manager's service name is required 425 if not service or service != busname: 426 args.append(TrivialArgument(value=busname, type="string")) 427 428 gpArgs = [] 429 gpArgs.append(path) 430 gpArgs.append(iface) 431 # Prepend '&' and append 'getPropertyByName' function on dbusMember 432 gpArgs.append( 433 TrivialArgument(value="&" + dbusMember + "::getPropertyByName") 434 ) 435 gpArgs.append(prop) 436 fArg = MethodCall( 437 name="getProperty", 438 namespace=["functor"], 439 templates=[Template(name=dbusMember, namespace=[])], 440 args=gpArgs, 441 ) 442 443 # Append getProperty functor 444 args.append( 445 GetProperty( 446 templates=[ 447 Template( 448 name=dbusMember + "::PropertiesVariant", 449 namespace=[], 450 ) 451 ], 452 args=[fArg], 453 ) 454 ) 455 456 kw["args"] = args 457 kw["namespace"] = ["functor"] 458 super(PropertyIs, self).__init__(**kw) 459 460 461class Event(MethodCall): 462 """Assemble an inventory manager event.""" 463 464 functor_map = { 465 "destroyObjects": DestroyObjects, 466 "createObjects": CreateObjects, 467 "propertyChangedTo": PropertyChanged, 468 "propertyIs": PropertyIs, 469 "setProperty": SetProperty, 470 } 471 472 def __init__(self, **kw): 473 self.summary = kw.pop("name") 474 475 filters = [ 476 self.functor_map[x["name"]](**x) for x in kw.pop("filters", []) 477 ] 478 filters = [Filter(args=[x]) for x in filters] 479 filters = Vector( 480 templates=[Template(name="Filter", namespace=[])], args=filters 481 ) 482 483 event = MethodCall( 484 name="make_shared", 485 namespace=["std"], 486 templates=[ 487 Template( 488 name=kw.pop("event"), 489 namespace=kw.pop("event_namespace", []), 490 ) 491 ], 492 args=kw.pop("event_args", []) + [filters], 493 ) 494 495 events = Vector( 496 templates=[Template(name="EventBasePtr", namespace=[])], 497 args=[event], 498 ) 499 500 action_type = Template(name="Action", namespace=[]) 501 action_args = [ 502 self.functor_map[x["name"]](**x) for x in kw.pop("actions", []) 503 ] 504 action_args = [Action(args=[x]) for x in action_args] 505 actions = Vector(templates=[action_type], args=action_args) 506 507 kw["name"] = "make_tuple" 508 kw["namespace"] = ["std"] 509 kw["args"] = [events, actions] 510 super(Event, self).__init__(**kw) 511 512 513class MatchEvent(Event): 514 """Associate one or more dbus signal match signatures with 515 a filter.""" 516 517 def __init__(self, **kw): 518 kw["event"] = "DbusSignal" 519 kw["event_namespace"] = [] 520 kw["event_args"] = [ 521 DbusSignature(**x) for x in kw.pop("signatures", []) 522 ] 523 524 super(MatchEvent, self).__init__(**kw) 525 526 527class StartupEvent(Event): 528 """Assemble a startup event.""" 529 530 def __init__(self, **kw): 531 kw["event"] = "StartupEvent" 532 kw["event_namespace"] = [] 533 super(StartupEvent, self).__init__(**kw) 534 535 536class Everything(Renderer): 537 """Parse/render entry point.""" 538 539 class_map = { 540 "match": MatchEvent, 541 "startup": StartupEvent, 542 } 543 544 @staticmethod 545 def load(args): 546 # Aggregate all the event YAML in the events.d directory 547 # into a single list of events. 548 549 events = [] 550 events_dir = os.path.join(args.inputdir, "events.d") 551 552 if os.path.exists(events_dir): 553 yaml_files = [ 554 x for x in os.listdir(events_dir) if x.endswith(".yaml") 555 ] 556 557 for x in yaml_files: 558 with open(os.path.join(events_dir, x), "r") as fd: 559 for e in yaml.safe_load(fd.read()).get("events", {}): 560 events.append(e) 561 562 interfaces, interface_composite = Everything.get_interfaces( 563 args.ifacesdir 564 ) 565 ( 566 extra_interfaces, 567 extra_interface_composite, 568 ) = Everything.get_interfaces( 569 os.path.join(args.inputdir, "extra_interfaces.d") 570 ) 571 interface_composite.update(extra_interface_composite) 572 interface_composite = InterfaceComposite(interface_composite) 573 # Update busname if configured differenly than the default 574 global busname 575 busname = args.busname 576 577 return Everything( 578 *events, 579 interfaces=interfaces + extra_interfaces, 580 interface_composite=interface_composite 581 ) 582 583 @staticmethod 584 def get_interfaces(targetdir): 585 """Scan the interfaces directory for interfaces that PIM can create.""" 586 587 yaml_files = [] 588 interfaces = [] 589 interface_composite = {} 590 591 if targetdir and os.path.exists(targetdir): 592 for directory, _, files in os.walk(targetdir): 593 if not files: 594 continue 595 596 yaml_files += [ 597 os.path.relpath(os.path.join(directory, f), targetdir) 598 for f in [ 599 f for f in files if f.endswith(".interface.yaml") 600 ] 601 ] 602 603 for y in yaml_files: 604 # parse only phosphor dbus related interface files 605 if not ( 606 y.startswith("xyz") 607 or y.startswith("com/ibm/ipzvpd") 608 or y.startswith("com/ibm/Control/Host") 609 ): 610 continue 611 with open(os.path.join(targetdir, y)) as fd: 612 i = y.replace(".interface.yaml", "").replace(os.sep, ".") 613 614 # PIM can't create interfaces with methods. 615 parsed = yaml.safe_load(fd.read()) 616 if parsed.get("methods", None): 617 continue 618 # Cereal can't understand the type sdbusplus::object_path. This 619 # type is a wrapper around std::string. Ignore interfaces 620 # having a property of this type for now. The only interface 621 # that has a property of this type now is 622 # xyz.openbmc_project.Association, which is an unused 623 # interface. No inventory objects implement this interface. 624 # TODO via openbmc/openbmc#2123 : figure out how to make Cereal 625 # understand sdbusplus::object_path. 626 properties = parsed.get("properties", None) 627 if properties: 628 if any("path" in p["type"] for p in properties): 629 continue 630 interface_composite[i] = properties 631 interfaces.append(i) 632 633 return interfaces, interface_composite 634 635 def __init__(self, *a, **kw): 636 self.interfaces = [Interface(x) for x in kw.pop("interfaces", [])] 637 self.interface_composite = kw.pop("interface_composite", {}) 638 self.events = [self.class_map[x["type"]](**x) for x in a] 639 super(Everything, self).__init__(**kw) 640 641 def generate_cpp(self, loader): 642 """Render the template with the provided events and interfaces.""" 643 with open(os.path.join(args.outputdir, "generated.cpp"), "w") as fd: 644 fd.write( 645 self.render( 646 loader, 647 "generated.mako.cpp", 648 events=self.events, 649 interfaces=self.interfaces, 650 indent=Indent(), 651 ) 652 ) 653 654 def generate_serialization(self, loader): 655 with open( 656 os.path.join(args.outputdir, "gen_serialization.hpp"), "w" 657 ) as fd: 658 fd.write( 659 self.render( 660 loader, 661 "gen_serialization.mako.hpp", 662 interfaces=self.interfaces, 663 interface_composite=self.interface_composite, 664 ) 665 ) 666 667 668if __name__ == "__main__": 669 script_dir = os.path.dirname(os.path.realpath(__file__)) 670 valid_commands = { 671 "generate-cpp": "generate_cpp", 672 "generate-serialization": "generate_serialization", 673 } 674 675 parser = argparse.ArgumentParser( 676 description=( 677 "Phosphor Inventory Manager (PIM) YAML scanner and code generator." 678 ) 679 ) 680 parser.add_argument( 681 "-o", 682 "--output-dir", 683 dest="outputdir", 684 default=".", 685 help="Output directory.", 686 ) 687 parser.add_argument( 688 "-i", 689 "--interfaces-dir", 690 dest="ifacesdir", 691 help="Location of interfaces to be supported.", 692 ) 693 parser.add_argument( 694 "-d", 695 "--dir", 696 dest="inputdir", 697 default=os.path.join(script_dir, "example"), 698 help="Location of files to process.", 699 ) 700 parser.add_argument( 701 "-b", 702 "--bus-name", 703 dest="busname", 704 default="xyz.openbmc_project.Inventory.Manager", 705 help="Inventory manager busname.", 706 ) 707 parser.add_argument( 708 "command", 709 metavar="COMMAND", 710 type=str, 711 choices=list(valid_commands.keys()), 712 help="%s." % " | ".join(list(valid_commands.keys())), 713 ) 714 715 args = parser.parse_args() 716 717 if sys.version_info < (3, 0): 718 lookup = mako.lookup.TemplateLookup( 719 directories=[script_dir], disable_unicode=True 720 ) 721 else: 722 lookup = mako.lookup.TemplateLookup(directories=[script_dir]) 723 724 function = getattr(Everything.load(args), valid_commands[args.command]) 725 function(lookup) 726 727 728# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 729