1#!/usr/bin/env python 2 3'''Phosphor DBus Monitor 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 os 19import sys 20import yaml 21import mako.lookup 22from argparse import ArgumentParser 23from sdbusplus.renderer import Renderer 24from sdbusplus.namedelement import NamedElement 25import sdbusplus.property 26 27 28class InvalidConfigError(BaseException): 29 '''General purpose config file parsing error.''' 30 31 def __init__(self, path, msg): 32 '''Display configuration file with the syntax 33 error and the error message.''' 34 35 self.config = path 36 self.msg = msg 37 38 39class NotUniqueError(InvalidConfigError): 40 '''Within a config file names must be unique. 41 Display the config file with the duplicate and 42 the duplicate itself.''' 43 44 def __init__(self, path, cls, *names): 45 fmt = 'Duplicate {0}: "{1}"' 46 super(NotUniqueError, self).__init__( 47 path, fmt.format(cls, ' '.join(names))) 48 49 50def get_index(objs, cls, name, config=None): 51 '''Items are usually rendered as C++ arrays and as 52 such are stored in python lists. Given an item name 53 its class, and an optional config file filter, find 54 the item index.''' 55 56 for i, x in enumerate(objs.get(cls, [])): 57 if config and x.configfile != config: 58 continue 59 if x.name != name: 60 continue 61 62 return i 63 raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name)) 64 65 66def exists(objs, cls, name, config=None): 67 '''Check to see if an item already exists in a list given 68 the item name.''' 69 70 try: 71 get_index(objs, cls, name, config) 72 except: 73 return False 74 75 return True 76 77 78def add_unique(obj, *a, **kw): 79 '''Add an item to one or more lists unless already present, 80 with an option to constrain the search to a specific config file.''' 81 82 for container in a: 83 if not exists(container, obj.cls, obj.name, config=kw.get('config')): 84 container.setdefault(obj.cls, []).append(obj) 85 86 87class Indent(object): 88 '''Help templates be depth agnostic.''' 89 90 def __init__(self, depth=0): 91 self.depth = depth 92 93 def __add__(self, depth): 94 return Indent(self.depth + depth) 95 96 def __call__(self, depth): 97 '''Render an indent at the current depth plus depth.''' 98 return 4*' '*(depth + self.depth) 99 100 101class ConfigEntry(NamedElement): 102 '''Base interface for rendered items.''' 103 104 def __init__(self, *a, **kw): 105 '''Pop the configfile/class/subclass keywords.''' 106 107 self.configfile = kw.pop('configfile') 108 self.cls = kw.pop('class') 109 self.subclass = kw.pop(self.cls) 110 super(ConfigEntry, self).__init__(**kw) 111 112 def factory(self, objs): 113 ''' Optional factory interface for subclasses to add 114 additional items to be rendered.''' 115 116 pass 117 118 def setup(self, objs): 119 ''' Optional setup interface for subclasses, invoked 120 after all factory methods have been run.''' 121 122 pass 123 124 125class Path(ConfigEntry): 126 '''Path/metadata association.''' 127 128 def __init__(self, *a, **kw): 129 super(Path, self).__init__(**kw) 130 131 def factory(self, objs): 132 '''Create path and metadata elements.''' 133 134 args = { 135 'class': 'pathname', 136 'pathname': 'element', 137 'name': self.name['path'] 138 } 139 add_unique(ConfigEntry( 140 configfile=self.configfile, **args), objs) 141 142 args = { 143 'class': 'meta', 144 'meta': 'element', 145 'name': self.name['meta'] 146 } 147 add_unique(ConfigEntry( 148 configfile=self.configfile, **args), objs) 149 150 super(Path, self).factory(objs) 151 152 def setup(self, objs): 153 '''Resolve path and metadata names to indicies.''' 154 155 self.path = get_index( 156 objs, 'pathname', self.name['path']) 157 self.meta = get_index( 158 objs, 'meta', self.name['meta']) 159 160 super(Path, self).setup(objs) 161 162 163class Property(ConfigEntry): 164 '''Property/interface/metadata association.''' 165 166 def __init__(self, *a, **kw): 167 super(Property, self).__init__(**kw) 168 169 def factory(self, objs): 170 '''Create interface, property name and metadata elements.''' 171 172 args = { 173 'class': 'interface', 174 'interface': 'element', 175 'name': self.name['interface'] 176 } 177 add_unique(ConfigEntry( 178 configfile=self.configfile, **args), objs) 179 180 args = { 181 'class': 'propertyname', 182 'propertyname': 'element', 183 'name': self.name['property'] 184 } 185 add_unique(ConfigEntry( 186 configfile=self.configfile, **args), objs) 187 188 args = { 189 'class': 'meta', 190 'meta': 'element', 191 'name': self.name['meta'] 192 } 193 add_unique(ConfigEntry( 194 configfile=self.configfile, **args), objs) 195 196 super(Property, self).factory(objs) 197 198 def setup(self, objs): 199 '''Resolve interface, property and metadata to indicies.''' 200 201 self.interface = get_index( 202 objs, 'interface', self.name['interface']) 203 self.prop = get_index( 204 objs, 'propertyname', self.name['property']) 205 self.meta = get_index( 206 objs, 'meta', self.name['meta']) 207 208 super(Property, self).setup(objs) 209 210 211class Instance(ConfigEntry): 212 '''Property/Path association.''' 213 214 def __init__(self, *a, **kw): 215 super(Instance, self).__init__(**kw) 216 217 def setup(self, objs): 218 '''Resolve elements to indicies.''' 219 220 self.interface = get_index( 221 objs, 'interface', self.name['property']['interface']) 222 self.prop = get_index( 223 objs, 'propertyname', self.name['property']['property']) 224 self.propmeta = get_index( 225 objs, 'meta', self.name['property']['meta']) 226 self.path = get_index( 227 objs, 'pathname', self.name['path']['path']) 228 self.pathmeta = get_index( 229 objs, 'meta', self.name['path']['meta']) 230 231 super(Instance, self).setup(objs) 232 233 234class Group(ConfigEntry): 235 '''Pop the members keyword for groups.''' 236 237 def __init__(self, *a, **kw): 238 self.members = kw.pop('members') 239 super(Group, self).__init__(**kw) 240 241 242class ImplicitGroup(Group): 243 '''Provide a factory method for groups whose members are 244 not explicitly declared in the config files.''' 245 246 def __init__(self, *a, **kw): 247 super(ImplicitGroup, self).__init__(**kw) 248 249 def factory(self, objs): 250 '''Create group members.''' 251 252 factory = Everything.classmap(self.subclass, 'element') 253 for m in self.members: 254 args = { 255 'class': self.subclass, 256 self.subclass: 'element', 257 'name': m 258 } 259 260 obj = factory(configfile=self.configfile, **args) 261 add_unique(obj, objs) 262 obj.factory(objs) 263 264 super(ImplicitGroup, self).factory(objs) 265 266 267class GroupOfPaths(ImplicitGroup): 268 '''Path group config file directive.''' 269 270 def __init__(self, *a, **kw): 271 super(GroupOfPaths, self).__init__(**kw) 272 273 def setup(self, objs): 274 '''Resolve group members.''' 275 276 def map_member(x): 277 path = get_index( 278 objs, 'pathname', x['path']) 279 meta = get_index( 280 objs, 'meta', x['meta']) 281 return (path, meta) 282 283 self.members = map( 284 map_member, 285 self.members) 286 287 super(GroupOfPaths, self).setup(objs) 288 289 290class GroupOfProperties(ImplicitGroup): 291 '''Property group config file directive.''' 292 293 def __init__(self, *a, **kw): 294 self.datatype = sdbusplus.property.Property( 295 name=kw.get('name'), 296 type=kw.pop('type')).cppTypeName 297 298 super(GroupOfProperties, self).__init__(**kw) 299 300 def setup(self, objs): 301 '''Resolve group members.''' 302 303 def map_member(x): 304 iface = get_index( 305 objs, 'interface', x['interface']) 306 prop = get_index( 307 objs, 'propertyname', x['property']) 308 meta = get_index( 309 objs, 'meta', x['meta']) 310 311 return (iface, prop, meta) 312 313 self.members = map( 314 map_member, 315 self.members) 316 317 super(GroupOfProperties, self).setup(objs) 318 319 320class GroupOfInstances(ImplicitGroup): 321 '''A group of property instances.''' 322 323 def __init__(self, *a, **kw): 324 super(GroupOfInstances, self).__init__(**kw) 325 326 def setup(self, objs): 327 '''Resolve group members.''' 328 329 def map_member(x): 330 path = get_index(objs, 'pathname', x['path']['path']) 331 pathmeta = get_index(objs, 'meta', x['path']['meta']) 332 interface = get_index( 333 objs, 'interface', x['property']['interface']) 334 prop = get_index(objs, 'propertyname', x['property']['property']) 335 propmeta = get_index(objs, 'meta', x['property']['meta']) 336 instance = get_index(objs, 'instance', x) 337 338 return (path, pathmeta, interface, prop, propmeta, instance) 339 340 self.members = map( 341 map_member, 342 self.members) 343 344 super(GroupOfInstances, self).setup(objs) 345 346 347class HasPropertyIndex(ConfigEntry): 348 '''Handle config file directives that require an index to be 349 constructed.''' 350 351 def __init__(self, *a, **kw): 352 self.paths = kw.pop('paths') 353 self.properties = kw.pop('properties') 354 super(HasPropertyIndex, self).__init__(**kw) 355 356 def factory(self, objs): 357 '''Create a group of instances for this index.''' 358 359 members = [] 360 path_group = get_index( 361 objs, 'pathgroup', self.paths, config=self.configfile) 362 property_group = get_index( 363 objs, 'propertygroup', self.properties, config=self.configfile) 364 365 for path in objs['pathgroup'][path_group].members: 366 for prop in objs['propertygroup'][property_group].members: 367 member = { 368 'path': path, 369 'property': prop, 370 } 371 members.append(member) 372 373 args = { 374 'members': members, 375 'class': 'instancegroup', 376 'instancegroup': 'instance', 377 'name': '{0} {1}'.format(self.paths, self.properties) 378 } 379 380 group = GroupOfInstances(configfile=self.configfile, **args) 381 add_unique(group, objs, config=self.configfile) 382 group.factory(objs) 383 384 super(HasPropertyIndex, self).factory(objs) 385 386 def setup(self, objs): 387 '''Resolve path, property, and instance groups.''' 388 389 self.instances = get_index( 390 objs, 391 'instancegroup', 392 '{0} {1}'.format(self.paths, self.properties), 393 config=self.configfile) 394 self.paths = get_index( 395 objs, 396 'pathgroup', 397 self.paths, 398 config=self.configfile) 399 self.properties = get_index( 400 objs, 401 'propertygroup', 402 self.properties, 403 config=self.configfile) 404 self.datatype = objs['propertygroup'][self.properties].datatype 405 406 super(HasPropertyIndex, self).setup(objs) 407 408 409class PropertyWatch(HasPropertyIndex): 410 '''Handle the property watch config file directive.''' 411 412 def __init__(self, *a, **kw): 413 self.callback = kw.pop('callback', None) 414 super(PropertyWatch, self).__init__(**kw) 415 416 def setup(self, objs): 417 '''Resolve optional callback.''' 418 419 if self.callback: 420 self.callback = get_index( 421 objs, 422 'callback', 423 self.callback, 424 config=self.configfile) 425 426 super(PropertyWatch, self).setup(objs) 427 428 429class Callback(HasPropertyIndex): 430 '''Interface and common logic for callbacks.''' 431 432 def __init__(self, *a, **kw): 433 super(Callback, self).__init__(**kw) 434 435 436class ConditionCallback(ConfigEntry, Renderer): 437 '''Handle the journal callback config file directive.''' 438 439 def __init__(self, *a, **kw): 440 self.condition = kw.pop('condition') 441 self.instance = kw.pop('instance') 442 super(ConditionCallback, self).__init__(**kw) 443 444 def factory(self, objs): 445 '''Create a graph instance for this callback.''' 446 447 args = { 448 'configfile': self.configfile, 449 'members': [self.instance], 450 'class': 'callbackgroup', 451 'callbackgroup': 'callback', 452 'name': [self.instance] 453 } 454 455 entry = CallbackGraphEntry(**args) 456 add_unique(entry, objs, config=self.configfile) 457 458 super(ConditionCallback, self).factory(objs) 459 460 def setup(self, objs): 461 '''Resolve condition and graph entry.''' 462 463 self.graph = get_index( 464 objs, 465 'callbackgroup', 466 [self.instance], 467 config=self.configfile) 468 469 self.condition = get_index( 470 objs, 471 'condition', 472 self.name, 473 config=self.configfile) 474 475 super(ConditionCallback, self).setup(objs) 476 477 def construct(self, loader, indent): 478 return self.render( 479 loader, 480 'conditional.mako.cpp', 481 c=self, 482 indent=indent) 483 484 485class Condition(HasPropertyIndex): 486 '''Interface and common logic for conditions.''' 487 488 def __init__(self, *a, **kw): 489 self.callback = kw.pop('callback') 490 super(Condition, self).__init__(**kw) 491 492 def factory(self, objs): 493 '''Create a callback instance for this conditional.''' 494 495 args = { 496 'configfile': self.configfile, 497 'condition': self.name, 498 'class': 'callback', 499 'callback': 'conditional', 500 'instance': self.callback, 501 'name': self.name, 502 } 503 504 callback = ConditionCallback(**args) 505 add_unique(callback, objs, config=self.configfile) 506 callback.factory(objs) 507 508 super(Condition, self).factory(objs) 509 510 511class CountCondition(Condition, Renderer): 512 '''Handle the count condition config file directive.''' 513 514 def __init__(self, *a, **kw): 515 self.countop = kw.pop('countop') 516 self.countbound = kw.pop('countbound') 517 self.op = kw.pop('op') 518 self.bound = kw.pop('bound') 519 super(CountCondition, self).__init__(**kw) 520 521 def construct(self, loader, indent): 522 return self.render( 523 loader, 524 'count.mako.cpp', 525 c=self, 526 indent=indent) 527 528 529class Journal(Callback, Renderer): 530 '''Handle the journal callback config file directive.''' 531 532 def __init__(self, *a, **kw): 533 self.severity = kw.pop('severity') 534 self.message = kw.pop('message') 535 super(Journal, self).__init__(**kw) 536 537 def construct(self, loader, indent): 538 return self.render( 539 loader, 540 'journal.mako.cpp', 541 c=self, 542 indent=indent) 543 544 545class CallbackGraphEntry(Group): 546 '''An entry in a traversal list for groups of callbacks.''' 547 548 def __init__(self, *a, **kw): 549 super(CallbackGraphEntry, self).__init__(**kw) 550 551 def setup(self, objs): 552 '''Resolve group members.''' 553 554 def map_member(x): 555 return get_index( 556 objs, 'callback', x, config=self.configfile) 557 558 self.members = map( 559 map_member, 560 self.members) 561 562 super(CallbackGraphEntry, self).setup(objs) 563 564 565class GroupOfCallbacks(ConfigEntry, Renderer): 566 '''Handle the callback group config file directive.''' 567 568 def __init__(self, *a, **kw): 569 self.members = kw.pop('members') 570 super(GroupOfCallbacks, self).__init__(**kw) 571 572 def factory(self, objs): 573 '''Create a graph instance for this group of callbacks.''' 574 575 args = { 576 'configfile': self.configfile, 577 'members': self.members, 578 'class': 'callbackgroup', 579 'callbackgroup': 'callback', 580 'name': self.members 581 } 582 583 entry = CallbackGraphEntry(**args) 584 add_unique(entry, objs, config=self.configfile) 585 586 super(GroupOfCallbacks, self).factory(objs) 587 588 def setup(self, objs): 589 '''Resolve graph entry.''' 590 591 self.graph = get_index( 592 objs, 'callbackgroup', self.members, config=self.configfile) 593 594 super(GroupOfCallbacks, self).setup(objs) 595 596 def construct(self, loader, indent): 597 return self.render( 598 loader, 599 'callbackgroup.mako.cpp', 600 c=self, 601 indent=indent) 602 603 604class Everything(Renderer): 605 '''Parse/render entry point.''' 606 607 @staticmethod 608 def classmap(cls, sub=None): 609 '''Map render item class and subclass entries to the appropriate 610 handler methods.''' 611 612 class_map = { 613 'path': { 614 'element': Path, 615 }, 616 'pathgroup': { 617 'path': GroupOfPaths, 618 }, 619 'propertygroup': { 620 'property': GroupOfProperties, 621 }, 622 'property': { 623 'element': Property, 624 }, 625 'watch': { 626 'property': PropertyWatch, 627 }, 628 'instance': { 629 'element': Instance, 630 }, 631 'callback': { 632 'journal': Journal, 633 'group': GroupOfCallbacks, 634 }, 635 'condition': { 636 'count': CountCondition, 637 }, 638 } 639 640 if cls not in class_map: 641 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 642 if sub not in class_map[cls]: 643 raise NotImplementedError('Unknown {0} type: "{1}"'.format( 644 cls, sub)) 645 646 return class_map[cls][sub] 647 648 @staticmethod 649 def load_one_yaml(path, fd, objs): 650 '''Parse a single YAML file. Parsing occurs in three phases. 651 In the first phase a factory method associated with each 652 configuration file directive is invoked. These factory 653 methods generate more factory methods. In the second 654 phase the factory methods created in the first phase 655 are invoked. In the last phase a callback is invoked on 656 each object created in phase two. Typically the callback 657 resolves references to other configuration file directives.''' 658 659 factory_objs = {} 660 for x in yaml.safe_load(fd.read()) or {}: 661 662 # Create factory object for this config file directive. 663 cls = x['class'] 664 sub = x.get(cls) 665 if cls == 'group': 666 cls = '{0}group'.format(sub) 667 668 factory = Everything.classmap(cls, sub) 669 obj = factory(configfile=path, **x) 670 671 # For a given class of directive, validate the file 672 # doesn't have any duplicate names (duplicates are 673 # ok across config files). 674 if exists(factory_objs, obj.cls, obj.name, config=path): 675 raise NotUniqueError(path, cls, obj.name) 676 677 factory_objs.setdefault(cls, []).append(obj) 678 objs.setdefault(cls, []).append(obj) 679 680 for cls, items in factory_objs.items(): 681 for obj in items: 682 # Add objects for template consumption. 683 obj.factory(objs) 684 685 @staticmethod 686 def load(args): 687 '''Aggregate all the YAML in the input directory 688 into a single aggregate.''' 689 690 objs = {} 691 yaml_files = filter( 692 lambda x: x.endswith('.yaml'), 693 os.listdir(args.inputdir)) 694 695 yaml_files.sort() 696 697 for x in yaml_files: 698 path = os.path.join(args.inputdir, x) 699 with open(path, 'r') as fd: 700 Everything.load_one_yaml(path, fd, objs) 701 702 # Configuration file directives reference each other via 703 # the name attribute; however, when rendered the reference 704 # is just an array index. 705 # 706 # At this point all objects have been created but references 707 # have not been resolved to array indicies. Instruct objects 708 # to do that now. 709 for cls, items in objs.items(): 710 for obj in items: 711 obj.setup(objs) 712 713 return Everything(**objs) 714 715 def __init__(self, *a, **kw): 716 self.pathmeta = kw.pop('path', []) 717 self.paths = kw.pop('pathname', []) 718 self.meta = kw.pop('meta', []) 719 self.pathgroups = kw.pop('pathgroup', []) 720 self.interfaces = kw.pop('interface', []) 721 self.properties = kw.pop('property', []) 722 self.propertynames = kw.pop('propertyname', []) 723 self.propertygroups = kw.pop('propertygroup', []) 724 self.instances = kw.pop('instance', []) 725 self.instancegroups = kw.pop('instancegroup', []) 726 self.watches = kw.pop('watch', []) 727 self.callbacks = kw.pop('callback', []) 728 self.callbackgroups = kw.pop('callbackgroup', []) 729 self.conditions = kw.pop('condition', []) 730 731 super(Everything, self).__init__(**kw) 732 733 def generate_cpp(self, loader): 734 '''Render the template with the provided data.''' 735 with open(args.output, 'w') as fd: 736 fd.write( 737 self.render( 738 loader, 739 args.template, 740 meta=self.meta, 741 properties=self.properties, 742 propertynames=self.propertynames, 743 interfaces=self.interfaces, 744 paths=self.paths, 745 pathmeta=self.pathmeta, 746 pathgroups=self.pathgroups, 747 propertygroups=self.propertygroups, 748 instances=self.instances, 749 watches=self.watches, 750 instancegroups=self.instancegroups, 751 callbacks=self.callbacks, 752 callbackgroups=self.callbackgroups, 753 conditions=self.conditions, 754 indent=Indent())) 755 756if __name__ == '__main__': 757 script_dir = os.path.dirname(os.path.realpath(__file__)) 758 valid_commands = { 759 'generate-cpp': 'generate_cpp', 760 } 761 762 parser = ArgumentParser( 763 description='Phosphor DBus Monitor (PDM) YAML ' 764 'scanner and code generator.') 765 766 parser.add_argument( 767 "-o", "--out", dest="output", 768 default='generated.cpp', 769 help="Generated output file name and path.") 770 parser.add_argument( 771 '-t', '--template', dest='template', 772 default='generated.mako.hpp', 773 help='The top level template to render.') 774 parser.add_argument( 775 '-p', '--template-path', dest='template_search', 776 default=script_dir, 777 help='The space delimited mako template search path.') 778 parser.add_argument( 779 '-d', '--dir', dest='inputdir', 780 default=os.path.join(script_dir, 'example'), 781 help='Location of files to process.') 782 parser.add_argument( 783 'command', metavar='COMMAND', type=str, 784 choices=valid_commands.keys(), 785 help='%s.' % " | ".join(valid_commands.keys())) 786 787 args = parser.parse_args() 788 789 if sys.version_info < (3, 0): 790 lookup = mako.lookup.TemplateLookup( 791 directories=args.template_search.split(), 792 disable_unicode=True) 793 else: 794 lookup = mako.lookup.TemplateLookup( 795 directories=args.template_search.split()) 796 try: 797 function = getattr( 798 Everything.load(args), 799 valid_commands[args.command]) 800 function(lookup) 801 except InvalidConfigError as e: 802 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg)) 803 raise 804