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