diff options
author | Brad Bishop <bradleyb@us.ibm.com> | 2015-11-13 21:28:16 -0500 |
---|---|---|
committer | Brad Bishop <bradleyb@us.ibm.com> | 2015-11-13 21:51:49 -0500 |
commit | b1cbdaf509fa9239a91341366ab153998823eb0d (patch) | |
tree | 9450344e4027bdb58d31037e101511a430dfcfe6 | |
parent | 697504667d9f2b74818b9203faa8556caab7611b (diff) | |
download | phosphor-rest-server-b1cbdaf509fa9239a91341366ab153998823eb0d.tar.gz phosphor-rest-server-b1cbdaf509fa9239a91341366ab153998823eb0d.zip |
Remove old rest server
-rw-r--r-- | bottle-rest | 593 | ||||
-rw-r--r-- | phosphor-rest | 821 |
2 files changed, 522 insertions, 892 deletions
diff --git a/bottle-rest b/bottle-rest deleted file mode 100644 index f655599..0000000 --- a/bottle-rest +++ /dev/null @@ -1,593 +0,0 @@ -#!/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 = '<path:path>/' - - 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', '<path:path>/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', '<path:path>/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 = '<path:path>/action/<method>' - 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 = '<path:path>/attr/<prop>' - - 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 = '<path:path>' - 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': <value> }, 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() diff --git a/phosphor-rest b/phosphor-rest index 026eb74..f655599 100644 --- a/phosphor-rest +++ b/phosphor-rest @@ -1,273 +1,140 @@ #!/usr/bin/env python -# Contributors Listed Below - COPYRIGHT 2015 -# [+] International Business Machines Corp. -# -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. - -import BaseHTTPServer -import SocketServer -import json +import os +import sys import dbus -from OpenBMCMapper import Path, Mapper, PathTree +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 -class RestException(Exception): - def __init__(self, msg, http_status=403): - self.status = http_status - super(RestException, self).__init__(msg) - -class Response(object): - def render(self, handler): - raise NotImplemented() - -class ErrorResponse(Response): - def __init__(self, ex): - self.ex = ex - - def render(self, handler): - err = {'status': 'error', 'error': self.ex.message,} - handler.send_response(self.ex.status) - handler.send_header('Content-Type', 'application/json') - handler.end_headers() - handler.wfile.write(json.dumps(err, indent=2, sort_keys=True)) - -class JSONResponse(Response): - def __init__(self, data): - self.data = data - - def render(self, handler): - handler.send_response(200) - handler.send_header('Content-Type', 'application/json') - handler.end_headers() - handler.wfile.write(json.dumps(self.data, indent=2, sort_keys=True)) - -class RequestHandler(object): - def __init__(self, req, path, data): - self.req = req - self.path = path - self.bus = req.server.bus - self.mapper = req.server.mapper - self.data = data - - def do_command(self): - f = getattr(self, 'do_' + self.req.command) - return f() - - def do_GET(self): - raise RestException("Not Implemented", 501) - - def do_PUT(self): - raise RestException("Not Implemented", 501) - - def do_POST(self): - raise RestException("Not Implemented", 501) - - def do_PATCH(self): - raise RestException("Not Implemented", 501) - - def do_DELETE(self): - raise RestException("Not Implemented", 501) - -class MethodHandler(RequestHandler): - def __init__(self, req, path, data): - super(MethodHandler, self).__init__(req, path, data) - self.method = Path(self.req.path).rel(first = -1) - - def find_method_in_interface(self, obj, interface): - try: - iface = dbus.Interface(obj, interface) - return getattr(iface, self.method) - except dbus.DBusException: - return None +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' - def find_method_on_bus(self, bus, interfaces): - obj = self.bus.get_object(bus, self.path) - for i in interfaces: - m = self.find_method_in_interface(obj, i) - if not m: - continue - return m +_4034_msg = "The specified %s cannot be %s: '%s'" - def find_method(self): - busses = self.mapper.get_object( - self.path) - for items in busses.iteritems(): - m = self.find_method_on_bus(*items) - if not m: - continue +def find_case_insensitive(value, lst): + return next((x for x in lst if x.lower() == value.lower()), None) - return m +def makelist(data): + if isinstance(data, list): + return data + elif data: + return [data] + else: + return [] - def do_POST(self): - try: - method = self.find_method() - except: - raise RestException("Not Found", 404) - try: - d = { 'result': method(*self.data), - 'status': 'OK'} - except Exception, e: - d = { 'error': str(e), - 'status': 'error'} - return d - -class InstanceHandler(RequestHandler): - def __init__(self, req, path, data, busses): - super(InstanceHandler, self).__init__(req, path, data) - self.busses = busses - - def get_one_iface(self, properties_iface, iface): +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 properties_iface.GetAll(iface) - except: - # interface doesn't have any properties - return {} - - def get_one_bus(self, bus, interfaces): - properties = {} - obj = self.bus.get_object(bus, self.path) - properties_iface = dbus.Interface( - obj, dbus_interface=dbus.PROPERTIES_IFACE) - for i in interfaces: - properties.update(self.get_one_iface(properties_iface, i)) - - return properties - - def do_GET(self): - properties = {} - for item in self.busses.iteritems(): - properties.update(self.get_one_bus(*item)) - - return properties - - def try_set_one_interface(self, prop, value, properties_iface, interface): + 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: - properties_iface.Set(interface, prop, value) - return True - except: - # property doesn't live on this interface/bus - return False + 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 - def try_set_one_bus(self, prop, value, bus, interfaces): - obj = self.bus.get_object(bus, self.path) - properties_iface = dbus.Interface( - obj, dbus_interface=dbus.PROPERTIES_IFACE) - - for iface in interfaces: - if self.try_set_one_interface(prop, value, - properties_iface, iface): - return True +class DirectoryHandler(RouteHandler): + verbs = 'GET' + rules = '<path:path>/' - return False + def __init__(self, app, bus): + super(DirectoryHandler, self).__init__( + app, bus, self.verbs, self.rules) - def set_one_property(self, prop, value): - for item in self.busses.iteritems(): - if not self.try_set_one_bus(prop, value, *item): - raise RestException("Not Found", 404) + def find(self, path = '/'): + return self.try_mapper_call( + self.mapper.get_subtree_paths, + path = path, depth = 1) - def validate_json(self): - if type(self.data) != dict: - raise RestException("Bad Request", 400) + def setup(self, path = '/'): + request.route_data['map'] = self.find(path) - obj = self.do_GET() - if len(self.data) != len(obj): - raise RestException("Bad Request", 400) - for x in obj.iterkeys(): - if x not in self.data: - raise RestException("Bad Request", 400) + def do_get(self, path = '/'): + return request.route_data['map'] - def do_PUT(self): - try: - self.validate_json() - for p in self.data.iteritems(): - self.set_one_property(*p) - - d = { 'status': 'OK'} - except Exception, e: - d = { 'error': str(e), - 'status': 'error'} - return d - - def do_POST(self): - for p in self.data.iteritems(): - self.set_one_property(*p) - -class AttrHandler(RequestHandler): - def __init__(self, req, path, data): - super(AttrHandler, self).__init__(req, path, data) - try: - self.inst = InstanceHandler(req, path, data, - self.mapper.get_object(path)) - except KeyError: - raise RestException("Not Found", 404) - self.attr = Path(self.req.path).rel(first = -1) - - def do_GET(self): - obj = self.inst.do_GET() - try: - return obj[self.attr] - except KeyError: - raise RestException("Not Found", 404) +class ListNamesHandler(RouteHandler): + verbs = 'GET' + rules = ['/list', '<path:path>/list'] - def do_PUT(self): - self.inst.set_one_property(self.attr, self.data) + def __init__(self, app, bus): + super(ListNamesHandler, self).__init__( + app, bus, self.verbs, self.rules) -class TypesHandler(RequestHandler): - def __init__(self, req, path, data): - super(TypesHandler, self).__init__(req, path, data) + def find(self, path = '/'): + return self.try_mapper_call( + self.mapper.get_subtree, path = path).keys() - def do_GET(self): - types = self.mapper.get_subtree_paths(self.path, 1) - if not types: - raise RestException("Not Found", 404) + def setup(self, path = '/'): + request.route_data['map'] = self.find(path) - return types + def do_get(self, path = '/'): + return request.route_data['map'] -class ListHandler(RequestHandler): - def __init__(self, req, path, data): - super(ListHandler, self).__init__(req, path, data) +class ListHandler(RouteHandler): + verbs = 'GET' + rules = ['/enumerate', '<path:path>/enumerate'] - def do_GET(self): - objs = self.mapper.get_subtree(self.path) - if not objs: - raise RestException("Not Found", 404) + def __init__(self, app, bus): + super(ListHandler, self).__init__( + app, bus, self.verbs, self.rules) - return objs.keys() - -class EnumerateHandler(RequestHandler): - def __init__(self, req, path, data): - super(EnumerateHandler, self).__init__(req, path, data) - - def get_enumerate(self, path, data): - busses = [] - for s, i in data.iteritems(): - if OpenBMCMapper.ENUMERATE_IFACE in i: - busses.append(s) - return busses + def find(self, path = '/'): + return self.try_mapper_call( + self.mapper.get_subtree, path = path) - def call_enumerate(self, path, busses): - objs = {} - for b in busses: - obj = self.bus.get_object(b, path) - iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE) - objs.update(iface.enumerate()) - return objs + def setup(self, path = '/'): + request.route_data['map'] = self.find(path) - def do_GET(self): + def do_get(self, path = '/'): objs = {} - mapper_data = self.mapper.get_subtree(self.path) + mapper_data = request.route_data['map'] tree = PathTree() for x,y in mapper_data.iteritems(): tree[x] = y @@ -276,95 +143,451 @@ class EnumerateHandler(RequestHandler): # Check to see if the root path implements # enumerate in addition to any sub tree # objects. - root = self.mapper.get_object(self.path) - mapper_data[self.path] = root + root = self.try_mapper_call(self.mapper.get_object, + path = path) + mapper_data[path] = root except: pass - have_enumerate = [ (x[0], self.get_enumerate(*x)) for x in mapper_data.iteritems() \ - if self.get_enumerate(*x) ] + 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] = InstanceHandler(self.req, x, self.data, y).do_GET() + objs[x] = self.app.instance_handler.do_get(x) + + return objs - if not objs: - raise RestException("Not Found", 404) + @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 DBusRestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - def get_real_handler(self, data): - path = Path(self.path) +class MethodHandler(RouteHandler): + verbs = 'POST' + rules = '<path:path>/action/<method>' + 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)) - if self.path[-1] == '/': - return TypesHandler(self, path.fq(), data) + def setup(self, path, method): + request.route_data['method'] = self.find(path, method) - if path.parts[-1] == 'list': - return ListHandler(self, path.fq(last = -1), data) + 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 - if path.parts[-1] == 'enumerate': - return EnumerateHandler(self, path.fq(last = -1), data) + 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 = '<path:path>/attr/<prop>' + + 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))) - if path.depth() > 1 and path.parts[-2] == 'attr': - return AttrHandler(self, path.fq(last = -2), data) + return { path: obj } - if path.depth() > 1 and path.parts[-2] == 'action': - return MethodHandler(self, path.fq(last = -2), data) + def setup(self, path, prop): + request.route_data['obj'] = self.find(path, prop) - # have to do an objectmapper query at this point - mapper_entry = self.server.mapper.get_object(path.fq()) - if mapper_entry: - return InstanceHandler(self, path.fq(), data, - mapper_entry) + def do_get(self, path, prop): + return request.route_data['obj'][path][prop] - raise RestException("Not Found", 404) + def do_put(self, path, prop, value = None): + if value is None: + value = request.parameter_list - def do_command(self): - data = None + prop, iface, properties_iface = self.get_host_interface( + path, prop, request.route_data['map'][path]) try: - if self.command in ['POST', 'PUT', 'PATCH']: - length = int(self.headers.getheader( - 'content-length')) - data = json.loads(self.rfile.read(length)) + 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) - resp = self.get_real_handler(data).do_command() - if not resp: - resp = {'status': 'OK' } - response = JSONResponse(resp) - except RestException, ex: - response = ErrorResponse(ex) + 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 - response.render(self) - self.wfile.close() + 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 = '<path:path>' + 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)) - def do_GET(self): - return self.do_command() + return properties - def do_POST(self): - return self.do_command() + @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 do_PATCH(self): - return self.do_command() + 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)) - def do_PUT(self): - return self.do_command() + return properties - def do_DELETE(self): - return self.do_command() + 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': <value> }, 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)) -class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): - def __init__(self, bind, handler, bus): - BaseHTTPServer.HTTPServer.__init__(self, bind, handler) + 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(self.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() - server = HTTPServer(('', 80), DBusRestHandler, bus) - server.serve_forever() + 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() |