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 25 26 27class InvalidConfigError(BaseException): 28 '''General purpose config file parsing error.''' 29 30 def __init__(self, path, msg): 31 '''Display configuration file with the syntax 32 error and the error message.''' 33 34 self.config = path 35 self.msg = msg 36 37 38class NotUniqueError(InvalidConfigError): 39 '''Within a config file names must be unique. 40 Display the config file with the duplicate and 41 the duplicate itself.''' 42 43 def __init__(self, path, cls, *names): 44 fmt = 'Duplicate {0}: "{1}"' 45 super(NotUniqueError, self).__init__( 46 path, fmt.format(cls, ' '.join(names))) 47 48 49def get_index(objs, cls, name, config=None): 50 '''Items are usually rendered as C++ arrays and as 51 such are stored in python lists. Given an item name 52 its class, and an optional config file filter, find 53 the item index.''' 54 55 for i, x in enumerate(objs.get(cls, [])): 56 if config and x.configfile != config: 57 continue 58 if x.name != name: 59 continue 60 61 return i 62 raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name)) 63 64 65def exists(objs, cls, name, config=None): 66 '''Check to see if an item already exists in a list given 67 the item name.''' 68 69 try: 70 get_index(objs, cls, name, config) 71 except: 72 return False 73 74 return True 75 76 77def add_unique(obj, *a, **kw): 78 '''Add an item to one or more lists unless already present, 79 with an option to constrain the search to a specific config file.''' 80 81 for container in a: 82 if not exists(container, obj.cls, obj.name, config=kw.get('config')): 83 container.setdefault(obj.cls, []).append(obj) 84 85 86class Indent(object): 87 '''Help templates be depth agnostic.''' 88 89 def __init__(self, depth=0): 90 self.depth = depth 91 92 def __add__(self, depth): 93 return Indent(self.depth + depth) 94 95 def __call__(self, depth): 96 '''Render an indent at the current depth plus depth.''' 97 return 4*' '*(depth + self.depth) 98 99 100class ConfigEntry(NamedElement): 101 '''Base interface for rendered items.''' 102 103 def __init__(self, *a, **kw): 104 '''Pop the configfile/class/subclass keywords.''' 105 106 self.configfile = kw.pop('configfile') 107 self.cls = kw.pop('class') 108 self.subclass = kw.pop(self.cls) 109 super(ConfigEntry, self).__init__(**kw) 110 111 def factory(self, objs): 112 ''' Optional factory interface for subclasses to add 113 additional items to be rendered.''' 114 115 pass 116 117 def setup(self, objs): 118 ''' Optional setup interface for subclasses, invoked 119 after all factory methods have been run.''' 120 121 pass 122 123 124class Path(ConfigEntry): 125 '''Path/metadata association.''' 126 127 def __init__(self, *a, **kw): 128 super(Path, self).__init__(**kw) 129 130 def factory(self, objs): 131 '''Create path and metadata elements.''' 132 133 args = { 134 'class': 'pathname', 135 'pathname': 'element', 136 'name': self.name['path'] 137 } 138 add_unique(ConfigEntry( 139 configfile=self.configfile, **args), objs) 140 141 args = { 142 'class': 'meta', 143 'meta': 'element', 144 'name': self.name['meta'] 145 } 146 add_unique(ConfigEntry( 147 configfile=self.configfile, **args), objs) 148 149 super(Path, self).factory(objs) 150 151 def setup(self, objs): 152 '''Resolve path and metadata names to indicies.''' 153 154 self.path = get_index( 155 objs, 'pathname', self.name['path']) 156 self.meta = get_index( 157 objs, 'meta', self.name['meta']) 158 159 super(Path, self).setup(objs) 160 161 162class Group(ConfigEntry): 163 '''Pop the members keyword for groups.''' 164 165 def __init__(self, *a, **kw): 166 self.members = kw.pop('members') 167 super(Group, self).__init__(**kw) 168 169 170class ImplicitGroup(Group): 171 '''Provide a factory method for groups whose members are 172 not explicitly declared in the config files.''' 173 174 def __init__(self, *a, **kw): 175 super(ImplicitGroup, self).__init__(**kw) 176 177 def factory(self, objs): 178 '''Create group members.''' 179 180 factory = Everything.classmap(self.subclass, 'element') 181 for m in self.members: 182 args = { 183 'class': self.subclass, 184 self.subclass: 'element', 185 'name': m 186 } 187 188 obj = factory(configfile=self.configfile, **args) 189 add_unique(obj, objs) 190 obj.factory(objs) 191 192 super(ImplicitGroup, self).factory(objs) 193 194 195class GroupOfPaths(ImplicitGroup): 196 '''Path group config file directive.''' 197 198 def __init__(self, *a, **kw): 199 super(GroupOfPaths, self).__init__(**kw) 200 201 def setup(self, objs): 202 '''Resolve group members.''' 203 204 def map_member(x): 205 path = get_index( 206 objs, 'pathname', x['path']) 207 meta = get_index( 208 objs, 'meta', x['meta']) 209 return (path, meta) 210 211 self.members = map( 212 map_member, 213 self.members) 214 215 super(GroupOfPaths, self).setup(objs) 216 217 218class Everything(Renderer): 219 '''Parse/render entry point.''' 220 221 @staticmethod 222 def classmap(cls, sub=None): 223 '''Map render item class and subclass entries to the appropriate 224 handler methods.''' 225 226 class_map = { 227 'path': { 228 'element': Path, 229 }, 230 'pathgroup': { 231 'path': GroupOfPaths, 232 }, 233 } 234 235 if cls not in class_map: 236 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 237 if sub not in class_map[cls]: 238 raise NotImplementedError('Unknown {0} type: "{1}"'.format( 239 cls, sub)) 240 241 return class_map[cls][sub] 242 243 @staticmethod 244 def load_one_yaml(path, fd, objs): 245 '''Parse a single YAML file. Parsing occurs in three phases. 246 In the first phase a factory method associated with each 247 configuration file directive is invoked. These factory 248 methods generate more factory methods. In the second 249 phase the factory methods created in the first phase 250 are invoked. In the last phase a callback is invoked on 251 each object created in phase two. Typically the callback 252 resolves references to other configuration file directives.''' 253 254 factory_objs = {} 255 for x in yaml.safe_load(fd.read()) or {}: 256 257 # Create factory object for this config file directive. 258 cls = x['class'] 259 sub = x.get(cls) 260 if cls == 'group': 261 cls = '{0}group'.format(sub) 262 263 factory = Everything.classmap(cls, sub) 264 obj = factory(configfile=path, **x) 265 266 # For a given class of directive, validate the file 267 # doesn't have any duplicate names (duplicates are 268 # ok across config files). 269 if exists(factory_objs, obj.cls, obj.name, config=path): 270 raise NotUniqueError(path, cls, obj.name) 271 272 factory_objs.setdefault(cls, []).append(obj) 273 objs.setdefault(cls, []).append(obj) 274 275 for cls, items in factory_objs.items(): 276 for obj in items: 277 # Add objects for template consumption. 278 obj.factory(objs) 279 280 @staticmethod 281 def load(args): 282 '''Aggregate all the YAML in the input directory 283 into a single aggregate.''' 284 285 objs = {} 286 yaml_files = filter( 287 lambda x: x.endswith('.yaml'), 288 os.listdir(args.inputdir)) 289 290 yaml_files.sort() 291 292 for x in yaml_files: 293 path = os.path.join(args.inputdir, x) 294 with open(path, 'r') as fd: 295 Everything.load_one_yaml(path, fd, objs) 296 297 # Configuration file directives reference each other via 298 # the name attribute; however, when rendered the reference 299 # is just an array index. 300 # 301 # At this point all objects have been created but references 302 # have not been resolved to array indicies. Instruct objects 303 # to do that now. 304 for cls, items in objs.items(): 305 for obj in items: 306 obj.setup(objs) 307 308 return Everything(**objs) 309 310 def __init__(self, *a, **kw): 311 self.pathmeta = kw.pop('path', []) 312 self.paths = kw.pop('pathname', []) 313 self.meta = kw.pop('meta', []) 314 self.pathgroups = kw.pop('pathgroup', []) 315 316 super(Everything, self).__init__(**kw) 317 318 def generate_cpp(self, loader): 319 '''Render the template with the provided data.''' 320 with open(args.output, 'w') as fd: 321 fd.write( 322 self.render( 323 loader, 324 args.template, 325 meta=self.meta, 326 paths=self.paths, 327 pathmeta=self.pathmeta, 328 pathgroups=self.pathgroups, 329 indent=Indent())) 330 331if __name__ == '__main__': 332 script_dir = os.path.dirname(os.path.realpath(__file__)) 333 valid_commands = { 334 'generate-cpp': 'generate_cpp', 335 } 336 337 parser = ArgumentParser( 338 description='Phosphor DBus Monitor (PDM) YAML ' 339 'scanner and code generator.') 340 341 parser.add_argument( 342 "-o", "--out", dest="output", 343 default='generated.cpp', 344 help="Generated output file name and path.") 345 parser.add_argument( 346 '-t', '--template', dest='template', 347 default='generated.mako.hpp', 348 help='The top level template to render.') 349 parser.add_argument( 350 '-p', '--template-path', dest='template_search', 351 default=script_dir, 352 help='The space delimited mako template search path.') 353 parser.add_argument( 354 '-d', '--dir', dest='inputdir', 355 default=os.path.join(script_dir, 'example'), 356 help='Location of files to process.') 357 parser.add_argument( 358 'command', metavar='COMMAND', type=str, 359 choices=valid_commands.keys(), 360 help='%s.' % " | ".join(valid_commands.keys())) 361 362 args = parser.parse_args() 363 364 if sys.version_info < (3, 0): 365 lookup = mako.lookup.TemplateLookup( 366 directories=args.template_search.split(), 367 disable_unicode=True) 368 else: 369 lookup = mako.lookup.TemplateLookup( 370 directories=args.template_search.split()) 371 try: 372 function = getattr( 373 Everything.load(args), 374 valid_commands[args.command]) 375 function(lookup) 376 except InvalidConfigError as e: 377 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg)) 378 raise 379