blob: 30832f5646c7b58a3f96f340ba58dc7358565d3a [file] [log] [blame]
Matthew Barthf24d7742020-03-17 16:12:15 -05001#!/usr/bin/env python3
Brad Bishop55935602017-06-13 13:31:24 -04002
Patrick Williams0f2588f2022-12-05 10:17:03 -06003"""
Brad Bishop55935602017-06-13 13:31:24 -04004Phosphor 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.
Patrick Williams0f2588f2022-12-05 10:17:03 -060020"""
Brad Bishop55935602017-06-13 13:31:24 -040021
22import os
23import sys
Brad Bishop55935602017-06-13 13:31:24 -040024from argparse import ArgumentParser
Patrick Williams0f2588f2022-12-05 10:17:03 -060025
Brad Bishop55935602017-06-13 13:31:24 -040026import mako.lookup
Patrick Williams0f2588f2022-12-05 10:17:03 -060027import yaml
Brad Bishop55935602017-06-13 13:31:24 -040028from sdbusplus.namedelement import NamedElement
Patrick Williams0f2588f2022-12-05 10:17:03 -060029from sdbusplus.renderer import Renderer
Brad Bishop55935602017-06-13 13:31:24 -040030
31
Patrick Williams0f2588f2022-12-05 10:17:03 -060032class InvalidConfigError(Exception):
33 """General purpose config file parsing error."""
Brad Bishop55935602017-06-13 13:31:24 -040034
35 def __init__(self, path, msg):
Patrick Williams0f2588f2022-12-05 10:17:03 -060036 """Display configuration file with the syntax
37 error and the error message."""
Brad Bishop55935602017-06-13 13:31:24 -040038
39 self.config = path
40 self.msg = msg
41
42
43class NotUniqueError(InvalidConfigError):
Patrick Williams0f2588f2022-12-05 10:17:03 -060044 """Within a config file names must be unique.
45 Display the duplicate item."""
Brad Bishop55935602017-06-13 13:31:24 -040046
47 def __init__(self, path, cls, *names):
48 fmt = 'Duplicate {0}: "{1}"'
49 super(NotUniqueError, self).__init__(
Patrick Williams0f2588f2022-12-05 10:17:03 -060050 path, fmt.format(cls, " ".join(names))
51 )
Brad Bishop55935602017-06-13 13:31:24 -040052
53
54def get_index(objs, cls, name):
Patrick Williams0f2588f2022-12-05 10:17:03 -060055 """Items are usually rendered as C++ arrays and as
Brad Bishop55935602017-06-13 13:31:24 -040056 such are stored in python lists. Given an item name
Patrick Williams0f2588f2022-12-05 10:17:03 -060057 its class, find the item index."""
Brad Bishop55935602017-06-13 13:31:24 -040058
59 for i, x in enumerate(objs.get(cls, [])):
60 if x.name != name:
61 continue
62
63 return i
64 raise InvalidConfigError('Could not find name: "{0}"'.format(name))
65
66
67def exists(objs, cls, name):
Patrick Williams0f2588f2022-12-05 10:17:03 -060068 """Check to see if an item already exists in a list given
69 the item name."""
Brad Bishop55935602017-06-13 13:31:24 -040070
71 try:
72 get_index(objs, cls, name)
Patrick Williams0f2588f2022-12-05 10:17:03 -060073 except Exception:
Brad Bishop55935602017-06-13 13:31:24 -040074 return False
75
76 return True
77
78
79def add_unique(obj, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -060080 """Add an item to one or more lists unless already present."""
Brad Bishop55935602017-06-13 13:31:24 -040081
82 for container in a:
83 if not exists(container, obj.cls, obj.name):
84 container.setdefault(obj.cls, []).append(obj)
85
86
87class Indent(object):
Patrick Williams0f2588f2022-12-05 10:17:03 -060088 """Help templates be depth agnostic."""
Brad Bishop55935602017-06-13 13:31:24 -040089
90 def __init__(self, depth=0):
91 self.depth = depth
92
93 def __add__(self, depth):
94 return Indent(self.depth + depth)
95
96 def __call__(self, depth):
Patrick Williams0f2588f2022-12-05 10:17:03 -060097 """Render an indent at the current depth plus depth."""
98 return 4 * " " * (depth + self.depth)
Brad Bishop55935602017-06-13 13:31:24 -040099
100
101class ConfigEntry(NamedElement):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600102 """Base interface for rendered items."""
Brad Bishop55935602017-06-13 13:31:24 -0400103
104 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600105 """Pop the class keyword."""
Brad Bishop55935602017-06-13 13:31:24 -0400106
Patrick Williams0f2588f2022-12-05 10:17:03 -0600107 self.cls = kw.pop("class")
Brad Bishop55935602017-06-13 13:31:24 -0400108 super(ConfigEntry, self).__init__(**kw)
109
110 def factory(self, objs):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600111 """Optional factory interface for subclasses to add
112 additional items to be rendered."""
Brad Bishop55935602017-06-13 13:31:24 -0400113
114 pass
115
116 def setup(self, objs):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600117 """Optional setup interface for subclasses, invoked
118 after all factory methods have been run."""
Brad Bishop55935602017-06-13 13:31:24 -0400119
120 pass
121
122
123class Sensor(ConfigEntry):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600124 """Convenience type for config file method:type handlers."""
Brad Bishop55935602017-06-13 13:31:24 -0400125
126 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600127 kw["class"] = "sensor"
128 kw.pop("type")
129 self.policy = kw.pop("policy")
Brad Bishop55935602017-06-13 13:31:24 -0400130 super(Sensor, self).__init__(**kw)
131
132 def setup(self, objs):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600133 """All sensors have an associated policy. Get the policy index."""
Brad Bishop55935602017-06-13 13:31:24 -0400134
Patrick Williams0f2588f2022-12-05 10:17:03 -0600135 self.policy = get_index(objs, "policy", self.policy)
Brad Bishop55935602017-06-13 13:31:24 -0400136
137
138class Gpio(Sensor, Renderer):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600139 """Handler for method:type:gpio."""
Brad Bishop55935602017-06-13 13:31:24 -0400140
141 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600142 self.key = kw.pop("key")
143 self.physpath = kw.pop("physpath")
144 self.devpath = kw.pop("devpath")
145 kw["name"] = "gpio-{}".format(self.key)
Brad Bishop55935602017-06-13 13:31:24 -0400146 super(Gpio, self).__init__(**kw)
147
148 def construct(self, loader, indent):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600149 return self.render(loader, "gpio.mako.hpp", g=self, indent=indent)
Brad Bishop55935602017-06-13 13:31:24 -0400150
151 def setup(self, objs):
152 super(Gpio, self).setup(objs)
153
154
155class Tach(Sensor, Renderer):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600156 """Handler for method:type:tach."""
Brad Bishop55935602017-06-13 13:31:24 -0400157
158 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600159 self.sensors = kw.pop("sensors")
160 kw["name"] = "tach-{}".format("-".join(self.sensors))
Brad Bishop55935602017-06-13 13:31:24 -0400161 super(Tach, self).__init__(**kw)
162
163 def construct(self, loader, indent):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600164 return self.render(loader, "tach.mako.hpp", t=self, indent=indent)
Brad Bishop55935602017-06-13 13:31:24 -0400165
166 def setup(self, objs):
167 super(Tach, self).setup(objs)
168
169
170class Rpolicy(ConfigEntry):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600171 """Convenience type for config file rpolicy:type handlers."""
Brad Bishop55935602017-06-13 13:31:24 -0400172
173 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600174 kw.pop("type", None)
175 self.fan = kw.pop("fan")
Brad Bishop55935602017-06-13 13:31:24 -0400176 self.sensors = []
Patrick Williams0f2588f2022-12-05 10:17:03 -0600177 kw["class"] = "policy"
Brad Bishop55935602017-06-13 13:31:24 -0400178 super(Rpolicy, self).__init__(**kw)
179
180 def setup(self, objs):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600181 """All policies have an associated fan and methods.
182 Resolve the indices."""
Brad Bishop55935602017-06-13 13:31:24 -0400183
184 sensors = []
185 for s in self.sensors:
Patrick Williams0f2588f2022-12-05 10:17:03 -0600186 sensors.append(get_index(objs, "sensor", s))
Brad Bishop55935602017-06-13 13:31:24 -0400187
188 self.sensors = sensors
Patrick Williams0f2588f2022-12-05 10:17:03 -0600189 self.fan = get_index(objs, "fan", self.fan)
Brad Bishop55935602017-06-13 13:31:24 -0400190
191
Brad Bishopfcbedca2017-07-25 19:59:46 -0400192class AnyOf(Rpolicy, Renderer):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600193 """Default policy handler (policy:type:anyof)."""
Brad Bishopfcbedca2017-07-25 19:59:46 -0400194
195 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600196 kw["name"] = "anyof-{}".format(kw["fan"])
Brad Bishopfcbedca2017-07-25 19:59:46 -0400197 super(AnyOf, self).__init__(**kw)
198
199 def setup(self, objs):
200 super(AnyOf, self).setup(objs)
201
202 def construct(self, loader, indent):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600203 return self.render(loader, "anyof.mako.hpp", f=self, indent=indent)
Brad Bishopfcbedca2017-07-25 19:59:46 -0400204
205
Brad Bishop55935602017-06-13 13:31:24 -0400206class Fallback(Rpolicy, Renderer):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600207 """Fallback policy handler (policy:type:fallback)."""
Brad Bishop55935602017-06-13 13:31:24 -0400208
209 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600210 kw["name"] = "fallback-{}".format(kw["fan"])
Brad Bishop55935602017-06-13 13:31:24 -0400211 super(Fallback, self).__init__(**kw)
212
213 def setup(self, objs):
214 super(Fallback, self).setup(objs)
215
216 def construct(self, loader, indent):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600217 return self.render(loader, "fallback.mako.hpp", f=self, indent=indent)
Brad Bishop55935602017-06-13 13:31:24 -0400218
219
220class Fan(ConfigEntry):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600221 """Fan directive handler. Fans entries consist of an inventory path,
222 optional redundancy policy and associated sensors."""
Brad Bishop55935602017-06-13 13:31:24 -0400223
224 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600225 self.path = kw.pop("path")
226 self.methods = kw.pop("methods")
227 self.rpolicy = kw.pop("rpolicy", None)
Brad Bishop55935602017-06-13 13:31:24 -0400228 super(Fan, self).__init__(**kw)
229
230 def factory(self, objs):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600231 """Create rpolicy and sensor(s) objects."""
Brad Bishop55935602017-06-13 13:31:24 -0400232
233 if self.rpolicy:
Patrick Williams0f2588f2022-12-05 10:17:03 -0600234 self.rpolicy["fan"] = self.name
235 factory = Everything.classmap(self.rpolicy["type"])
Brad Bishop55935602017-06-13 13:31:24 -0400236 rpolicy = factory(**self.rpolicy)
237 else:
Brad Bishopfcbedca2017-07-25 19:59:46 -0400238 rpolicy = AnyOf(fan=self.name)
Brad Bishop55935602017-06-13 13:31:24 -0400239
240 for m in self.methods:
Patrick Williams0f2588f2022-12-05 10:17:03 -0600241 m["policy"] = rpolicy.name
242 factory = Everything.classmap(m["type"])
Brad Bishop55935602017-06-13 13:31:24 -0400243 sensor = factory(**m)
244 rpolicy.sensors.append(sensor.name)
245 add_unique(sensor, objs)
246
247 add_unique(rpolicy, objs)
248 super(Fan, self).factory(objs)
249
250
251class Everything(Renderer):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600252 """Parse/render entry point."""
Brad Bishop55935602017-06-13 13:31:24 -0400253
254 @staticmethod
255 def classmap(cls):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600256 """Map render item class entries to the appropriate
257 handler methods."""
Brad Bishop55935602017-06-13 13:31:24 -0400258
259 class_map = {
Patrick Williams0f2588f2022-12-05 10:17:03 -0600260 "anyof": AnyOf,
261 "fan": Fan,
262 "fallback": Fallback,
263 "gpio": Gpio,
264 "tach": Tach,
Brad Bishop55935602017-06-13 13:31:24 -0400265 }
266
267 if cls not in class_map:
268 raise NotImplementedError('Unknown class: "{0}"'.format(cls))
269
270 return class_map[cls]
271
272 @staticmethod
273 def load(args):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600274 """Load the configuration file. Parsing occurs in three phases.
Brad Bishop55935602017-06-13 13:31:24 -0400275 In the first phase a factory method associated with each
276 configuration file directive is invoked. These factory
277 methods generate more factory methods. In the second
278 phase the factory methods created in the first phase
279 are invoked. In the last phase a callback is invoked on
280 each object created in phase two. Typically the callback
Patrick Williams0f2588f2022-12-05 10:17:03 -0600281 resolves references to other configuration file directives."""
Brad Bishop55935602017-06-13 13:31:24 -0400282
283 factory_objs = {}
284 objs = {}
Patrick Williams0f2588f2022-12-05 10:17:03 -0600285 with open(args.input, "r") as fd:
Brad Bishop55935602017-06-13 13:31:24 -0400286 for x in yaml.safe_load(fd.read()) or {}:
Brad Bishop55935602017-06-13 13:31:24 -0400287 # The top level elements all represent fans.
Patrick Williams0f2588f2022-12-05 10:17:03 -0600288 x["class"] = "fan"
Brad Bishop55935602017-06-13 13:31:24 -0400289 # Create factory object for this config file directive.
Patrick Williams0f2588f2022-12-05 10:17:03 -0600290 factory = Everything.classmap(x["class"])
Brad Bishop55935602017-06-13 13:31:24 -0400291 obj = factory(**x)
292
293 # For a given class of directive, validate the file
294 # doesn't have any duplicate names.
295 if exists(factory_objs, obj.cls, obj.name):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600296 raise NotUniqueError(args.input, "fan", obj.name)
Brad Bishop55935602017-06-13 13:31:24 -0400297
Patrick Williams0f2588f2022-12-05 10:17:03 -0600298 factory_objs.setdefault("fan", []).append(obj)
299 objs.setdefault("fan", []).append(obj)
Brad Bishop55935602017-06-13 13:31:24 -0400300
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600301 for cls, items in list(factory_objs.items()):
Brad Bishop55935602017-06-13 13:31:24 -0400302 for obj in items:
303 # Add objects for template consumption.
304 obj.factory(objs)
305
306 # Configuration file directives reference each other via
307 # the name attribute; however, when rendered the reference
308 # is just an array index.
309 #
310 # At this point all objects have been created but references
Gunnar Mills9a7688a2017-10-25 17:06:04 -0500311 # have not been resolved to array indices. Instruct objects
Brad Bishop55935602017-06-13 13:31:24 -0400312 # to do that now.
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600313 for cls, items in list(objs.items()):
Brad Bishop55935602017-06-13 13:31:24 -0400314 for obj in items:
315 obj.setup(objs)
316
317 return Everything(**objs)
318
319 def __init__(self, *a, **kw):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600320 self.fans = kw.pop("fan", [])
321 self.policies = kw.pop("policy", [])
322 self.sensors = kw.pop("sensor", [])
Brad Bishop55935602017-06-13 13:31:24 -0400323 super(Everything, self).__init__(**kw)
324
325 def generate_cpp(self, loader):
Patrick Williams0f2588f2022-12-05 10:17:03 -0600326 """Render the template with the provided data."""
Brad Bishop55935602017-06-13 13:31:24 -0400327 sys.stdout.write(
328 self.render(
329 loader,
330 args.template,
331 fans=self.fans,
332 sensors=self.sensors,
333 policies=self.policies,
Patrick Williams0f2588f2022-12-05 10:17:03 -0600334 indent=Indent(),
335 )
336 )
Brad Bishop55935602017-06-13 13:31:24 -0400337
Patrick Williams0f2588f2022-12-05 10:17:03 -0600338
339if __name__ == "__main__":
Brad Bishop55935602017-06-13 13:31:24 -0400340 script_dir = os.path.dirname(os.path.realpath(__file__))
341 valid_commands = {
Patrick Williams0f2588f2022-12-05 10:17:03 -0600342 "generate-cpp": "generate_cpp",
Brad Bishop55935602017-06-13 13:31:24 -0400343 }
344
345 parser = ArgumentParser(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600346 description=(
347 "Phosphor Fan Presence (PFP) YAML scanner and code generator."
348 )
349 )
Brad Bishop55935602017-06-13 13:31:24 -0400350
351 parser.add_argument(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600352 "-i",
353 "--input",
354 dest="input",
355 default=os.path.join(script_dir, "example", "example.yaml"),
356 help="Location of config file to process.",
357 )
Brad Bishop55935602017-06-13 13:31:24 -0400358 parser.add_argument(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600359 "-t",
360 "--template",
361 dest="template",
362 default="generated.mako.hpp",
363 help="The top level template to render.",
364 )
Brad Bishop55935602017-06-13 13:31:24 -0400365 parser.add_argument(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600366 "-p",
367 "--template-path",
368 dest="template_search",
369 default=os.path.join(script_dir, "templates"),
370 help="The space delimited mako template search path.",
371 )
Brad Bishop55935602017-06-13 13:31:24 -0400372 parser.add_argument(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600373 "command",
374 metavar="COMMAND",
375 type=str,
Matthew Barth9dc3e0d2020-02-13 13:02:27 -0600376 choices=list(valid_commands.keys()),
Patrick Williams0f2588f2022-12-05 10:17:03 -0600377 help="%s." % " | ".join(list(valid_commands.keys())),
378 )
Brad Bishop55935602017-06-13 13:31:24 -0400379
380 args = parser.parse_args()
381
382 if sys.version_info < (3, 0):
383 lookup = mako.lookup.TemplateLookup(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600384 directories=args.template_search.split(), disable_unicode=True
385 )
Brad Bishop55935602017-06-13 13:31:24 -0400386 else:
387 lookup = mako.lookup.TemplateLookup(
Patrick Williams0f2588f2022-12-05 10:17:03 -0600388 directories=args.template_search.split()
389 )
Brad Bishop55935602017-06-13 13:31:24 -0400390 try:
Patrick Williams0f2588f2022-12-05 10:17:03 -0600391 function = getattr(Everything.load(args), valid_commands[args.command])
Brad Bishop55935602017-06-13 13:31:24 -0400392 function(lookup)
393 except InvalidConfigError as e:
Patrick Williams0f2588f2022-12-05 10:17:03 -0600394 sys.stderr.write("{0}: {1}\n\n".format(e.config, e.msg))
Brad Bishop55935602017-06-13 13:31:24 -0400395 raise