1#!/usr/bin/env python 2 3''' 4Phosphor Fan Presence (PFP) YAML parser and code generator. 5 6Parse the provided PFP configuration file and generate C++ code. 7 8The parser workflow is broken down as follows: 9 1 - Import the YAML configuration file as native python type(s) 10 instance(s). 11 2 - Create an instance of the Everything class from the 12 native python type instance(s) with the Everything.load 13 method. 14 3 - The Everything class constructor orchestrates conversion of the 15 native python type(s) instances(s) to render helper types. 16 Each render helper type constructor imports its attributes 17 from the native python type(s) instances(s). 18 4 - Present the converted YAML to the command processing method 19 requested by the script user. 20''' 21 22import os 23import sys 24import yaml 25from argparse import ArgumentParser 26import mako.lookup 27from sdbusplus.renderer import Renderer 28from sdbusplus.namedelement import NamedElement 29 30 31class InvalidConfigError(BaseException): 32 '''General purpose config file parsing error.''' 33 34 def __init__(self, path, msg): 35 '''Display configuration file with the syntax 36 error and the error message.''' 37 38 self.config = path 39 self.msg = msg 40 41 42class NotUniqueError(InvalidConfigError): 43 '''Within a config file names must be unique. 44 Display the duplicate item.''' 45 46 def __init__(self, path, cls, *names): 47 fmt = 'Duplicate {0}: "{1}"' 48 super(NotUniqueError, self).__init__( 49 path, fmt.format(cls, ' '.join(names))) 50 51 52def get_index(objs, cls, name): 53 '''Items are usually rendered as C++ arrays and as 54 such are stored in python lists. Given an item name 55 its class, find the item index.''' 56 57 for i, x in enumerate(objs.get(cls, [])): 58 if x.name != name: 59 continue 60 61 return i 62 raise InvalidConfigError('Could not find name: "{0}"'.format(name)) 63 64 65def exists(objs, cls, name): 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) 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 80 for container in a: 81 if not exists(container, obj.cls, obj.name): 82 container.setdefault(obj.cls, []).append(obj) 83 84 85class Indent(object): 86 '''Help templates be depth agnostic.''' 87 88 def __init__(self, depth=0): 89 self.depth = depth 90 91 def __add__(self, depth): 92 return Indent(self.depth + depth) 93 94 def __call__(self, depth): 95 '''Render an indent at the current depth plus depth.''' 96 return 4*' '*(depth + self.depth) 97 98 99class ConfigEntry(NamedElement): 100 '''Base interface for rendered items.''' 101 102 def __init__(self, *a, **kw): 103 '''Pop the class keyword.''' 104 105 self.cls = kw.pop('class') 106 super(ConfigEntry, self).__init__(**kw) 107 108 def factory(self, objs): 109 ''' Optional factory interface for subclasses to add 110 additional items to be rendered.''' 111 112 pass 113 114 def setup(self, objs): 115 ''' Optional setup interface for subclasses, invoked 116 after all factory methods have been run.''' 117 118 pass 119 120 121class Sensor(ConfigEntry): 122 '''Convenience type for config file method:type handlers.''' 123 124 def __init__(self, *a, **kw): 125 kw['class'] = 'sensor' 126 kw.pop('type') 127 self.policy = kw.pop('policy') 128 super(Sensor, self).__init__(**kw) 129 130 def setup(self, objs): 131 '''All sensors have an associated policy. Get the policy index.''' 132 133 self.policy = get_index(objs, 'policy', self.policy) 134 135 136class Gpio(Sensor, Renderer): 137 '''Handler for method:type:gpio.''' 138 139 def __init__(self, *a, **kw): 140 self.key = kw.pop('key') 141 self.physpath = kw.pop('physpath') 142 kw['name'] = 'gpio-{}'.format(self.key) 143 super(Gpio, self).__init__(**kw) 144 145 def construct(self, loader, indent): 146 return self.render( 147 loader, 148 'gpio.mako.hpp', 149 g=self, 150 indent=indent) 151 152 def setup(self, objs): 153 super(Gpio, self).setup(objs) 154 155 156class Tach(Sensor, Renderer): 157 '''Handler for method:type:tach.''' 158 159 def __init__(self, *a, **kw): 160 self.sensors = kw.pop('sensors') 161 kw['name'] = 'tach-{}'.format('-'.join(self.sensors)) 162 super(Tach, self).__init__(**kw) 163 164 def construct(self, loader, indent): 165 return self.render( 166 loader, 167 'tach.mako.hpp', 168 t=self, 169 indent=indent) 170 171 def setup(self, objs): 172 super(Tach, self).setup(objs) 173 174 175class Rpolicy(ConfigEntry): 176 '''Convenience type for config file rpolicy:type handlers.''' 177 178 def __init__(self, *a, **kw): 179 kw.pop('type', None) 180 self.fan = kw.pop('fan') 181 self.sensors = [] 182 kw['class'] = 'policy' 183 super(Rpolicy, self).__init__(**kw) 184 185 def setup(self, objs): 186 '''All policies have an associated fan and methods. 187 Resolve the indicies.''' 188 189 sensors = [] 190 for s in self.sensors: 191 sensors.append(get_index(objs, 'sensor', s)) 192 193 self.sensors = sensors 194 self.fan = get_index(objs, 'fan', self.fan) 195 196 197class AnyOf(Rpolicy, Renderer): 198 '''Default policy handler (policy:type:anyof).''' 199 200 def __init__(self, *a, **kw): 201 kw['name'] = 'anyof-{}'.format(kw['fan']) 202 super(AnyOf, self).__init__(**kw) 203 204 def setup(self, objs): 205 super(AnyOf, self).setup(objs) 206 207 def construct(self, loader, indent): 208 return self.render( 209 loader, 210 'anyof.mako.hpp', 211 f=self, 212 indent=indent) 213 214 215class Fallback(Rpolicy, Renderer): 216 '''Fallback policy handler (policy:type:fallback).''' 217 218 def __init__(self, *a, **kw): 219 kw['name'] = 'fallback-{}'.format(kw['fan']) 220 super(Fallback, self).__init__(**kw) 221 222 def setup(self, objs): 223 super(Fallback, self).setup(objs) 224 225 def construct(self, loader, indent): 226 return self.render( 227 loader, 228 'fallback.mako.hpp', 229 f=self, 230 indent=indent) 231 232 233class Fan(ConfigEntry): 234 '''Fan directive handler. Fans entries consist of an inventory path, 235 optional redundancy policy and associated sensors.''' 236 237 def __init__(self, *a, **kw): 238 self.path = kw.pop('path') 239 self.methods = kw.pop('methods') 240 self.rpolicy = kw.pop('rpolicy', None) 241 super(Fan, self).__init__(**kw) 242 243 def factory(self, objs): 244 ''' Create rpolicy and sensor(s) objects.''' 245 246 if self.rpolicy: 247 self.rpolicy['fan'] = self.name 248 factory = Everything.classmap(self.rpolicy['type']) 249 rpolicy = factory(**self.rpolicy) 250 else: 251 rpolicy = AnyOf(fan=self.name) 252 253 for m in self.methods: 254 m['policy'] = rpolicy.name 255 factory = Everything.classmap(m['type']) 256 sensor = factory(**m) 257 rpolicy.sensors.append(sensor.name) 258 add_unique(sensor, objs) 259 260 add_unique(rpolicy, objs) 261 super(Fan, self).factory(objs) 262 263 264class Everything(Renderer): 265 '''Parse/render entry point.''' 266 267 @staticmethod 268 def classmap(cls): 269 '''Map render item class entries to the appropriate 270 handler methods.''' 271 272 class_map = { 273 'anyof': AnyOf, 274 'fan': Fan, 275 'fallback': Fallback, 276 'gpio': Gpio, 277 'tach': Tach, 278 } 279 280 if cls not in class_map: 281 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 282 283 return class_map[cls] 284 285 @staticmethod 286 def load(args): 287 '''Load the configuration file. Parsing occurs in three phases. 288 In the first phase a factory method associated with each 289 configuration file directive is invoked. These factory 290 methods generate more factory methods. In the second 291 phase the factory methods created in the first phase 292 are invoked. In the last phase a callback is invoked on 293 each object created in phase two. Typically the callback 294 resolves references to other configuration file directives.''' 295 296 factory_objs = {} 297 objs = {} 298 with open(args.input, 'r') as fd: 299 for x in yaml.safe_load(fd.read()) or {}: 300 301 # The top level elements all represent fans. 302 x['class'] = 'fan' 303 # Create factory object for this config file directive. 304 factory = Everything.classmap(x['class']) 305 obj = factory(**x) 306 307 # For a given class of directive, validate the file 308 # doesn't have any duplicate names. 309 if exists(factory_objs, obj.cls, obj.name): 310 raise NotUniqueError(args.input, 'fan', obj.name) 311 312 factory_objs.setdefault('fan', []).append(obj) 313 objs.setdefault('fan', []).append(obj) 314 315 for cls, items in factory_objs.items(): 316 for obj in items: 317 # Add objects for template consumption. 318 obj.factory(objs) 319 320 # Configuration file directives reference each other via 321 # the name attribute; however, when rendered the reference 322 # is just an array index. 323 # 324 # At this point all objects have been created but references 325 # have not been resolved to array indicies. Instruct objects 326 # to do that now. 327 for cls, items in objs.items(): 328 for obj in items: 329 obj.setup(objs) 330 331 return Everything(**objs) 332 333 def __init__(self, *a, **kw): 334 self.fans = kw.pop('fan', []) 335 self.policies = kw.pop('policy', []) 336 self.sensors = kw.pop('sensor', []) 337 super(Everything, self).__init__(**kw) 338 339 def generate_cpp(self, loader): 340 '''Render the template with the provided data.''' 341 sys.stdout.write( 342 self.render( 343 loader, 344 args.template, 345 fans=self.fans, 346 sensors=self.sensors, 347 policies=self.policies, 348 indent=Indent())) 349 350if __name__ == '__main__': 351 script_dir = os.path.dirname(os.path.realpath(__file__)) 352 valid_commands = { 353 'generate-cpp': 'generate_cpp', 354 } 355 356 parser = ArgumentParser( 357 description='Phosphor Fan Presence (PFP) YAML ' 358 'scanner and code generator.') 359 360 parser.add_argument( 361 '-i', '--input', dest='input', 362 default=os.path.join(script_dir, 'example', 'example.yaml'), 363 help='Location of config file to process.') 364 parser.add_argument( 365 '-t', '--template', dest='template', 366 default='generated.mako.hpp', 367 help='The top level template to render.') 368 parser.add_argument( 369 '-p', '--template-path', dest='template_search', 370 default=os.path.join(script_dir, 'templates'), 371 help='The space delimited mako template search path.') 372 parser.add_argument( 373 'command', metavar='COMMAND', type=str, 374 choices=valid_commands.keys(), 375 help='%s.' % ' | '.join(valid_commands.keys())) 376 377 args = parser.parse_args() 378 379 if sys.version_info < (3, 0): 380 lookup = mako.lookup.TemplateLookup( 381 directories=args.template_search.split(), 382 disable_unicode=True) 383 else: 384 lookup = mako.lookup.TemplateLookup( 385 directories=args.template_search.split()) 386 try: 387 function = getattr( 388 Everything.load(args), 389 valid_commands[args.command]) 390 function(lookup) 391 except InvalidConfigError as e: 392 sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg)) 393 raise 394