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 Group(ConfigEntry): 212 '''Pop the members keyword for groups.''' 213 214 def __init__(self, *a, **kw): 215 self.members = kw.pop('members') 216 super(Group, self).__init__(**kw) 217 218 219class ImplicitGroup(Group): 220 '''Provide a factory method for groups whose members are 221 not explicitly declared in the config files.''' 222 223 def __init__(self, *a, **kw): 224 super(ImplicitGroup, self).__init__(**kw) 225 226 def factory(self, objs): 227 '''Create group members.''' 228 229 factory = Everything.classmap(self.subclass, 'element') 230 for m in self.members: 231 args = { 232 'class': self.subclass, 233 self.subclass: 'element', 234 'name': m 235 } 236 237 obj = factory(configfile=self.configfile, **args) 238 add_unique(obj, objs) 239 obj.factory(objs) 240 241 super(ImplicitGroup, self).factory(objs) 242 243 244class GroupOfPaths(ImplicitGroup): 245 '''Path group config file directive.''' 246 247 def __init__(self, *a, **kw): 248 super(GroupOfPaths, self).__init__(**kw) 249 250 def setup(self, objs): 251 '''Resolve group members.''' 252 253 def map_member(x): 254 path = get_index( 255 objs, 'pathname', x['path']) 256 meta = get_index( 257 objs, 'meta', x['meta']) 258 return (path, meta) 259 260 self.members = map( 261 map_member, 262 self.members) 263 264 super(GroupOfPaths, self).setup(objs) 265 266 267class GroupOfProperties(ImplicitGroup): 268 '''Property group config file directive.''' 269 270 def __init__(self, *a, **kw): 271 self.datatype = sdbusplus.property.Property( 272 name=kw.get('name'), 273 type=kw.pop('type')).cppTypeName 274 275 super(GroupOfProperties, self).__init__(**kw) 276 277 def setup(self, objs): 278 '''Resolve group members.''' 279 280 def map_member(x): 281 iface = get_index( 282 objs, 'interface', x['interface']) 283 prop = get_index( 284 objs, 'propertyname', x['property']) 285 meta = get_index( 286 objs, 'meta', x['meta']) 287 288 return (iface, prop, meta) 289 290 self.members = map( 291 map_member, 292 self.members) 293 294 super(GroupOfProperties, self).setup(objs) 295 296 297class Everything(Renderer): 298 '''Parse/render entry point.''' 299 300 @staticmethod 301 def classmap(cls, sub=None): 302 '''Map render item class and subclass entries to the appropriate 303 handler methods.''' 304 305 class_map = { 306 'path': { 307 'element': Path, 308 }, 309 'pathgroup': { 310 'path': GroupOfPaths, 311 }, 312 'propertygroup': { 313 'property': GroupOfProperties, 314 }, 315 'property': { 316 'element': Property, 317 }, 318 } 319 320 if cls not in class_map: 321 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 322 if sub not in class_map[cls]: 323 raise NotImplementedError('Unknown {0} type: "{1}"'.format( 324 cls, sub)) 325 326 return class_map[cls][sub] 327 328 @staticmethod 329 def load_one_yaml(path, fd, objs): 330 '''Parse a single YAML file. Parsing occurs in three phases. 331 In the first phase a factory method associated with each 332 configuration file directive is invoked. These factory 333 methods generate more factory methods. In the second 334 phase the factory methods created in the first phase 335 are invoked. In the last phase a callback is invoked on 336 each object created in phase two. Typically the callback 337 resolves references to other configuration file directives.''' 338 339 factory_objs = {} 340 for x in yaml.safe_load(fd.read()) or {}: 341 342 # Create factory object for this config file directive. 343 cls = x['class'] 344 sub = x.get(cls) 345 if cls == 'group': 346 cls = '{0}group'.format(sub) 347 348 factory = Everything.classmap(cls, sub) 349 obj = factory(configfile=path, **x) 350 351 # For a given class of directive, validate the file 352 # doesn't have any duplicate names (duplicates are 353 # ok across config files). 354 if exists(factory_objs, obj.cls, obj.name, config=path): 355 raise NotUniqueError(path, cls, obj.name) 356 357 factory_objs.setdefault(cls, []).append(obj) 358 objs.setdefault(cls, []).append(obj) 359 360 for cls, items in factory_objs.items(): 361 for obj in items: 362 # Add objects for template consumption. 363 obj.factory(objs) 364 365 @staticmethod 366 def load(args): 367 '''Aggregate all the YAML in the input directory 368 into a single aggregate.''' 369 370 objs = {} 371 yaml_files = filter( 372 lambda x: x.endswith('.yaml'), 373 os.listdir(args.inputdir)) 374 375 yaml_files.sort() 376 377 for x in yaml_files: 378 path = os.path.join(args.inputdir, x) 379 with open(path, 'r') as fd: 380 Everything.load_one_yaml(path, fd, objs) 381 382 # Configuration file directives reference each other via 383 # the name attribute; however, when rendered the reference 384 # is just an array index. 385 # 386 # At this point all objects have been created but references 387 # have not been resolved to array indicies. Instruct objects 388 # to do that now. 389 for cls, items in objs.items(): 390 for obj in items: 391 obj.setup(objs) 392 393 return Everything(**objs) 394 395 def __init__(self, *a, **kw): 396 self.pathmeta = kw.pop('path', []) 397 self.paths = kw.pop('pathname', []) 398 self.meta = kw.pop('meta', []) 399 self.pathgroups = kw.pop('pathgroup', []) 400 self.interfaces = kw.pop('interface', []) 401 self.properties = kw.pop('property', []) 402 self.propertynames = kw.pop('propertyname', []) 403 self.propertygroups = kw.pop('propertygroup', []) 404 405 super(Everything, self).__init__(**kw) 406 407 def generate_cpp(self, loader): 408 '''Render the template with the provided data.''' 409 with open(args.output, 'w') as fd: 410 fd.write( 411 self.render( 412 loader, 413 args.template, 414 meta=self.meta, 415 properties=self.properties, 416 propertynames=self.propertynames, 417 interfaces=self.interfaces, 418 paths=self.paths, 419 pathmeta=self.pathmeta, 420 pathgroups=self.pathgroups, 421 propertygroups=self.propertygroups, 422 indent=Indent())) 423 424if __name__ == '__main__': 425 script_dir = os.path.dirname(os.path.realpath(__file__)) 426 valid_commands = { 427 'generate-cpp': 'generate_cpp', 428 } 429 430 parser = ArgumentParser( 431 description='Phosphor DBus Monitor (PDM) YAML ' 432 'scanner and code generator.') 433 434 parser.add_argument( 435 "-o", "--out", dest="output", 436 default='generated.cpp', 437 help="Generated output file name and path.") 438 parser.add_argument( 439 '-t', '--template', dest='template', 440 default='generated.mako.hpp', 441 help='The top level template to render.') 442 parser.add_argument( 443 '-p', '--template-path', dest='template_search', 444 default=script_dir, 445 help='The space delimited mako template search path.') 446 parser.add_argument( 447 '-d', '--dir', dest='inputdir', 448 default=os.path.join(script_dir, 'example'), 449 help='Location of files to process.') 450 parser.add_argument( 451 'command', metavar='COMMAND', type=str, 452 choices=valid_commands.keys(), 453 help='%s.' % " | ".join(valid_commands.keys())) 454 455 args = parser.parse_args() 456 457 if sys.version_info < (3, 0): 458 lookup = mako.lookup.TemplateLookup( 459 directories=args.template_search.split(), 460 disable_unicode=True) 461 else: 462 lookup = mako.lookup.TemplateLookup( 463 directories=args.template_search.split()) 464 try: 465 function = getattr( 466 Everything.load(args), 467 valid_commands[args.command]) 468 function(lookup) 469 except InvalidConfigError as e: 470 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg)) 471 raise 472