1f24d7749SMatthew Barth#!/usr/bin/env python3 25593560bSBrad Bishop 3*0f2588f2SPatrick Williams""" 45593560bSBrad BishopPhosphor Fan Presence (PFP) YAML parser and code generator. 55593560bSBrad Bishop 65593560bSBrad BishopParse the provided PFP configuration file and generate C++ code. 75593560bSBrad Bishop 85593560bSBrad BishopThe parser workflow is broken down as follows: 95593560bSBrad Bishop 1 - Import the YAML configuration file as native python type(s) 105593560bSBrad Bishop instance(s). 115593560bSBrad Bishop 2 - Create an instance of the Everything class from the 125593560bSBrad Bishop native python type instance(s) with the Everything.load 135593560bSBrad Bishop method. 145593560bSBrad Bishop 3 - The Everything class constructor orchestrates conversion of the 155593560bSBrad Bishop native python type(s) instances(s) to render helper types. 165593560bSBrad Bishop Each render helper type constructor imports its attributes 175593560bSBrad Bishop from the native python type(s) instances(s). 185593560bSBrad Bishop 4 - Present the converted YAML to the command processing method 195593560bSBrad Bishop requested by the script user. 20*0f2588f2SPatrick Williams""" 215593560bSBrad Bishop 225593560bSBrad Bishopimport os 235593560bSBrad Bishopimport sys 245593560bSBrad Bishopfrom argparse import ArgumentParser 25*0f2588f2SPatrick Williams 265593560bSBrad Bishopimport mako.lookup 27*0f2588f2SPatrick Williamsimport yaml 285593560bSBrad Bishopfrom sdbusplus.namedelement import NamedElement 29*0f2588f2SPatrick Williamsfrom sdbusplus.renderer import Renderer 305593560bSBrad Bishop 315593560bSBrad Bishop 32*0f2588f2SPatrick Williamsclass InvalidConfigError(Exception): 33*0f2588f2SPatrick Williams """General purpose config file parsing error.""" 345593560bSBrad Bishop 355593560bSBrad Bishop def __init__(self, path, msg): 36*0f2588f2SPatrick Williams """Display configuration file with the syntax 37*0f2588f2SPatrick Williams error and the error message.""" 385593560bSBrad Bishop 395593560bSBrad Bishop self.config = path 405593560bSBrad Bishop self.msg = msg 415593560bSBrad Bishop 425593560bSBrad Bishop 435593560bSBrad Bishopclass NotUniqueError(InvalidConfigError): 44*0f2588f2SPatrick Williams """Within a config file names must be unique. 45*0f2588f2SPatrick Williams Display the duplicate item.""" 465593560bSBrad Bishop 475593560bSBrad Bishop def __init__(self, path, cls, *names): 485593560bSBrad Bishop fmt = 'Duplicate {0}: "{1}"' 495593560bSBrad Bishop super(NotUniqueError, self).__init__( 50*0f2588f2SPatrick Williams path, fmt.format(cls, " ".join(names)) 51*0f2588f2SPatrick Williams ) 525593560bSBrad Bishop 535593560bSBrad Bishop 545593560bSBrad Bishopdef get_index(objs, cls, name): 55*0f2588f2SPatrick Williams """Items are usually rendered as C++ arrays and as 565593560bSBrad Bishop such are stored in python lists. Given an item name 57*0f2588f2SPatrick Williams its class, find the item index.""" 585593560bSBrad Bishop 595593560bSBrad Bishop for i, x in enumerate(objs.get(cls, [])): 605593560bSBrad Bishop if x.name != name: 615593560bSBrad Bishop continue 625593560bSBrad Bishop 635593560bSBrad Bishop return i 645593560bSBrad Bishop raise InvalidConfigError('Could not find name: "{0}"'.format(name)) 655593560bSBrad Bishop 665593560bSBrad Bishop 675593560bSBrad Bishopdef exists(objs, cls, name): 68*0f2588f2SPatrick Williams """Check to see if an item already exists in a list given 69*0f2588f2SPatrick Williams the item name.""" 705593560bSBrad Bishop 715593560bSBrad Bishop try: 725593560bSBrad Bishop get_index(objs, cls, name) 73*0f2588f2SPatrick Williams except Exception: 745593560bSBrad Bishop return False 755593560bSBrad Bishop 765593560bSBrad Bishop return True 775593560bSBrad Bishop 785593560bSBrad Bishop 795593560bSBrad Bishopdef add_unique(obj, *a, **kw): 80*0f2588f2SPatrick Williams """Add an item to one or more lists unless already present.""" 815593560bSBrad Bishop 825593560bSBrad Bishop for container in a: 835593560bSBrad Bishop if not exists(container, obj.cls, obj.name): 845593560bSBrad Bishop container.setdefault(obj.cls, []).append(obj) 855593560bSBrad Bishop 865593560bSBrad Bishop 875593560bSBrad Bishopclass Indent(object): 88*0f2588f2SPatrick Williams """Help templates be depth agnostic.""" 895593560bSBrad Bishop 905593560bSBrad Bishop def __init__(self, depth=0): 915593560bSBrad Bishop self.depth = depth 925593560bSBrad Bishop 935593560bSBrad Bishop def __add__(self, depth): 945593560bSBrad Bishop return Indent(self.depth + depth) 955593560bSBrad Bishop 965593560bSBrad Bishop def __call__(self, depth): 97*0f2588f2SPatrick Williams """Render an indent at the current depth plus depth.""" 98*0f2588f2SPatrick Williams return 4 * " " * (depth + self.depth) 995593560bSBrad Bishop 1005593560bSBrad Bishop 1015593560bSBrad Bishopclass ConfigEntry(NamedElement): 102*0f2588f2SPatrick Williams """Base interface for rendered items.""" 1035593560bSBrad Bishop 1045593560bSBrad Bishop def __init__(self, *a, **kw): 105*0f2588f2SPatrick Williams """Pop the class keyword.""" 1065593560bSBrad Bishop 107*0f2588f2SPatrick Williams self.cls = kw.pop("class") 1085593560bSBrad Bishop super(ConfigEntry, self).__init__(**kw) 1095593560bSBrad Bishop 1105593560bSBrad Bishop def factory(self, objs): 111*0f2588f2SPatrick Williams """Optional factory interface for subclasses to add 112*0f2588f2SPatrick Williams additional items to be rendered.""" 1135593560bSBrad Bishop 1145593560bSBrad Bishop pass 1155593560bSBrad Bishop 1165593560bSBrad Bishop def setup(self, objs): 117*0f2588f2SPatrick Williams """Optional setup interface for subclasses, invoked 118*0f2588f2SPatrick Williams after all factory methods have been run.""" 1195593560bSBrad Bishop 1205593560bSBrad Bishop pass 1215593560bSBrad Bishop 1225593560bSBrad Bishop 1235593560bSBrad Bishopclass Sensor(ConfigEntry): 124*0f2588f2SPatrick Williams """Convenience type for config file method:type handlers.""" 1255593560bSBrad Bishop 1265593560bSBrad Bishop def __init__(self, *a, **kw): 127*0f2588f2SPatrick Williams kw["class"] = "sensor" 128*0f2588f2SPatrick Williams kw.pop("type") 129*0f2588f2SPatrick Williams self.policy = kw.pop("policy") 1305593560bSBrad Bishop super(Sensor, self).__init__(**kw) 1315593560bSBrad Bishop 1325593560bSBrad Bishop def setup(self, objs): 133*0f2588f2SPatrick Williams """All sensors have an associated policy. Get the policy index.""" 1345593560bSBrad Bishop 135*0f2588f2SPatrick Williams self.policy = get_index(objs, "policy", self.policy) 1365593560bSBrad Bishop 1375593560bSBrad Bishop 1385593560bSBrad Bishopclass Gpio(Sensor, Renderer): 139*0f2588f2SPatrick Williams """Handler for method:type:gpio.""" 1405593560bSBrad Bishop 1415593560bSBrad Bishop def __init__(self, *a, **kw): 142*0f2588f2SPatrick Williams self.key = kw.pop("key") 143*0f2588f2SPatrick Williams self.physpath = kw.pop("physpath") 144*0f2588f2SPatrick Williams self.devpath = kw.pop("devpath") 145*0f2588f2SPatrick Williams kw["name"] = "gpio-{}".format(self.key) 1465593560bSBrad Bishop super(Gpio, self).__init__(**kw) 1475593560bSBrad Bishop 1485593560bSBrad Bishop def construct(self, loader, indent): 149*0f2588f2SPatrick Williams return self.render(loader, "gpio.mako.hpp", g=self, indent=indent) 1505593560bSBrad Bishop 1515593560bSBrad Bishop def setup(self, objs): 1525593560bSBrad Bishop super(Gpio, self).setup(objs) 1535593560bSBrad Bishop 1545593560bSBrad Bishop 1555593560bSBrad Bishopclass Tach(Sensor, Renderer): 156*0f2588f2SPatrick Williams """Handler for method:type:tach.""" 1575593560bSBrad Bishop 1585593560bSBrad Bishop def __init__(self, *a, **kw): 159*0f2588f2SPatrick Williams self.sensors = kw.pop("sensors") 160*0f2588f2SPatrick Williams kw["name"] = "tach-{}".format("-".join(self.sensors)) 1615593560bSBrad Bishop super(Tach, self).__init__(**kw) 1625593560bSBrad Bishop 1635593560bSBrad Bishop def construct(self, loader, indent): 164*0f2588f2SPatrick Williams return self.render(loader, "tach.mako.hpp", t=self, indent=indent) 1655593560bSBrad Bishop 1665593560bSBrad Bishop def setup(self, objs): 1675593560bSBrad Bishop super(Tach, self).setup(objs) 1685593560bSBrad Bishop 1695593560bSBrad Bishop 1705593560bSBrad Bishopclass Rpolicy(ConfigEntry): 171*0f2588f2SPatrick Williams """Convenience type for config file rpolicy:type handlers.""" 1725593560bSBrad Bishop 1735593560bSBrad Bishop def __init__(self, *a, **kw): 174*0f2588f2SPatrick Williams kw.pop("type", None) 175*0f2588f2SPatrick Williams self.fan = kw.pop("fan") 1765593560bSBrad Bishop self.sensors = [] 177*0f2588f2SPatrick Williams kw["class"] = "policy" 1785593560bSBrad Bishop super(Rpolicy, self).__init__(**kw) 1795593560bSBrad Bishop 1805593560bSBrad Bishop def setup(self, objs): 181*0f2588f2SPatrick Williams """All policies have an associated fan and methods. 182*0f2588f2SPatrick Williams Resolve the indices.""" 1835593560bSBrad Bishop 1845593560bSBrad Bishop sensors = [] 1855593560bSBrad Bishop for s in self.sensors: 186*0f2588f2SPatrick Williams sensors.append(get_index(objs, "sensor", s)) 1875593560bSBrad Bishop 1885593560bSBrad Bishop self.sensors = sensors 189*0f2588f2SPatrick Williams self.fan = get_index(objs, "fan", self.fan) 1905593560bSBrad Bishop 1915593560bSBrad Bishop 192fcbedca0SBrad Bishopclass AnyOf(Rpolicy, Renderer): 193*0f2588f2SPatrick Williams """Default policy handler (policy:type:anyof).""" 194fcbedca0SBrad Bishop 195fcbedca0SBrad Bishop def __init__(self, *a, **kw): 196*0f2588f2SPatrick Williams kw["name"] = "anyof-{}".format(kw["fan"]) 197fcbedca0SBrad Bishop super(AnyOf, self).__init__(**kw) 198fcbedca0SBrad Bishop 199fcbedca0SBrad Bishop def setup(self, objs): 200fcbedca0SBrad Bishop super(AnyOf, self).setup(objs) 201fcbedca0SBrad Bishop 202fcbedca0SBrad Bishop def construct(self, loader, indent): 203*0f2588f2SPatrick Williams return self.render(loader, "anyof.mako.hpp", f=self, indent=indent) 204fcbedca0SBrad Bishop 205fcbedca0SBrad Bishop 2065593560bSBrad Bishopclass Fallback(Rpolicy, Renderer): 207*0f2588f2SPatrick Williams """Fallback policy handler (policy:type:fallback).""" 2085593560bSBrad Bishop 2095593560bSBrad Bishop def __init__(self, *a, **kw): 210*0f2588f2SPatrick Williams kw["name"] = "fallback-{}".format(kw["fan"]) 2115593560bSBrad Bishop super(Fallback, self).__init__(**kw) 2125593560bSBrad Bishop 2135593560bSBrad Bishop def setup(self, objs): 2145593560bSBrad Bishop super(Fallback, self).setup(objs) 2155593560bSBrad Bishop 2165593560bSBrad Bishop def construct(self, loader, indent): 217*0f2588f2SPatrick Williams return self.render(loader, "fallback.mako.hpp", f=self, indent=indent) 2185593560bSBrad Bishop 2195593560bSBrad Bishop 2205593560bSBrad Bishopclass Fan(ConfigEntry): 221*0f2588f2SPatrick Williams """Fan directive handler. Fans entries consist of an inventory path, 222*0f2588f2SPatrick Williams optional redundancy policy and associated sensors.""" 2235593560bSBrad Bishop 2245593560bSBrad Bishop def __init__(self, *a, **kw): 225*0f2588f2SPatrick Williams self.path = kw.pop("path") 226*0f2588f2SPatrick Williams self.methods = kw.pop("methods") 227*0f2588f2SPatrick Williams self.rpolicy = kw.pop("rpolicy", None) 2285593560bSBrad Bishop super(Fan, self).__init__(**kw) 2295593560bSBrad Bishop 2305593560bSBrad Bishop def factory(self, objs): 231*0f2588f2SPatrick Williams """Create rpolicy and sensor(s) objects.""" 2325593560bSBrad Bishop 2335593560bSBrad Bishop if self.rpolicy: 234*0f2588f2SPatrick Williams self.rpolicy["fan"] = self.name 235*0f2588f2SPatrick Williams factory = Everything.classmap(self.rpolicy["type"]) 2365593560bSBrad Bishop rpolicy = factory(**self.rpolicy) 2375593560bSBrad Bishop else: 238fcbedca0SBrad Bishop rpolicy = AnyOf(fan=self.name) 2395593560bSBrad Bishop 2405593560bSBrad Bishop for m in self.methods: 241*0f2588f2SPatrick Williams m["policy"] = rpolicy.name 242*0f2588f2SPatrick Williams factory = Everything.classmap(m["type"]) 2435593560bSBrad Bishop sensor = factory(**m) 2445593560bSBrad Bishop rpolicy.sensors.append(sensor.name) 2455593560bSBrad Bishop add_unique(sensor, objs) 2465593560bSBrad Bishop 2475593560bSBrad Bishop add_unique(rpolicy, objs) 2485593560bSBrad Bishop super(Fan, self).factory(objs) 2495593560bSBrad Bishop 2505593560bSBrad Bishop 2515593560bSBrad Bishopclass Everything(Renderer): 252*0f2588f2SPatrick Williams """Parse/render entry point.""" 2535593560bSBrad Bishop 2545593560bSBrad Bishop @staticmethod 2555593560bSBrad Bishop def classmap(cls): 256*0f2588f2SPatrick Williams """Map render item class entries to the appropriate 257*0f2588f2SPatrick Williams handler methods.""" 2585593560bSBrad Bishop 2595593560bSBrad Bishop class_map = { 260*0f2588f2SPatrick Williams "anyof": AnyOf, 261*0f2588f2SPatrick Williams "fan": Fan, 262*0f2588f2SPatrick Williams "fallback": Fallback, 263*0f2588f2SPatrick Williams "gpio": Gpio, 264*0f2588f2SPatrick Williams "tach": Tach, 2655593560bSBrad Bishop } 2665593560bSBrad Bishop 2675593560bSBrad Bishop if cls not in class_map: 2685593560bSBrad Bishop raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 2695593560bSBrad Bishop 2705593560bSBrad Bishop return class_map[cls] 2715593560bSBrad Bishop 2725593560bSBrad Bishop @staticmethod 2735593560bSBrad Bishop def load(args): 274*0f2588f2SPatrick Williams """Load the configuration file. Parsing occurs in three phases. 2755593560bSBrad Bishop In the first phase a factory method associated with each 2765593560bSBrad Bishop configuration file directive is invoked. These factory 2775593560bSBrad Bishop methods generate more factory methods. In the second 2785593560bSBrad Bishop phase the factory methods created in the first phase 2795593560bSBrad Bishop are invoked. In the last phase a callback is invoked on 2805593560bSBrad Bishop each object created in phase two. Typically the callback 281*0f2588f2SPatrick Williams resolves references to other configuration file directives.""" 2825593560bSBrad Bishop 2835593560bSBrad Bishop factory_objs = {} 2845593560bSBrad Bishop objs = {} 285*0f2588f2SPatrick Williams with open(args.input, "r") as fd: 2865593560bSBrad Bishop for x in yaml.safe_load(fd.read()) or {}: 2875593560bSBrad Bishop # The top level elements all represent fans. 288*0f2588f2SPatrick Williams x["class"] = "fan" 2895593560bSBrad Bishop # Create factory object for this config file directive. 290*0f2588f2SPatrick Williams factory = Everything.classmap(x["class"]) 2915593560bSBrad Bishop obj = factory(**x) 2925593560bSBrad Bishop 2935593560bSBrad Bishop # For a given class of directive, validate the file 2945593560bSBrad Bishop # doesn't have any duplicate names. 2955593560bSBrad Bishop if exists(factory_objs, obj.cls, obj.name): 296*0f2588f2SPatrick Williams raise NotUniqueError(args.input, "fan", obj.name) 2975593560bSBrad Bishop 298*0f2588f2SPatrick Williams factory_objs.setdefault("fan", []).append(obj) 299*0f2588f2SPatrick Williams objs.setdefault("fan", []).append(obj) 3005593560bSBrad Bishop 3019dc3e0dfSMatthew Barth for cls, items in list(factory_objs.items()): 3025593560bSBrad Bishop for obj in items: 3035593560bSBrad Bishop # Add objects for template consumption. 3045593560bSBrad Bishop obj.factory(objs) 3055593560bSBrad Bishop 3065593560bSBrad Bishop # Configuration file directives reference each other via 3075593560bSBrad Bishop # the name attribute; however, when rendered the reference 3085593560bSBrad Bishop # is just an array index. 3095593560bSBrad Bishop # 3105593560bSBrad Bishop # At this point all objects have been created but references 3119a7688a4SGunnar Mills # have not been resolved to array indices. Instruct objects 3125593560bSBrad Bishop # to do that now. 3139dc3e0dfSMatthew Barth for cls, items in list(objs.items()): 3145593560bSBrad Bishop for obj in items: 3155593560bSBrad Bishop obj.setup(objs) 3165593560bSBrad Bishop 3175593560bSBrad Bishop return Everything(**objs) 3185593560bSBrad Bishop 3195593560bSBrad Bishop def __init__(self, *a, **kw): 320*0f2588f2SPatrick Williams self.fans = kw.pop("fan", []) 321*0f2588f2SPatrick Williams self.policies = kw.pop("policy", []) 322*0f2588f2SPatrick Williams self.sensors = kw.pop("sensor", []) 3235593560bSBrad Bishop super(Everything, self).__init__(**kw) 3245593560bSBrad Bishop 3255593560bSBrad Bishop def generate_cpp(self, loader): 326*0f2588f2SPatrick Williams """Render the template with the provided data.""" 3275593560bSBrad Bishop sys.stdout.write( 3285593560bSBrad Bishop self.render( 3295593560bSBrad Bishop loader, 3305593560bSBrad Bishop args.template, 3315593560bSBrad Bishop fans=self.fans, 3325593560bSBrad Bishop sensors=self.sensors, 3335593560bSBrad Bishop policies=self.policies, 334*0f2588f2SPatrick Williams indent=Indent(), 335*0f2588f2SPatrick Williams ) 336*0f2588f2SPatrick Williams ) 3375593560bSBrad Bishop 338*0f2588f2SPatrick Williams 339*0f2588f2SPatrick Williamsif __name__ == "__main__": 3405593560bSBrad Bishop script_dir = os.path.dirname(os.path.realpath(__file__)) 3415593560bSBrad Bishop valid_commands = { 342*0f2588f2SPatrick Williams "generate-cpp": "generate_cpp", 3435593560bSBrad Bishop } 3445593560bSBrad Bishop 3455593560bSBrad Bishop parser = ArgumentParser( 346*0f2588f2SPatrick Williams description=( 347*0f2588f2SPatrick Williams "Phosphor Fan Presence (PFP) YAML scanner and code generator." 348*0f2588f2SPatrick Williams ) 349*0f2588f2SPatrick Williams ) 3505593560bSBrad Bishop 3515593560bSBrad Bishop parser.add_argument( 352*0f2588f2SPatrick Williams "-i", 353*0f2588f2SPatrick Williams "--input", 354*0f2588f2SPatrick Williams dest="input", 355*0f2588f2SPatrick Williams default=os.path.join(script_dir, "example", "example.yaml"), 356*0f2588f2SPatrick Williams help="Location of config file to process.", 357*0f2588f2SPatrick Williams ) 3585593560bSBrad Bishop parser.add_argument( 359*0f2588f2SPatrick Williams "-t", 360*0f2588f2SPatrick Williams "--template", 361*0f2588f2SPatrick Williams dest="template", 362*0f2588f2SPatrick Williams default="generated.mako.hpp", 363*0f2588f2SPatrick Williams help="The top level template to render.", 364*0f2588f2SPatrick Williams ) 3655593560bSBrad Bishop parser.add_argument( 366*0f2588f2SPatrick Williams "-p", 367*0f2588f2SPatrick Williams "--template-path", 368*0f2588f2SPatrick Williams dest="template_search", 369*0f2588f2SPatrick Williams default=os.path.join(script_dir, "templates"), 370*0f2588f2SPatrick Williams help="The space delimited mako template search path.", 371*0f2588f2SPatrick Williams ) 3725593560bSBrad Bishop parser.add_argument( 373*0f2588f2SPatrick Williams "command", 374*0f2588f2SPatrick Williams metavar="COMMAND", 375*0f2588f2SPatrick Williams type=str, 3769dc3e0dfSMatthew Barth choices=list(valid_commands.keys()), 377*0f2588f2SPatrick Williams help="%s." % " | ".join(list(valid_commands.keys())), 378*0f2588f2SPatrick Williams ) 3795593560bSBrad Bishop 3805593560bSBrad Bishop args = parser.parse_args() 3815593560bSBrad Bishop 3825593560bSBrad Bishop if sys.version_info < (3, 0): 3835593560bSBrad Bishop lookup = mako.lookup.TemplateLookup( 384*0f2588f2SPatrick Williams directories=args.template_search.split(), disable_unicode=True 385*0f2588f2SPatrick Williams ) 3865593560bSBrad Bishop else: 3875593560bSBrad Bishop lookup = mako.lookup.TemplateLookup( 388*0f2588f2SPatrick Williams directories=args.template_search.split() 389*0f2588f2SPatrick Williams ) 3905593560bSBrad Bishop try: 391*0f2588f2SPatrick Williams function = getattr(Everything.load(args), valid_commands[args.command]) 3925593560bSBrad Bishop function(lookup) 3935593560bSBrad Bishop except InvalidConfigError as e: 394*0f2588f2SPatrick Williams sys.stderr.write("{0}: {1}\n\n".format(e.config, e.msg)) 3955593560bSBrad Bishop raise 396