summaryrefslogtreecommitdiffstats
path: root/presence
diff options
context:
space:
mode:
authorBrad Bishop <bradleyb@fuzziesquirrel.com>2017-06-13 13:31:24 -0400
committerPatrick Williams <patrick@stwcx.xyz>2017-08-02 20:18:19 +0000
commit5593560b1e1a7785a491d4650c4f3f61ffdaba90 (patch)
tree86ea09c93fb2632c0b50ef23b806868e88d99b71 /presence
parentbfb8160710d95d208f7cb1565a97c2a909459d0c (diff)
downloadphosphor-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.yaml66
-rwxr-xr-xpresence/pfpgen.py374
-rw-r--r--presence/templates/fallback.mako.hpp7
-rw-r--r--presence/templates/generated.mako.hpp78
-rw-r--r--presence/templates/gpio.mako.hpp2
-rw-r--r--presence/templates/tach.mako.hpp6
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
+})\
OpenPOWER on IntegriCloud