blob: 00ba900d65a56b7da3655b66dd1a2fa520ca7979 [file] [log] [blame]
Brad Bishop55935602017-06-13 13:31:24 -04001#!/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
Brad Bishopfcbedca2017-07-25 19:59:46 -0400197class 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
Brad Bishop55935602017-06-13 13:31:24 -0400215class Fallback(Rpolicy, Renderer):
Brad Bishopfcbedca2017-07-25 19:59:46 -0400216 '''Fallback policy handler (policy:type:fallback).'''
Brad Bishop55935602017-06-13 13:31:24 -0400217
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:
Brad Bishopfcbedca2017-07-25 19:59:46 -0400251 rpolicy = AnyOf(fan=self.name)
Brad Bishop55935602017-06-13 13:31:24 -0400252
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 = {
Brad Bishopfcbedca2017-07-25 19:59:46 -0400273 'anyof': AnyOf,
Brad Bishop55935602017-06-13 13:31:24 -0400274 '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