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