blob: 1e22ac781ec7d7049804f9b33938b5764f31bb8b [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
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