15593560bSBrad Bishop#!/usr/bin/env python 25593560bSBrad Bishop 35593560bSBrad Bishop''' 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. 205593560bSBrad Bishop''' 215593560bSBrad Bishop 225593560bSBrad Bishopimport os 235593560bSBrad Bishopimport sys 245593560bSBrad Bishopimport yaml 255593560bSBrad Bishopfrom argparse import ArgumentParser 265593560bSBrad Bishopimport mako.lookup 275593560bSBrad Bishopfrom sdbusplus.renderer import Renderer 285593560bSBrad Bishopfrom sdbusplus.namedelement import NamedElement 295593560bSBrad Bishop 305593560bSBrad Bishop 315593560bSBrad Bishopclass InvalidConfigError(BaseException): 325593560bSBrad Bishop '''General purpose config file parsing error.''' 335593560bSBrad Bishop 345593560bSBrad Bishop def __init__(self, path, msg): 355593560bSBrad Bishop '''Display configuration file with the syntax 365593560bSBrad Bishop error and the error message.''' 375593560bSBrad Bishop 385593560bSBrad Bishop self.config = path 395593560bSBrad Bishop self.msg = msg 405593560bSBrad Bishop 415593560bSBrad Bishop 425593560bSBrad Bishopclass NotUniqueError(InvalidConfigError): 435593560bSBrad Bishop '''Within a config file names must be unique. 445593560bSBrad Bishop Display the duplicate item.''' 455593560bSBrad Bishop 465593560bSBrad Bishop def __init__(self, path, cls, *names): 475593560bSBrad Bishop fmt = 'Duplicate {0}: "{1}"' 485593560bSBrad Bishop super(NotUniqueError, self).__init__( 495593560bSBrad Bishop path, fmt.format(cls, ' '.join(names))) 505593560bSBrad Bishop 515593560bSBrad Bishop 525593560bSBrad Bishopdef get_index(objs, cls, name): 535593560bSBrad Bishop '''Items are usually rendered as C++ arrays and as 545593560bSBrad Bishop such are stored in python lists. Given an item name 555593560bSBrad Bishop its class, find the item index.''' 565593560bSBrad Bishop 575593560bSBrad Bishop for i, x in enumerate(objs.get(cls, [])): 585593560bSBrad Bishop if x.name != name: 595593560bSBrad Bishop continue 605593560bSBrad Bishop 615593560bSBrad Bishop return i 625593560bSBrad Bishop raise InvalidConfigError('Could not find name: "{0}"'.format(name)) 635593560bSBrad Bishop 645593560bSBrad Bishop 655593560bSBrad Bishopdef exists(objs, cls, name): 665593560bSBrad Bishop '''Check to see if an item already exists in a list given 675593560bSBrad Bishop the item name.''' 685593560bSBrad Bishop 695593560bSBrad Bishop try: 705593560bSBrad Bishop get_index(objs, cls, name) 715593560bSBrad Bishop except: 725593560bSBrad Bishop return False 735593560bSBrad Bishop 745593560bSBrad Bishop return True 755593560bSBrad Bishop 765593560bSBrad Bishop 775593560bSBrad Bishopdef add_unique(obj, *a, **kw): 785593560bSBrad Bishop '''Add an item to one or more lists unless already present.''' 795593560bSBrad Bishop 805593560bSBrad Bishop for container in a: 815593560bSBrad Bishop if not exists(container, obj.cls, obj.name): 825593560bSBrad Bishop container.setdefault(obj.cls, []).append(obj) 835593560bSBrad Bishop 845593560bSBrad Bishop 855593560bSBrad Bishopclass Indent(object): 865593560bSBrad Bishop '''Help templates be depth agnostic.''' 875593560bSBrad Bishop 885593560bSBrad Bishop def __init__(self, depth=0): 895593560bSBrad Bishop self.depth = depth 905593560bSBrad Bishop 915593560bSBrad Bishop def __add__(self, depth): 925593560bSBrad Bishop return Indent(self.depth + depth) 935593560bSBrad Bishop 945593560bSBrad Bishop def __call__(self, depth): 955593560bSBrad Bishop '''Render an indent at the current depth plus depth.''' 965593560bSBrad Bishop return 4*' '*(depth + self.depth) 975593560bSBrad Bishop 985593560bSBrad Bishop 995593560bSBrad Bishopclass ConfigEntry(NamedElement): 1005593560bSBrad Bishop '''Base interface for rendered items.''' 1015593560bSBrad Bishop 1025593560bSBrad Bishop def __init__(self, *a, **kw): 1035593560bSBrad Bishop '''Pop the class keyword.''' 1045593560bSBrad Bishop 1055593560bSBrad Bishop self.cls = kw.pop('class') 1065593560bSBrad Bishop super(ConfigEntry, self).__init__(**kw) 1075593560bSBrad Bishop 1085593560bSBrad Bishop def factory(self, objs): 1095593560bSBrad Bishop ''' Optional factory interface for subclasses to add 1105593560bSBrad Bishop additional items to be rendered.''' 1115593560bSBrad Bishop 1125593560bSBrad Bishop pass 1135593560bSBrad Bishop 1145593560bSBrad Bishop def setup(self, objs): 1155593560bSBrad Bishop ''' Optional setup interface for subclasses, invoked 1165593560bSBrad Bishop after all factory methods have been run.''' 1175593560bSBrad Bishop 1185593560bSBrad Bishop pass 1195593560bSBrad Bishop 1205593560bSBrad Bishop 1215593560bSBrad Bishopclass Sensor(ConfigEntry): 1225593560bSBrad Bishop '''Convenience type for config file method:type handlers.''' 1235593560bSBrad Bishop 1245593560bSBrad Bishop def __init__(self, *a, **kw): 1255593560bSBrad Bishop kw['class'] = 'sensor' 1265593560bSBrad Bishop kw.pop('type') 1275593560bSBrad Bishop self.policy = kw.pop('policy') 1285593560bSBrad Bishop super(Sensor, self).__init__(**kw) 1295593560bSBrad Bishop 1305593560bSBrad Bishop def setup(self, objs): 1315593560bSBrad Bishop '''All sensors have an associated policy. Get the policy index.''' 1325593560bSBrad Bishop 1335593560bSBrad Bishop self.policy = get_index(objs, 'policy', self.policy) 1345593560bSBrad Bishop 1355593560bSBrad Bishop 1365593560bSBrad Bishopclass Gpio(Sensor, Renderer): 1375593560bSBrad Bishop '''Handler for method:type:gpio.''' 1385593560bSBrad Bishop 1395593560bSBrad Bishop def __init__(self, *a, **kw): 1405593560bSBrad Bishop self.key = kw.pop('key') 1415593560bSBrad Bishop self.physpath = kw.pop('physpath') 1422e9788d7SBrad Bishop self.devpath = kw.pop('devpath') 1435593560bSBrad Bishop kw['name'] = 'gpio-{}'.format(self.key) 1445593560bSBrad Bishop super(Gpio, self).__init__(**kw) 1455593560bSBrad Bishop 1465593560bSBrad Bishop def construct(self, loader, indent): 1475593560bSBrad Bishop return self.render( 1485593560bSBrad Bishop loader, 1495593560bSBrad Bishop 'gpio.mako.hpp', 1505593560bSBrad Bishop g=self, 1515593560bSBrad Bishop indent=indent) 1525593560bSBrad Bishop 1535593560bSBrad Bishop def setup(self, objs): 1545593560bSBrad Bishop super(Gpio, self).setup(objs) 1555593560bSBrad Bishop 1565593560bSBrad Bishop 1575593560bSBrad Bishopclass Tach(Sensor, Renderer): 1585593560bSBrad Bishop '''Handler for method:type:tach.''' 1595593560bSBrad Bishop 1605593560bSBrad Bishop def __init__(self, *a, **kw): 1615593560bSBrad Bishop self.sensors = kw.pop('sensors') 1625593560bSBrad Bishop kw['name'] = 'tach-{}'.format('-'.join(self.sensors)) 1635593560bSBrad Bishop super(Tach, self).__init__(**kw) 1645593560bSBrad Bishop 1655593560bSBrad Bishop def construct(self, loader, indent): 1665593560bSBrad Bishop return self.render( 1675593560bSBrad Bishop loader, 1685593560bSBrad Bishop 'tach.mako.hpp', 1695593560bSBrad Bishop t=self, 1705593560bSBrad Bishop indent=indent) 1715593560bSBrad Bishop 1725593560bSBrad Bishop def setup(self, objs): 1735593560bSBrad Bishop super(Tach, self).setup(objs) 1745593560bSBrad Bishop 1755593560bSBrad Bishop 1765593560bSBrad Bishopclass Rpolicy(ConfigEntry): 1775593560bSBrad Bishop '''Convenience type for config file rpolicy:type handlers.''' 1785593560bSBrad Bishop 1795593560bSBrad Bishop def __init__(self, *a, **kw): 1805593560bSBrad Bishop kw.pop('type', None) 1815593560bSBrad Bishop self.fan = kw.pop('fan') 1825593560bSBrad Bishop self.sensors = [] 1835593560bSBrad Bishop kw['class'] = 'policy' 1845593560bSBrad Bishop super(Rpolicy, self).__init__(**kw) 1855593560bSBrad Bishop 1865593560bSBrad Bishop def setup(self, objs): 1875593560bSBrad Bishop '''All policies have an associated fan and methods. 1889a7688a4SGunnar Mills Resolve the indices.''' 1895593560bSBrad Bishop 1905593560bSBrad Bishop sensors = [] 1915593560bSBrad Bishop for s in self.sensors: 1925593560bSBrad Bishop sensors.append(get_index(objs, 'sensor', s)) 1935593560bSBrad Bishop 1945593560bSBrad Bishop self.sensors = sensors 1955593560bSBrad Bishop self.fan = get_index(objs, 'fan', self.fan) 1965593560bSBrad Bishop 1975593560bSBrad Bishop 198fcbedca0SBrad Bishopclass AnyOf(Rpolicy, Renderer): 199fcbedca0SBrad Bishop '''Default policy handler (policy:type:anyof).''' 200fcbedca0SBrad Bishop 201fcbedca0SBrad Bishop def __init__(self, *a, **kw): 202fcbedca0SBrad Bishop kw['name'] = 'anyof-{}'.format(kw['fan']) 203fcbedca0SBrad Bishop super(AnyOf, self).__init__(**kw) 204fcbedca0SBrad Bishop 205fcbedca0SBrad Bishop def setup(self, objs): 206fcbedca0SBrad Bishop super(AnyOf, self).setup(objs) 207fcbedca0SBrad Bishop 208fcbedca0SBrad Bishop def construct(self, loader, indent): 209fcbedca0SBrad Bishop return self.render( 210fcbedca0SBrad Bishop loader, 211fcbedca0SBrad Bishop 'anyof.mako.hpp', 212fcbedca0SBrad Bishop f=self, 213fcbedca0SBrad Bishop indent=indent) 214fcbedca0SBrad Bishop 215fcbedca0SBrad Bishop 2165593560bSBrad Bishopclass Fallback(Rpolicy, Renderer): 217fcbedca0SBrad Bishop '''Fallback policy handler (policy:type:fallback).''' 2185593560bSBrad Bishop 2195593560bSBrad Bishop def __init__(self, *a, **kw): 2205593560bSBrad Bishop kw['name'] = 'fallback-{}'.format(kw['fan']) 2215593560bSBrad Bishop super(Fallback, self).__init__(**kw) 2225593560bSBrad Bishop 2235593560bSBrad Bishop def setup(self, objs): 2245593560bSBrad Bishop super(Fallback, self).setup(objs) 2255593560bSBrad Bishop 2265593560bSBrad Bishop def construct(self, loader, indent): 2275593560bSBrad Bishop return self.render( 2285593560bSBrad Bishop loader, 2295593560bSBrad Bishop 'fallback.mako.hpp', 2305593560bSBrad Bishop f=self, 2315593560bSBrad Bishop indent=indent) 2325593560bSBrad Bishop 2335593560bSBrad Bishop 2345593560bSBrad Bishopclass Fan(ConfigEntry): 2355593560bSBrad Bishop '''Fan directive handler. Fans entries consist of an inventory path, 2365593560bSBrad Bishop optional redundancy policy and associated sensors.''' 2375593560bSBrad Bishop 2385593560bSBrad Bishop def __init__(self, *a, **kw): 2395593560bSBrad Bishop self.path = kw.pop('path') 2405593560bSBrad Bishop self.methods = kw.pop('methods') 2415593560bSBrad Bishop self.rpolicy = kw.pop('rpolicy', None) 2425593560bSBrad Bishop super(Fan, self).__init__(**kw) 2435593560bSBrad Bishop 2445593560bSBrad Bishop def factory(self, objs): 2455593560bSBrad Bishop ''' Create rpolicy and sensor(s) objects.''' 2465593560bSBrad Bishop 2475593560bSBrad Bishop if self.rpolicy: 2485593560bSBrad Bishop self.rpolicy['fan'] = self.name 2495593560bSBrad Bishop factory = Everything.classmap(self.rpolicy['type']) 2505593560bSBrad Bishop rpolicy = factory(**self.rpolicy) 2515593560bSBrad Bishop else: 252fcbedca0SBrad Bishop rpolicy = AnyOf(fan=self.name) 2535593560bSBrad Bishop 2545593560bSBrad Bishop for m in self.methods: 2555593560bSBrad Bishop m['policy'] = rpolicy.name 2565593560bSBrad Bishop factory = Everything.classmap(m['type']) 2575593560bSBrad Bishop sensor = factory(**m) 2585593560bSBrad Bishop rpolicy.sensors.append(sensor.name) 2595593560bSBrad Bishop add_unique(sensor, objs) 2605593560bSBrad Bishop 2615593560bSBrad Bishop add_unique(rpolicy, objs) 2625593560bSBrad Bishop super(Fan, self).factory(objs) 2635593560bSBrad Bishop 2645593560bSBrad Bishop 2655593560bSBrad Bishopclass Everything(Renderer): 2665593560bSBrad Bishop '''Parse/render entry point.''' 2675593560bSBrad Bishop 2685593560bSBrad Bishop @staticmethod 2695593560bSBrad Bishop def classmap(cls): 2705593560bSBrad Bishop '''Map render item class entries to the appropriate 2715593560bSBrad Bishop handler methods.''' 2725593560bSBrad Bishop 2735593560bSBrad Bishop class_map = { 274fcbedca0SBrad Bishop 'anyof': AnyOf, 2755593560bSBrad Bishop 'fan': Fan, 2765593560bSBrad Bishop 'fallback': Fallback, 2775593560bSBrad Bishop 'gpio': Gpio, 2785593560bSBrad Bishop 'tach': Tach, 2795593560bSBrad Bishop } 2805593560bSBrad Bishop 2815593560bSBrad Bishop if cls not in class_map: 2825593560bSBrad Bishop raise NotImplementedError('Unknown class: "{0}"'.format(cls)) 2835593560bSBrad Bishop 2845593560bSBrad Bishop return class_map[cls] 2855593560bSBrad Bishop 2865593560bSBrad Bishop @staticmethod 2875593560bSBrad Bishop def load(args): 2885593560bSBrad Bishop '''Load the configuration file. Parsing occurs in three phases. 2895593560bSBrad Bishop In the first phase a factory method associated with each 2905593560bSBrad Bishop configuration file directive is invoked. These factory 2915593560bSBrad Bishop methods generate more factory methods. In the second 2925593560bSBrad Bishop phase the factory methods created in the first phase 2935593560bSBrad Bishop are invoked. In the last phase a callback is invoked on 2945593560bSBrad Bishop each object created in phase two. Typically the callback 2955593560bSBrad Bishop resolves references to other configuration file directives.''' 2965593560bSBrad Bishop 2975593560bSBrad Bishop factory_objs = {} 2985593560bSBrad Bishop objs = {} 2995593560bSBrad Bishop with open(args.input, 'r') as fd: 3005593560bSBrad Bishop for x in yaml.safe_load(fd.read()) or {}: 3015593560bSBrad Bishop 3025593560bSBrad Bishop # The top level elements all represent fans. 3035593560bSBrad Bishop x['class'] = 'fan' 3045593560bSBrad Bishop # Create factory object for this config file directive. 3055593560bSBrad Bishop factory = Everything.classmap(x['class']) 3065593560bSBrad Bishop obj = factory(**x) 3075593560bSBrad Bishop 3085593560bSBrad Bishop # For a given class of directive, validate the file 3095593560bSBrad Bishop # doesn't have any duplicate names. 3105593560bSBrad Bishop if exists(factory_objs, obj.cls, obj.name): 3115593560bSBrad Bishop raise NotUniqueError(args.input, 'fan', obj.name) 3125593560bSBrad Bishop 3135593560bSBrad Bishop factory_objs.setdefault('fan', []).append(obj) 3145593560bSBrad Bishop objs.setdefault('fan', []).append(obj) 3155593560bSBrad Bishop 316*9dc3e0dfSMatthew Barth for cls, items in list(factory_objs.items()): 3175593560bSBrad Bishop for obj in items: 3185593560bSBrad Bishop # Add objects for template consumption. 3195593560bSBrad Bishop obj.factory(objs) 3205593560bSBrad Bishop 3215593560bSBrad Bishop # Configuration file directives reference each other via 3225593560bSBrad Bishop # the name attribute; however, when rendered the reference 3235593560bSBrad Bishop # is just an array index. 3245593560bSBrad Bishop # 3255593560bSBrad Bishop # At this point all objects have been created but references 3269a7688a4SGunnar Mills # have not been resolved to array indices. Instruct objects 3275593560bSBrad Bishop # to do that now. 328*9dc3e0dfSMatthew Barth for cls, items in list(objs.items()): 3295593560bSBrad Bishop for obj in items: 3305593560bSBrad Bishop obj.setup(objs) 3315593560bSBrad Bishop 3325593560bSBrad Bishop return Everything(**objs) 3335593560bSBrad Bishop 3345593560bSBrad Bishop def __init__(self, *a, **kw): 3355593560bSBrad Bishop self.fans = kw.pop('fan', []) 3365593560bSBrad Bishop self.policies = kw.pop('policy', []) 3375593560bSBrad Bishop self.sensors = kw.pop('sensor', []) 3385593560bSBrad Bishop super(Everything, self).__init__(**kw) 3395593560bSBrad Bishop 3405593560bSBrad Bishop def generate_cpp(self, loader): 3415593560bSBrad Bishop '''Render the template with the provided data.''' 3425593560bSBrad Bishop sys.stdout.write( 3435593560bSBrad Bishop self.render( 3445593560bSBrad Bishop loader, 3455593560bSBrad Bishop args.template, 3465593560bSBrad Bishop fans=self.fans, 3475593560bSBrad Bishop sensors=self.sensors, 3485593560bSBrad Bishop policies=self.policies, 3495593560bSBrad Bishop indent=Indent())) 3505593560bSBrad Bishop 3515593560bSBrad Bishopif __name__ == '__main__': 3525593560bSBrad Bishop script_dir = os.path.dirname(os.path.realpath(__file__)) 3535593560bSBrad Bishop valid_commands = { 3545593560bSBrad Bishop 'generate-cpp': 'generate_cpp', 3555593560bSBrad Bishop } 3565593560bSBrad Bishop 3575593560bSBrad Bishop parser = ArgumentParser( 3585593560bSBrad Bishop description='Phosphor Fan Presence (PFP) YAML ' 3595593560bSBrad Bishop 'scanner and code generator.') 3605593560bSBrad Bishop 3615593560bSBrad Bishop parser.add_argument( 3625593560bSBrad Bishop '-i', '--input', dest='input', 3635593560bSBrad Bishop default=os.path.join(script_dir, 'example', 'example.yaml'), 3645593560bSBrad Bishop help='Location of config file to process.') 3655593560bSBrad Bishop parser.add_argument( 3665593560bSBrad Bishop '-t', '--template', dest='template', 3675593560bSBrad Bishop default='generated.mako.hpp', 3685593560bSBrad Bishop help='The top level template to render.') 3695593560bSBrad Bishop parser.add_argument( 3705593560bSBrad Bishop '-p', '--template-path', dest='template_search', 3715593560bSBrad Bishop default=os.path.join(script_dir, 'templates'), 3725593560bSBrad Bishop help='The space delimited mako template search path.') 3735593560bSBrad Bishop parser.add_argument( 3745593560bSBrad Bishop 'command', metavar='COMMAND', type=str, 375*9dc3e0dfSMatthew Barth choices=list(valid_commands.keys()), 376*9dc3e0dfSMatthew Barth help='%s.' % ' | '.join(list(valid_commands.keys()))) 3775593560bSBrad Bishop 3785593560bSBrad Bishop args = parser.parse_args() 3795593560bSBrad Bishop 3805593560bSBrad Bishop if sys.version_info < (3, 0): 3815593560bSBrad Bishop lookup = mako.lookup.TemplateLookup( 3825593560bSBrad Bishop directories=args.template_search.split(), 3835593560bSBrad Bishop disable_unicode=True) 3845593560bSBrad Bishop else: 3855593560bSBrad Bishop lookup = mako.lookup.TemplateLookup( 3865593560bSBrad Bishop directories=args.template_search.split()) 3875593560bSBrad Bishop try: 3885593560bSBrad Bishop function = getattr( 3895593560bSBrad Bishop Everything.load(args), 3905593560bSBrad Bishop valid_commands[args.command]) 3915593560bSBrad Bishop function(lookup) 3925593560bSBrad Bishop except InvalidConfigError as e: 3935593560bSBrad Bishop sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg)) 3945593560bSBrad Bishop raise 395