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