blob: 1080a22f6b5f3e9b9cdf58b439021da631376054 [file] [log] [blame]
Matthew Barthf24d7742020-03-17 16:12:15 -05001#!/usr/bin/env python3
Brad Bishop55935602017-06-13 13:31:24 -04002
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')
Brad Bishop2e9788d2017-07-28 22:24:03 -0400142 self.devpath = kw.pop('devpath')
Brad Bishop55935602017-06-13 13:31:24 -0400143 kw['name'] = 'gpio-{}'.format(self.key)
144 super(Gpio, self).__init__(**kw)
145
146 def construct(self, loader, indent):
147 return self.render(
148 loader,
149 'gpio.mako.hpp',
150 g=self,
151 indent=indent)
152
153 def setup(self, objs):
154 super(Gpio, self).setup(objs)
155
156
157class Tach(Sensor, Renderer):
158 '''Handler for method:type:tach.'''
159
160 def __init__(self, *a, **kw):
161 self.sensors = kw.pop('sensors')
162 kw['name'] = 'tach-{}'.format('-'.join(self.sensors))
163 super(Tach, self).__init__(**kw)
164
165 def construct(self, loader, indent):
166 return self.render(
167 loader,
168 'tach.mako.hpp',
169 t=self,
170 indent=indent)
171
172 def setup(self, objs):
173 super(Tach, self).setup(objs)
174
175
176class Rpolicy(ConfigEntry):
177 '''Convenience type for config file rpolicy:type handlers.'''
178
179 def __init__(self, *a, **kw):
180 kw.pop('type', None)
181 self.fan = kw.pop('fan')
182 self.sensors = []
183 kw['class'] = 'policy'
184 super(Rpolicy, self).__init__(**kw)
185
186 def setup(self, objs):
187 '''All policies have an associated fan and methods.
Gunnar Mills9a7688a2017-10-25 17:06:04 -0500188 Resolve the indices.'''
Brad Bishop55935602017-06-13 13:31:24 -0400189
190 sensors = []
191 for s in self.sensors:
192 sensors.append(get_index(objs, 'sensor', s))
193
194 self.sensors = sensors
195 self.fan = get_index(objs, 'fan', self.fan)
196
197
Brad Bishopfcbedca2017-07-25 19:59:46 -0400198class AnyOf(Rpolicy, Renderer):
199 '''Default policy handler (policy:type:anyof).'''
200
201 def __init__(self, *a, **kw):
202 kw['name'] = 'anyof-{}'.format(kw['fan'])
203 super(AnyOf, self).__init__(**kw)
204
205 def setup(self, objs):
206 super(AnyOf, self).setup(objs)
207
208 def construct(self, loader, indent):
209 return self.render(
210 loader,
211 'anyof.mako.hpp',
212 f=self,
213 indent=indent)
214
215
Brad Bishop55935602017-06-13 13:31:24 -0400216class Fallback(Rpolicy, Renderer):
Brad Bishopfcbedca2017-07-25 19:59:46 -0400217 '''Fallback policy handler (policy:type:fallback).'''
Brad Bishop55935602017-06-13 13:31:24 -0400218
219 def __init__(self, *a, **kw):
220 kw['name'] = 'fallback-{}'.format(kw['fan'])
221 super(Fallback, self).__init__(**kw)
222
223 def setup(self, objs):
224 super(Fallback, self).setup(objs)
225
226 def construct(self, loader, indent):
227 return self.render(
228 loader,
229 'fallback.mako.hpp',
230 f=self,
231 indent=indent)
232
233
234class Fan(ConfigEntry):
235 '''Fan directive handler. Fans entries consist of an inventory path,
236 optional redundancy policy and associated sensors.'''
237
238 def __init__(self, *a, **kw):
239 self.path = kw.pop('path')
240 self.methods = kw.pop('methods')
241 self.rpolicy = kw.pop('rpolicy', None)
242 super(Fan, self).__init__(**kw)
243
244 def factory(self, objs):
245 ''' Create rpolicy and sensor(s) objects.'''
246
247 if self.rpolicy:
248 self.rpolicy['fan'] = self.name
249 factory = Everything.classmap(self.rpolicy['type'])
250 rpolicy = factory(**self.rpolicy)
251 else:
Brad Bishopfcbedca2017-07-25 19:59:46 -0400252 rpolicy = AnyOf(fan=self.name)
Brad Bishop55935602017-06-13 13:31:24 -0400253
254 for m in self.methods:
255 m['policy'] = rpolicy.name
256 factory = Everything.classmap(m['type'])
257 sensor = factory(**m)
258 rpolicy.sensors.append(sensor.name)
259 add_unique(sensor, objs)
260
261 add_unique(rpolicy, objs)
262 super(Fan, self).factory(objs)
263
264
265class Everything(Renderer):
266 '''Parse/render entry point.'''
267
268 @staticmethod
269 def classmap(cls):
270 '''Map render item class entries to the appropriate
271 handler methods.'''
272
273 class_map = {
Brad Bishopfcbedca2017-07-25 19:59:46 -0400274 'anyof': AnyOf,
Brad Bishop55935602017-06-13 13:31:24 -0400275 'fan': Fan,
276 'fallback': Fallback,
277 'gpio': Gpio,
278 'tach': Tach,
279 }
280
281 if cls not in class_map:
282 raise NotImplementedError('Unknown class: "{0}"'.format(cls))
283
284 return class_map[cls]
285
286 @staticmethod
287 def load(args):
288 '''Load the configuration file. Parsing occurs in three phases.
289 In the first phase a factory method associated with each
290 configuration file directive is invoked. These factory
291 methods generate more factory methods. In the second
292 phase the factory methods created in the first phase
293 are invoked. In the last phase a callback is invoked on
294 each object created in phase two. Typically the callback
295 resolves references to other configuration file directives.'''
296
297 factory_objs = {}
298 objs = {}
299 with open(args.input, 'r') as fd:
300 for x in yaml.safe_load(fd.read()) or {}:
301
302 # The top level elements all represent fans.
303 x['class'] = 'fan'
304 # Create factory object for this config file directive.
305 factory = Everything.classmap(x['class'])
306 obj = factory(**x)
307
308 # For a given class of directive, validate the file
309 # doesn't have any duplicate names.
310 if exists(factory_objs, obj.cls, obj.name):
311 raise NotUniqueError(args.input, 'fan', obj.name)
312
313 factory_objs.setdefault('fan', []).append(obj)
314 objs.setdefault('fan', []).append(obj)
315
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600316 for cls, items in list(factory_objs.items()):
Brad Bishop55935602017-06-13 13:31:24 -0400317 for obj in items:
318 # Add objects for template consumption.
319 obj.factory(objs)
320
321 # Configuration file directives reference each other via
322 # the name attribute; however, when rendered the reference
323 # is just an array index.
324 #
325 # At this point all objects have been created but references
Gunnar Mills9a7688a2017-10-25 17:06:04 -0500326 # have not been resolved to array indices. Instruct objects
Brad Bishop55935602017-06-13 13:31:24 -0400327 # to do that now.
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600328 for cls, items in list(objs.items()):
Brad Bishop55935602017-06-13 13:31:24 -0400329 for obj in items:
330 obj.setup(objs)
331
332 return Everything(**objs)
333
334 def __init__(self, *a, **kw):
335 self.fans = kw.pop('fan', [])
336 self.policies = kw.pop('policy', [])
337 self.sensors = kw.pop('sensor', [])
338 super(Everything, self).__init__(**kw)
339
340 def generate_cpp(self, loader):
341 '''Render the template with the provided data.'''
342 sys.stdout.write(
343 self.render(
344 loader,
345 args.template,
346 fans=self.fans,
347 sensors=self.sensors,
348 policies=self.policies,
349 indent=Indent()))
350
351if __name__ == '__main__':
352 script_dir = os.path.dirname(os.path.realpath(__file__))
353 valid_commands = {
354 'generate-cpp': 'generate_cpp',
355 }
356
357 parser = ArgumentParser(
358 description='Phosphor Fan Presence (PFP) YAML '
359 'scanner and code generator.')
360
361 parser.add_argument(
362 '-i', '--input', dest='input',
363 default=os.path.join(script_dir, 'example', 'example.yaml'),
364 help='Location of config file to process.')
365 parser.add_argument(
366 '-t', '--template', dest='template',
367 default='generated.mako.hpp',
368 help='The top level template to render.')
369 parser.add_argument(
370 '-p', '--template-path', dest='template_search',
371 default=os.path.join(script_dir, 'templates'),
372 help='The space delimited mako template search path.')
373 parser.add_argument(
374 'command', metavar='COMMAND', type=str,
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600375 choices=list(valid_commands.keys()),
376 help='%s.' % ' | '.join(list(valid_commands.keys())))
Brad Bishop55935602017-06-13 13:31:24 -0400377
378 args = parser.parse_args()
379
380 if sys.version_info < (3, 0):
381 lookup = mako.lookup.TemplateLookup(
382 directories=args.template_search.split(),
383 disable_unicode=True)
384 else:
385 lookup = mako.lookup.TemplateLookup(
386 directories=args.template_search.split())
387 try:
388 function = getattr(
389 Everything.load(args),
390 valid_commands[args.command])
391 function(lookup)
392 except InvalidConfigError as e:
393 sys.stderr.write('{0}: {1}\n\n'.format(e.config, e.msg))
394 raise