blob: f655599b28a2a178a4720face5b83adc79e6f684 [file] [log] [blame]
Brad Bishop69750462015-11-13 14:10:35 -05001#!/usr/bin/env python
2
3import os
4import sys
5import dbus
6import dbus.exceptions
7import json
8import logging
9from xml.etree import ElementTree
10from rocket import Rocket
11from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
12import OpenBMCMapper
13from OpenBMCMapper import Mapper, PathTree, IntrospectionNodeParser, ListMatch
14
15DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
16DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
17DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
18DELETE_IFACE = 'org.openbmc.object.Delete'
19
20_4034_msg = "The specified %s cannot be %s: '%s'"
21
22def find_case_insensitive(value, lst):
23 return next((x for x in lst if x.lower() == value.lower()), None)
24
25def makelist(data):
26 if isinstance(data, list):
27 return data
28 elif data:
29 return [data]
30 else:
31 return []
32
33class RouteHandler(object):
34 def __init__(self, app, bus, verbs, rules):
35 self.app = app
36 self.bus = bus
37 self.mapper = Mapper(bus)
38 self._verbs = makelist(verbs)
39 self._rules = rules
40
41 def _setup(self, **kw):
42 request.route_data = {}
43 if request.method in self._verbs:
44 return self.setup(**kw)
45 else:
46 self.find(**kw)
47 raise HTTPError(405, "Method not allowed.",
48 Allow=','.join(self._verbs))
49
50 def __call__(self, **kw):
51 return getattr(self, 'do_' + request.method.lower())(**kw)
52
53 def install(self):
54 self.app.route(self._rules, callback = self,
55 method = ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
56
57 @staticmethod
58 def try_mapper_call(f, callback = None, **kw):
59 try:
60 return f(**kw)
61 except dbus.exceptions.DBusException, e:
62 if e.get_dbus_name() != OpenBMCMapper.MAPPER_NOT_FOUND:
63 raise
64 if callback is None:
65 def callback(e, **kw):
66 abort(404, str(e))
67
68 callback(e, **kw)
69
70 @staticmethod
71 def try_properties_interface(f, *a):
72 try:
73 return f(*a)
74 except dbus.exceptions.DBusException, e:
75 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
76 # interface doesn't have any properties
77 return None
78 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
79 # properties interface not implemented at all
80 return None
81 raise
82
83class DirectoryHandler(RouteHandler):
84 verbs = 'GET'
85 rules = '<path:path>/'
86
87 def __init__(self, app, bus):
88 super(DirectoryHandler, self).__init__(
89 app, bus, self.verbs, self.rules)
90
91 def find(self, path = '/'):
92 return self.try_mapper_call(
93 self.mapper.get_subtree_paths,
94 path = path, depth = 1)
95
96 def setup(self, path = '/'):
97 request.route_data['map'] = self.find(path)
98
99 def do_get(self, path = '/'):
100 return request.route_data['map']
101
102class ListNamesHandler(RouteHandler):
103 verbs = 'GET'
104 rules = ['/list', '<path:path>/list']
105
106 def __init__(self, app, bus):
107 super(ListNamesHandler, self).__init__(
108 app, bus, self.verbs, self.rules)
109
110 def find(self, path = '/'):
111 return self.try_mapper_call(
112 self.mapper.get_subtree, path = path).keys()
113
114 def setup(self, path = '/'):
115 request.route_data['map'] = self.find(path)
116
117 def do_get(self, path = '/'):
118 return request.route_data['map']
119
120class ListHandler(RouteHandler):
121 verbs = 'GET'
122 rules = ['/enumerate', '<path:path>/enumerate']
123
124 def __init__(self, app, bus):
125 super(ListHandler, self).__init__(
126 app, bus, self.verbs, self.rules)
127
128 def find(self, path = '/'):
129 return self.try_mapper_call(
130 self.mapper.get_subtree, path = path)
131
132 def setup(self, path = '/'):
133 request.route_data['map'] = self.find(path)
134
135 def do_get(self, path = '/'):
136 objs = {}
137 mapper_data = request.route_data['map']
138 tree = PathTree()
139 for x,y in mapper_data.iteritems():
140 tree[x] = y
141
142 try:
143 # Check to see if the root path implements
144 # enumerate in addition to any sub tree
145 # objects.
146 root = self.try_mapper_call(self.mapper.get_object,
147 path = path)
148 mapper_data[path] = root
149 except:
150 pass
151
152 have_enumerate = [ (x[0], self.enumerate_capable(*x)) \
153 for x in mapper_data.iteritems() \
154 if self.enumerate_capable(*x) ]
155
156 for x,y in have_enumerate:
157 objs.update(self.call_enumerate(x, y))
158 tmp = tree[x]
159 # remove the subtree
160 del tree[x]
161 # add the new leaf back since enumerate results don't
162 # include the object enumerate is being invoked on
163 tree[x] = tmp
164
165 # make dbus calls for any remaining objects
166 for x,y in tree.dataitems():
167 objs[x] = self.app.instance_handler.do_get(x)
168
169 return objs
170
171 @staticmethod
172 def enumerate_capable(path, bus_data):
173 busses = []
174 for name, ifaces in bus_data.iteritems():
175 if OpenBMCMapper.ENUMERATE_IFACE in ifaces:
176 busses.append(name)
177 return busses
178
179 def call_enumerate(self, path, busses):
180 objs = {}
181 for b in busses:
182 obj = self.bus.get_object(b, path, introspect = False)
183 iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE)
184 objs.update(iface.enumerate())
185 return objs
186
187class MethodHandler(RouteHandler):
188 verbs = 'POST'
189 rules = '<path:path>/action/<method>'
190 request_type = list
191
192 def __init__(self, app, bus):
193 super(MethodHandler, self).__init__(
194 app, bus, self.verbs, self.rules)
195
196 def find(self, path, method):
197 busses = self.try_mapper_call(self.mapper.get_object,
198 path = path)
199 for items in busses.iteritems():
200 m = self.find_method_on_bus(path, method, *items)
201 if m:
202 return m
203
204 abort(404, _4034_msg %('method', 'found', method))
205
206 def setup(self, path, method):
207 request.route_data['method'] = self.find(path, method)
208
209 def do_post(self, path, method):
210 try:
211 if request.parameter_list:
212 return request.route_data['method'](*request.parameter_list)
213 else:
214 return request.route_data['method']()
215
216 except dbus.exceptions.DBusException, e:
217 if e.get_dbus_name() == DBUS_INVALID_ARGS:
218 abort(400, str(e))
219 raise
220
221 @staticmethod
222 def find_method_in_interface(method, obj, interface, methods):
223 if methods is None:
224 return None
225
226 method = find_case_insensitive(method, methods.keys())
227 if method is not None:
228 iface = dbus.Interface(obj, interface)
229 return iface.get_dbus_method(method)
230
231 def find_method_on_bus(self, path, method, bus, interfaces):
232 obj = self.bus.get_object(bus, path, introspect = False)
233 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
234 data = iface.Introspect()
235 parser = IntrospectionNodeParser(
236 ElementTree.fromstring(data),
237 intf_match = ListMatch(interfaces))
238 for x,y in parser.get_interfaces().iteritems():
239 m = self.find_method_in_interface(method, obj, x,
240 y.get('method'))
241 if m:
242 return m
243
244class PropertyHandler(RouteHandler):
245 verbs = ['PUT', 'GET']
246 rules = '<path:path>/attr/<prop>'
247
248 def __init__(self, app, bus):
249 super(PropertyHandler, self).__init__(
250 app, bus, self.verbs, self.rules)
251
252 def find(self, path, prop):
253 self.app.instance_handler.setup(path)
254 obj = self.app.instance_handler.do_get(path)
255 try:
256 obj[prop]
257 except KeyError, e:
258 if request.method == 'PUT':
259 abort(403, _4034_msg %('property', 'created', str(e)))
260 else:
261 abort(404, _4034_msg %('property', 'found', str(e)))
262
263 return { path: obj }
264
265 def setup(self, path, prop):
266 request.route_data['obj'] = self.find(path, prop)
267
268 def do_get(self, path, prop):
269 return request.route_data['obj'][path][prop]
270
271 def do_put(self, path, prop, value = None):
272 if value is None:
273 value = request.parameter_list
274
275 prop, iface, properties_iface = self.get_host_interface(
276 path, prop, request.route_data['map'][path])
277 try:
278 properties_iface.Set(iface, prop, value)
279 except ValueError, e:
280 abort(400, str(e))
281 except dbus.exceptions.DBusException, e:
282 if e.get_dbus_name() == DBUS_INVALID_ARGS:
283 abort(403, str(e))
284 raise
285
286 def get_host_interface(self, path, prop, bus_info):
287 for bus, interfaces in bus_info.iteritems():
288 obj = self.bus.get_object(bus, path, introspect = True)
289 properties_iface = dbus.Interface(
290 obj, dbus_interface=dbus.PROPERTIES_IFACE)
291
292 info = self.get_host_interface_on_bus(
293 path, prop, properties_iface,
294 bus, interfaces)
295 if info is not None:
296 prop, iface = info
297 return prop, iface, properties_iface
298
299 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
300 for i in interfaces:
301 properties = self.try_properties_interface(iface.GetAll, i)
302 if properties is None:
303 continue
304 prop = find_case_insensitive(prop, properties.keys())
305 if prop is None:
306 continue
307 return prop, i
308
309class InstanceHandler(RouteHandler):
310 verbs = ['GET', 'PUT', 'DELETE']
311 rules = '<path:path>'
312 request_type = dict
313
314 def __init__(self, app, bus):
315 super(InstanceHandler, self).__init__(
316 app, bus, self.verbs, self.rules)
317
318 def find(self, path, callback = None):
319 return { path: self.try_mapper_call(
320 self.mapper.get_object,
321 callback,
322 path = path) }
323
324 def setup(self, path):
325 callback = None
326 if request.method == 'PUT':
327 def callback(e, **kw):
328 abort(403, _4034_msg %('resource',
329 'created', path))
330
331 if request.route_data.get('map') is None:
332 request.route_data['map'] = self.find(path, callback)
333
334 def do_get(self, path):
335 properties = {}
336 for item in request.route_data['map'][path].iteritems():
337 properties.update(self.get_properties_on_bus(
338 path, *item))
339
340 return properties
341
342 @staticmethod
343 def get_properties_on_iface(properties_iface, iface):
344 properties = InstanceHandler.try_properties_interface(
345 properties_iface.GetAll, iface)
346 if properties is None:
347 return {}
348 return properties
349
350 def get_properties_on_bus(self, path, bus, interfaces):
351 properties = {}
352 obj = self.bus.get_object(bus, path, introspect = False)
353 properties_iface = dbus.Interface(
354 obj, dbus_interface=dbus.PROPERTIES_IFACE)
355 for i in interfaces:
356 properties.update(self.get_properties_on_iface(
357 properties_iface, i))
358
359 return properties
360
361 def do_put(self, path):
362 # make sure all properties exist in the request
363 obj = set(self.do_get(path).keys())
364 req = set(request.parameter_list.keys())
365
366 diff = list(obj.difference(req))
367 if diff:
368 abort(403, _4034_msg %('resource', 'removed',
369 '%s/attr/%s' %(path, diff[0])))
370
371 diff = list(req.difference(obj))
372 if diff:
373 abort(403, _4034_msg %('resource', 'created',
374 '%s/attr/%s' %(path, diff[0])))
375
376 for p,v in request.parameter_list.iteritems():
377 self.app.property_handler.do_put(
378 path, p, v)
379
380 def do_delete(self, path):
381 for bus_info in request.route_data['map'][path].iteritems():
382 if self.bus_missing_delete(path, *bus_info):
383 abort(403, _4034_msg %('resource', 'removed',
384 path))
385
386 for bus in request.route_data['map'][path].iterkeys():
387 self.delete_on_bus(path, bus)
388
389 def bus_missing_delete(self, path, bus, interfaces):
390 return DELETE_IFACE not in interfaces
391
392 def delete_on_bus(self, path, bus):
393 obj = self.bus.get_object(bus, path, introspect = False)
394 delete_iface = dbus.Interface(
395 obj, dbus_interface = DELETE_IFACE)
396 delete_iface.Delete()
397
398class JsonApiRequestPlugin(object):
399 ''' Ensures request content satisfies the OpenBMC json api format. '''
400 name = 'json_api_request'
401 api = 2
402
403 error_str = "Expecting request format { 'data': <value> }, got '%s'"
404 type_error_str = "Unsupported Content-Type: '%s'"
405 json_type = "application/json"
406 request_methods = ['PUT', 'POST', 'PATCH']
407
408 @staticmethod
409 def content_expected():
410 return request.method in JsonApiRequestPlugin.request_methods
411
412 def validate_request(self):
413 if request.content_length > 0 and \
414 request.content_type != self.json_type:
415 abort(415, self.type_error_str %(request.content_type))
416
417 try:
418 request.parameter_list = request.json.get('data')
419 except ValueError, e:
420 abort(400, str(e))
421 except (AttributeError, KeyError, TypeError):
422 abort(400, self.error_str %(request.json))
423
424 def apply(self, callback, route):
425 verbs = getattr(route.get_undecorated_callback(),
426 '_verbs', None)
427 if verbs is None:
428 return callback
429
430 if not set(self.request_methods).intersection(verbs):
431 return callback
432
433 def wrap(*a, **kw):
434 if self.content_expected():
435 self.validate_request()
436 return callback(*a, **kw)
437
438 return wrap
439
440class JsonApiRequestTypePlugin(object):
441 ''' Ensures request content type satisfies the OpenBMC json api format. '''
442 name = 'json_api_method_request'
443 api = 2
444
445 error_str = "Expecting request format { 'data': %s }, got '%s'"
446
447 def apply(self, callback, route):
448 request_type = getattr(route.get_undecorated_callback(),
449 'request_type', None)
450 if request_type is None:
451 return callback
452
453 def validate_request():
454 if not isinstance(request.parameter_list, request_type):
455 abort(400, self.error_str %(str(request_type), request.json))
456
457 def wrap(*a, **kw):
458 if JsonApiRequestPlugin.content_expected():
459 validate_request()
460 return callback(*a, **kw)
461
462 return wrap
463
464class JsonApiResponsePlugin(object):
465 ''' Emits normal responses in the OpenBMC json api format. '''
466 name = 'json_api_response'
467 api = 2
468
469 def apply(self, callback, route):
470 def wrap(*a, **kw):
471 resp = { 'data': callback(*a, **kw) }
472 resp['status'] = 'ok'
473 resp['message'] = response.status_line
474 return resp
475 return wrap
476
477class JsonApiErrorsPlugin(object):
478 ''' Emits error responses in the OpenBMC json api format. '''
479 name = 'json_api_errors'
480 api = 2
481
482 def __init__(self, **kw):
483 self.app = None
484 self.function_type = None
485 self.original = None
486 self.json_opts = { x:y for x,y in kw.iteritems() \
487 if x in ['indent','sort_keys'] }
488
489 def setup(self, app):
490 self.app = app
491 self.function_type = type(app.default_error_handler)
492 self.original = app.default_error_handler
493 self.app.default_error_handler = self.function_type(
494 self.json_errors, app, Bottle)
495
496 def apply(self, callback, route):
497 return callback
498
499 def close(self):
500 self.app.default_error_handler = self.function_type(
501 self.original, self.app, Bottle)
502
503 def json_errors(self, res, error):
504 response_object = {'status': 'error', 'data': {} }
505 response_object['message'] = error.status_line
506 response_object['data']['description'] = str(error.body)
507 if error.status_code == 500:
508 response_object['data']['exception'] = repr(error.exception)
509 response_object['data']['traceback'] = error.traceback.splitlines()
510
511 json_response = json.dumps(response_object, **self.json_opts)
512 res.content_type = 'application/json'
513 return json_response
514
515class RestApp(Bottle):
516 def __init__(self, bus):
517 super(RestApp, self).__init__(autojson = False)
518 self.bus = bus
519 self.mapper = Mapper(bus)
520
521 self.install_hooks()
522 self.install_plugins()
523 self.create_handlers()
524 self.install_handlers()
525
526 def install_plugins(self):
527 # install json api plugins
528 json_kw = {'indent': 2, 'sort_keys': True}
529 self.install(JSONPlugin(**json_kw))
530 self.install(JsonApiErrorsPlugin(**json_kw))
531 self.install(JsonApiResponsePlugin())
532 self.install(JsonApiRequestPlugin())
533 self.install(JsonApiRequestTypePlugin())
534
535 def install_hooks(self):
536 self.real_router_match = self.router.match
537 self.router.match = self.custom_router_match
538 self.add_hook('before_request', self.strip_extra_slashes)
539
540 def create_handlers(self):
541 # create route handlers
542 self.directory_handler = DirectoryHandler(self, self.bus)
543 self.list_names_handler = ListNamesHandler(self, self.bus)
544 self.list_handler = ListHandler(self, self.bus)
545 self.method_handler = MethodHandler(self, self.bus)
546 self.property_handler = PropertyHandler(self, self.bus)
547 self.instance_handler = InstanceHandler(self, self.bus)
548
549 def install_handlers(self):
550 self.directory_handler.install()
551 self.list_names_handler.install()
552 self.list_handler.install()
553 self.method_handler.install()
554 self.property_handler.install()
555 # this has to come last, since it matches everything
556 self.instance_handler.install()
557
558 def custom_router_match(self, environ):
559 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
560 needed doesn't work for us since the instance rules match everything.
561 This monkey-patch lets the route handler figure out which response is
562 needed. This could be accomplished with a hook but that would require
563 calling the router match function twice.
564 '''
565 route, args = self.real_router_match(environ)
566 if isinstance(route.callback, RouteHandler):
567 route.callback._setup(**args)
568
569 return route, args
570
571 @staticmethod
572 def strip_extra_slashes():
573 path = request.environ['PATH_INFO']
574 trailing = ("","/")[path[-1] == '/']
575 parts = filter(bool, path.split('/'))
576 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
577
578if __name__ == '__main__':
579 log = logging.getLogger('Rocket.Errors')
580 log.setLevel(logging.INFO)
581 log.addHandler(logging.StreamHandler(sys.stdout))
582
583 bus = dbus.SystemBus()
584 app = RestApp(bus)
585 default_cert = os.path.join(sys.prefix, 'share',
586 os.path.basename(__file__), 'cert.pem')
587
588 server = Rocket(('0.0.0.0',
589 443,
590 default_cert,
591 default_cert),
592 'wsgi', {'wsgi_app': app})
593 server.start()