blob: fcda96022edfdb75a695432b7c776dcf7d4f30ba [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
25
26
27class InvalidConfigError(BaseException):
28 '''General purpose config file parsing error.'''
29
30 def __init__(self, path, msg):
31 '''Display configuration file with the syntax
32 error and the error message.'''
33
34 self.config = path
35 self.msg = msg
36
37
38class NotUniqueError(InvalidConfigError):
39 '''Within a config file names must be unique.
40 Display the config file with the duplicate and
41 the duplicate itself.'''
42
43 def __init__(self, path, cls, *names):
44 fmt = 'Duplicate {0}: "{1}"'
45 super(NotUniqueError, self).__init__(
46 path, fmt.format(cls, ' '.join(names)))
47
48
49def get_index(objs, cls, name, config=None):
50 '''Items are usually rendered as C++ arrays and as
51 such are stored in python lists. Given an item name
52 its class, and an optional config file filter, find
53 the item index.'''
54
55 for i, x in enumerate(objs.get(cls, [])):
56 if config and x.configfile != config:
57 continue
58 if x.name != name:
59 continue
60
61 return i
62 raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
63
64
65def exists(objs, cls, name, config=None):
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, config)
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 with an option to constrain the search to a specific config file.'''
80
81 for container in a:
82 if not exists(container, obj.cls, obj.name, config=kw.get('config')):
83 container.setdefault(obj.cls, []).append(obj)
Matthew Barthdb440d42017-04-17 15:49:37 -050084
85
Brad Bishop34a7acd2017-04-27 23:47:23 -040086class Indent(object):
87 '''Help templates be depth agnostic.'''
Matthew Barthdb440d42017-04-17 15:49:37 -050088
Brad Bishop34a7acd2017-04-27 23:47:23 -040089 def __init__(self, depth=0):
90 self.depth = depth
Matthew Barthdb440d42017-04-17 15:49:37 -050091
Brad Bishop34a7acd2017-04-27 23:47:23 -040092 def __add__(self, depth):
93 return Indent(self.depth + depth)
94
95 def __call__(self, depth):
96 '''Render an indent at the current depth plus depth.'''
97 return 4*' '*(depth + self.depth)
98
99
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400100class ConfigEntry(NamedElement):
101 '''Base interface for rendered items.'''
102
103 def __init__(self, *a, **kw):
104 '''Pop the configfile/class/subclass keywords.'''
105
106 self.configfile = kw.pop('configfile')
107 self.cls = kw.pop('class')
108 self.subclass = kw.pop(self.cls)
109 super(ConfigEntry, self).__init__(**kw)
110
111 def factory(self, objs):
112 ''' Optional factory interface for subclasses to add
113 additional items to be rendered.'''
114
115 pass
116
117 def setup(self, objs):
118 ''' Optional setup interface for subclasses, invoked
119 after all factory methods have been run.'''
120
121 pass
122
123
Brad Bishop0e7df132017-05-23 17:58:12 -0400124class Path(ConfigEntry):
125 '''Path/metadata association.'''
126
127 def __init__(self, *a, **kw):
128 super(Path, self).__init__(**kw)
129
130 def factory(self, objs):
131 '''Create path and metadata elements.'''
132
133 args = {
134 'class': 'pathname',
135 'pathname': 'element',
136 'name': self.name['path']
137 }
138 add_unique(ConfigEntry(
139 configfile=self.configfile, **args), objs)
140
141 args = {
142 'class': 'meta',
143 'meta': 'element',
144 'name': self.name['meta']
145 }
146 add_unique(ConfigEntry(
147 configfile=self.configfile, **args), objs)
148
149 super(Path, self).factory(objs)
150
151 def setup(self, objs):
152 '''Resolve path and metadata names to indicies.'''
153
154 self.path = get_index(
155 objs, 'pathname', self.name['path'])
156 self.meta = get_index(
157 objs, 'meta', self.name['meta'])
158
159 super(Path, self).setup(objs)
160
161
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400162class Group(ConfigEntry):
163 '''Pop the members keyword for groups.'''
164
165 def __init__(self, *a, **kw):
166 self.members = kw.pop('members')
167 super(Group, self).__init__(**kw)
168
169
170class ImplicitGroup(Group):
171 '''Provide a factory method for groups whose members are
172 not explicitly declared in the config files.'''
173
174 def __init__(self, *a, **kw):
175 super(ImplicitGroup, self).__init__(**kw)
176
177 def factory(self, objs):
178 '''Create group members.'''
179
180 factory = Everything.classmap(self.subclass, 'element')
181 for m in self.members:
182 args = {
183 'class': self.subclass,
184 self.subclass: 'element',
185 'name': m
186 }
187
188 obj = factory(configfile=self.configfile, **args)
189 add_unique(obj, objs)
190 obj.factory(objs)
191
192 super(ImplicitGroup, self).factory(objs)
193
194
Brad Bishop0e7df132017-05-23 17:58:12 -0400195class GroupOfPaths(ImplicitGroup):
196 '''Path group config file directive.'''
197
198 def __init__(self, *a, **kw):
199 super(GroupOfPaths, self).__init__(**kw)
200
201 def setup(self, objs):
202 '''Resolve group members.'''
203
204 def map_member(x):
205 path = get_index(
206 objs, 'pathname', x['path'])
207 meta = get_index(
208 objs, 'meta', x['meta'])
209 return (path, meta)
210
211 self.members = map(
212 map_member,
213 self.members)
214
215 super(GroupOfPaths, self).setup(objs)
216
217
Brad Bishop34a7acd2017-04-27 23:47:23 -0400218class Everything(Renderer):
219 '''Parse/render entry point.'''
220
221 @staticmethod
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400222 def classmap(cls, sub=None):
223 '''Map render item class and subclass entries to the appropriate
224 handler methods.'''
225
226 class_map = {
Brad Bishop0e7df132017-05-23 17:58:12 -0400227 'path': {
228 'element': Path,
229 },
230 'pathgroup': {
231 'path': GroupOfPaths,
232 },
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400233 }
234
235 if cls not in class_map:
236 raise NotImplementedError('Unknown class: "{0}"'.format(cls))
237 if sub not in class_map[cls]:
238 raise NotImplementedError('Unknown {0} type: "{1}"'.format(
239 cls, sub))
240
241 return class_map[cls][sub]
242
243 @staticmethod
244 def load_one_yaml(path, fd, objs):
245 '''Parse a single YAML file. Parsing occurs in three phases.
246 In the first phase a factory method associated with each
247 configuration file directive is invoked. These factory
248 methods generate more factory methods. In the second
249 phase the factory methods created in the first phase
250 are invoked. In the last phase a callback is invoked on
251 each object created in phase two. Typically the callback
252 resolves references to other configuration file directives.'''
253
254 factory_objs = {}
255 for x in yaml.safe_load(fd.read()) or {}:
256
257 # Create factory object for this config file directive.
258 cls = x['class']
259 sub = x.get(cls)
260 if cls == 'group':
261 cls = '{0}group'.format(sub)
262
263 factory = Everything.classmap(cls, sub)
264 obj = factory(configfile=path, **x)
265
266 # For a given class of directive, validate the file
267 # doesn't have any duplicate names (duplicates are
268 # ok across config files).
269 if exists(factory_objs, obj.cls, obj.name, config=path):
270 raise NotUniqueError(path, cls, obj.name)
271
272 factory_objs.setdefault(cls, []).append(obj)
273 objs.setdefault(cls, []).append(obj)
274
275 for cls, items in factory_objs.items():
276 for obj in items:
277 # Add objects for template consumption.
278 obj.factory(objs)
279
280 @staticmethod
Brad Bishop34a7acd2017-04-27 23:47:23 -0400281 def load(args):
282 '''Aggregate all the YAML in the input directory
283 into a single aggregate.'''
284
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400285 objs = {}
286 yaml_files = filter(
287 lambda x: x.endswith('.yaml'),
288 os.listdir(args.inputdir))
Brad Bishop34a7acd2017-04-27 23:47:23 -0400289
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400290 yaml_files.sort()
Brad Bishop34a7acd2017-04-27 23:47:23 -0400291
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400292 for x in yaml_files:
293 path = os.path.join(args.inputdir, x)
294 with open(path, 'r') as fd:
295 Everything.load_one_yaml(path, fd, objs)
296
297 # Configuration file directives reference each other via
298 # the name attribute; however, when rendered the reference
299 # is just an array index.
300 #
301 # At this point all objects have been created but references
302 # have not been resolved to array indicies. Instruct objects
303 # to do that now.
304 for cls, items in objs.items():
305 for obj in items:
306 obj.setup(objs)
307
308 return Everything(**objs)
Brad Bishop34a7acd2017-04-27 23:47:23 -0400309
310 def __init__(self, *a, **kw):
Brad Bishop0e7df132017-05-23 17:58:12 -0400311 self.pathmeta = kw.pop('path', [])
312 self.paths = kw.pop('pathname', [])
313 self.meta = kw.pop('meta', [])
314 self.pathgroups = kw.pop('pathgroup', [])
315
Brad Bishop34a7acd2017-04-27 23:47:23 -0400316 super(Everything, self).__init__(**kw)
317
318 def generate_cpp(self, loader):
319 '''Render the template with the provided data.'''
Brad Bishope3a01af2017-05-15 17:09:04 -0400320 with open(args.output, 'w') as fd:
Brad Bishop34a7acd2017-04-27 23:47:23 -0400321 fd.write(
322 self.render(
323 loader,
Brad Bishope3a01af2017-05-15 17:09:04 -0400324 args.template,
Brad Bishop0e7df132017-05-23 17:58:12 -0400325 meta=self.meta,
326 paths=self.paths,
327 pathmeta=self.pathmeta,
328 pathgroups=self.pathgroups,
Brad Bishop34a7acd2017-04-27 23:47:23 -0400329 indent=Indent()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500330
331if __name__ == '__main__':
Brad Bishop34a7acd2017-04-27 23:47:23 -0400332 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 DBus Monitor (PDM) YAML '
339 'scanner and code generator.')
340
Matthew Barthdb440d42017-04-17 15:49:37 -0500341 parser.add_argument(
Brad Bishope3a01af2017-05-15 17:09:04 -0400342 "-o", "--out", dest="output",
343 default='generated.cpp',
344 help="Generated output file name and path.")
345 parser.add_argument(
346 '-t', '--template', dest='template',
Brad Bishop870c3fc2017-05-22 23:23:13 -0400347 default='generated.mako.hpp',
Brad Bishope3a01af2017-05-15 17:09:04 -0400348 help='The top level template to render.')
349 parser.add_argument(
350 '-p', '--template-path', dest='template_search',
351 default=script_dir,
352 help='The space delimited mako template search path.')
Brad Bishop34a7acd2017-04-27 23:47:23 -0400353 parser.add_argument(
354 '-d', '--dir', dest='inputdir',
355 default=os.path.join(script_dir, 'example'),
356 help='Location of files to process.')
357 parser.add_argument(
358 'command', metavar='COMMAND', type=str,
359 choices=valid_commands.keys(),
360 help='%s.' % " | ".join(valid_commands.keys()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500361
Brad Bishop34a7acd2017-04-27 23:47:23 -0400362 args = parser.parse_args()
363
364 if sys.version_info < (3, 0):
365 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400366 directories=args.template_search.split(),
Brad Bishop34a7acd2017-04-27 23:47:23 -0400367 disable_unicode=True)
368 else:
369 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400370 directories=args.template_search.split())
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400371 try:
372 function = getattr(
373 Everything.load(args),
374 valid_commands[args.command])
375 function(lookup)
376 except InvalidConfigError as e:
377 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
378 raise