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 Fallback(Rpolicy, Renderer): 198 '''Default policy handler (policy:type:fallback).''' 199 200 def __init__(self, *a, **kw): 201 kw['name'] = 'fallback-{}'.format(kw['fan']) 202 super(Fallback, self).__init__(**kw) 203 204 def setup(self, objs): 205 super(Fallback, self).setup(objs) 206 207 def construct(self, loader, indent): 208 return self.render( 209 loader, 210 'fallback.mako.hpp', 211 f=self, 212 indent=indent) 213 214 215class Fan(ConfigEntry): 216 '''Fan directive handler. Fans entries consist of an inventory path, 217 optional redundancy policy and associated sensors.''' 218 219 def __init__(self, *a, **kw): 220 self.path = kw.pop('path') 221 self.methods = kw.pop('methods') 222 self.rpolicy = kw.pop('rpolicy', None) 223 super(Fan, self).__init__(**kw) 224 225 def factory(self, objs): 226 ''' Create rpolicy and sensor(s) objects.''' 227 228 if self.rpolicy: 229 self.rpolicy['fan'] = self.name 230 factory = Everything.classmap(self.rpolicy['type']) 231 rpolicy = factory(**self.rpolicy) 232 else: 233 rpolicy = Fallback(fan=self.name) 234 235 for m in self.methods: 236 m['policy'] = rpolicy.name 237 factory = Everything.classmap(m['type']) 238 sensor = factory(**m) 239 rpolicy.sensors.append(sensor.name) 240 add_unique(sensor, objs) 241 242 add_unique(rpolicy, objs) 243 super(Fan, self).factory(objs) 244 245 246class Everything(Renderer): 247 '''Parse/render entry point.''' 248 249 @staticmethod 250 def classmap(cls): 251 '''Map render item class entries to the appropriate 252 handler methods.''' 253 254 class_map = { 255 'fan': Fan, 256 'fallback': Fallback, 257 'gpio': Gpio, 258 'tach': Tach, 259 } 260 261 if cls not in class_map: 262 raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 263 264 return class_map[cls] 265 266 @staticmethod 267 def load(args): 268 '''Load the configuration file. Parsing occurs in three phases. 269 In the first phase a factory method associated with each 270 configuration file directive is invoked. These factory 271 methods generate more factory methods. In the second 272 phase the factory methods created in the first phase 273 are invoked. In the last phase a callback is invoked on 274 each object created in phase two. Typically the callback 275 resolves references to other configuration file directives.''' 276 277 factory_objs = {} 278 objs = {} 279 with open(args.input, 'r') as fd: 280 for x in yaml.safe_load(fd.read()) or {}: 281 282 # The top level elements all represent fans. 283 x['class'] = 'fan' 284 # Create factory object for this config file directive. 285 factory = Everything.classmap(x['class']) 286 obj = factory(**x) 287 288 # For a given class of directive, validate the file 289 # doesn't have any duplicate names. 290 if exists(factory_objs, obj.cls, obj.name): 291 raise NotUniqueError(args.input, 'fan', obj.name) 292 293 factory_objs.setdefault('fan', []).append(obj) 294 objs.setdefault('fan', []).append(obj) 295 296 for cls, items in factory_objs.items(): 297 for obj in items: 298 # Add objects for template consumption. 299 obj.factory(objs) 300 301 # Configuration file directives reference each other via 302 # the name attribute; however, when rendered the reference 303 # is just an array index. 304 # 305 # At this point all objects have been created but references 306 # have not been resolved to array indicies. Instruct objects 307 # to do that now. 308 for cls, items in objs.items(): 309 for obj in items: 310 obj.setup(objs) 311 312 return Everything(**objs) 313 314 def __init__(self, *a, **kw): 315 self.fans = kw.pop('fan', []) 316 self.policies = kw.pop('policy', []) 317 self.sensors = kw.pop('sensor', []) 318 super(Everything, self).__init__(**kw) 319 320 def generate_cpp(self, loader): 321 '''Render the template with the provided data.''' 322 sys.stdout.write( 323 self.render( 324 loader, 325 args.template, 326 fans=self.fans, 327 sensors=self.sensors, 328 policies=self.policies, 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 Fan Presence (PFP) YAML ' 339 'scanner and code generator.') 340 341 parser.add_argument( 342 '-i', '--input', dest='input', 343 default=os.path.join(script_dir, 'example', 'example.yaml'), 344 help='Location of config file to process.') 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=os.path.join(script_dir, 'templates'), 352 help='The space delimited mako template search path.') 353 parser.add_argument( 354 'command', metavar='COMMAND', type=str, 355 choices=valid_commands.keys(), 356 help='%s.' % ' | '.join(valid_commands.keys())) 357 358 args = parser.parse_args() 359 360 if sys.version_info < (3, 0): 361 lookup = mako.lookup.TemplateLookup( 362 directories=args.template_search.split(), 363 disable_unicode=True) 364 else: 365 lookup = mako.lookup.TemplateLookup( 366 directories=args.template_search.split()) 367 try: 368 function = getattr( 369 Everything.load(args), 370 valid_commands[args.command]) 371 function(lookup) 372 except InvalidConfigError as e: 373 sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg)) 374 raise 375