xref: /openbmc/phosphor-fan-presence/presence/pfpgen.py (revision fcbedca0d847c27fdeab59cda9c6659d8640b6dd)
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 AnyOf(Rpolicy, Renderer):
198    '''Default policy handler (policy:type:anyof).'''
199
200    def __init__(self, *a, **kw):
201        kw['name'] = 'anyof-{}'.format(kw['fan'])
202        super(AnyOf, self).__init__(**kw)
203
204    def setup(self, objs):
205        super(AnyOf, self).setup(objs)
206
207    def construct(self, loader, indent):
208        return self.render(
209            loader,
210            'anyof.mako.hpp',
211            f=self,
212            indent=indent)
213
214
215class Fallback(Rpolicy, Renderer):
216    '''Fallback policy handler (policy:type:fallback).'''
217
218    def __init__(self, *a, **kw):
219        kw['name'] = 'fallback-{}'.format(kw['fan'])
220        super(Fallback, self).__init__(**kw)
221
222    def setup(self, objs):
223        super(Fallback, self).setup(objs)
224
225    def construct(self, loader, indent):
226        return self.render(
227            loader,
228            'fallback.mako.hpp',
229            f=self,
230            indent=indent)
231
232
233class Fan(ConfigEntry):
234    '''Fan directive handler.  Fans entries consist of an inventory path,
235    optional redundancy policy and associated sensors.'''
236
237    def __init__(self, *a, **kw):
238        self.path = kw.pop('path')
239        self.methods = kw.pop('methods')
240        self.rpolicy = kw.pop('rpolicy', None)
241        super(Fan, self).__init__(**kw)
242
243    def factory(self, objs):
244        ''' Create rpolicy and sensor(s) objects.'''
245
246        if self.rpolicy:
247            self.rpolicy['fan'] = self.name
248            factory = Everything.classmap(self.rpolicy['type'])
249            rpolicy = factory(**self.rpolicy)
250        else:
251            rpolicy = AnyOf(fan=self.name)
252
253        for m in self.methods:
254            m['policy'] = rpolicy.name
255            factory = Everything.classmap(m['type'])
256            sensor = factory(**m)
257            rpolicy.sensors.append(sensor.name)
258            add_unique(sensor, objs)
259
260        add_unique(rpolicy, objs)
261        super(Fan, self).factory(objs)
262
263
264class Everything(Renderer):
265    '''Parse/render entry point.'''
266
267    @staticmethod
268    def classmap(cls):
269        '''Map render item class entries to the appropriate
270        handler methods.'''
271
272        class_map = {
273            'anyof': AnyOf,
274            'fan': Fan,
275            'fallback': Fallback,
276            'gpio': Gpio,
277            'tach': Tach,
278        }
279
280        if cls not in class_map:
281            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
282
283        return class_map[cls]
284
285    @staticmethod
286    def load(args):
287        '''Load the configuration file.  Parsing occurs in three phases.
288        In the first phase a factory method associated with each
289        configuration file directive is invoked.  These factory
290        methods generate more factory methods.  In the second
291        phase the factory methods created in the first phase
292        are invoked.  In the last phase a callback is invoked on
293        each object created in phase two.  Typically the callback
294        resolves references to other configuration file directives.'''
295
296        factory_objs = {}
297        objs = {}
298        with open(args.input, 'r') as fd:
299            for x in yaml.safe_load(fd.read()) or {}:
300
301                # The top level elements all represent fans.
302                x['class'] = 'fan'
303                # Create factory object for this config file directive.
304                factory = Everything.classmap(x['class'])
305                obj = factory(**x)
306
307                # For a given class of directive, validate the file
308                # doesn't have any duplicate names.
309                if exists(factory_objs, obj.cls, obj.name):
310                    raise NotUniqueError(args.input, 'fan', obj.name)
311
312                factory_objs.setdefault('fan', []).append(obj)
313                objs.setdefault('fan', []).append(obj)
314
315            for cls, items in factory_objs.items():
316                for obj in items:
317                    # Add objects for template consumption.
318                    obj.factory(objs)
319
320            # Configuration file directives reference each other via
321            # the name attribute; however, when rendered the reference
322            # is just an array index.
323            #
324            # At this point all objects have been created but references
325            # have not been resolved to array indicies.  Instruct objects
326            # to do that now.
327            for cls, items in objs.items():
328                for obj in items:
329                    obj.setup(objs)
330
331        return Everything(**objs)
332
333    def __init__(self, *a, **kw):
334        self.fans = kw.pop('fan', [])
335        self.policies = kw.pop('policy', [])
336        self.sensors = kw.pop('sensor', [])
337        super(Everything, self).__init__(**kw)
338
339    def generate_cpp(self, loader):
340        '''Render the template with the provided data.'''
341        sys.stdout.write(
342            self.render(
343                loader,
344                args.template,
345                fans=self.fans,
346                sensors=self.sensors,
347                policies=self.policies,
348                indent=Indent()))
349
350if __name__ == '__main__':
351    script_dir = os.path.dirname(os.path.realpath(__file__))
352    valid_commands = {
353        'generate-cpp': 'generate_cpp',
354    }
355
356    parser = ArgumentParser(
357        description='Phosphor Fan Presence (PFP) YAML '
358        'scanner and code generator.')
359
360    parser.add_argument(
361        '-i', '--input', dest='input',
362        default=os.path.join(script_dir, 'example', 'example.yaml'),
363        help='Location of config file to process.')
364    parser.add_argument(
365        '-t', '--template', dest='template',
366        default='generated.mako.hpp',
367        help='The top level template to render.')
368    parser.add_argument(
369        '-p', '--template-path', dest='template_search',
370        default=os.path.join(script_dir, 'templates'),
371        help='The space delimited mako template search path.')
372    parser.add_argument(
373        'command', metavar='COMMAND', type=str,
374        choices=valid_commands.keys(),
375        help='%s.' % ' | '.join(valid_commands.keys()))
376
377    args = parser.parse_args()
378
379    if sys.version_info < (3, 0):
380        lookup = mako.lookup.TemplateLookup(
381            directories=args.template_search.split(),
382            disable_unicode=True)
383    else:
384        lookup = mako.lookup.TemplateLookup(
385            directories=args.template_search.split())
386    try:
387        function = getattr(
388            Everything.load(args),
389            valid_commands[args.command])
390        function(lookup)
391    except InvalidConfigError as e:
392        sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg))
393        raise
394