summaryrefslogtreecommitdiffstats
path: root/presence/pfpgen.py
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/pfpgen.py
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/pfpgen.py')
-rwxr-xr-xpresence/pfpgen.py374
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
OpenPOWER on IntegriCloud