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