blob: 017a7368b0f6548bddae25be7fa56c5902673212 [file] [log] [blame]
#!/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)
response.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()