diff options
author | Brad Bishop <bradleyb@fuzziesquirrel.com> | 2017-06-13 13:31:24 -0400 |
---|---|---|
committer | Patrick Williams <patrick@stwcx.xyz> | 2017-08-02 20:18:19 +0000 |
commit | 5593560b1e1a7785a491d4650c4f3f61ffdaba90 (patch) | |
tree | 86ea09c93fb2632c0b50ef23b806868e88d99b71 /presence/pfpgen.py | |
parent | bfb8160710d95d208f7cb1565a97c2a909459d0c (diff) | |
download | phosphor-fan-presence-5593560b1e1a7785a491d4650c4f3f61ffdaba90.tar.gz phosphor-fan-presence-5593560b1e1a7785a491d4650c4f3f61ffdaba90.zip |
presence: New parser
Adopt an easy on the tongue acronym similar to other projects.
Add a robust parser with support for sensors and policies.
Sensors: gpio, tach
Policies: fallback
Add an example yaml file.
Change-Id: I9158a0ce2a08ef6b7bb3f5d659ea0e0433af5b96
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
Diffstat (limited to 'presence/pfpgen.py')
-rwxr-xr-x | presence/pfpgen.py | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/presence/pfpgen.py b/presence/pfpgen.py new file mode 100755 index 0000000..1e22ac7 --- /dev/null +++ b/presence/pfpgen.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python + +''' +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') + 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 indicies.''' + + 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 Fallback(Rpolicy, Renderer): + '''Default 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 = Fallback(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 = { + '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 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 indicies. Instruct objects + # to do that now. + for cls, items in 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=valid_commands.keys(), + help='%s.' % ' | '.join(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 |