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>
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
+})\