| #!/usr/bin/env python3 |
| |
| ''' |
| Phosphor Fan Presence (PFP) YAML parser and code generator. |
| |
| Parse the provided PFP configuration file and generate C++ code. |
| |
| The parser workflow is broken down as follows: |
| 1 - Import the YAML configuration file as native python type(s) |
| instance(s). |
| 2 - Create an instance of the Everything class from the |
| native python type instance(s) with the Everything.load |
| method. |
| 3 - The Everything class constructor orchestrates conversion of the |
| native python type(s) instances(s) to render helper types. |
| Each render helper type constructor imports its attributes |
| from the native python type(s) instances(s). |
| 4 - Present the converted YAML to the command processing method |
| requested by the script user. |
| ''' |
| |
| import os |
| import sys |
| import yaml |
| from argparse import ArgumentParser |
| import mako.lookup |
| from sdbusplus.renderer import Renderer |
| from sdbusplus.namedelement import NamedElement |
| |
| |
| class InvalidConfigError(BaseException): |
| '''General purpose config file parsing error.''' |
| |
| def __init__(self, path, msg): |
| '''Display configuration file with the syntax |
| error and the error message.''' |
| |
| self.config = path |
| self.msg = msg |
| |
| |
| class NotUniqueError(InvalidConfigError): |
| '''Within a config file names must be unique. |
| Display the duplicate item.''' |
| |
| def __init__(self, path, cls, *names): |
| fmt = 'Duplicate {0}: "{1}"' |
| super(NotUniqueError, self).__init__( |
| path, fmt.format(cls, ' '.join(names))) |
| |
| |
| def get_index(objs, cls, name): |
| '''Items are usually rendered as C++ arrays and as |
| such are stored in python lists. Given an item name |
| its class, find the item index.''' |
| |
| for i, x in enumerate(objs.get(cls, [])): |
| if x.name != name: |
| continue |
| |
| return i |
| raise InvalidConfigError('Could not find name: "{0}"'.format(name)) |
| |
| |
| def exists(objs, cls, name): |
| '''Check to see if an item already exists in a list given |
| the item name.''' |
| |
| try: |
| get_index(objs, cls, name) |
| except: |
| return False |
| |
| return True |
| |
| |
| def add_unique(obj, *a, **kw): |
| '''Add an item to one or more lists unless already present.''' |
| |
| for container in a: |
| if not exists(container, obj.cls, obj.name): |
| container.setdefault(obj.cls, []).append(obj) |
| |
| |
| class Indent(object): |
| '''Help templates be depth agnostic.''' |
| |
| def __init__(self, depth=0): |
| self.depth = depth |
| |
| def __add__(self, depth): |
| return Indent(self.depth + depth) |
| |
| def __call__(self, depth): |
| '''Render an indent at the current depth plus depth.''' |
| return 4*' '*(depth + self.depth) |
| |
| |
| class ConfigEntry(NamedElement): |
| '''Base interface for rendered items.''' |
| |
| def __init__(self, *a, **kw): |
| '''Pop the class keyword.''' |
| |
| self.cls = kw.pop('class') |
| super(ConfigEntry, self).__init__(**kw) |
| |
| def factory(self, objs): |
| ''' Optional factory interface for subclasses to add |
| additional items to be rendered.''' |
| |
| pass |
| |
| def setup(self, objs): |
| ''' Optional setup interface for subclasses, invoked |
| after all factory methods have been run.''' |
| |
| pass |
| |
| |
| class Sensor(ConfigEntry): |
| '''Convenience type for config file method:type handlers.''' |
| |
| def __init__(self, *a, **kw): |
| kw['class'] = 'sensor' |
| kw.pop('type') |
| self.policy = kw.pop('policy') |
| super(Sensor, self).__init__(**kw) |
| |
| def setup(self, objs): |
| '''All sensors have an associated policy. Get the policy index.''' |
| |
| self.policy = get_index(objs, 'policy', self.policy) |
| |
| |
| class Gpio(Sensor, Renderer): |
| '''Handler for method:type:gpio.''' |
| |
| def __init__(self, *a, **kw): |
| self.key = kw.pop('key') |
| self.physpath = kw.pop('physpath') |
| self.devpath = kw.pop('devpath') |
| kw['name'] = 'gpio-{}'.format(self.key) |
| super(Gpio, self).__init__(**kw) |
| |
| def construct(self, loader, indent): |
| return self.render( |
| loader, |
| 'gpio.mako.hpp', |
| g=self, |
| indent=indent) |
| |
| def setup(self, objs): |
| super(Gpio, self).setup(objs) |
| |
| |
| class Tach(Sensor, Renderer): |
| '''Handler for method:type:tach.''' |
| |
| def __init__(self, *a, **kw): |
| self.sensors = kw.pop('sensors') |
| kw['name'] = 'tach-{}'.format('-'.join(self.sensors)) |
| super(Tach, self).__init__(**kw) |
| |
| def construct(self, loader, indent): |
| return self.render( |
| loader, |
| 'tach.mako.hpp', |
| t=self, |
| indent=indent) |
| |
| def setup(self, objs): |
| super(Tach, self).setup(objs) |
| |
| |
| class Rpolicy(ConfigEntry): |
| '''Convenience type for config file rpolicy:type handlers.''' |
| |
| def __init__(self, *a, **kw): |
| kw.pop('type', None) |
| self.fan = kw.pop('fan') |
| self.sensors = [] |
| kw['class'] = 'policy' |
| super(Rpolicy, self).__init__(**kw) |
| |
| def setup(self, objs): |
| '''All policies have an associated fan and methods. |
| Resolve the indices.''' |
| |
| sensors = [] |
| for s in self.sensors: |
| sensors.append(get_index(objs, 'sensor', s)) |
| |
| self.sensors = sensors |
| self.fan = get_index(objs, 'fan', self.fan) |
| |
| |
| class AnyOf(Rpolicy, Renderer): |
| '''Default policy handler (policy:type:anyof).''' |
| |
| def __init__(self, *a, **kw): |
| kw['name'] = 'anyof-{}'.format(kw['fan']) |
| super(AnyOf, self).__init__(**kw) |
| |
| def setup(self, objs): |
| super(AnyOf, self).setup(objs) |
| |
| def construct(self, loader, indent): |
| return self.render( |
| loader, |
| 'anyof.mako.hpp', |
| f=self, |
| indent=indent) |
| |
| |
| class Fallback(Rpolicy, Renderer): |
| '''Fallback policy handler (policy:type:fallback).''' |
| |
| def __init__(self, *a, **kw): |
| kw['name'] = 'fallback-{}'.format(kw['fan']) |
| super(Fallback, self).__init__(**kw) |
| |
| def setup(self, objs): |
| super(Fallback, self).setup(objs) |
| |
| def construct(self, loader, indent): |
| return self.render( |
| loader, |
| 'fallback.mako.hpp', |
| f=self, |
| indent=indent) |
| |
| |
| class Fan(ConfigEntry): |
| '''Fan directive handler. Fans entries consist of an inventory path, |
| optional redundancy policy and associated sensors.''' |
| |
| def __init__(self, *a, **kw): |
| self.path = kw.pop('path') |
| self.methods = kw.pop('methods') |
| self.rpolicy = kw.pop('rpolicy', None) |
| super(Fan, self).__init__(**kw) |
| |
| def factory(self, objs): |
| ''' Create rpolicy and sensor(s) objects.''' |
| |
| if self.rpolicy: |
| self.rpolicy['fan'] = self.name |
| factory = Everything.classmap(self.rpolicy['type']) |
| rpolicy = factory(**self.rpolicy) |
| else: |
| rpolicy = AnyOf(fan=self.name) |
| |
| for m in self.methods: |
| m['policy'] = rpolicy.name |
| factory = Everything.classmap(m['type']) |
| sensor = factory(**m) |
| rpolicy.sensors.append(sensor.name) |
| add_unique(sensor, objs) |
| |
| add_unique(rpolicy, objs) |
| super(Fan, self).factory(objs) |
| |
| |
| class Everything(Renderer): |
| '''Parse/render entry point.''' |
| |
| @staticmethod |
| def classmap(cls): |
| '''Map render item class entries to the appropriate |
| handler methods.''' |
| |
| class_map = { |
| 'anyof': AnyOf, |
| 'fan': Fan, |
| 'fallback': Fallback, |
| 'gpio': Gpio, |
| 'tach': Tach, |
| } |
| |
| if cls not in class_map: |
| raise NotImplementedError('Unknown class: "{0}"'.format(cls)) |
| |
| return class_map[cls] |
| |
| @staticmethod |
| def load(args): |
| '''Load the configuration file. Parsing occurs in three phases. |
| In the first phase a factory method associated with each |
| configuration file directive is invoked. These factory |
| methods generate more factory methods. In the second |
| phase the factory methods created in the first phase |
| are invoked. In the last phase a callback is invoked on |
| each object created in phase two. Typically the callback |
| resolves references to other configuration file directives.''' |
| |
| factory_objs = {} |
| objs = {} |
| with open(args.input, 'r') as fd: |
| for x in yaml.safe_load(fd.read()) or {}: |
| |
| # The top level elements all represent fans. |
| x['class'] = 'fan' |
| # Create factory object for this config file directive. |
| factory = Everything.classmap(x['class']) |
| obj = factory(**x) |
| |
| # For a given class of directive, validate the file |
| # doesn't have any duplicate names. |
| if exists(factory_objs, obj.cls, obj.name): |
| raise NotUniqueError(args.input, 'fan', obj.name) |
| |
| factory_objs.setdefault('fan', []).append(obj) |
| objs.setdefault('fan', []).append(obj) |
| |
| for cls, items in list(factory_objs.items()): |
| for obj in items: |
| # Add objects for template consumption. |
| obj.factory(objs) |
| |
| # Configuration file directives reference each other via |
| # the name attribute; however, when rendered the reference |
| # is just an array index. |
| # |
| # At this point all objects have been created but references |
| # have not been resolved to array indices. Instruct objects |
| # to do that now. |
| for cls, items in list(objs.items()): |
| for obj in items: |
| obj.setup(objs) |
| |
| return Everything(**objs) |
| |
| def __init__(self, *a, **kw): |
| self.fans = kw.pop('fan', []) |
| self.policies = kw.pop('policy', []) |
| self.sensors = kw.pop('sensor', []) |
| super(Everything, self).__init__(**kw) |
| |
| def generate_cpp(self, loader): |
| '''Render the template with the provided data.''' |
| sys.stdout.write( |
| self.render( |
| loader, |
| args.template, |
| fans=self.fans, |
| sensors=self.sensors, |
| policies=self.policies, |
| indent=Indent())) |
| |
| if __name__ == '__main__': |
| script_dir = os.path.dirname(os.path.realpath(__file__)) |
| valid_commands = { |
| 'generate-cpp': 'generate_cpp', |
| } |
| |
| parser = ArgumentParser( |
| description='Phosphor Fan Presence (PFP) YAML ' |
| 'scanner and code generator.') |
| |
| parser.add_argument( |
| '-i', '--input', dest='input', |
| default=os.path.join(script_dir, 'example', 'example.yaml'), |
| help='Location of config file to process.') |
| parser.add_argument( |
| '-t', '--template', dest='template', |
| default='generated.mako.hpp', |
| help='The top level template to render.') |
| parser.add_argument( |
| '-p', '--template-path', dest='template_search', |
| default=os.path.join(script_dir, 'templates'), |
| help='The space delimited mako template search path.') |
| parser.add_argument( |
| 'command', metavar='COMMAND', type=str, |
| choices=list(valid_commands.keys()), |
| help='%s.' % ' | '.join(list(valid_commands.keys()))) |
| |
| args = parser.parse_args() |
| |
| if sys.version_info < (3, 0): |
| lookup = mako.lookup.TemplateLookup( |
| directories=args.template_search.split(), |
| disable_unicode=True) |
| else: |
| lookup = mako.lookup.TemplateLookup( |
| directories=args.template_search.split()) |
| try: |
| function = getattr( |
| Everything.load(args), |
| valid_commands[args.command]) |
| function(lookup) |
| except InvalidConfigError as e: |
| sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg)) |
| raise |