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 | |
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')
-rw-r--r-- | presence/example/example.yaml | 66 | ||||
-rwxr-xr-x | presence/pfpgen.py | 374 | ||||
-rw-r--r-- | presence/templates/fallback.mako.hpp | 7 | ||||
-rw-r--r-- | presence/templates/generated.mako.hpp | 78 | ||||
-rw-r--r-- | presence/templates/gpio.mako.hpp | 2 | ||||
-rw-r--r-- | presence/templates/tach.mako.hpp | 6 |
6 files changed, 533 insertions, 0 deletions
diff --git a/presence/example/example.yaml b/presence/example/example.yaml new file mode 100644 index 0000000..aea0139 --- /dev/null +++ b/presence/example/example.yaml @@ -0,0 +1,66 @@ +# Phosphor Fan Presence (PFP) example configuration file. +# +# The configuration represents an array of fans the application +# should check for presence. The name attribute is required +# and is used as the PrettyName attribute for the +# xyz.openbmc_project.Inventory.Item interface. Path is required +# and is the location where the fan inventory object will be +# created. Additional configuration directives described below +# in the examples. + +- name: Example Fan0 + description: > + 'Example fan with tach feedback detection method. + + Fans without any special presence detection hardware + can use one or more tach speed sensor feedbacks as + an indicator of presence. Listed sensors are expected to + be found in the /xyz/openbmc_project/sensors/fan_tach + namespace as required by the OpenBMC DBus API. + + Supported policy types are all_of or any_of.' + path: /system/chassis/motherboard/fan0 + methods: + - type: tach + sensors: + - fan0 + +- name: Example Fan1 + description: > + 'Example fan with gpio detection method. + + Fans with dedicated gpios can use the gpio detection + method. The gpio detection uses Linux gpio-keys: the + event number must be provided via the key property.' + path: /system/chassis/motherboard/fan1 + methods: + - type: gpio + key: 123 + physpath: /sys/devices/foo/bar + +- name: Example Fan2 + description: > + 'Example fan with fallback redundancy policy. + + Multiple detection methods for a single fan are allowed. + When multiple detection methods are provided a redundancy + algorithm must be specified with the rpolicy attribute. + + Note that the redundancy policy algorithm may or may not + factor the order the detection methods are listed into + its logic. + + The fallback algorithm falls back to subsequently listed + detection methods when the first method does not detect + a fan and the second method does.' + path: /system/chassis/motherboard/fan2 + methods: + - type: gpio + key: 124 + physpath: /sys/devices/foo/bar + - type: tach + sensors: + - fan2 + rpolicy: + type: fallback + 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 diff --git a/presence/templates/fallback.mako.hpp b/presence/templates/fallback.mako.hpp new file mode 100644 index 0000000..6469847 --- /dev/null +++ b/presence/templates/fallback.mako.hpp @@ -0,0 +1,7 @@ +std::make_unique<Fallback>( +${indent(1)}ConfigFans::get()[${f.fan}], +${indent(1)}std::vector<std::reference_wrapper<PresenceSensor>>{ +% for s in f.sensors: +${indent(2)}*ConfigSensors::get()[${s}], +% endfor +${indent(1)}})\ diff --git a/presence/templates/generated.mako.hpp b/presence/templates/generated.mako.hpp new file mode 100644 index 0000000..3afd29f --- /dev/null +++ b/presence/templates/generated.mako.hpp @@ -0,0 +1,78 @@ +## This file is a template, the comment below is emitted into the generated file +/* This is an auto generated file. Do not edit. */ +#pragma once + +#include <array> +#include <memory> +#include <string> +#include "fallback.hpp" +#include "fan.hpp" +#include "gpio.hpp" +#include "tach.hpp" + +using namespace std::string_literals; + +namespace phosphor +{ +namespace fan +{ +namespace presence +{ + +struct ConfigPolicy; + +struct ConfigSensors +{ + using Sensors = std::array<std::unique_ptr<PresenceSensor>, ${len(sensors)}>; + + static auto& get() + { + static const Sensors sensors = + { +% for s in sensors: + ${s.construct(loader, indent=indent +3)}, +% endfor + }; + return sensors; + } +}; + +struct ConfigFans +{ + using Fans = std::array<Fan, ${len(fans)}>; + + static auto& get() + { + static const Fans fans = + { + { +% for f in fans: + Fans::value_type{ + "${f.name}"s, + "${f.path}"s, + }, +% endfor + } + }; + return fans; + } +}; + +struct ConfigPolicy +{ + using Policies = std::array<std::unique_ptr<RedundancyPolicy>, ${len(policies)}>; + + static auto& get() + { + static const Policies policies = + { +% for p in policies: + ${p.construct(loader, indent=indent +3)}, +% endfor + }; + return policies; + } +}; +} // namespace presence +} // namespace fan +} // namespace phosphor diff --git a/presence/templates/gpio.mako.hpp b/presence/templates/gpio.mako.hpp new file mode 100644 index 0000000..fb1f46f --- /dev/null +++ b/presence/templates/gpio.mako.hpp @@ -0,0 +1,2 @@ +std::make_unique<PolicyAccess<Gpio, ConfigPolicy>>( +${indent(1)}${g.policy}, "${g.physpath}"s, ${g.key})\ diff --git a/presence/templates/tach.mako.hpp b/presence/templates/tach.mako.hpp new file mode 100644 index 0000000..d25a063 --- /dev/null +++ b/presence/templates/tach.mako.hpp @@ -0,0 +1,6 @@ +std::make_unique<PolicyAccess<Tach, ConfigPolicy>>( +${indent(1)}${t.policy}, std::vector<std::string>{\ +% for s in t.sensors: +"${s}",\ +% endfor +})\ |