xref: /openbmc/phosphor-fan-presence/presence/pfpgen.py (revision 5593560b1e1a7785a491d4650c4f3f61ffdaba90)
1#!/usr/bin/env python
2
3'''
4Phosphor Fan Presence (PFP) YAML parser and code generator.
5
6Parse the provided PFP configuration file and generate C++ code.
7
8The parser workflow is broken down as follows:
9  1 - Import the YAML configuration file as native python type(s)
10        instance(s).
11  2 - Create an instance of the Everything class from the
12        native python type instance(s) with the Everything.load
13        method.
14  3 - The Everything class constructor orchestrates conversion of the
15        native python type(s) instances(s) to render helper types.
16        Each render helper type constructor imports its attributes
17        from the native python type(s) instances(s).
18  4 - Present the converted YAML to the command processing method
19        requested by the script user.
20'''
21
22import os
23import sys
24import yaml
25from argparse import ArgumentParser
26import mako.lookup
27from sdbusplus.renderer import Renderer
28from sdbusplus.namedelement import NamedElement
29
30
31class InvalidConfigError(BaseException):
32    '''General purpose config file parsing error.'''
33
34    def __init__(self, path, msg):
35        '''Display configuration file with the syntax
36        error and the error message.'''
37
38        self.config = path
39        self.msg = msg
40
41
42class NotUniqueError(InvalidConfigError):
43    '''Within a config file names must be unique.
44    Display the duplicate item.'''
45
46    def __init__(self, path, cls, *names):
47        fmt = 'Duplicate {0}: "{1}"'
48        super(NotUniqueError, self).__init__(
49            path, fmt.format(cls, ' '.join(names)))
50
51
52def get_index(objs, cls, name):
53    '''Items are usually rendered as C++ arrays and as
54    such are stored in python lists.  Given an item name
55    its class, find the item index.'''
56
57    for i, x in enumerate(objs.get(cls, [])):
58        if x.name != name:
59            continue
60
61        return i
62    raise InvalidConfigError('Could not find name: "{0}"'.format(name))
63
64
65def exists(objs, cls, name):
66    '''Check to see if an item already exists in a list given
67    the item name.'''
68
69    try:
70        get_index(objs, cls, name)
71    except:
72        return False
73
74    return True
75
76
77def add_unique(obj, *a, **kw):
78    '''Add an item to one or more lists unless already present.'''
79
80    for container in a:
81        if not exists(container, obj.cls, obj.name):
82            container.setdefault(obj.cls, []).append(obj)
83
84
85class Indent(object):
86    '''Help templates be depth agnostic.'''
87
88    def __init__(self, depth=0):
89        self.depth = depth
90
91    def __add__(self, depth):
92        return Indent(self.depth + depth)
93
94    def __call__(self, depth):
95        '''Render an indent at the current depth plus depth.'''
96        return 4*' '*(depth + self.depth)
97
98
99class ConfigEntry(NamedElement):
100    '''Base interface for rendered items.'''
101
102    def __init__(self, *a, **kw):
103        '''Pop the class keyword.'''
104
105        self.cls = kw.pop('class')
106        super(ConfigEntry, self).__init__(**kw)
107
108    def factory(self, objs):
109        ''' Optional factory interface for subclasses to add
110        additional items to be rendered.'''
111
112        pass
113
114    def setup(self, objs):
115        ''' Optional setup interface for subclasses, invoked
116        after all factory methods have been run.'''
117
118        pass
119
120
121class Sensor(ConfigEntry):
122    '''Convenience type for config file method:type handlers.'''
123
124    def __init__(self, *a, **kw):
125        kw['class'] = 'sensor'
126        kw.pop('type')
127        self.policy = kw.pop('policy')
128        super(Sensor, self).__init__(**kw)
129
130    def setup(self, objs):
131        '''All sensors have an associated policy.  Get the policy index.'''
132
133        self.policy = get_index(objs, 'policy', self.policy)
134
135
136class Gpio(Sensor, Renderer):
137    '''Handler for method:type:gpio.'''
138
139    def __init__(self, *a, **kw):
140        self.key = kw.pop('key')
141        self.physpath = kw.pop('physpath')
142        kw['name'] = 'gpio-{}'.format(self.key)
143        super(Gpio, self).__init__(**kw)
144
145    def construct(self, loader, indent):
146        return self.render(
147            loader,
148            'gpio.mako.hpp',
149            g=self,
150            indent=indent)
151
152    def setup(self, objs):
153        super(Gpio, self).setup(objs)
154
155
156class Tach(Sensor, Renderer):
157    '''Handler for method:type:tach.'''
158
159    def __init__(self, *a, **kw):
160        self.sensors = kw.pop('sensors')
161        kw['name'] = 'tach-{}'.format('-'.join(self.sensors))
162        super(Tach, self).__init__(**kw)
163
164    def construct(self, loader, indent):
165        return self.render(
166            loader,
167            'tach.mako.hpp',
168            t=self,
169            indent=indent)
170
171    def setup(self, objs):
172        super(Tach, self).setup(objs)
173
174
175class Rpolicy(ConfigEntry):
176    '''Convenience type for config file rpolicy:type handlers.'''
177
178    def __init__(self, *a, **kw):
179        kw.pop('type', None)
180        self.fan = kw.pop('fan')
181        self.sensors = []
182        kw['class'] = 'policy'
183        super(Rpolicy, self).__init__(**kw)
184
185    def setup(self, objs):
186        '''All policies have an associated fan and methods.
187        Resolve the indicies.'''
188
189        sensors = []
190        for s in self.sensors:
191            sensors.append(get_index(objs, 'sensor', s))
192
193        self.sensors = sensors
194        self.fan = get_index(objs, 'fan', self.fan)
195
196
197class Fallback(Rpolicy, Renderer):
198    '''Default policy handler (policy:type:fallback).'''
199
200    def __init__(self, *a, **kw):
201        kw['name'] = 'fallback-{}'.format(kw['fan'])
202        super(Fallback, self).__init__(**kw)
203
204    def setup(self, objs):
205        super(Fallback, self).setup(objs)
206
207    def construct(self, loader, indent):
208        return self.render(
209            loader,
210            'fallback.mako.hpp',
211            f=self,
212            indent=indent)
213
214
215class Fan(ConfigEntry):
216    '''Fan directive handler.  Fans entries consist of an inventory path,
217    optional redundancy policy and associated sensors.'''
218
219    def __init__(self, *a, **kw):
220        self.path = kw.pop('path')
221        self.methods = kw.pop('methods')
222        self.rpolicy = kw.pop('rpolicy', None)
223        super(Fan, self).__init__(**kw)
224
225    def factory(self, objs):
226        ''' Create rpolicy and sensor(s) objects.'''
227
228        if self.rpolicy:
229            self.rpolicy['fan'] = self.name
230            factory = Everything.classmap(self.rpolicy['type'])
231            rpolicy = factory(**self.rpolicy)
232        else:
233            rpolicy = Fallback(fan=self.name)
234
235        for m in self.methods:
236            m['policy'] = rpolicy.name
237            factory = Everything.classmap(m['type'])
238            sensor = factory(**m)
239            rpolicy.sensors.append(sensor.name)
240            add_unique(sensor, objs)
241
242        add_unique(rpolicy, objs)
243        super(Fan, self).factory(objs)
244
245
246class Everything(Renderer):
247    '''Parse/render entry point.'''
248
249    @staticmethod
250    def classmap(cls):
251        '''Map render item class entries to the appropriate
252        handler methods.'''
253
254        class_map = {
255            'fan': Fan,
256            'fallback': Fallback,
257            'gpio': Gpio,
258            'tach': Tach,
259        }
260
261        if cls not in class_map:
262            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
263
264        return class_map[cls]
265
266    @staticmethod
267    def load(args):
268        '''Load the configuration file.  Parsing occurs in three phases.
269        In the first phase a factory method associated with each
270        configuration file directive is invoked.  These factory
271        methods generate more factory methods.  In the second
272        phase the factory methods created in the first phase
273        are invoked.  In the last phase a callback is invoked on
274        each object created in phase two.  Typically the callback
275        resolves references to other configuration file directives.'''
276
277        factory_objs = {}
278        objs = {}
279        with open(args.input, 'r') as fd:
280            for x in yaml.safe_load(fd.read()) or {}:
281
282                # The top level elements all represent fans.
283                x['class'] = 'fan'
284                # Create factory object for this config file directive.
285                factory = Everything.classmap(x['class'])
286                obj = factory(**x)
287
288                # For a given class of directive, validate the file
289                # doesn't have any duplicate names.
290                if exists(factory_objs, obj.cls, obj.name):
291                    raise NotUniqueError(args.input, 'fan', obj.name)
292
293                factory_objs.setdefault('fan', []).append(obj)
294                objs.setdefault('fan', []).append(obj)
295
296            for cls, items in factory_objs.items():
297                for obj in items:
298                    # Add objects for template consumption.
299                    obj.factory(objs)
300
301            # Configuration file directives reference each other via
302            # the name attribute; however, when rendered the reference
303            # is just an array index.
304            #
305            # At this point all objects have been created but references
306            # have not been resolved to array indicies.  Instruct objects
307            # to do that now.
308            for cls, items in objs.items():
309                for obj in items:
310                    obj.setup(objs)
311
312        return Everything(**objs)
313
314    def __init__(self, *a, **kw):
315        self.fans = kw.pop('fan', [])
316        self.policies = kw.pop('policy', [])
317        self.sensors = kw.pop('sensor', [])
318        super(Everything, self).__init__(**kw)
319
320    def generate_cpp(self, loader):
321        '''Render the template with the provided data.'''
322        sys.stdout.write(
323            self.render(
324                loader,
325                args.template,
326                fans=self.fans,
327                sensors=self.sensors,
328                policies=self.policies,
329                indent=Indent()))
330
331if __name__ == '__main__':
332    script_dir = os.path.dirname(os.path.realpath(__file__))
333    valid_commands = {
334        'generate-cpp': 'generate_cpp',
335    }
336
337    parser = ArgumentParser(
338        description='Phosphor Fan Presence (PFP) YAML '
339        'scanner and code generator.')
340
341    parser.add_argument(
342        '-i', '--input', dest='input',
343        default=os.path.join(script_dir, 'example', 'example.yaml'),
344        help='Location of config file to process.')
345    parser.add_argument(
346        '-t', '--template', dest='template',
347        default='generated.mako.hpp',
348        help='The top level template to render.')
349    parser.add_argument(
350        '-p', '--template-path', dest='template_search',
351        default=os.path.join(script_dir, 'templates'),
352        help='The space delimited mako template search path.')
353    parser.add_argument(
354        'command', metavar='COMMAND', type=str,
355        choices=valid_commands.keys(),
356        help='%s.' % ' | '.join(valid_commands.keys()))
357
358    args = parser.parse_args()
359
360    if sys.version_info < (3, 0):
361        lookup = mako.lookup.TemplateLookup(
362            directories=args.template_search.split(),
363            disable_unicode=True)
364    else:
365        lookup = mako.lookup.TemplateLookup(
366            directories=args.template_search.split())
367    try:
368        function = getattr(
369            Everything.load(args),
370            valid_commands[args.command])
371        function(lookup)
372    except InvalidConfigError as e:
373        sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg))
374        raise
375