1#!/usr/bin/env python3 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 self.devpath = kw.pop('devpath') 143 kw['name'] = 'gpio-{}'.format(self.key) 144 super(Gpio, self).__init__(**kw) 145 146 def construct(self, loader, indent): 147 return self.render( 148 loader, 149 'gpio.mako.hpp', 150 g=self, 151 indent=indent) 152 153 def setup(self, objs): 154 super(Gpio, self).setup(objs) 155 156 157class Tach(Sensor, Renderer): 158 '''Handler for method:type:tach.''' 159 160 def __init__(self, *a, **kw): 161 self.sensors = kw.pop('sensors') 162 kw['name'] = 'tach-{}'.format('-'.join(self.sensors)) 163 super(Tach, self).__init__(**kw) 164 165 def construct(self, loader, indent): 166 return self.render( 167 loader, 168 'tach.mako.hpp', 169 t=self, 170 indent=indent) 171 172 def setup(self, objs): 173 super(Tach, self).setup(objs) 174 175 176class Rpolicy(ConfigEntry): 177 '''Convenience type for config file rpolicy:type handlers.''' 178 179 def __init__(self, *a, **kw): 180 kw.pop('type', None) 181 self.fan = kw.pop('fan') 182 self.sensors = [] 183 kw['class'] = 'policy' 184 super(Rpolicy, self).__init__(**kw) 185 186 def setup(self, objs): 187 '''All policies have an associated fan and methods. 188 Resolve the indices.''' 189 190 sensors = [] 191 for s in self.sensors: 192 sensors.append(get_index(objs, 'sensor', s)) 193 194 self.sensors = sensors 195 self.fan = get_index(objs, 'fan', self.fan) 196 197 198class AnyOf(Rpolicy, Renderer): 199 '''Default policy handler (policy:type:anyof).''' 200 201 def __init__(self, *a, **kw): 202 kw['name'] = 'anyof-{}'.format(kw['fan']) 203 super(AnyOf, self).__init__(**kw) 204 205 def setup(self, objs): 206 super(AnyOf, self).setup(objs) 207 208 def construct(self, loader, indent): 209 return self.render( 210 loader, 211 'anyof.mako.hpp', 212 f=self, 213 indent=indent) 214 215 216class Fallback(Rpolicy, Renderer): 217 '''Fallback policy handler (policy:type:fallback).''' 218 219 def __init__(self, *a, **kw): 220 kw['name'] = 'fallback-{}'.format(kw['fan']) 221 super(Fallback, self).__init__(**kw) 222 223 def setup(self, objs): 224 super(Fallback, self).setup(objs) 225 226 def construct(self, loader, indent): 227 return self.render( 228 loader, 229 'fallback.mako.hpp', 230 f=self, 231 indent=indent) 232 233 234class Fan(ConfigEntry): 235 '''Fan directive handler. Fans entries consist of an inventory path, 236 optional redundancy policy and associated sensors.''' 237 238 def __init__(self, *a, **kw): 239 self.path = kw.pop('path') 240 self.methods = kw.pop('methods') 241 self.rpolicy = kw.pop('rpolicy', None) 242 super(Fan, self).__init__(**kw) 243 244 def factory(self, objs): 245 ''' Create rpolicy and sensor(s) objects.''' 246 247 if self.rpolicy: 248 self.rpolicy['fan'] = self.name 249 factory = Everything.classmap(self.rpolicy['type']) 250 rpolicy = factory(**self.rpolicy) 251 else: 252 rpolicy = AnyOf(fan=self.name) 253 254 for m in self.methods: 255 m['policy'] = rpolicy.name 256 factory = Everything.classmap(m['type']) 257 sensor = factory(**m) 258 rpolicy.sensors.append(sensor.name) 259 add_unique(sensor, objs) 260 261 add_unique(rpolicy, objs) 262 super(Fan, self).factory(objs) 263 264 265class Everything(Renderer): 266 '''Parse/render entry point.''' 267 268 @staticmethod 269 def classmap(cls): 270 '''Map render item class entries to the appropriate 271 handler methods.''' 272 273 class_map = { 274 'anyof': AnyOf, 275 'fan': Fan, 276 'fallback': Fallback, 277 'gpio': Gpio, 278 'tach': Tach, 279 } 280 281 if cls not in class_map: 282 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 283 284 return class_map[cls] 285 286 @staticmethod 287 def load(args): 288 '''Load the configuration file. Parsing occurs in three phases. 289 In the first phase a factory method associated with each 290 configuration file directive is invoked. These factory 291 methods generate more factory methods. In the second 292 phase the factory methods created in the first phase 293 are invoked. In the last phase a callback is invoked on 294 each object created in phase two. Typically the callback 295 resolves references to other configuration file directives.''' 296 297 factory_objs = {} 298 objs = {} 299 with open(args.input, 'r') as fd: 300 for x in yaml.safe_load(fd.read()) or {}: 301 302 # The top level elements all represent fans. 303 x['class'] = 'fan' 304 # Create factory object for this config file directive. 305 factory = Everything.classmap(x['class']) 306 obj = factory(**x) 307 308 # For a given class of directive, validate the file 309 # doesn't have any duplicate names. 310 if exists(factory_objs, obj.cls, obj.name): 311 raise NotUniqueError(args.input, 'fan', obj.name) 312 313 factory_objs.setdefault('fan', []).append(obj) 314 objs.setdefault('fan', []).append(obj) 315 316 for cls, items in list(factory_objs.items()): 317 for obj in items: 318 # Add objects for template consumption. 319 obj.factory(objs) 320 321 # Configuration file directives reference each other via 322 # the name attribute; however, when rendered the reference 323 # is just an array index. 324 # 325 # At this point all objects have been created but references 326 # have not been resolved to array indices. Instruct objects 327 # to do that now. 328 for cls, items in list(objs.items()): 329 for obj in items: 330 obj.setup(objs) 331 332 return Everything(**objs) 333 334 def __init__(self, *a, **kw): 335 self.fans = kw.pop('fan', []) 336 self.policies = kw.pop('policy', []) 337 self.sensors = kw.pop('sensor', []) 338 super(Everything, self).__init__(**kw) 339 340 def generate_cpp(self, loader): 341 '''Render the template with the provided data.''' 342 sys.stdout.write( 343 self.render( 344 loader, 345 args.template, 346 fans=self.fans, 347 sensors=self.sensors, 348 policies=self.policies, 349 indent=Indent())) 350 351if __name__ == '__main__': 352 script_dir = os.path.dirname(os.path.realpath(__file__)) 353 valid_commands = { 354 'generate-cpp': 'generate_cpp', 355 } 356 357 parser = ArgumentParser( 358 description='Phosphor Fan Presence (PFP) YAML ' 359 'scanner and code generator.') 360 361 parser.add_argument( 362 '-i', '--input', dest='input', 363 default=os.path.join(script_dir, 'example', 'example.yaml'), 364 help='Location of config file to process.') 365 parser.add_argument( 366 '-t', '--template', dest='template', 367 default='generated.mako.hpp', 368 help='The top level template to render.') 369 parser.add_argument( 370 '-p', '--template-path', dest='template_search', 371 default=os.path.join(script_dir, 'templates'), 372 help='The space delimited mako template search path.') 373 parser.add_argument( 374 'command', metavar='COMMAND', type=str, 375 choices=list(valid_commands.keys()), 376 help='%s.' % ' | '.join(list(valid_commands.keys()))) 377 378 args = parser.parse_args() 379 380 if sys.version_info < (3, 0): 381 lookup = mako.lookup.TemplateLookup( 382 directories=args.template_search.split(), 383 disable_unicode=True) 384 else: 385 lookup = mako.lookup.TemplateLookup( 386 directories=args.template_search.split()) 387 try: 388 function = getattr( 389 Everything.load(args), 390 valid_commands[args.command]) 391 function(lookup) 392 except InvalidConfigError as e: 393 sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg)) 394 raise 395