blob: 61546ebaea49ba78a70609bc293364fc70a13fd7 [file] [log] [blame]
Matthew Barthdb440d42017-04-17 15:49:37 -05001#!/usr/bin/env python
2
Brad Bishop34a7acd2017-04-27 23:47:23 -04003'''Phosphor DBus Monitor YAML parser and code generator.
4
5The parser workflow is broken down as follows:
6 1 - Import YAML files as native python type(s) instance(s).
7 2 - Create an instance of the Everything class from the
8 native python type instance(s) with the Everything.load
9 method.
10 3 - The Everything class constructor orchestrates conversion of the
11 native python type(s) instances(s) to render helper types.
12 Each render helper type constructor imports its attributes
13 from the native python type(s) instances(s).
14 4 - Present the converted YAML to the command processing method
15 requested by the script user.
16'''
17
Matthew Barthdb440d42017-04-17 15:49:37 -050018import os
19import sys
20import yaml
Brad Bishop34a7acd2017-04-27 23:47:23 -040021import mako.lookup
Matthew Barthdb440d42017-04-17 15:49:37 -050022from argparse import ArgumentParser
Brad Bishop34a7acd2017-04-27 23:47:23 -040023from sdbusplus.renderer import Renderer
Brad Bishop05b0c1e2017-05-23 00:24:01 -040024from sdbusplus.namedelement import NamedElement
Brad Bishope73b2c32017-05-23 18:01:54 -040025import sdbusplus.property
Brad Bishop05b0c1e2017-05-23 00:24:01 -040026
27
28class InvalidConfigError(BaseException):
29 '''General purpose config file parsing error.'''
30
31 def __init__(self, path, msg):
32 '''Display configuration file with the syntax
33 error and the error message.'''
34
35 self.config = path
36 self.msg = msg
37
38
39class NotUniqueError(InvalidConfigError):
40 '''Within a config file names must be unique.
41 Display the config file with the duplicate and
42 the duplicate itself.'''
43
44 def __init__(self, path, cls, *names):
45 fmt = 'Duplicate {0}: "{1}"'
46 super(NotUniqueError, self).__init__(
47 path, fmt.format(cls, ' '.join(names)))
48
49
50def get_index(objs, cls, name, config=None):
51 '''Items are usually rendered as C++ arrays and as
52 such are stored in python lists. Given an item name
53 its class, and an optional config file filter, find
54 the item index.'''
55
56 for i, x in enumerate(objs.get(cls, [])):
57 if config and x.configfile != config:
58 continue
59 if x.name != name:
60 continue
61
62 return i
63 raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
64
65
66def exists(objs, cls, name, config=None):
67 '''Check to see if an item already exists in a list given
68 the item name.'''
69
70 try:
71 get_index(objs, cls, name, config)
72 except:
73 return False
74
75 return True
76
77
78def add_unique(obj, *a, **kw):
79 '''Add an item to one or more lists unless already present,
80 with an option to constrain the search to a specific config file.'''
81
82 for container in a:
83 if not exists(container, obj.cls, obj.name, config=kw.get('config')):
84 container.setdefault(obj.cls, []).append(obj)
Matthew Barthdb440d42017-04-17 15:49:37 -050085
86
Brad Bishop34a7acd2017-04-27 23:47:23 -040087class Indent(object):
88 '''Help templates be depth agnostic.'''
Matthew Barthdb440d42017-04-17 15:49:37 -050089
Brad Bishop34a7acd2017-04-27 23:47:23 -040090 def __init__(self, depth=0):
91 self.depth = depth
Matthew Barthdb440d42017-04-17 15:49:37 -050092
Brad Bishop34a7acd2017-04-27 23:47:23 -040093 def __add__(self, depth):
94 return Indent(self.depth + depth)
95
96 def __call__(self, depth):
97 '''Render an indent at the current depth plus depth.'''
98 return 4*' '*(depth + self.depth)
99
100
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400101class ConfigEntry(NamedElement):
102 '''Base interface for rendered items.'''
103
104 def __init__(self, *a, **kw):
105 '''Pop the configfile/class/subclass keywords.'''
106
107 self.configfile = kw.pop('configfile')
108 self.cls = kw.pop('class')
109 self.subclass = kw.pop(self.cls)
110 super(ConfigEntry, self).__init__(**kw)
111
112 def factory(self, objs):
113 ''' Optional factory interface for subclasses to add
114 additional items to be rendered.'''
115
116 pass
117
118 def setup(self, objs):
119 ''' Optional setup interface for subclasses, invoked
120 after all factory methods have been run.'''
121
122 pass
123
124
Brad Bishop0e7df132017-05-23 17:58:12 -0400125class Path(ConfigEntry):
126 '''Path/metadata association.'''
127
128 def __init__(self, *a, **kw):
129 super(Path, self).__init__(**kw)
130
131 def factory(self, objs):
132 '''Create path and metadata elements.'''
133
134 args = {
135 'class': 'pathname',
136 'pathname': 'element',
137 'name': self.name['path']
138 }
139 add_unique(ConfigEntry(
140 configfile=self.configfile, **args), objs)
141
142 args = {
143 'class': 'meta',
144 'meta': 'element',
145 'name': self.name['meta']
146 }
147 add_unique(ConfigEntry(
148 configfile=self.configfile, **args), objs)
149
150 super(Path, self).factory(objs)
151
152 def setup(self, objs):
153 '''Resolve path and metadata names to indicies.'''
154
155 self.path = get_index(
156 objs, 'pathname', self.name['path'])
157 self.meta = get_index(
158 objs, 'meta', self.name['meta'])
159
160 super(Path, self).setup(objs)
161
162
Brad Bishope73b2c32017-05-23 18:01:54 -0400163class Property(ConfigEntry):
164 '''Property/interface/metadata association.'''
165
166 def __init__(self, *a, **kw):
167 super(Property, self).__init__(**kw)
168
169 def factory(self, objs):
170 '''Create interface, property name and metadata elements.'''
171
172 args = {
173 'class': 'interface',
174 'interface': 'element',
175 'name': self.name['interface']
176 }
177 add_unique(ConfigEntry(
178 configfile=self.configfile, **args), objs)
179
180 args = {
181 'class': 'propertyname',
182 'propertyname': 'element',
183 'name': self.name['property']
184 }
185 add_unique(ConfigEntry(
186 configfile=self.configfile, **args), objs)
187
188 args = {
189 'class': 'meta',
190 'meta': 'element',
191 'name': self.name['meta']
192 }
193 add_unique(ConfigEntry(
194 configfile=self.configfile, **args), objs)
195
196 super(Property, self).factory(objs)
197
198 def setup(self, objs):
199 '''Resolve interface, property and metadata to indicies.'''
200
201 self.interface = get_index(
202 objs, 'interface', self.name['interface'])
203 self.prop = get_index(
204 objs, 'propertyname', self.name['property'])
205 self.meta = get_index(
206 objs, 'meta', self.name['meta'])
207
208 super(Property, self).setup(objs)
209
210
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400211class Group(ConfigEntry):
212 '''Pop the members keyword for groups.'''
213
214 def __init__(self, *a, **kw):
215 self.members = kw.pop('members')
216 super(Group, self).__init__(**kw)
217
218
219class ImplicitGroup(Group):
220 '''Provide a factory method for groups whose members are
221 not explicitly declared in the config files.'''
222
223 def __init__(self, *a, **kw):
224 super(ImplicitGroup, self).__init__(**kw)
225
226 def factory(self, objs):
227 '''Create group members.'''
228
229 factory = Everything.classmap(self.subclass, 'element')
230 for m in self.members:
231 args = {
232 'class': self.subclass,
233 self.subclass: 'element',
234 'name': m
235 }
236
237 obj = factory(configfile=self.configfile, **args)
238 add_unique(obj, objs)
239 obj.factory(objs)
240
241 super(ImplicitGroup, self).factory(objs)
242
243
Brad Bishop0e7df132017-05-23 17:58:12 -0400244class GroupOfPaths(ImplicitGroup):
245 '''Path group config file directive.'''
246
247 def __init__(self, *a, **kw):
248 super(GroupOfPaths, self).__init__(**kw)
249
250 def setup(self, objs):
251 '''Resolve group members.'''
252
253 def map_member(x):
254 path = get_index(
255 objs, 'pathname', x['path'])
256 meta = get_index(
257 objs, 'meta', x['meta'])
258 return (path, meta)
259
260 self.members = map(
261 map_member,
262 self.members)
263
264 super(GroupOfPaths, self).setup(objs)
265
266
Brad Bishope73b2c32017-05-23 18:01:54 -0400267class GroupOfProperties(ImplicitGroup):
268 '''Property group config file directive.'''
269
270 def __init__(self, *a, **kw):
271 self.datatype = sdbusplus.property.Property(
272 name=kw.get('name'),
273 type=kw.pop('type')).cppTypeName
274
275 super(GroupOfProperties, self).__init__(**kw)
276
277 def setup(self, objs):
278 '''Resolve group members.'''
279
280 def map_member(x):
281 iface = get_index(
282 objs, 'interface', x['interface'])
283 prop = get_index(
284 objs, 'propertyname', x['property'])
285 meta = get_index(
286 objs, 'meta', x['meta'])
287
288 return (iface, prop, meta)
289
290 self.members = map(
291 map_member,
292 self.members)
293
294 super(GroupOfProperties, self).setup(objs)
295
296
Brad Bishop34a7acd2017-04-27 23:47:23 -0400297class Everything(Renderer):
298 '''Parse/render entry point.'''
299
300 @staticmethod
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400301 def classmap(cls, sub=None):
302 '''Map render item class and subclass entries to the appropriate
303 handler methods.'''
304
305 class_map = {
Brad Bishop0e7df132017-05-23 17:58:12 -0400306 'path': {
307 'element': Path,
308 },
309 'pathgroup': {
310 'path': GroupOfPaths,
311 },
Brad Bishope73b2c32017-05-23 18:01:54 -0400312 'propertygroup': {
313 'property': GroupOfProperties,
314 },
315 'property': {
316 'element': Property,
317 },
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400318 }
319
320 if cls not in class_map:
321 raise NotImplementedError('Unknown class: "{0}"'.format(cls))
322 if sub not in class_map[cls]:
323 raise NotImplementedError('Unknown {0} type: "{1}"'.format(
324 cls, sub))
325
326 return class_map[cls][sub]
327
328 @staticmethod
329 def load_one_yaml(path, fd, objs):
330 '''Parse a single YAML file. Parsing occurs in three phases.
331 In the first phase a factory method associated with each
332 configuration file directive is invoked. These factory
333 methods generate more factory methods. In the second
334 phase the factory methods created in the first phase
335 are invoked. In the last phase a callback is invoked on
336 each object created in phase two. Typically the callback
337 resolves references to other configuration file directives.'''
338
339 factory_objs = {}
340 for x in yaml.safe_load(fd.read()) or {}:
341
342 # Create factory object for this config file directive.
343 cls = x['class']
344 sub = x.get(cls)
345 if cls == 'group':
346 cls = '{0}group'.format(sub)
347
348 factory = Everything.classmap(cls, sub)
349 obj = factory(configfile=path, **x)
350
351 # For a given class of directive, validate the file
352 # doesn't have any duplicate names (duplicates are
353 # ok across config files).
354 if exists(factory_objs, obj.cls, obj.name, config=path):
355 raise NotUniqueError(path, cls, obj.name)
356
357 factory_objs.setdefault(cls, []).append(obj)
358 objs.setdefault(cls, []).append(obj)
359
360 for cls, items in factory_objs.items():
361 for obj in items:
362 # Add objects for template consumption.
363 obj.factory(objs)
364
365 @staticmethod
Brad Bishop34a7acd2017-04-27 23:47:23 -0400366 def load(args):
367 '''Aggregate all the YAML in the input directory
368 into a single aggregate.'''
369
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400370 objs = {}
371 yaml_files = filter(
372 lambda x: x.endswith('.yaml'),
373 os.listdir(args.inputdir))
Brad Bishop34a7acd2017-04-27 23:47:23 -0400374
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400375 yaml_files.sort()
Brad Bishop34a7acd2017-04-27 23:47:23 -0400376
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400377 for x in yaml_files:
378 path = os.path.join(args.inputdir, x)
379 with open(path, 'r') as fd:
380 Everything.load_one_yaml(path, fd, objs)
381
382 # Configuration file directives reference each other via
383 # the name attribute; however, when rendered the reference
384 # is just an array index.
385 #
386 # At this point all objects have been created but references
387 # have not been resolved to array indicies. Instruct objects
388 # to do that now.
389 for cls, items in objs.items():
390 for obj in items:
391 obj.setup(objs)
392
393 return Everything(**objs)
Brad Bishop34a7acd2017-04-27 23:47:23 -0400394
395 def __init__(self, *a, **kw):
Brad Bishop0e7df132017-05-23 17:58:12 -0400396 self.pathmeta = kw.pop('path', [])
397 self.paths = kw.pop('pathname', [])
398 self.meta = kw.pop('meta', [])
399 self.pathgroups = kw.pop('pathgroup', [])
Brad Bishope73b2c32017-05-23 18:01:54 -0400400 self.interfaces = kw.pop('interface', [])
401 self.properties = kw.pop('property', [])
402 self.propertynames = kw.pop('propertyname', [])
403 self.propertygroups = kw.pop('propertygroup', [])
Brad Bishop0e7df132017-05-23 17:58:12 -0400404
Brad Bishop34a7acd2017-04-27 23:47:23 -0400405 super(Everything, self).__init__(**kw)
406
407 def generate_cpp(self, loader):
408 '''Render the template with the provided data.'''
Brad Bishope3a01af2017-05-15 17:09:04 -0400409 with open(args.output, 'w') as fd:
Brad Bishop34a7acd2017-04-27 23:47:23 -0400410 fd.write(
411 self.render(
412 loader,
Brad Bishope3a01af2017-05-15 17:09:04 -0400413 args.template,
Brad Bishop0e7df132017-05-23 17:58:12 -0400414 meta=self.meta,
Brad Bishope73b2c32017-05-23 18:01:54 -0400415 properties=self.properties,
416 propertynames=self.propertynames,
417 interfaces=self.interfaces,
Brad Bishop0e7df132017-05-23 17:58:12 -0400418 paths=self.paths,
419 pathmeta=self.pathmeta,
420 pathgroups=self.pathgroups,
Brad Bishope73b2c32017-05-23 18:01:54 -0400421 propertygroups=self.propertygroups,
Brad Bishop34a7acd2017-04-27 23:47:23 -0400422 indent=Indent()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500423
424if __name__ == '__main__':
Brad Bishop34a7acd2017-04-27 23:47:23 -0400425 script_dir = os.path.dirname(os.path.realpath(__file__))
426 valid_commands = {
427 'generate-cpp': 'generate_cpp',
428 }
429
430 parser = ArgumentParser(
431 description='Phosphor DBus Monitor (PDM) YAML '
432 'scanner and code generator.')
433
Matthew Barthdb440d42017-04-17 15:49:37 -0500434 parser.add_argument(
Brad Bishope3a01af2017-05-15 17:09:04 -0400435 "-o", "--out", dest="output",
436 default='generated.cpp',
437 help="Generated output file name and path.")
438 parser.add_argument(
439 '-t', '--template', dest='template',
Brad Bishop870c3fc2017-05-22 23:23:13 -0400440 default='generated.mako.hpp',
Brad Bishope3a01af2017-05-15 17:09:04 -0400441 help='The top level template to render.')
442 parser.add_argument(
443 '-p', '--template-path', dest='template_search',
444 default=script_dir,
445 help='The space delimited mako template search path.')
Brad Bishop34a7acd2017-04-27 23:47:23 -0400446 parser.add_argument(
447 '-d', '--dir', dest='inputdir',
448 default=os.path.join(script_dir, 'example'),
449 help='Location of files to process.')
450 parser.add_argument(
451 'command', metavar='COMMAND', type=str,
452 choices=valid_commands.keys(),
453 help='%s.' % " | ".join(valid_commands.keys()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500454
Brad Bishop34a7acd2017-04-27 23:47:23 -0400455 args = parser.parse_args()
456
457 if sys.version_info < (3, 0):
458 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400459 directories=args.template_search.split(),
Brad Bishop34a7acd2017-04-27 23:47:23 -0400460 disable_unicode=True)
461 else:
462 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400463 directories=args.template_search.split())
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400464 try:
465 function = getattr(
466 Everything.load(args),
467 valid_commands[args.command])
468 function(lookup)
469 except InvalidConfigError as e:
470 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
471 raise