blob: 91f7f8ca752ba2062c2075e62f4245a49867d936 [file] [log] [blame]
Brad Bishop68caa1e2016-03-04 15:42:08 -05001# Contributors Listed Below - COPYRIGHT 2016
2# [+] International Business Machines Corp.
3#
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied. See the License for the specific language governing
15# permissions and limitations under the License.
16
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050017import os
Brad Bishopaa65f6e2015-10-27 16:28:51 -040018import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import dbus.exceptions
20import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050021from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Jayanth Othayoth9bc94992017-06-29 06:30:40 -050023from bottle import static_file
Brad Bishopb103d2d2016-03-04 16:19:14 -050024import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050025from obmc.dbuslib.introspection import IntrospectionNodeParser
26import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050027import spwd
28import grp
29import crypt
Deepak Kodihalli1af301a2017-04-11 07:29:01 -050030import tempfile
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050031import re
Brad Bishopaa65f6e2015-10-27 16:28:51 -040032
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050033DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040034DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050035DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
36DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050037DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050038DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050039
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050040_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040041
Brad Bishop87b63c12016-03-18 14:47:51 -040042
Brad Bishop2f428582015-12-02 10:56:11 -050043def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040044 ''' Authorization plugin callback that checks
45 that the user is logged in. '''
46 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040047 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040048
Brad Bishop2f428582015-12-02 10:56:11 -050049
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050050def get_type_signature_by_introspection(bus, service, object_path,
51 property_name):
52 obj = bus.get_object(service, object_path)
53 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
54 xml_string = iface.Introspect()
55 for child in ElementTree.fromstring(xml_string):
56 # Iterate over each interfaces's properties to find
57 # matching property_name, and return its signature string
58 if child.tag == 'interface':
59 for i in child.iter():
60 if ('name' in i.attrib) and \
61 (i.attrib['name'] == property_name):
62 type_signature = i.attrib['type']
63 return type_signature
64
65
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053066def get_method_signature(bus, service, object_path, interface, method):
67 obj = bus.get_object(service, object_path)
68 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
69 xml_string = iface.Introspect()
70 arglist = []
71
72 root = ElementTree.fromstring(xml_string)
73 for dbus_intf in root.findall('interface'):
74 if (dbus_intf.get('name') == interface):
75 for dbus_method in dbus_intf.findall('method'):
76 if(dbus_method.get('name') == method):
77 for arg in dbus_method.findall('arg'):
78 arglist.append(arg.get('type'))
79 return arglist
80
81
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050082def split_struct_signature(signature):
83 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
84 struct_matches = re.findall(struct_regex, signature)
85 return struct_matches
86
87
88def convert_type(signature, value):
89 # Basic Types
90 converted_value = None
91 converted_container = None
92 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
93 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
94 't': dbus.UInt64, 'd': float, 's': str}
95 array_matches = re.match(r'a\((\S+)\)', signature)
96 struct_matches = re.match(r'\((\S+)\)', signature)
97 dictionary_matches = re.match(r'a{(\S+)}', signature)
98 if signature in basic_types:
99 converted_value = basic_types[signature](value)
100 return converted_value
101 # Array
102 if array_matches:
103 element_type = array_matches.group(1)
104 converted_container = list()
105 # Test if value is a list
106 # to avoid iterating over each character in a string.
107 # Iterate over each item and convert type
108 if isinstance(value, list):
109 for i in value:
110 converted_element = convert_type(element_type, i)
111 converted_container.append(converted_element)
112 # Convert non-sequence to expected type, and append to list
113 else:
114 converted_element = convert_type(element_type, value)
115 converted_container.append(converted_element)
116 return converted_container
117 # Struct
118 if struct_matches:
119 element_types = struct_matches.group(1)
120 split_element_types = split_struct_signature(element_types)
121 converted_container = list()
122 # Test if value is a list
123 if isinstance(value, list):
124 for index, val in enumerate(value):
125 converted_element = convert_type(split_element_types[index],
126 value[index])
127 converted_container.append(converted_element)
128 else:
129 converted_element = convert_type(element_types, value)
130 converted_container.append(converted_element)
131 return tuple(converted_container)
132 # Dictionary
133 if dictionary_matches:
134 element_types = dictionary_matches.group(1)
135 split_element_types = split_struct_signature(element_types)
136 converted_container = dict()
137 # Convert each element of dict
138 for key, val in value.iteritems():
139 converted_key = convert_type(split_element_types[0], key)
140 converted_val = convert_type(split_element_types[1], val)
141 converted_container[converted_key] = converted_val
142 return converted_container
143
144
Brad Bishop2f428582015-12-02 10:56:11 -0500145class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400146 ''' Authorization plugin callback that checks that the user is logged in
147 and a member of a group. '''
148 def __init__(self, group):
149 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500150
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 def __call__(self, session, *a, **kw):
152 valid_user(session, *a, **kw)
153 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500154
Brad Bishop87b63c12016-03-18 14:47:51 -0400155 try:
156 res = session['user'] in grp.getgrnam(self.group)[3]
157 except KeyError:
158 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500159
Brad Bishop87b63c12016-03-18 14:47:51 -0400160 if not res:
161 abort(403, 'Insufficient access')
162
Brad Bishop2f428582015-12-02 10:56:11 -0500163
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500164class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400165 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500166 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400167
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500168 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400169 self.app = app
170 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500171 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400172 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400173 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500174 self._content_type = content_type
Brad Bishop0f79e522016-03-18 13:33:17 -0400175 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400176
Brad Bishop88c76a42017-02-21 00:02:02 -0500177 if 'GET' in self._verbs:
178 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500179 if 'OPTIONS' not in self._verbs:
180 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500181
Brad Bishop87b63c12016-03-18 14:47:51 -0400182 def _setup(self, **kw):
183 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500184
Brad Bishop87b63c12016-03-18 14:47:51 -0400185 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500186 if request.method != 'OPTIONS':
187 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500188
Brad Bishopd4c1c552017-02-21 00:07:28 -0500189 # Javascript implementations will not send credentials
190 # with an OPTIONS request. Don't help malicious clients
191 # by checking the path here and returning a 404 if the
192 # path doesn't exist.
193 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500194
Brad Bishopd4c1c552017-02-21 00:07:28 -0500195 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500196 raise HTTPError(
197 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400198
Brad Bishop87b63c12016-03-18 14:47:51 -0400199 def __call__(self, **kw):
200 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400201
Brad Bishop88c76a42017-02-21 00:02:02 -0500202 def do_head(self, **kw):
203 return self.do_get(**kw)
204
Brad Bishopd4c1c552017-02-21 00:07:28 -0500205 def do_options(self, **kw):
206 for v in self._verbs:
207 response.set_header(
208 'Allow',
209 ','.join(self._verbs))
210 return None
211
Brad Bishop87b63c12016-03-18 14:47:51 -0400212 def install(self):
213 self.app.route(
214 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500215 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400216
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 @staticmethod
218 def try_mapper_call(f, callback=None, **kw):
219 try:
220 return f(**kw)
221 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500222 if e.get_dbus_name() == \
223 'org.freedesktop.DBus.Error.ObjectPathInUse':
224 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500225 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400226 raise
227 if callback is None:
228 def callback(e, **kw):
229 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 callback(e, **kw)
232
233 @staticmethod
234 def try_properties_interface(f, *a):
235 try:
236 return f(*a)
237 except dbus.exceptions.DBusException, e:
238 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
239 # interface doesn't have any properties
240 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400241 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
242 # interface doesn't have any properties
243 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400244 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
245 # properties interface not implemented at all
246 return None
247 raise
248
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400249
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500250class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400251 verbs = 'GET'
252 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400253
Brad Bishop87b63c12016-03-18 14:47:51 -0400254 def __init__(self, app, bus):
255 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400256 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400257
Brad Bishop87b63c12016-03-18 14:47:51 -0400258 def find(self, path='/'):
259 return self.try_mapper_call(
260 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400261
Brad Bishop87b63c12016-03-18 14:47:51 -0400262 def setup(self, path='/'):
263 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400264
Brad Bishop87b63c12016-03-18 14:47:51 -0400265 def do_get(self, path='/'):
266 return request.route_data['map']
267
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400268
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500269class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 verbs = 'GET'
271 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 def __init__(self, app, bus):
274 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400275 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400276
Brad Bishop87b63c12016-03-18 14:47:51 -0400277 def find(self, path='/'):
278 return self.try_mapper_call(
279 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400280
Brad Bishop87b63c12016-03-18 14:47:51 -0400281 def setup(self, path='/'):
282 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400283
Brad Bishop87b63c12016-03-18 14:47:51 -0400284 def do_get(self, path='/'):
285 return request.route_data['map']
286
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400287
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500288class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400289 verbs = 'GET'
290 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400291
Brad Bishop87b63c12016-03-18 14:47:51 -0400292 def __init__(self, app, bus):
293 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400294 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400295
Brad Bishop87b63c12016-03-18 14:47:51 -0400296 def find(self, path='/'):
297 return self.try_mapper_call(
298 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400299
Brad Bishop87b63c12016-03-18 14:47:51 -0400300 def setup(self, path='/'):
301 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400304 return {x: y for x, y in self.mapper.enumerate_subtree(
305 path,
306 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400307
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400308
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500309class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 verbs = 'POST'
311 rules = '<path:path>/action/<method>'
312 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500313 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400314
Brad Bishop87b63c12016-03-18 14:47:51 -0400315 def __init__(self, app, bus):
316 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500317 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530318 self.service = ''
319 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400320
Brad Bishop87b63c12016-03-18 14:47:51 -0400321 def find(self, path, method):
322 busses = self.try_mapper_call(
323 self.mapper.get_object, path=path)
324 for items in busses.iteritems():
325 m = self.find_method_on_bus(path, method, *items)
326 if m:
327 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400328
Brad Bishop87b63c12016-03-18 14:47:51 -0400329 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400330
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 def setup(self, path, method):
332 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400333
Brad Bishop87b63c12016-03-18 14:47:51 -0400334 def do_post(self, path, method):
335 try:
336 if request.parameter_list:
337 return request.route_data['method'](*request.parameter_list)
338 else:
339 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400340
Brad Bishop87b63c12016-03-18 14:47:51 -0400341 except dbus.exceptions.DBusException, e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530342 paramlist = []
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530344
345 signature_list = get_method_signature(self.bus, self.service,
346 path, self.interface,
347 method)
348 if not signature_list:
349 abort(400, "Failed to get method signature: %s" % str(e))
350 if len(signature_list) != len(request.parameter_list):
351 abort(400, "Invalid number of args")
352 converted_value = None
353 try:
354 for index, expected_type in enumerate(signature_list):
355 value = request.parameter_list[index]
356 converted_value = convert_type(expected_type, value)
357 paramlist.append(converted_value)
358 request.parameter_list = paramlist
359 self.do_post(path, method)
360 return
361 except Exception as ex:
362 abort(400, "Failed to convert the types")
Brad Bishop87b63c12016-03-18 14:47:51 -0400363 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530364
Brad Bishop87b63c12016-03-18 14:47:51 -0400365 if e.get_dbus_name() == DBUS_TYPE_ERROR:
366 abort(400, str(e))
367 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400368
Brad Bishop87b63c12016-03-18 14:47:51 -0400369 @staticmethod
370 def find_method_in_interface(method, obj, interface, methods):
371 if methods is None:
372 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400373
Brad Bishop6d190602016-04-15 13:09:39 -0400374 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400375 if method is not None:
376 iface = dbus.Interface(obj, interface)
377 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400378
Brad Bishop87b63c12016-03-18 14:47:51 -0400379 def find_method_on_bus(self, path, method, bus, interfaces):
380 obj = self.bus.get_object(bus, path, introspect=False)
381 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
382 data = iface.Introspect()
383 parser = IntrospectionNodeParser(
384 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500385 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 for x, y in parser.get_interfaces().iteritems():
387 m = self.find_method_in_interface(
388 method, obj, x, y.get('method'))
389 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530390 self.service = bus
391 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400392 return m
393
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400394
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500395class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400396 verbs = ['PUT', 'GET']
397 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500398 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400399
Brad Bishop87b63c12016-03-18 14:47:51 -0400400 def __init__(self, app, bus):
401 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500402 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400403
Brad Bishop87b63c12016-03-18 14:47:51 -0400404 def find(self, path, prop):
405 self.app.instance_handler.setup(path)
406 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500407 real_name = obmc.utils.misc.find_case_insensitive(
408 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400409
Brad Bishop56ad87f2017-02-21 23:33:29 -0500410 if not real_name:
411 if request.method == 'PUT':
412 abort(403, _4034_msg % ('property', 'created', prop))
413 else:
414 abort(404, _4034_msg % ('property', 'found', prop))
415 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500416
Brad Bishop87b63c12016-03-18 14:47:51 -0400417 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500418 name, obj = self.find(path, prop)
419 request.route_data['obj'] = obj
420 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500421
Brad Bishop87b63c12016-03-18 14:47:51 -0400422 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500423 name = request.route_data['name']
424 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500425
Brad Bishop87b63c12016-03-18 14:47:51 -0400426 def do_put(self, path, prop, value=None):
427 if value is None:
428 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 prop, iface, properties_iface = self.get_host_interface(
431 path, prop, request.route_data['map'][path])
432 try:
433 properties_iface.Set(iface, prop, value)
434 except ValueError, e:
435 abort(400, str(e))
436 except dbus.exceptions.DBusException, e:
437 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500438 bus_name = properties_iface.bus_name
439 expected_type = get_type_signature_by_introspection(self.bus,
440 bus_name,
441 path,
442 prop)
443 if not expected_type:
444 abort(403, "Failed to get expected type: %s" % str(e))
445 converted_value = None
446 try:
447 converted_value = convert_type(expected_type, value)
448 self.do_put(path, prop, converted_value)
449 return
450 except Exception as ex:
451 abort(403, "Failed to convert %s to type %s" %
452 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400453 abort(403, str(e))
454 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 def get_host_interface(self, path, prop, bus_info):
457 for bus, interfaces in bus_info.iteritems():
458 obj = self.bus.get_object(bus, path, introspect=True)
459 properties_iface = dbus.Interface(
460 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500461
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 info = self.get_host_interface_on_bus(
463 path, prop, properties_iface, bus, interfaces)
464 if info is not None:
465 prop, iface = info
466 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500467
Brad Bishop87b63c12016-03-18 14:47:51 -0400468 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
469 for i in interfaces:
470 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500471 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400472 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500473 match = obmc.utils.misc.find_case_insensitive(
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500474 prop, properties.keys())
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500475 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400476 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500477 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 return prop, i
479
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500480
Brad Bishop2503bd62015-12-16 17:56:12 -0500481class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 verbs = ['GET']
483 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500484
Brad Bishop87b63c12016-03-18 14:47:51 -0400485 def __init__(self, app, bus):
486 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400487 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500488
Brad Bishop87b63c12016-03-18 14:47:51 -0400489 def find(self, path):
490 return self.try_mapper_call(
491 self.mapper.get_object,
492 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500493
Brad Bishop87b63c12016-03-18 14:47:51 -0400494 def setup(self, path):
495 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 def do_get(self, path):
498 schema = {}
499 for x in request.route_data['map'].iterkeys():
500 obj = self.bus.get_object(x, path, introspect=False)
501 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
502 data = iface.Introspect()
503 parser = IntrospectionNodeParser(
504 ElementTree.fromstring(data))
505 for x, y in parser.get_interfaces().iteritems():
506 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500507
Brad Bishop87b63c12016-03-18 14:47:51 -0400508 return schema
509
Brad Bishop2503bd62015-12-16 17:56:12 -0500510
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500511class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 verbs = ['GET', 'PUT', 'DELETE']
513 rules = '<path:path>'
514 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500515
Brad Bishop87b63c12016-03-18 14:47:51 -0400516 def __init__(self, app, bus):
517 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400518 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500519
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 def find(self, path, callback=None):
521 return {path: self.try_mapper_call(
522 self.mapper.get_object,
523 callback,
524 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500525
Brad Bishop87b63c12016-03-18 14:47:51 -0400526 def setup(self, path):
527 callback = None
528 if request.method == 'PUT':
529 def callback(e, **kw):
530 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500531
Brad Bishop87b63c12016-03-18 14:47:51 -0400532 if request.route_data.get('map') is None:
533 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500534
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400536 return self.mapper.enumerate_object(
537 path,
538 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500539
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 def do_put(self, path):
541 # make sure all properties exist in the request
542 obj = set(self.do_get(path).keys())
543 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500544
Brad Bishop87b63c12016-03-18 14:47:51 -0400545 diff = list(obj.difference(req))
546 if diff:
547 abort(403, _4034_msg % (
548 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500549
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 diff = list(req.difference(obj))
551 if diff:
552 abort(403, _4034_msg % (
553 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500554
Brad Bishop87b63c12016-03-18 14:47:51 -0400555 for p, v in request.parameter_list.iteritems():
556 self.app.property_handler.do_put(
557 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 def do_delete(self, path):
560 for bus_info in request.route_data['map'][path].iteritems():
561 if self.bus_missing_delete(path, *bus_info):
562 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 for bus in request.route_data['map'][path].iterkeys():
565 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500566
Brad Bishop87b63c12016-03-18 14:47:51 -0400567 def bus_missing_delete(self, path, bus, interfaces):
568 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500569
Brad Bishop87b63c12016-03-18 14:47:51 -0400570 def delete_on_bus(self, path, bus):
571 obj = self.bus.get_object(bus, path, introspect=False)
572 delete_iface = dbus.Interface(
573 obj, dbus_interface=DELETE_IFACE)
574 delete_iface.Delete()
575
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500576
Brad Bishop2f428582015-12-02 10:56:11 -0500577class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400578 ''' Handles the /login and /logout routes, manages
579 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500580
Brad Bishop87b63c12016-03-18 14:47:51 -0400581 rules = ['/login', '/logout']
582 login_str = "User '%s' logged %s"
583 bad_passwd_str = "Invalid username or password"
584 no_user_str = "No user logged in"
585 bad_json_str = "Expecting request format { 'data': " \
586 "[<username>, <password>] }, got '%s'"
587 _require_auth = None
588 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500589
Brad Bishop87b63c12016-03-18 14:47:51 -0400590 def __init__(self, app, bus):
591 super(SessionHandler, self).__init__(
592 app, bus)
593 self.hmac_key = os.urandom(128)
594 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500595
Brad Bishop87b63c12016-03-18 14:47:51 -0400596 @staticmethod
597 def authenticate(username, clear):
598 try:
599 encoded = spwd.getspnam(username)[1]
600 return encoded == crypt.crypt(clear, encoded)
601 except KeyError:
602 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500603
Brad Bishop87b63c12016-03-18 14:47:51 -0400604 def invalidate_session(self, session):
605 try:
606 self.session_store.remove(session)
607 except ValueError:
608 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500609
Brad Bishop87b63c12016-03-18 14:47:51 -0400610 def new_session(self):
611 sid = os.urandom(32)
612 if self.MAX_SESSIONS <= len(self.session_store):
613 self.session_store.pop()
614 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500615
Brad Bishop87b63c12016-03-18 14:47:51 -0400616 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500617
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 def get_session(self, sid):
619 sids = [x['sid'] for x in self.session_store]
620 try:
621 return self.session_store[sids.index(sid)]
622 except ValueError:
623 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 def get_session_from_cookie(self):
626 return self.get_session(
627 request.get_cookie(
628 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500629
Brad Bishop87b63c12016-03-18 14:47:51 -0400630 def do_post(self, **kw):
631 if request.path == '/login':
632 return self.do_login(**kw)
633 else:
634 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500635
Brad Bishop87b63c12016-03-18 14:47:51 -0400636 def do_logout(self, **kw):
637 session = self.get_session_from_cookie()
638 if session is not None:
639 user = session['user']
640 self.invalidate_session(session)
641 response.delete_cookie('sid')
642 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500643
Brad Bishop87b63c12016-03-18 14:47:51 -0400644 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500645
Brad Bishop87b63c12016-03-18 14:47:51 -0400646 def do_login(self, **kw):
647 session = self.get_session_from_cookie()
648 if session is not None:
649 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500650
Brad Bishop87b63c12016-03-18 14:47:51 -0400651 if len(request.parameter_list) != 2:
652 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500653
Brad Bishop87b63c12016-03-18 14:47:51 -0400654 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400655 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500656
Brad Bishop87b63c12016-03-18 14:47:51 -0400657 user = request.parameter_list[0]
658 session = self.new_session()
659 session['user'] = user
660 response.set_cookie(
661 'sid', session['sid'], secret=self.hmac_key,
662 secure=True,
663 httponly=True)
664 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500665
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 def find(self, **kw):
667 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500668
Brad Bishop87b63c12016-03-18 14:47:51 -0400669 def setup(self, **kw):
670 pass
671
Brad Bishop2f428582015-12-02 10:56:11 -0500672
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500673class ImageUploadUtils:
674 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500675
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500676 file_loc = '/tmp/images'
677 file_prefix = 'img'
678 file_suffix = ''
679
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500680 @classmethod
681 def do_upload(cls, filename=''):
682 if not os.path.exists(cls.file_loc):
683 os.makedirs(cls.file_loc)
684 if not filename:
685 handle, filename = tempfile.mkstemp(cls.file_suffix,
686 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500687 else:
688 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500689 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500690 try:
691 file_contents = request.body.read()
692 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500693 os.write(handle, file_contents)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500694 except (IOError, ValueError), e:
695 abort(400, str(e))
696 except:
697 abort(400, "Unexpected Error")
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500698 finally:
699 os.close(handle)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500700
701
702class ImagePostHandler(RouteHandler):
703 ''' Handles the /upload/image route. '''
704
705 verbs = ['POST']
706 rules = ['/upload/image']
707 content_type = 'application/octet-stream'
708
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500709 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500710 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500711 app, bus, self.verbs, self.rules, self.content_type)
712
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500713 def do_post(self, filename=''):
714 ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500715
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500716 def find(self, **kw):
717 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500718
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500719 def setup(self, **kw):
720 pass
721
722
723class ImagePutHandler(RouteHandler):
724 ''' Handles the /upload/image/<filename> route. '''
725
726 verbs = ['PUT']
727 rules = ['/upload/image/<filename>']
728 content_type = 'application/octet-stream'
729
730 def __init__(self, app, bus):
731 super(ImagePutHandler, self).__init__(
732 app, bus, self.verbs, self.rules, self.content_type)
733
734 def do_put(self, filename=''):
735 ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500736
737 def find(self, **kw):
738 pass
739
740 def setup(self, **kw):
741 pass
742
743
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500744class DownloadDumpHandler(RouteHandler):
745 ''' Handles the /download/dump route. '''
746
747 verbs = 'GET'
748 rules = ['/download/dump/<dumpid>']
749 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -0500750 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -0400751 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500752
753 def __init__(self, app, bus):
754 super(DownloadDumpHandler, self).__init__(
755 app, bus, self.verbs, self.rules, self.content_type)
756
757 def do_get(self, dumpid):
758 return self.do_download(dumpid)
759
760 def find(self, **kw):
761 pass
762
763 def setup(self, **kw):
764 pass
765
766 def do_download(self, dumpid):
767 dump_loc = os.path.join(self.dump_loc, dumpid)
768 if not os.path.exists(dump_loc):
769 abort(404, "Path not found")
770
771 files = os.listdir(dump_loc)
772 num_files = len(files)
773 if num_files == 0:
774 abort(404, "Dump not found")
775
776 return static_file(os.path.basename(files[0]), root=dump_loc,
777 download=True, mimetype=self.content_type)
778
779
Brad Bishop2f428582015-12-02 10:56:11 -0500780class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400781 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500782
Brad Bishop87b63c12016-03-18 14:47:51 -0400783 name = 'authorization'
784 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500785
Brad Bishop87b63c12016-03-18 14:47:51 -0400786 class Compose:
787 def __init__(self, validators, callback, session_mgr):
788 self.validators = validators
789 self.callback = callback
790 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500791
Brad Bishop87b63c12016-03-18 14:47:51 -0400792 def __call__(self, *a, **kw):
793 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
794 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500795 if request.method != 'OPTIONS':
796 for x in self.validators:
797 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500798
Brad Bishop87b63c12016-03-18 14:47:51 -0400799 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500800
Brad Bishop87b63c12016-03-18 14:47:51 -0400801 def apply(self, callback, route):
802 undecorated = route.get_undecorated_callback()
803 if not isinstance(undecorated, RouteHandler):
804 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500805
Brad Bishop87b63c12016-03-18 14:47:51 -0400806 auth_types = getattr(
807 undecorated, '_require_auth', None)
808 if not auth_types:
809 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500810
Brad Bishop87b63c12016-03-18 14:47:51 -0400811 return self.Compose(
812 auth_types, callback, undecorated.app.session_handler)
813
Brad Bishop2f428582015-12-02 10:56:11 -0500814
Brad Bishopd0c404a2017-02-21 09:23:25 -0500815class CorsPlugin(object):
816 ''' Add CORS headers. '''
817
818 name = 'cors'
819 api = 2
820
821 @staticmethod
822 def process_origin():
823 origin = request.headers.get('Origin')
824 if origin:
825 response.add_header('Access-Control-Allow-Origin', origin)
826 response.add_header(
827 'Access-Control-Allow-Credentials', 'true')
828
829 @staticmethod
830 def process_method_and_headers(verbs):
831 method = request.headers.get('Access-Control-Request-Method')
832 headers = request.headers.get('Access-Control-Request-Headers')
833 if headers:
834 headers = [x.lower() for x in headers.split(',')]
835
836 if method in verbs \
837 and headers == ['content-type']:
838 response.add_header('Access-Control-Allow-Methods', method)
839 response.add_header(
840 'Access-Control-Allow-Headers', 'Content-Type')
841
842 def __init__(self, app):
843 app.install_error_callback(self.error_callback)
844
845 def apply(self, callback, route):
846 undecorated = route.get_undecorated_callback()
847 if not isinstance(undecorated, RouteHandler):
848 return callback
849
850 if not getattr(undecorated, '_enable_cors', None):
851 return callback
852
853 def wrap(*a, **kw):
854 self.process_origin()
855 self.process_method_and_headers(undecorated._verbs)
856 return callback(*a, **kw)
857
858 return wrap
859
860 def error_callback(self, **kw):
861 self.process_origin()
862
863
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500864class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400865 ''' Ensures request content satisfies the OpenBMC json api format. '''
866 name = 'json_api_request'
867 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500868
Brad Bishop87b63c12016-03-18 14:47:51 -0400869 error_str = "Expecting request format { 'data': <value> }, got '%s'"
870 type_error_str = "Unsupported Content-Type: '%s'"
871 json_type = "application/json"
872 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500873
Brad Bishop87b63c12016-03-18 14:47:51 -0400874 @staticmethod
875 def content_expected():
876 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500877
Brad Bishop87b63c12016-03-18 14:47:51 -0400878 def validate_request(self):
879 if request.content_length > 0 and \
880 request.content_type != self.json_type:
881 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500882
Brad Bishop87b63c12016-03-18 14:47:51 -0400883 try:
884 request.parameter_list = request.json.get('data')
885 except ValueError, e:
886 abort(400, str(e))
887 except (AttributeError, KeyError, TypeError):
888 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500889
Brad Bishop87b63c12016-03-18 14:47:51 -0400890 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500891 content_type = getattr(
892 route.get_undecorated_callback(), '_content_type', None)
893 if self.json_type != content_type:
894 return callback
895
Brad Bishop87b63c12016-03-18 14:47:51 -0400896 verbs = getattr(
897 route.get_undecorated_callback(), '_verbs', None)
898 if verbs is None:
899 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500900
Brad Bishop87b63c12016-03-18 14:47:51 -0400901 if not set(self.request_methods).intersection(verbs):
902 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500903
Brad Bishop87b63c12016-03-18 14:47:51 -0400904 def wrap(*a, **kw):
905 if self.content_expected():
906 self.validate_request()
907 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500908
Brad Bishop87b63c12016-03-18 14:47:51 -0400909 return wrap
910
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500911
912class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400913 ''' Ensures request content type satisfies the OpenBMC json api format. '''
914 name = 'json_api_method_request'
915 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500916
Brad Bishop87b63c12016-03-18 14:47:51 -0400917 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500918 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500919
Brad Bishop87b63c12016-03-18 14:47:51 -0400920 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500921 content_type = getattr(
922 route.get_undecorated_callback(), '_content_type', None)
923 if self.json_type != content_type:
924 return callback
925
Brad Bishop87b63c12016-03-18 14:47:51 -0400926 request_type = getattr(
927 route.get_undecorated_callback(), 'request_type', None)
928 if request_type is None:
929 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500930
Brad Bishop87b63c12016-03-18 14:47:51 -0400931 def validate_request():
932 if not isinstance(request.parameter_list, request_type):
933 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500934
Brad Bishop87b63c12016-03-18 14:47:51 -0400935 def wrap(*a, **kw):
936 if JsonApiRequestPlugin.content_expected():
937 validate_request()
938 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500939
Brad Bishop87b63c12016-03-18 14:47:51 -0400940 return wrap
941
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500942
Brad Bishop080a48e2017-02-21 22:34:43 -0500943class JsonErrorsPlugin(JSONPlugin):
944 ''' Extend the Bottle JSONPlugin such that it also encodes error
945 responses. '''
946
947 def __init__(self, app, **kw):
948 super(JsonErrorsPlugin, self).__init__(**kw)
949 self.json_opts = {
950 x: y for x, y in kw.iteritems()
951 if x in ['indent', 'sort_keys']}
952 app.install_error_callback(self.error_callback)
953
954 def error_callback(self, response_object, response_body, **kw):
955 response_body['body'] = json.dumps(response_object, **self.json_opts)
956 response.content_type = 'application/json'
957
958
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500959class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500960 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400961 name = 'json_api_response'
962 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500963
Brad Bishopd4c1c552017-02-21 00:07:28 -0500964 @staticmethod
965 def has_body():
966 return request.method not in ['OPTIONS']
967
Brad Bishop080a48e2017-02-21 22:34:43 -0500968 def __init__(self, app):
969 app.install_error_callback(self.error_callback)
970
Brad Bishop87b63c12016-03-18 14:47:51 -0400971 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -0400972 skip = getattr(
973 route.get_undecorated_callback(), 'suppress_json_resp', None)
974 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500975 return callback
976
Brad Bishop87b63c12016-03-18 14:47:51 -0400977 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500978 data = callback(*a, **kw)
979 if self.has_body():
980 resp = {'data': data}
981 resp['status'] = 'ok'
982 resp['message'] = response.status_line
983 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400984 return wrap
985
Brad Bishop080a48e2017-02-21 22:34:43 -0500986 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400987 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -0500988 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -0500989 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400990 if error.status_code == 500:
991 response_object['data']['exception'] = repr(error.exception)
992 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500993
Brad Bishop87b63c12016-03-18 14:47:51 -0400994
Brad Bishop080a48e2017-02-21 22:34:43 -0500995class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400996 ''' Json javascript wrapper. '''
997 name = 'jsonp'
998 api = 2
999
Brad Bishop080a48e2017-02-21 22:34:43 -05001000 def __init__(self, app, **kw):
1001 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001002
1003 @staticmethod
1004 def to_jsonp(json):
1005 jwrapper = request.query.callback or None
1006 if(jwrapper):
1007 response.set_header('Content-Type', 'application/javascript')
1008 json = jwrapper + '(' + json + ');'
1009 return json
1010
1011 def apply(self, callback, route):
1012 def wrap(*a, **kw):
1013 return self.to_jsonp(callback(*a, **kw))
1014 return wrap
1015
Brad Bishop080a48e2017-02-21 22:34:43 -05001016 def error_callback(self, response_body, **kw):
1017 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001018
1019
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001020class ContentCheckerPlugin(object):
1021 ''' Ensures that a route is associated with the expected content-type
1022 header. '''
1023 name = 'content_checker'
1024 api = 2
1025
1026 class Checker:
1027 def __init__(self, type, callback):
1028 self.expected_type = type
1029 self.callback = callback
1030 self.error_str = "Expecting content type '%s', got '%s'"
1031
1032 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001033 if request.method in ['PUT', 'POST', 'PATCH'] and \
1034 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001035 self.expected_type != request.content_type:
1036 abort(415, self.error_str % (self.expected_type,
1037 request.content_type))
1038
1039 return self.callback(*a, **kw)
1040
1041 def apply(self, callback, route):
1042 content_type = getattr(
1043 route.get_undecorated_callback(), '_content_type', None)
1044
1045 return self.Checker(content_type, callback)
1046
1047
Brad Bishop2c6fc762016-08-29 15:53:25 -04001048class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -04001049 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001050 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -04001051 self.bus = dbus.SystemBus()
1052 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001053 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001054
Brad Bishop87b63c12016-03-18 14:47:51 -04001055 self.install_hooks()
1056 self.install_plugins()
1057 self.create_handlers()
1058 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001059
Brad Bishop87b63c12016-03-18 14:47:51 -04001060 def install_plugins(self):
1061 # install json api plugins
1062 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001063 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001064 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001065 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001066 self.install(JsonpPlugin(self, **json_kw))
1067 self.install(JsonErrorsPlugin(self, **json_kw))
1068 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001069 self.install(JsonApiRequestPlugin())
1070 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001071
Brad Bishop87b63c12016-03-18 14:47:51 -04001072 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001073 self.error_handler_type = type(self.default_error_handler)
1074 self.original_error_handler = self.default_error_handler
1075 self.default_error_handler = self.error_handler_type(
1076 self.custom_error_handler, self, Bottle)
1077
Brad Bishop87b63c12016-03-18 14:47:51 -04001078 self.real_router_match = self.router.match
1079 self.router.match = self.custom_router_match
1080 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001081
Brad Bishop87b63c12016-03-18 14:47:51 -04001082 def create_handlers(self):
1083 # create route handlers
1084 self.session_handler = SessionHandler(self, self.bus)
1085 self.directory_handler = DirectoryHandler(self, self.bus)
1086 self.list_names_handler = ListNamesHandler(self, self.bus)
1087 self.list_handler = ListHandler(self, self.bus)
1088 self.method_handler = MethodHandler(self, self.bus)
1089 self.property_handler = PropertyHandler(self, self.bus)
1090 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001091 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1092 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001093 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001094 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001095
Brad Bishop87b63c12016-03-18 14:47:51 -04001096 def install_handlers(self):
1097 self.session_handler.install()
1098 self.directory_handler.install()
1099 self.list_names_handler.install()
1100 self.list_handler.install()
1101 self.method_handler.install()
1102 self.property_handler.install()
1103 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001104 self.image_upload_post_handler.install()
1105 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001106 self.download_dump_get_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001107 # this has to come last, since it matches everything
1108 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001109
Brad Bishop080a48e2017-02-21 22:34:43 -05001110 def install_error_callback(self, callback):
1111 self.error_callbacks.insert(0, callback)
1112
Brad Bishop87b63c12016-03-18 14:47:51 -04001113 def custom_router_match(self, environ):
1114 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1115 needed doesn't work for us since the instance rules match
1116 everything. This monkey-patch lets the route handler figure
1117 out which response is needed. This could be accomplished
1118 with a hook but that would require calling the router match
1119 function twice.
1120 '''
1121 route, args = self.real_router_match(environ)
1122 if isinstance(route.callback, RouteHandler):
1123 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001124
Brad Bishop87b63c12016-03-18 14:47:51 -04001125 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001126
Brad Bishop080a48e2017-02-21 22:34:43 -05001127 def custom_error_handler(self, res, error):
1128 ''' Allow plugins to modify error reponses too via this custom
1129 error handler. '''
1130
1131 response_object = {}
1132 response_body = {}
1133 for x in self.error_callbacks:
1134 x(error=error,
1135 response_object=response_object,
1136 response_body=response_body)
1137
1138 return response_body.get('body', "")
1139
Brad Bishop87b63c12016-03-18 14:47:51 -04001140 @staticmethod
1141 def strip_extra_slashes():
1142 path = request.environ['PATH_INFO']
1143 trailing = ("", "/")[path[-1] == '/']
1144 parts = filter(bool, path.split('/'))
1145 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing