1#!/usr/bin/env python
2
3'''Phosphor Inventory Manager 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
18import sys
19import os
20import argparse
21import subprocess
22import yaml
23import mako.lookup
24import sdbusplus.property
25from sdbusplus.namedelement import NamedElement
26from sdbusplus.renderer import Renderer
27
28
29class Interface(list):
30    '''Provide various interface transformations.'''
31
32    def __init__(self, iface):
33        super(Interface, self).__init__(iface.split('.'))
34
35    def namespace(self):
36        '''Represent as an sdbusplus namespace.'''
37        return '::'.join(['sdbusplus'] + self[:-1] + ['server', self[-1]])
38
39    def header(self):
40        '''Represent as an sdbusplus server binding header.'''
41        return os.sep.join(self + ['server.hpp'])
42
43    def __str__(self):
44        return '.'.join(self)
45
46
47class Argument(sdbusplus.property.Property):
48    '''Bridge sdbusplus property typenames to syntatically correct c++.'''
49
50    def __init__(self, **kw):
51        self.value = kw.pop('value')
52        super(Argument, self).__init__(**kw)
53
54    def cppArg(self):
55        '''Transform string types to c++ string constants.'''
56        if self.typeName == 'string':
57            return '"%s"' % self.value
58
59        return self.value
60
61
62class MethodCall(NamedElement, Renderer):
63    '''Render syntatically correct c++ method calls.'''
64
65    def __init__(self, **kw):
66        self.namespace = kw.pop('namespace', [])
67        self.pointer = kw.pop('pointer', False)
68        self.args = \
69            [Argument(**x) for x in kw.pop('args', [])]
70        super(MethodCall, self).__init__(**kw)
71
72    def bare_method(self):
73        '''Provide the method name and encompassing
74        namespace without any arguments.'''
75        return '::'.join(self.namespace + [self.name])
76
77
78class Filter(MethodCall):
79    '''Provide common attributes for any filter.'''
80
81    def __init__(self, **kw):
82        kw['namespace'] = ['filters']
83        super(Filter, self).__init__(**kw)
84
85
86class Action(MethodCall):
87    '''Provide common attributes for any action.'''
88
89    def __init__(self, **kw):
90        kw['namespace'] = ['actions']
91        super(Action, self).__init__(**kw)
92
93
94class DbusSignature(NamedElement, Renderer):
95    '''Represent a dbus signal match signature.'''
96
97    def __init__(self, **kw):
98        self.sig = {x: y for x, y in kw.iteritems()}
99        kw.clear()
100        super(DbusSignature, self).__init__(**kw)
101
102
103class DestroyObject(Action):
104    '''Render a destroyObject action.'''
105
106    def __init__(self, **kw):
107        mapped = kw.pop('args')
108        kw['args'] = [
109            {'value': mapped['path'], 'type':'string'},
110        ]
111        super(DestroyObject, self).__init__(**kw)
112
113
114class NoopAction(Action):
115    '''Render a noop action.'''
116
117    def __init__(self, **kw):
118        kw['pointer'] = True
119        super(NoopAction, self).__init__(**kw)
120
121
122class NoopFilter(Filter):
123    '''Render a noop filter.'''
124
125    def __init__(self, **kw):
126        kw['pointer'] = True
127        super(NoopFilter, self).__init__(**kw)
128
129
130class PropertyChanged(Filter):
131    '''Render a propertyChanged filter.'''
132
133    def __init__(self, **kw):
134        mapped = kw.pop('args')
135        kw['args'] = [
136            {'value': mapped['interface'], 'type':'string'},
137            {'value': mapped['property'], 'type':'string'},
138            mapped['value']
139        ]
140        super(PropertyChanged, self).__init__(**kw)
141
142
143class Event(NamedElement, Renderer):
144    '''Render an inventory manager event.'''
145
146    action_map = {
147        'noop': NoopAction,
148        'destroyObject': DestroyObject,
149    }
150
151    def __init__(self, **kw):
152        self.cls = kw.pop('type')
153        self.actions = \
154            [self.action_map[x['name']](**x)
155                for x in kw.pop('actions', [{'name': 'noop'}])]
156        super(Event, self).__init__(**kw)
157
158
159class MatchEvent(Event):
160    '''Associate one or more dbus signal match signatures with
161    a filter.'''
162
163    filter_map = {
164        'none': NoopFilter,
165        'propertyChangedTo': PropertyChanged,
166    }
167
168    def __init__(self, **kw):
169        self.signatures = \
170            [DbusSignature(**x) for x in kw.pop('signatures', [])]
171        self.filters = \
172            [self.filter_map[x['name']](**x)
173                for x in kw.pop('filters', [{'name': 'none'}])]
174        super(MatchEvent, self).__init__(**kw)
175
176
177class Everything(Renderer):
178    '''Parse/render entry point.'''
179
180    class_map = {
181        'match': MatchEvent,
182    }
183
184    @staticmethod
185    def load(args):
186        # Invoke sdbus++ to generate any extra interface bindings for
187        # extra interfaces that aren't defined externally.
188        yaml_files = []
189        extra_ifaces_dir = os.path.join(args.inputdir, 'extra_interfaces.d')
190        if os.path.exists(extra_ifaces_dir):
191            for directory, _, files in os.walk(extra_ifaces_dir):
192                if not files:
193                    continue
194
195                yaml_files += map(
196                    lambda f: os.path.relpath(
197                        os.path.join(directory, f),
198                        extra_ifaces_dir),
199                    filter(lambda f: f.endswith('.interface.yaml'), files))
200
201        genfiles = {
202            'server-cpp': lambda x: '%s.cpp' % (
203                x.replace(os.sep, '.')),
204            'server-header': lambda x: os.path.join(
205                os.path.join(
206                    *x.split('.')), 'server.hpp')
207        }
208
209        for i in yaml_files:
210            iface = i.replace('.interface.yaml', '').replace(os.sep, '.')
211            for process, f in genfiles.iteritems():
212
213                dest = os.path.join(args.outputdir, f(iface))
214                parent = os.path.dirname(dest)
215                if parent and not os.path.exists(parent):
216                    os.makedirs(parent)
217
218                with open(dest, 'w') as fd:
219                    subprocess.call([
220                        'sdbus++',
221                        '-r',
222                        extra_ifaces_dir,
223                        'interface',
224                        process,
225                        iface],
226                        stdout=fd)
227
228        # Aggregate all the event YAML in the events.d directory
229        # into a single list of events.
230
231        events_dir = os.path.join(args.inputdir, 'events.d')
232        yaml_files = filter(
233            lambda x: x.endswith('.yaml'),
234            os.listdir(events_dir))
235
236        events = []
237        for x in yaml_files:
238            with open(os.path.join(events_dir, x), 'r') as fd:
239                for e in yaml.load(fd.read()).get('events', {}):
240                    events.append(e)
241
242        return Everything(
243            *events,
244            interfaces=Everything.get_interfaces(args))
245
246    @staticmethod
247    def get_interfaces(args):
248        '''Aggregate all the interface YAML in the interfaces.d
249        directory into a single list of interfaces.'''
250
251        interfaces_dir = os.path.join(args.inputdir, 'interfaces.d')
252        yaml_files = filter(
253            lambda x: x.endswith('.yaml'),
254            os.listdir(interfaces_dir))
255
256        interfaces = []
257        for x in yaml_files:
258            with open(os.path.join(interfaces_dir, x), 'r') as fd:
259                for i in yaml.load(fd.read()):
260                    interfaces.append(i)
261
262        return interfaces
263
264    def __init__(self, *a, **kw):
265        self.interfaces = \
266            [Interface(x) for x in kw.pop('interfaces', [])]
267        self.events = [
268            self.class_map[x['type']](**x) for x in a]
269        super(Everything, self).__init__(**kw)
270
271    def list_interfaces(self, *a):
272        print ' '.join([str(i) for i in self.interfaces])
273
274    def generate_cpp(self, loader):
275        '''Render the template with the provided events and interfaces.'''
276        with open(os.path.join(
277                args.outputdir,
278                'generated.cpp'), 'w') as fd:
279            fd.write(
280                self.render(
281                    loader,
282                    'generated.mako.cpp',
283                    events=self.events,
284                    interfaces=self.interfaces))
285
286
287if __name__ == '__main__':
288    script_dir = os.path.dirname(os.path.realpath(__file__))
289    valid_commands = {
290        'generate-cpp': 'generate_cpp',
291        'list-interfaces': 'list_interfaces'
292    }
293
294    parser = argparse.ArgumentParser(
295        description='Phosphor Inventory Manager (PIM) YAML '
296        'scanner and code generator.')
297    parser.add_argument(
298        '-o', '--output-dir', dest='outputdir',
299        default='.', help='Output directory.')
300    parser.add_argument(
301        '-d', '--dir', dest='inputdir',
302        default=os.path.join(script_dir, 'example'),
303        help='Location of files to process.')
304    parser.add_argument(
305        'command', metavar='COMMAND', type=str,
306        choices=valid_commands.keys(),
307        help='Command to run.')
308
309    args = parser.parse_args()
310
311    if sys.version_info < (3, 0):
312        lookup = mako.lookup.TemplateLookup(
313            directories=[script_dir],
314            disable_unicode=True)
315    else:
316        lookup = mako.lookup.TemplateLookup(
317            directories=[script_dir])
318
319    function = getattr(
320        Everything.load(args),
321        valid_commands[args.command])
322    function(lookup)
323
324
325# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
326