#!/usr/bin/env python import os import sys import dbus import dbus.exceptions import json import logging from xml.etree import ElementTree from rocket import Rocket from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError import OpenBMCMapper from OpenBMCMapper import Mapper, PathTree, IntrospectionNodeParser, ListMatch DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface' DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs' DELETE_IFACE = 'org.openbmc.object.Delete' _4034_msg = "The specified %s cannot be %s: '%s'" def find_case_insensitive(value, lst): return next((x for x in lst if x.lower() == value.lower()), None) def makelist(data): if isinstance(data, list): return data elif data: return [data] else: return [] class RouteHandler(object): def __init__(self, app, bus, verbs, rules): self.app = app self.bus = bus self.mapper = Mapper(bus) self._verbs = makelist(verbs) self._rules = rules def _setup(self, **kw): request.route_data = {} if request.method in self._verbs: return self.setup(**kw) else: self.find(**kw) raise HTTPError(405, "Method not allowed.", Allow=','.join(self._verbs)) def __call__(self, **kw): return getattr(self, 'do_' + request.method.lower())(**kw) def install(self): self.app.route(self._rules, callback = self, method = ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) @staticmethod def try_mapper_call(f, callback = None, **kw): try: return f(**kw) except dbus.exceptions.DBusException, e: if e.get_dbus_name() != OpenBMCMapper.MAPPER_NOT_FOUND: raise if callback is None: def callback(e, **kw): abort(404, str(e)) callback(e, **kw) @staticmethod def try_properties_interface(f, *a): try: return f(*a) except dbus.exceptions.DBusException, e: if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message(): # interface doesn't have any properties return None if DBUS_UNKNOWN_METHOD == e.get_dbus_name(): # properties interface not implemented at all return None raise class DirectoryHandler(RouteHandler): verbs = 'GET' rules = '/' def __init__(self, app, bus): super(DirectoryHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path = '/'): return self.try_mapper_call( self.mapper.get_subtree_paths, path = path, depth = 1) def setup(self, path = '/'): request.route_data['map'] = self.find(path) def do_get(self, path = '/'): return request.route_data['map'] class ListNamesHandler(RouteHandler): verbs = 'GET' rules = ['/list', '/list'] def __init__(self, app, bus): super(ListNamesHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path = '/'): return self.try_mapper_call( self.mapper.get_subtree, path = path).keys() def setup(self, path = '/'): request.route_data['map'] = self.find(path) def do_get(self, path = '/'): return request.route_data['map'] class ListHandler(RouteHandler): verbs = 'GET' rules = ['/enumerate', '/enumerate'] def __init__(self, app, bus): super(ListHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path = '/'): return self.try_mapper_call( self.mapper.get_subtree, path = path) def setup(self, path = '/'): request.route_data['map'] = self.find(path) def do_get(self, path = '/'): objs = {} mapper_data = request.route_data['map'] tree = PathTree() for x,y in mapper_data.iteritems(): tree[x] = y try: # Check to see if the root path implements # enumerate in addition to any sub tree # objects. root = self.try_mapper_call(self.mapper.get_object, path = path) mapper_data[path] = root except: pass have_enumerate = [ (x[0], self.enumerate_capable(*x)) \ for x in mapper_data.iteritems() \ if self.enumerate_capable(*x) ] for x,y in have_enumerate: objs.update(self.call_enumerate(x, y)) tmp = tree[x] # remove the subtree del tree[x] # add the new leaf back since enumerate results don't # include the object enumerate is being invoked on tree[x] = tmp # make dbus calls for any remaining objects for x,y in tree.dataitems(): objs[x] = self.app.instance_handler.do_get(x) return objs @staticmethod def enumerate_capable(path, bus_data): busses = [] for name, ifaces in bus_data.iteritems(): if OpenBMCMapper.ENUMERATE_IFACE in ifaces: busses.append(name) return busses def call_enumerate(self, path, busses): objs = {} for b in busses: obj = self.bus.get_object(b, path, introspect = False) iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE) objs.update(iface.enumerate()) return objs class MethodHandler(RouteHandler): verbs = 'POST' rules = '/action/' request_type = list def __init__(self, app, bus): super(MethodHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path, method): busses = self.try_mapper_call(self.mapper.get_object, path = path) for items in busses.iteritems(): m = self.find_method_on_bus(path, method, *items) if m: return m abort(404, _4034_msg %('method', 'found', method)) def setup(self, path, method): request.route_data['method'] = self.find(path, method) def do_post(self, path, method): try: if request.parameter_list: return request.route_data['method'](*request.parameter_list) else: return request.route_data['method']() except dbus.exceptions.DBusException, e: if e.get_dbus_name() == DBUS_INVALID_ARGS: abort(400, str(e)) raise @staticmethod def find_method_in_interface(method, obj, interface, methods): if methods is None: return None method = find_case_insensitive(method, methods.keys()) if method is not None: iface = dbus.Interface(obj, interface) return iface.get_dbus_method(method) def find_method_on_bus(self, path, method, bus, interfaces): obj = self.bus.get_object(bus, path, introspect = False) iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) data = iface.Introspect() parser = IntrospectionNodeParser( ElementTree.fromstring(data), intf_match = ListMatch(interfaces)) for x,y in parser.get_interfaces().iteritems(): m = self.find_method_in_interface(method, obj, x, y.get('method')) if m: return m class PropertyHandler(RouteHandler): verbs = ['PUT', 'GET'] rules = '/attr/' def __init__(self, app, bus): super(PropertyHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path, prop): self.app.instance_handler.setup(path) obj = self.app.instance_handler.do_get(path) try: obj[prop] except KeyError, e: if request.method == 'PUT': abort(403, _4034_msg %('property', 'created', str(e))) else: abort(404, _4034_msg %('property', 'found', str(e))) return { path: obj } def setup(self, path, prop): request.route_data['obj'] = self.find(path, prop) def do_get(self, path, prop): return request.route_data['obj'][path][prop] def do_put(self, path, prop, value = None): if value is None: value = request.parameter_list prop, iface, properties_iface = self.get_host_interface( path, prop, request.route_data['map'][path]) try: properties_iface.Set(iface, prop, value) except ValueError, e: abort(400, str(e)) except dbus.exceptions.DBusException, e: if e.get_dbus_name() == DBUS_INVALID_ARGS: abort(403, str(e)) raise def get_host_interface(self, path, prop, bus_info): for bus, interfaces in bus_info.iteritems(): obj = self.bus.get_object(bus, path, introspect = True) properties_iface = dbus.Interface( obj, dbus_interface=dbus.PROPERTIES_IFACE) info = self.get_host_interface_on_bus( path, prop, properties_iface, bus, interfaces) if info is not None: prop, iface = info return prop, iface, properties_iface def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces): for i in interfaces: properties = self.try_properties_interface(iface.GetAll, i) if properties is None: continue prop = find_case_insensitive(prop, properties.keys()) if prop is None: continue return prop, i class InstanceHandler(RouteHandler): verbs = ['GET', 'PUT', 'DELETE'] rules = '' request_type = dict def __init__(self, app, bus): super(InstanceHandler, self).__init__( app, bus, self.verbs, self.rules) def find(self, path, callback = None): return { path: self.try_mapper_call( self.mapper.get_object, callback, path = path) } def setup(self, path): callback = None if request.method == 'PUT': def callback(e, **kw): abort(403, _4034_msg %('resource', 'created', path)) if request.route_data.get('map') is None: request.route_data['map'] = self.find(path, callback) def do_get(self, path): properties = {} for item in request.route_data['map'][path].iteritems(): properties.update(self.get_properties_on_bus( path, *item)) return properties @staticmethod def get_properties_on_iface(properties_iface, iface): properties = InstanceHandler.try_properties_interface( properties_iface.GetAll, iface) if properties is None: return {} return properties def get_properties_on_bus(self, path, bus, interfaces): properties = {} obj = self.bus.get_object(bus, path, introspect = False) properties_iface = dbus.Interface( obj, dbus_interface=dbus.PROPERTIES_IFACE) for i in interfaces: properties.update(self.get_properties_on_iface( properties_iface, i)) return properties def do_put(self, path): # make sure all properties exist in the request obj = set(self.do_get(path).keys()) req = set(request.parameter_list.keys()) diff = list(obj.difference(req)) if diff: abort(403, _4034_msg %('resource', 'removed', '%s/attr/%s' %(path, diff[0]))) diff = list(req.difference(obj)) if diff: abort(403, _4034_msg %('resource', 'created', '%s/attr/%s' %(path, diff[0]))) for p,v in request.parameter_list.iteritems(): self.app.property_handler.do_put( path, p, v) def do_delete(self, path): for bus_info in request.route_data['map'][path].iteritems(): if self.bus_missing_delete(path, *bus_info): abort(403, _4034_msg %('resource', 'removed', path)) for bus in request.route_data['map'][path].iterkeys(): self.delete_on_bus(path, bus) def bus_missing_delete(self, path, bus, interfaces): return DELETE_IFACE not in interfaces def delete_on_bus(self, path, bus): obj = self.bus.get_object(bus, path, introspect = False) delete_iface = dbus.Interface( obj, dbus_interface = DELETE_IFACE) delete_iface.Delete() class JsonApiRequestPlugin(object): ''' Ensures request content satisfies the OpenBMC json api format. ''' name = 'json_api_request' api = 2 error_str = "Expecting request format { 'data': }, got '%s'" type_error_str = "Unsupported Content-Type: '%s'" json_type = "application/json" request_methods = ['PUT', 'POST', 'PATCH'] @staticmethod def content_expected(): return request.method in JsonApiRequestPlugin.request_methods def validate_request(self): if request.content_length > 0 and \ request.content_type != self.json_type: abort(415, self.type_error_str %(request.content_type)) try: request.parameter_list = request.json.get('data') except ValueError, e: abort(400, str(e)) except (AttributeError, KeyError, TypeError): abort(400, self.error_str %(request.json)) def apply(self, callback, route): verbs = getattr(route.get_undecorated_callback(), '_verbs', None) if verbs is None: return callback if not set(self.request_methods).intersection(verbs): return callback def wrap(*a, **kw): if self.content_expected(): self.validate_request() return callback(*a, **kw) return wrap class JsonApiRequestTypePlugin(object): ''' Ensures request content type satisfies the OpenBMC json api format. ''' name = 'json_api_method_request' api = 2 error_str = "Expecting request format { 'data': %s }, got '%s'" def apply(self, callback, route): request_type = getattr(route.get_undecorated_callback(), 'request_type', None) if request_type is None: return callback def validate_request(): if not isinstance(request.parameter_list, request_type): abort(400, self.error_str %(str(request_type), request.json)) def wrap(*a, **kw): if JsonApiRequestPlugin.content_expected(): validate_request() return callback(*a, **kw) return wrap class JsonApiResponsePlugin(object): ''' Emits normal responses in the OpenBMC json api format. ''' name = 'json_api_response' api = 2 def apply(self, callback, route): def wrap(*a, **kw): resp = { 'data': callback(*a, **kw) } resp['status'] = 'ok' resp['message'] = response.status_line return resp return wrap class JsonApiErrorsPlugin(object): ''' Emits error responses in the OpenBMC json api format. ''' name = 'json_api_errors' api = 2 def __init__(self, **kw): self.app = None self.function_type = None self.original = None self.json_opts = { x:y for x,y in kw.iteritems() \ if x in ['indent','sort_keys'] } def setup(self, app): self.app = app self.function_type = type(app.default_error_handler) self.original = app.default_error_handler self.app.default_error_handler = self.function_type( self.json_errors, app, Bottle) def apply(self, callback, route): return callback def close(self): self.app.default_error_handler = self.function_type( self.original, self.app, Bottle) def json_errors(self, res, error): response_object = {'status': 'error', 'data': {} } response_object['message'] = error.status_line response_object['data']['description'] = str(error.body) if error.status_code == 500: response_object['data']['exception'] = repr(error.exception) response_object['data']['traceback'] = error.traceback.splitlines() json_response = json.dumps(response_object, **self.json_opts) res.content_type = 'application/json' return json_response class RestApp(Bottle): def __init__(self, bus): super(RestApp, self).__init__(autojson = False) self.bus = bus self.mapper = Mapper(bus) self.install_hooks() self.install_plugins() self.create_handlers() self.install_handlers() def install_plugins(self): # install json api plugins json_kw = {'indent': 2, 'sort_keys': True} self.install(JSONPlugin(**json_kw)) self.install(JsonApiErrorsPlugin(**json_kw)) self.install(JsonApiResponsePlugin()) self.install(JsonApiRequestPlugin()) self.install(JsonApiRequestTypePlugin()) def install_hooks(self): self.real_router_match = self.router.match self.router.match = self.custom_router_match self.add_hook('before_request', self.strip_extra_slashes) def create_handlers(self): # create route handlers self.directory_handler = DirectoryHandler(self, self.bus) self.list_names_handler = ListNamesHandler(self, self.bus) self.list_handler = ListHandler(self, self.bus) self.method_handler = MethodHandler(self, self.bus) self.property_handler = PropertyHandler(self, self.bus) self.instance_handler = InstanceHandler(self, self.bus) def install_handlers(self): self.directory_handler.install() self.list_names_handler.install() self.list_handler.install() self.method_handler.install() self.property_handler.install() # this has to come last, since it matches everything self.instance_handler.install() def custom_router_match(self, environ): ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is needed doesn't work for us since the instance rules match everything. This monkey-patch lets the route handler figure out which response is needed. This could be accomplished with a hook but that would require calling the router match function twice. ''' route, args = self.real_router_match(environ) if isinstance(route.callback, RouteHandler): route.callback._setup(**args) return route, args @staticmethod def strip_extra_slashes(): path = request.environ['PATH_INFO'] trailing = ("","/")[path[-1] == '/'] parts = filter(bool, path.split('/')) request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing if __name__ == '__main__': log = logging.getLogger('Rocket.Errors') log.setLevel(logging.INFO) log.addHandler(logging.StreamHandler(sys.stdout)) bus = dbus.SystemBus() app = RestApp(bus) default_cert = os.path.join(sys.prefix, 'share', os.path.basename(__file__), 'cert.pem') server = Rocket(('0.0.0.0', 443, default_cert, default_cert), 'wsgi', {'wsgi_app': app}) server.start()