From 2c6fc760919cc214413874d60489e3643b639692 Mon Sep 17 00:00:00 2001 From: Brad Bishop Date: Mon, 29 Aug 2016 15:53:25 -0400 Subject: Split server and application Provide the REST server as a python module, enabling it to be hosted by any WSGI provider. Provide a Rocket startup script with configurable WSGI application. Change-Id: I1a9c25b10c33b08dfb8f60dc6c33aaf727562a9f Signed-off-by: Brad Bishop --- cert.pem | 48 --- module/obmc/wsgi/apps/rest_dbus.py | 749 +++++++++++++++++++++++++++++++++++ module/setup.py | 7 + phosphor-rest | 771 ------------------------------------- servers/cert.pem | 48 +++ servers/rocket/cert.pem | 1 + servers/rocket/phosphor-rocket | 44 +++ servers/rocket/setup.cfg | 1 + servers/rocket/setup.py | 8 + servers/setup.cfg | 2 + setup.cfg | 2 - setup.py | 7 - 12 files changed, 860 insertions(+), 828 deletions(-) delete mode 100644 cert.pem create mode 100644 module/obmc/wsgi/apps/rest_dbus.py create mode 100644 module/setup.py delete mode 100644 phosphor-rest create mode 100644 servers/cert.pem create mode 120000 servers/rocket/cert.pem create mode 100644 servers/rocket/phosphor-rocket create mode 120000 servers/rocket/setup.cfg create mode 100644 servers/rocket/setup.py create mode 100644 servers/setup.cfg delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/cert.pem b/cert.pem deleted file mode 100644 index 095da08..0000000 --- a/cert.pem +++ /dev/null @@ -1,48 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDbzCCAlegAwIBAgIJAL6IhnBSzF8HMA0GCSqGSIb3DQEBCwUAME4xCzAJBgNV -BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxFDASBgNVBAoMC29wZW5ibWMu -b3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTUxMTA2MjAzNTA2WhcNMTYxMTA1 -MjAzNTA2WjBOMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRQw -EgYDVQQKDAtvcGVuYm1jLm9yZzESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wl2ji+GPUpg21yjyo5jh+KwnZsV91wV -EO1DM6bdHbM/p34GkNRC6PO6DMDc3lGXaoVLHgdxhNMO/7lPwrAbO2dUyjoo4RCP -O23hWadodstfhgCMa5/h6rSbxtpVsnWD3nBh88tlug+jBZ625d6S+5MglfgrrQai -+gYC9xDzCmMN7yWsfKWO8WfrMfebkB7c+TuWnF/O6u/wmUEZOybavrfP79MuLbwC -+UkxF6PwiThGD7yINXSTxCDCN58MTrDML8AF7CWzBsYycJlJfcs2ix32Csk1FIey -t+Ze+tzEIzvOb/1G0hkIg40msFaWWEgXX9MIDHDGONwYyejb4nO8mQIDAQABo1Aw -TjAdBgNVHQ4EFgQUgoShQ/gqY2/oZWMRU4jJdAim38cwHwYDVR0jBBgwFoAUgoSh -Q/gqY2/oZWMRU4jJdAim38cwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AQEAg2qFx8mu5Z0QBSZK3FZhNVAsbZwZuH7cjRGxWYwNlyaS24fVdts7q8YBRjpm -OZJLO+gBYkCRJnZ/AlfJ0yorT7p7Z0GTP4GrtPmh8eLoh/q5NLUGN7T2IkTea+Q+ -4iorkAuYzmSviJNUZF99MhpbuPY8wCNQWPI13X6YRrJUxQGWOj7LmqR7gE9lSPiw -xXlrWvR1tc48Z20bEXR08nDH1m5G208BgtLRk53FdYGExebpoEQokQaYyJ6b4L2A -zkFLExO+ngn49ceC9XASlN4MnKvl4FiSOQqfLhKHjwpzVuMWifV/hlY9GGVbodeg -lUFi3n1XfYxNtiNSDZvWcBWKtA== ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0wl2ji+GPUpg21yjyo5jh+KwnZsV91wVEO1DM6bdHbM/p34G -kNRC6PO6DMDc3lGXaoVLHgdxhNMO/7lPwrAbO2dUyjoo4RCPO23hWadodstfhgCM -a5/h6rSbxtpVsnWD3nBh88tlug+jBZ625d6S+5MglfgrrQai+gYC9xDzCmMN7yWs -fKWO8WfrMfebkB7c+TuWnF/O6u/wmUEZOybavrfP79MuLbwC+UkxF6PwiThGD7yI -NXSTxCDCN58MTrDML8AF7CWzBsYycJlJfcs2ix32Csk1FIeyt+Ze+tzEIzvOb/1G -0hkIg40msFaWWEgXX9MIDHDGONwYyejb4nO8mQIDAQABAoIBAQCE/a2spN5fuYOY -OaUufNTUSVMrvxP0sh7EcACtiDZIBTHUB2Nz2Y/g5dcVOmT15U0aX62a2u362lbV -aJ6O/hPrN48DcetZCep5dSFSMmFum3MzKx1SpYrlMbQJeIYQ1GWpxAC2djNBMaF2 -ZTK1YbIWv/0FBUPg4hHKpgcwU4oVvHj8W45ORY9/tJB4yPrdVewwtBQ1iJkEPgfO -H5/jjGfwPHgLYnzMtu1PU1tqNVWgXw19Zn3W6wf1v+f4abrhSZSRoHqciMC10da9 -sMmhmdsi0ZWYQSn4TRRI4i9zZXuQhz/5HOUtt38JCpnkqWeEA7OHwrzyi1uHZHh0 -7TrcW11BAoGBAPenwCy7jaZXdTnpckfNWDLT2Qp07dmw8WocrbCdMUUK5DTXH8Yh -xdEt5O4Vu6qdC8ccfIlS5DTVfddqrNLPuCpOpUmZVbGQeX6h9FoAyHG5YSuiZtXD -QHfWM8CJY8tT5OYTDGGgvbLSQehVKQ+a5n9ubB8YvFmtoEl2qaWgi6+FAoGBANol -2LnV8jXggwsN/13Jw1uIPNrKj6VClDUizLIQAsDwhS4aJeFLldTtIeVAPDovMDQX -/r33EDd0eev2w3E9Sd8Th4GaxOo9Iho8I2o0aqbjdfnT3/DsuAo5KARvzjiEvbnf -J5oTM+6eqdCyL1woy2Slqp5Wm1538ZoPALteG8MFAoGAQ9tl75vQOyS5jQ2m73+X -TA356UCSr1QpQb7r5Hmdt2I9lzDeluZIEoG1uXqg+iWfxYXLpcDdoJBis7SZ+AVM -W+NCrMDj1wxUDduIXWTbhzWZJ2CPNsESGAPMGFRM0LiC/nt3qARoFehAgM1cu9bg -k0gJPhgD+7p0MczevPAZdhUCgYBPUi+p8wdtW8OKg351heXJJJKsI7dzqe/mGk8/ -995odYyXpN5dO3Sxxb/rch30MjBe4NK6FFoLMAkdKc8LH/P6b0l4cORlH/GEhJWE -Cqc1I8REISxumESbQwkwA8+CcZHjQidOOOlLPNoWjpP1+MdsQ2j0xh0cjpSFJitn -9eI2WQKBgBNC7ap56noSUH+aYQ/kNuiyJyaodPGsbG6eOkP2BeAGpzOGHluW7bun -zzfP9nichU4lMijWCj0OnM8hzoyOJgA4pM5xoHJICMqRyfAgjUvOvG/bYKBlJv11 -i3KtkYzDp7auH+FetsCJsDGLSSi3qEa42cmK7Gy9PQXcetImH+tu ------END RSA PRIVATE KEY----- diff --git a/module/obmc/wsgi/apps/rest_dbus.py b/module/obmc/wsgi/apps/rest_dbus.py new file mode 100644 index 0000000..246396c --- /dev/null +++ b/module/obmc/wsgi/apps/rest_dbus.py @@ -0,0 +1,749 @@ +# Contributors Listed Below - COPYRIGHT 2016 +# [+] 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 os +import dbus +import dbus.exceptions +import json +from xml.etree import ElementTree +from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError +import obmc.utils.misc +from obmc.dbuslib.introspection import IntrospectionNodeParser +import obmc.mapper +import spwd +import grp +import crypt + +DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface' +DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface' +DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' +DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs' +DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError' +DELETE_IFACE = 'org.openbmc.Object.Delete' + +_4034_msg = "The specified %s cannot be %s: '%s'" + + +def valid_user(session, *a, **kw): + ''' Authorization plugin callback that checks + that the user is logged in. ''' + if session is None: + abort(403, 'Login required') + + +class UserInGroup: + ''' Authorization plugin callback that checks that the user is logged in + and a member of a group. ''' + def __init__(self, group): + self.group = group + + def __call__(self, session, *a, **kw): + valid_user(session, *a, **kw) + res = False + + try: + res = session['user'] in grp.getgrnam(self.group)[3] + except KeyError: + pass + + if not res: + abort(403, 'Insufficient access') + + +class RouteHandler(object): + _require_auth = obmc.utils.misc.makelist(valid_user) + + def __init__(self, app, bus, verbs, rules): + self.app = app + self.bus = bus + self.mapper = obmc.mapper.Mapper(bus) + self._verbs = obmc.utils.misc.makelist(verbs) + self._rules = rules + self.intf_match = obmc.utils.misc.org_dot_openbmc_match + + 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() != obmc.mapper.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_INTERFACE_ERROR in e.get_dbus_name(): + # 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='/'): + return {x: y for x, y in self.mapper.enumerate_subtree( + path, + mapper_data=request.route_data['map']).dataitems()} + + +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)) + if e.get_dbus_name() == DBUS_TYPE_ERROR: + abort(400, str(e)) + raise + + @staticmethod + def find_method_in_interface(method, obj, interface, methods): + if methods is None: + return None + + method = obmc.utils.misc.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=obmc.utils.misc.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 = obmc.utils.misc.find_case_insensitive(prop, properties.keys()) + if prop is None: + continue + return prop, i + + +class SchemaHandler(RouteHandler): + verbs = ['GET'] + rules = '/schema' + + def __init__(self, app, bus): + super(SchemaHandler, self).__init__( + app, bus, self.verbs, self.rules) + + def find(self, path): + return self.try_mapper_call( + self.mapper.get_object, + path=path) + + def setup(self, path): + request.route_data['map'] = self.find(path) + + def do_get(self, path): + schema = {} + for x in request.route_data['map'].iterkeys(): + obj = self.bus.get_object(x, path, introspect=False) + iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) + data = iface.Introspect() + parser = IntrospectionNodeParser( + ElementTree.fromstring(data)) + for x, y in parser.get_interfaces().iteritems(): + schema[x] = y + + return schema + + +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): + return self.mapper.enumerate_object( + path, + mapper_data=request.route_data['map']) + + 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 SessionHandler(MethodHandler): + ''' Handles the /login and /logout routes, manages + server side session store and session cookies. ''' + + rules = ['/login', '/logout'] + login_str = "User '%s' logged %s" + bad_passwd_str = "Invalid username or password" + no_user_str = "No user logged in" + bad_json_str = "Expecting request format { 'data': " \ + "[, ] }, got '%s'" + _require_auth = None + MAX_SESSIONS = 16 + + def __init__(self, app, bus): + super(SessionHandler, self).__init__( + app, bus) + self.hmac_key = os.urandom(128) + self.session_store = [] + + @staticmethod + def authenticate(username, clear): + try: + encoded = spwd.getspnam(username)[1] + return encoded == crypt.crypt(clear, encoded) + except KeyError: + return False + + def invalidate_session(self, session): + try: + self.session_store.remove(session) + except ValueError: + pass + + def new_session(self): + sid = os.urandom(32) + if self.MAX_SESSIONS <= len(self.session_store): + self.session_store.pop() + self.session_store.insert(0, {'sid': sid}) + + return self.session_store[0] + + def get_session(self, sid): + sids = [x['sid'] for x in self.session_store] + try: + return self.session_store[sids.index(sid)] + except ValueError: + return None + + def get_session_from_cookie(self): + return self.get_session( + request.get_cookie( + 'sid', secret=self.hmac_key)) + + def do_post(self, **kw): + if request.path == '/login': + return self.do_login(**kw) + else: + return self.do_logout(**kw) + + def do_logout(self, **kw): + session = self.get_session_from_cookie() + if session is not None: + user = session['user'] + self.invalidate_session(session) + response.delete_cookie('sid') + return self.login_str % (user, 'out') + + return self.no_user_str + + def do_login(self, **kw): + session = self.get_session_from_cookie() + if session is not None: + return self.login_str % (session['user'], 'in') + + if len(request.parameter_list) != 2: + abort(400, self.bad_json_str % (request.json)) + + if not self.authenticate(*request.parameter_list): + return self.bad_passwd_str + + user = request.parameter_list[0] + session = self.new_session() + session['user'] = user + response.set_cookie( + 'sid', session['sid'], secret=self.hmac_key, + secure=True, + httponly=True) + return self.login_str % (user, 'in') + + def find(self, **kw): + pass + + def setup(self, **kw): + pass + + +class AuthorizationPlugin(object): + ''' Invokes an optional list of authorization callbacks. ''' + + name = 'authorization' + api = 2 + + class Compose: + def __init__(self, validators, callback, session_mgr): + self.validators = validators + self.callback = callback + self.session_mgr = session_mgr + + def __call__(self, *a, **kw): + sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) + session = self.session_mgr.get_session(sid) + for x in self.validators: + x(session, *a, **kw) + + return self.callback(*a, **kw) + + def apply(self, callback, route): + undecorated = route.get_undecorated_callback() + if not isinstance(undecorated, RouteHandler): + return callback + + auth_types = getattr( + undecorated, '_require_auth', None) + if not auth_types: + return callback + + return self.Compose( + auth_types, callback, undecorated.app.session_handler) + + +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) + response.content_type = 'application/json' + return json_response + + +class JsonpPlugin(JsonApiErrorsPlugin): + ''' Json javascript wrapper. ''' + name = 'jsonp' + api = 2 + + def __init__(self, **kw): + super(JsonpPlugin, self).__init__(**kw) + + @staticmethod + def to_jsonp(json): + jwrapper = request.query.callback or None + if(jwrapper): + response.set_header('Content-Type', 'application/javascript') + json = jwrapper + '(' + json + ');' + return json + + def apply(self, callback, route): + def wrap(*a, **kw): + return self.to_jsonp(callback(*a, **kw)) + return wrap + + def json_errors(self, res, error): + json = super(JsonpPlugin, self).json_errors(res, error) + return self.to_jsonp(json) + + +class App(Bottle): + def __init__(self): + super(App, self).__init__(autojson=False) + self.bus = dbus.SystemBus() + self.mapper = obmc.mapper.Mapper(self.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(AuthorizationPlugin()) + self.install(JsonpPlugin(**json_kw)) + self.install(JSONPlugin(**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.session_handler = SessionHandler(self, self.bus) + 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.schema_handler = SchemaHandler(self, self.bus) + self.instance_handler = InstanceHandler(self, self.bus) + + def install_handlers(self): + self.session_handler.install() + self.directory_handler.install() + self.list_names_handler.install() + self.list_handler.install() + self.method_handler.install() + self.property_handler.install() + self.schema_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 diff --git a/module/setup.py b/module/setup.py new file mode 100644 index 0000000..dd2ff3b --- /dev/null +++ b/module/setup.py @@ -0,0 +1,7 @@ +from distutils.core import setup + +setup( + name='phosphor-rest-dbus', + version='1.0', + py_modules=['obmc.wsgi.apps.rest_dbus'], + ) diff --git a/phosphor-rest b/phosphor-rest deleted file mode 100644 index d8307ee..0000000 --- a/phosphor-rest +++ /dev/null @@ -1,771 +0,0 @@ -#!/usr/bin/env python - -# Contributors Listed Below - COPYRIGHT 2016 -# [+] 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 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 obmc.utils.misc -import obmc.utils.pathtree -from obmc.dbuslib.introspection import IntrospectionNodeParser -import obmc.mapper -import spwd -import grp -import crypt - -DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface' -DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface' -DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' -DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs' -DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError' -DELETE_IFACE = 'org.openbmc.Object.Delete' - -_4034_msg = "The specified %s cannot be %s: '%s'" - - -def valid_user(session, *a, **kw): - ''' Authorization plugin callback that checks - that the user is logged in. ''' - if session is None: - abort(403, 'Login required') - - -class UserInGroup: - ''' Authorization plugin callback that checks that the user is logged in - and a member of a group. ''' - def __init__(self, group): - self.group = group - - def __call__(self, session, *a, **kw): - valid_user(session, *a, **kw) - res = False - - try: - res = session['user'] in grp.getgrnam(self.group)[3] - except KeyError: - pass - - if not res: - abort(403, 'Insufficient access') - - -class RouteHandler(object): - _require_auth = obmc.utils.misc.makelist(valid_user) - - def __init__(self, app, bus, verbs, rules): - self.app = app - self.bus = bus - self.mapper = obmc.mapper.Mapper(bus) - self._verbs = obmc.utils.misc.makelist(verbs) - self._rules = rules - self.intf_match = obmc.utils.misc.org_dot_openbmc_match - - 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() != obmc.mapper.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_INTERFACE_ERROR in e.get_dbus_name(): - # 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='/'): - return {x: y for x, y in self.mapper.enumerate_subtree( - path, - mapper_data=request.route_data['map']).dataitems()} - - -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)) - if e.get_dbus_name() == DBUS_TYPE_ERROR: - abort(400, str(e)) - raise - - @staticmethod - def find_method_in_interface(method, obj, interface, methods): - if methods is None: - return None - - method = obmc.utils.misc.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=obmc.utils.misc.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 = obmc.utils.misc.find_case_insensitive(prop, properties.keys()) - if prop is None: - continue - return prop, i - - -class SchemaHandler(RouteHandler): - verbs = ['GET'] - rules = '/schema' - - def __init__(self, app, bus): - super(SchemaHandler, self).__init__( - app, bus, self.verbs, self.rules) - - def find(self, path): - return self.try_mapper_call( - self.mapper.get_object, - path=path) - - def setup(self, path): - request.route_data['map'] = self.find(path) - - def do_get(self, path): - schema = {} - for x in request.route_data['map'].iterkeys(): - obj = self.bus.get_object(x, path, introspect=False) - iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) - data = iface.Introspect() - parser = IntrospectionNodeParser( - ElementTree.fromstring(data)) - for x, y in parser.get_interfaces().iteritems(): - schema[x] = y - - return schema - - -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): - return self.mapper.enumerate_object( - path, - mapper_data=request.route_data['map']) - - 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 SessionHandler(MethodHandler): - ''' Handles the /login and /logout routes, manages - server side session store and session cookies. ''' - - rules = ['/login', '/logout'] - login_str = "User '%s' logged %s" - bad_passwd_str = "Invalid username or password" - no_user_str = "No user logged in" - bad_json_str = "Expecting request format { 'data': " \ - "[, ] }, got '%s'" - _require_auth = None - MAX_SESSIONS = 16 - - def __init__(self, app, bus): - super(SessionHandler, self).__init__( - app, bus) - self.hmac_key = os.urandom(128) - self.session_store = [] - - @staticmethod - def authenticate(username, clear): - try: - encoded = spwd.getspnam(username)[1] - return encoded == crypt.crypt(clear, encoded) - except KeyError: - return False - - def invalidate_session(self, session): - try: - self.session_store.remove(session) - except ValueError: - pass - - def new_session(self): - sid = os.urandom(32) - if self.MAX_SESSIONS <= len(self.session_store): - self.session_store.pop() - self.session_store.insert(0, {'sid': sid}) - - return self.session_store[0] - - def get_session(self, sid): - sids = [x['sid'] for x in self.session_store] - try: - return self.session_store[sids.index(sid)] - except ValueError: - return None - - def get_session_from_cookie(self): - return self.get_session( - request.get_cookie( - 'sid', secret=self.hmac_key)) - - def do_post(self, **kw): - if request.path == '/login': - return self.do_login(**kw) - else: - return self.do_logout(**kw) - - def do_logout(self, **kw): - session = self.get_session_from_cookie() - if session is not None: - user = session['user'] - self.invalidate_session(session) - response.delete_cookie('sid') - return self.login_str % (user, 'out') - - return self.no_user_str - - def do_login(self, **kw): - session = self.get_session_from_cookie() - if session is not None: - return self.login_str % (session['user'], 'in') - - if len(request.parameter_list) != 2: - abort(400, self.bad_json_str % (request.json)) - - if not self.authenticate(*request.parameter_list): - return self.bad_passwd_str - - user = request.parameter_list[0] - session = self.new_session() - session['user'] = user - response.set_cookie( - 'sid', session['sid'], secret=self.hmac_key, - secure=True, - httponly=True) - return self.login_str % (user, 'in') - - def find(self, **kw): - pass - - def setup(self, **kw): - pass - - -class AuthorizationPlugin(object): - ''' Invokes an optional list of authorization callbacks. ''' - - name = 'authorization' - api = 2 - - class Compose: - def __init__(self, validators, callback, session_mgr): - self.validators = validators - self.callback = callback - self.session_mgr = session_mgr - - def __call__(self, *a, **kw): - sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) - session = self.session_mgr.get_session(sid) - for x in self.validators: - x(session, *a, **kw) - - return self.callback(*a, **kw) - - def apply(self, callback, route): - undecorated = route.get_undecorated_callback() - if not isinstance(undecorated, RouteHandler): - return callback - - auth_types = getattr( - undecorated, '_require_auth', None) - if not auth_types: - return callback - - return self.Compose( - auth_types, callback, undecorated.app.session_handler) - - -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) - response.content_type = 'application/json' - return json_response - - -class JsonpPlugin(JsonApiErrorsPlugin): - ''' Json javascript wrapper. ''' - name = 'jsonp' - api = 2 - - def __init__(self, **kw): - super(JsonpPlugin, self).__init__(**kw) - - @staticmethod - def to_jsonp(json): - jwrapper = request.query.callback or None - if(jwrapper): - response.set_header('Content-Type', 'application/javascript') - json = jwrapper + '(' + json + ');' - return json - - def apply(self, callback, route): - def wrap(*a, **kw): - return self.to_jsonp(callback(*a, **kw)) - return wrap - - def json_errors(self, res, error): - json = super(JsonpPlugin, self).json_errors(res, error) - return self.to_jsonp(json) - - -class RestApp(Bottle): - def __init__(self): - super(RestApp, self).__init__(autojson=False) - self.bus = dbus.SystemBus() - self.mapper = obmc.mapper.Mapper(self.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(AuthorizationPlugin()) - self.install(JsonpPlugin(**json_kw)) - self.install(JSONPlugin(**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.session_handler = SessionHandler(self, self.bus) - 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.schema_handler = SchemaHandler(self, self.bus) - self.instance_handler = InstanceHandler(self, self.bus) - - def install_handlers(self): - self.session_handler.install() - self.directory_handler.install() - self.list_names_handler.install() - self.list_handler.install() - self.method_handler.install() - self.property_handler.install() - self.schema_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)) - - app = RestApp() - 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}, - min_threads=1, - max_threads=1) - server.start() diff --git a/servers/cert.pem b/servers/cert.pem new file mode 100644 index 0000000..095da08 --- /dev/null +++ b/servers/cert.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDbzCCAlegAwIBAgIJAL6IhnBSzF8HMA0GCSqGSIb3DQEBCwUAME4xCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxFDASBgNVBAoMC29wZW5ibWMu +b3JnMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTUxMTA2MjAzNTA2WhcNMTYxMTA1 +MjAzNTA2WjBOMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRQw +EgYDVQQKDAtvcGVuYm1jLm9yZzESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wl2ji+GPUpg21yjyo5jh+KwnZsV91wV +EO1DM6bdHbM/p34GkNRC6PO6DMDc3lGXaoVLHgdxhNMO/7lPwrAbO2dUyjoo4RCP +O23hWadodstfhgCMa5/h6rSbxtpVsnWD3nBh88tlug+jBZ625d6S+5MglfgrrQai ++gYC9xDzCmMN7yWsfKWO8WfrMfebkB7c+TuWnF/O6u/wmUEZOybavrfP79MuLbwC ++UkxF6PwiThGD7yINXSTxCDCN58MTrDML8AF7CWzBsYycJlJfcs2ix32Csk1FIey +t+Ze+tzEIzvOb/1G0hkIg40msFaWWEgXX9MIDHDGONwYyejb4nO8mQIDAQABo1Aw +TjAdBgNVHQ4EFgQUgoShQ/gqY2/oZWMRU4jJdAim38cwHwYDVR0jBBgwFoAUgoSh +Q/gqY2/oZWMRU4jJdAim38cwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAg2qFx8mu5Z0QBSZK3FZhNVAsbZwZuH7cjRGxWYwNlyaS24fVdts7q8YBRjpm +OZJLO+gBYkCRJnZ/AlfJ0yorT7p7Z0GTP4GrtPmh8eLoh/q5NLUGN7T2IkTea+Q+ +4iorkAuYzmSviJNUZF99MhpbuPY8wCNQWPI13X6YRrJUxQGWOj7LmqR7gE9lSPiw +xXlrWvR1tc48Z20bEXR08nDH1m5G208BgtLRk53FdYGExebpoEQokQaYyJ6b4L2A +zkFLExO+ngn49ceC9XASlN4MnKvl4FiSOQqfLhKHjwpzVuMWifV/hlY9GGVbodeg +lUFi3n1XfYxNtiNSDZvWcBWKtA== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0wl2ji+GPUpg21yjyo5jh+KwnZsV91wVEO1DM6bdHbM/p34G +kNRC6PO6DMDc3lGXaoVLHgdxhNMO/7lPwrAbO2dUyjoo4RCPO23hWadodstfhgCM +a5/h6rSbxtpVsnWD3nBh88tlug+jBZ625d6S+5MglfgrrQai+gYC9xDzCmMN7yWs +fKWO8WfrMfebkB7c+TuWnF/O6u/wmUEZOybavrfP79MuLbwC+UkxF6PwiThGD7yI +NXSTxCDCN58MTrDML8AF7CWzBsYycJlJfcs2ix32Csk1FIeyt+Ze+tzEIzvOb/1G +0hkIg40msFaWWEgXX9MIDHDGONwYyejb4nO8mQIDAQABAoIBAQCE/a2spN5fuYOY +OaUufNTUSVMrvxP0sh7EcACtiDZIBTHUB2Nz2Y/g5dcVOmT15U0aX62a2u362lbV +aJ6O/hPrN48DcetZCep5dSFSMmFum3MzKx1SpYrlMbQJeIYQ1GWpxAC2djNBMaF2 +ZTK1YbIWv/0FBUPg4hHKpgcwU4oVvHj8W45ORY9/tJB4yPrdVewwtBQ1iJkEPgfO +H5/jjGfwPHgLYnzMtu1PU1tqNVWgXw19Zn3W6wf1v+f4abrhSZSRoHqciMC10da9 +sMmhmdsi0ZWYQSn4TRRI4i9zZXuQhz/5HOUtt38JCpnkqWeEA7OHwrzyi1uHZHh0 +7TrcW11BAoGBAPenwCy7jaZXdTnpckfNWDLT2Qp07dmw8WocrbCdMUUK5DTXH8Yh +xdEt5O4Vu6qdC8ccfIlS5DTVfddqrNLPuCpOpUmZVbGQeX6h9FoAyHG5YSuiZtXD +QHfWM8CJY8tT5OYTDGGgvbLSQehVKQ+a5n9ubB8YvFmtoEl2qaWgi6+FAoGBANol +2LnV8jXggwsN/13Jw1uIPNrKj6VClDUizLIQAsDwhS4aJeFLldTtIeVAPDovMDQX +/r33EDd0eev2w3E9Sd8Th4GaxOo9Iho8I2o0aqbjdfnT3/DsuAo5KARvzjiEvbnf +J5oTM+6eqdCyL1woy2Slqp5Wm1538ZoPALteG8MFAoGAQ9tl75vQOyS5jQ2m73+X +TA356UCSr1QpQb7r5Hmdt2I9lzDeluZIEoG1uXqg+iWfxYXLpcDdoJBis7SZ+AVM +W+NCrMDj1wxUDduIXWTbhzWZJ2CPNsESGAPMGFRM0LiC/nt3qARoFehAgM1cu9bg +k0gJPhgD+7p0MczevPAZdhUCgYBPUi+p8wdtW8OKg351heXJJJKsI7dzqe/mGk8/ +995odYyXpN5dO3Sxxb/rch30MjBe4NK6FFoLMAkdKc8LH/P6b0l4cORlH/GEhJWE +Cqc1I8REISxumESbQwkwA8+CcZHjQidOOOlLPNoWjpP1+MdsQ2j0xh0cjpSFJitn +9eI2WQKBgBNC7ap56noSUH+aYQ/kNuiyJyaodPGsbG6eOkP2BeAGpzOGHluW7bun +zzfP9nichU4lMijWCj0OnM8hzoyOJgA4pM5xoHJICMqRyfAgjUvOvG/bYKBlJv11 +i3KtkYzDp7auH+FetsCJsDGLSSi3qEa42cmK7Gy9PQXcetImH+tu +-----END RSA PRIVATE KEY----- diff --git a/servers/rocket/cert.pem b/servers/rocket/cert.pem new file mode 120000 index 0000000..84d0899 --- /dev/null +++ b/servers/rocket/cert.pem @@ -0,0 +1 @@ +../cert.pem \ No newline at end of file diff --git a/servers/rocket/phosphor-rocket b/servers/rocket/phosphor-rocket new file mode 100644 index 0000000..139509c --- /dev/null +++ b/servers/rocket/phosphor-rocket @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# Contributors Listed Below - COPYRIGHT 2016 +# [+] 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 sys +import os +import logging +from rocket import Rocket + +if __name__ == '__main__': + if len(sys.argv) < 2: + sys.stderr.write('WSGI application required!') + sys.exit(1) + + exec 'from obmc.wsgi.apps.%s import App' % sys.argv[1] + + log = logging.getLogger('Rocket.Errors') + log.setLevel(logging.INFO) + log.addHandler(logging.StreamHandler(sys.stdout)) + + default_cert = os.path.join( + sys.prefix, 'share', os.path.basename(__file__), 'cert.pem') + + app = App() + server = Rocket( + ('0.0.0.0', 443, default_cert, default_cert), + 'wsgi', {'wsgi_app': app}, + min_threads=1, + max_threads=1) + server.start() diff --git a/servers/rocket/setup.cfg b/servers/rocket/setup.cfg new file mode 120000 index 0000000..29939b5 --- /dev/null +++ b/servers/rocket/setup.cfg @@ -0,0 +1 @@ +../setup.cfg \ No newline at end of file diff --git a/servers/rocket/setup.py b/servers/rocket/setup.py new file mode 100644 index 0000000..08dfa39 --- /dev/null +++ b/servers/rocket/setup.py @@ -0,0 +1,8 @@ +from distutils.core import setup + +setup( + name='phosphor-rocket', + version='1.0', + scripts=['phosphor-rocket'], + data_files=[('phosphor-rocket', ['cert.pem'])], + ) diff --git a/servers/setup.cfg b/servers/setup.cfg new file mode 100644 index 0000000..ed3bf6e --- /dev/null +++ b/servers/setup.cfg @@ -0,0 +1,2 @@ +[install] +install_scripts=/usr/sbin diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ed3bf6e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[install] -install_scripts=/usr/sbin diff --git a/setup.py b/setup.py deleted file mode 100644 index 568a00b..0000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from distutils.core import setup - -setup(name='phosphor-rest', - version='1.0', - scripts=['phosphor-rest'], - data_files=[('phosphor-rest', ['cert.pem'])], - ) -- cgit v1.2.1