blob: f4ae606c87671d8f1ffb526dc54543349a199910 [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
66def split_struct_signature(signature):
67 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
68 struct_matches = re.findall(struct_regex, signature)
69 return struct_matches
70
71
72def convert_type(signature, value):
73 # Basic Types
74 converted_value = None
75 converted_container = None
76 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
77 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
78 't': dbus.UInt64, 'd': float, 's': str}
79 array_matches = re.match(r'a\((\S+)\)', signature)
80 struct_matches = re.match(r'\((\S+)\)', signature)
81 dictionary_matches = re.match(r'a{(\S+)}', signature)
82 if signature in basic_types:
83 converted_value = basic_types[signature](value)
84 return converted_value
85 # Array
86 if array_matches:
87 element_type = array_matches.group(1)
88 converted_container = list()
89 # Test if value is a list
90 # to avoid iterating over each character in a string.
91 # Iterate over each item and convert type
92 if isinstance(value, list):
93 for i in value:
94 converted_element = convert_type(element_type, i)
95 converted_container.append(converted_element)
96 # Convert non-sequence to expected type, and append to list
97 else:
98 converted_element = convert_type(element_type, value)
99 converted_container.append(converted_element)
100 return converted_container
101 # Struct
102 if struct_matches:
103 element_types = struct_matches.group(1)
104 split_element_types = split_struct_signature(element_types)
105 converted_container = list()
106 # Test if value is a list
107 if isinstance(value, list):
108 for index, val in enumerate(value):
109 converted_element = convert_type(split_element_types[index],
110 value[index])
111 converted_container.append(converted_element)
112 else:
113 converted_element = convert_type(element_types, value)
114 converted_container.append(converted_element)
115 return tuple(converted_container)
116 # Dictionary
117 if dictionary_matches:
118 element_types = dictionary_matches.group(1)
119 split_element_types = split_struct_signature(element_types)
120 converted_container = dict()
121 # Convert each element of dict
122 for key, val in value.iteritems():
123 converted_key = convert_type(split_element_types[0], key)
124 converted_val = convert_type(split_element_types[1], val)
125 converted_container[converted_key] = converted_val
126 return converted_container
127
128
Brad Bishop2f428582015-12-02 10:56:11 -0500129class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400130 ''' Authorization plugin callback that checks that the user is logged in
131 and a member of a group. '''
132 def __init__(self, group):
133 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500134
Brad Bishop87b63c12016-03-18 14:47:51 -0400135 def __call__(self, session, *a, **kw):
136 valid_user(session, *a, **kw)
137 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500138
Brad Bishop87b63c12016-03-18 14:47:51 -0400139 try:
140 res = session['user'] in grp.getgrnam(self.group)[3]
141 except KeyError:
142 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500143
Brad Bishop87b63c12016-03-18 14:47:51 -0400144 if not res:
145 abort(403, 'Insufficient access')
146
Brad Bishop2f428582015-12-02 10:56:11 -0500147
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500148class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400149 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500150 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400151
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500152 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400153 self.app = app
154 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500155 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400156 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400157 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500158 self._content_type = content_type
Brad Bishop0f79e522016-03-18 13:33:17 -0400159 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400160
Brad Bishop88c76a42017-02-21 00:02:02 -0500161 if 'GET' in self._verbs:
162 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500163 if 'OPTIONS' not in self._verbs:
164 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500165
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 def _setup(self, **kw):
167 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500168
Brad Bishop87b63c12016-03-18 14:47:51 -0400169 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500170 if request.method != 'OPTIONS':
171 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500172
Brad Bishopd4c1c552017-02-21 00:07:28 -0500173 # Javascript implementations will not send credentials
174 # with an OPTIONS request. Don't help malicious clients
175 # by checking the path here and returning a 404 if the
176 # path doesn't exist.
177 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500178
Brad Bishopd4c1c552017-02-21 00:07:28 -0500179 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500180 raise HTTPError(
181 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400182
Brad Bishop87b63c12016-03-18 14:47:51 -0400183 def __call__(self, **kw):
184 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400185
Brad Bishop88c76a42017-02-21 00:02:02 -0500186 def do_head(self, **kw):
187 return self.do_get(**kw)
188
Brad Bishopd4c1c552017-02-21 00:07:28 -0500189 def do_options(self, **kw):
190 for v in self._verbs:
191 response.set_header(
192 'Allow',
193 ','.join(self._verbs))
194 return None
195
Brad Bishop87b63c12016-03-18 14:47:51 -0400196 def install(self):
197 self.app.route(
198 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500199 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400200
Brad Bishop87b63c12016-03-18 14:47:51 -0400201 @staticmethod
202 def try_mapper_call(f, callback=None, **kw):
203 try:
204 return f(**kw)
205 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500206 if e.get_dbus_name() == \
207 'org.freedesktop.DBus.Error.ObjectPathInUse':
208 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500209 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400210 raise
211 if callback is None:
212 def callback(e, **kw):
213 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400214
Brad Bishop87b63c12016-03-18 14:47:51 -0400215 callback(e, **kw)
216
217 @staticmethod
218 def try_properties_interface(f, *a):
219 try:
220 return f(*a)
221 except dbus.exceptions.DBusException, e:
222 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
223 # interface doesn't have any properties
224 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400225 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
226 # interface doesn't have any properties
227 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400228 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
229 # properties interface not implemented at all
230 return None
231 raise
232
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400233
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500234class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400235 verbs = 'GET'
236 rules = '<path:path>/'
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500237 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400238
Brad Bishop87b63c12016-03-18 14:47:51 -0400239 def __init__(self, app, bus):
240 super(DirectoryHandler, self).__init__(
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500241 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400242
Brad Bishop87b63c12016-03-18 14:47:51 -0400243 def find(self, path='/'):
244 return self.try_mapper_call(
245 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400246
Brad Bishop87b63c12016-03-18 14:47:51 -0400247 def setup(self, path='/'):
248 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400249
Brad Bishop87b63c12016-03-18 14:47:51 -0400250 def do_get(self, path='/'):
251 return request.route_data['map']
252
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400253
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500254class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 verbs = 'GET'
256 rules = ['/list', '<path:path>/list']
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500257 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400258
Brad Bishop87b63c12016-03-18 14:47:51 -0400259 def __init__(self, app, bus):
260 super(ListNamesHandler, self).__init__(
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500261 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400262
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 def find(self, path='/'):
264 return self.try_mapper_call(
265 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400266
Brad Bishop87b63c12016-03-18 14:47:51 -0400267 def setup(self, path='/'):
268 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400269
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 def do_get(self, path='/'):
271 return request.route_data['map']
272
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500274class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400275 verbs = 'GET'
276 rules = ['/enumerate', '<path:path>/enumerate']
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500277 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def __init__(self, app, bus):
280 super(ListHandler, self).__init__(
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500281 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400282
Brad Bishop87b63c12016-03-18 14:47:51 -0400283 def find(self, path='/'):
284 return self.try_mapper_call(
285 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400286
Brad Bishop87b63c12016-03-18 14:47:51 -0400287 def setup(self, path='/'):
288 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400291 return {x: y for x, y in self.mapper.enumerate_subtree(
292 path,
293 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400294
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400295
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500296class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400297 verbs = 'POST'
298 rules = '<path:path>/action/<method>'
299 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500300 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400301
Brad Bishop87b63c12016-03-18 14:47:51 -0400302 def __init__(self, app, bus):
303 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500304 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400305
Brad Bishop87b63c12016-03-18 14:47:51 -0400306 def find(self, path, method):
307 busses = self.try_mapper_call(
308 self.mapper.get_object, path=path)
309 for items in busses.iteritems():
310 m = self.find_method_on_bus(path, method, *items)
311 if m:
312 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400313
Brad Bishop87b63c12016-03-18 14:47:51 -0400314 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400315
Brad Bishop87b63c12016-03-18 14:47:51 -0400316 def setup(self, path, method):
317 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400318
Brad Bishop87b63c12016-03-18 14:47:51 -0400319 def do_post(self, path, method):
320 try:
321 if request.parameter_list:
322 return request.route_data['method'](*request.parameter_list)
323 else:
324 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400325
Brad Bishop87b63c12016-03-18 14:47:51 -0400326 except dbus.exceptions.DBusException, e:
327 if e.get_dbus_name() == DBUS_INVALID_ARGS:
328 abort(400, str(e))
329 if e.get_dbus_name() == DBUS_TYPE_ERROR:
330 abort(400, str(e))
331 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400332
Brad Bishop87b63c12016-03-18 14:47:51 -0400333 @staticmethod
334 def find_method_in_interface(method, obj, interface, methods):
335 if methods is None:
336 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400337
Brad Bishop6d190602016-04-15 13:09:39 -0400338 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 if method is not None:
340 iface = dbus.Interface(obj, interface)
341 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def find_method_on_bus(self, path, method, bus, interfaces):
344 obj = self.bus.get_object(bus, path, introspect=False)
345 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
346 data = iface.Introspect()
347 parser = IntrospectionNodeParser(
348 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500349 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400350 for x, y in parser.get_interfaces().iteritems():
351 m = self.find_method_in_interface(
352 method, obj, x, y.get('method'))
353 if m:
354 return m
355
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400356
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500357class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 verbs = ['PUT', 'GET']
359 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500360 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400361
Brad Bishop87b63c12016-03-18 14:47:51 -0400362 def __init__(self, app, bus):
363 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500364 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400365
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 def find(self, path, prop):
367 self.app.instance_handler.setup(path)
368 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500369 real_name = obmc.utils.misc.find_case_insensitive(
370 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400371
Brad Bishop56ad87f2017-02-21 23:33:29 -0500372 if not real_name:
373 if request.method == 'PUT':
374 abort(403, _4034_msg % ('property', 'created', prop))
375 else:
376 abort(404, _4034_msg % ('property', 'found', prop))
377 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500378
Brad Bishop87b63c12016-03-18 14:47:51 -0400379 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500380 name, obj = self.find(path, prop)
381 request.route_data['obj'] = obj
382 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500383
Brad Bishop87b63c12016-03-18 14:47:51 -0400384 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500385 name = request.route_data['name']
386 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500387
Brad Bishop87b63c12016-03-18 14:47:51 -0400388 def do_put(self, path, prop, value=None):
389 if value is None:
390 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500391
Brad Bishop87b63c12016-03-18 14:47:51 -0400392 prop, iface, properties_iface = self.get_host_interface(
393 path, prop, request.route_data['map'][path])
394 try:
395 properties_iface.Set(iface, prop, value)
396 except ValueError, e:
397 abort(400, str(e))
398 except dbus.exceptions.DBusException, e:
399 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500400 bus_name = properties_iface.bus_name
401 expected_type = get_type_signature_by_introspection(self.bus,
402 bus_name,
403 path,
404 prop)
405 if not expected_type:
406 abort(403, "Failed to get expected type: %s" % str(e))
407 converted_value = None
408 try:
409 converted_value = convert_type(expected_type, value)
410 self.do_put(path, prop, converted_value)
411 return
412 except Exception as ex:
413 abort(403, "Failed to convert %s to type %s" %
414 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400415 abort(403, str(e))
416 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500417
Brad Bishop87b63c12016-03-18 14:47:51 -0400418 def get_host_interface(self, path, prop, bus_info):
419 for bus, interfaces in bus_info.iteritems():
420 obj = self.bus.get_object(bus, path, introspect=True)
421 properties_iface = dbus.Interface(
422 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500423
Brad Bishop87b63c12016-03-18 14:47:51 -0400424 info = self.get_host_interface_on_bus(
425 path, prop, properties_iface, bus, interfaces)
426 if info is not None:
427 prop, iface = info
428 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
431 for i in interfaces:
432 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500433 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400434 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500435 match = obmc.utils.misc.find_case_insensitive(
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500436 prop, properties.keys())
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500437 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400438 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500439 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400440 return prop, i
441
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500442
Brad Bishop2503bd62015-12-16 17:56:12 -0500443class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 verbs = ['GET']
445 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500446
Brad Bishop87b63c12016-03-18 14:47:51 -0400447 def __init__(self, app, bus):
448 super(SchemaHandler, self).__init__(
449 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500450
Brad Bishop87b63c12016-03-18 14:47:51 -0400451 def find(self, path):
452 return self.try_mapper_call(
453 self.mapper.get_object,
454 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 def setup(self, path):
457 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500458
Brad Bishop87b63c12016-03-18 14:47:51 -0400459 def do_get(self, path):
460 schema = {}
461 for x in request.route_data['map'].iterkeys():
462 obj = self.bus.get_object(x, path, introspect=False)
463 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
464 data = iface.Introspect()
465 parser = IntrospectionNodeParser(
466 ElementTree.fromstring(data))
467 for x, y in parser.get_interfaces().iteritems():
468 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500469
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 return schema
471
Brad Bishop2503bd62015-12-16 17:56:12 -0500472
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500473class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400474 verbs = ['GET', 'PUT', 'DELETE']
475 rules = '<path:path>'
476 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500477
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 def __init__(self, app, bus):
479 super(InstanceHandler, self).__init__(
480 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500481
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 def find(self, path, callback=None):
483 return {path: self.try_mapper_call(
484 self.mapper.get_object,
485 callback,
486 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500487
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 def setup(self, path):
489 callback = None
490 if request.method == 'PUT':
491 def callback(e, **kw):
492 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500493
Brad Bishop87b63c12016-03-18 14:47:51 -0400494 if request.route_data.get('map') is None:
495 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400498 return self.mapper.enumerate_object(
499 path,
500 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500501
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 def do_put(self, path):
503 # make sure all properties exist in the request
504 obj = set(self.do_get(path).keys())
505 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500506
Brad Bishop87b63c12016-03-18 14:47:51 -0400507 diff = list(obj.difference(req))
508 if diff:
509 abort(403, _4034_msg % (
510 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 diff = list(req.difference(obj))
513 if diff:
514 abort(403, _4034_msg % (
515 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500516
Brad Bishop87b63c12016-03-18 14:47:51 -0400517 for p, v in request.parameter_list.iteritems():
518 self.app.property_handler.do_put(
519 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500520
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 def do_delete(self, path):
522 for bus_info in request.route_data['map'][path].iteritems():
523 if self.bus_missing_delete(path, *bus_info):
524 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500525
Brad Bishop87b63c12016-03-18 14:47:51 -0400526 for bus in request.route_data['map'][path].iterkeys():
527 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 def bus_missing_delete(self, path, bus, interfaces):
530 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500531
Brad Bishop87b63c12016-03-18 14:47:51 -0400532 def delete_on_bus(self, path, bus):
533 obj = self.bus.get_object(bus, path, introspect=False)
534 delete_iface = dbus.Interface(
535 obj, dbus_interface=DELETE_IFACE)
536 delete_iface.Delete()
537
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500538
Brad Bishop2f428582015-12-02 10:56:11 -0500539class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 ''' Handles the /login and /logout routes, manages
541 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 rules = ['/login', '/logout']
544 login_str = "User '%s' logged %s"
545 bad_passwd_str = "Invalid username or password"
546 no_user_str = "No user logged in"
547 bad_json_str = "Expecting request format { 'data': " \
548 "[<username>, <password>] }, got '%s'"
549 _require_auth = None
550 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500551
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 def __init__(self, app, bus):
553 super(SessionHandler, self).__init__(
554 app, bus)
555 self.hmac_key = os.urandom(128)
556 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500557
Brad Bishop87b63c12016-03-18 14:47:51 -0400558 @staticmethod
559 def authenticate(username, clear):
560 try:
561 encoded = spwd.getspnam(username)[1]
562 return encoded == crypt.crypt(clear, encoded)
563 except KeyError:
564 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500565
Brad Bishop87b63c12016-03-18 14:47:51 -0400566 def invalidate_session(self, session):
567 try:
568 self.session_store.remove(session)
569 except ValueError:
570 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 def new_session(self):
573 sid = os.urandom(32)
574 if self.MAX_SESSIONS <= len(self.session_store):
575 self.session_store.pop()
576 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500577
Brad Bishop87b63c12016-03-18 14:47:51 -0400578 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500579
Brad Bishop87b63c12016-03-18 14:47:51 -0400580 def get_session(self, sid):
581 sids = [x['sid'] for x in self.session_store]
582 try:
583 return self.session_store[sids.index(sid)]
584 except ValueError:
585 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500586
Brad Bishop87b63c12016-03-18 14:47:51 -0400587 def get_session_from_cookie(self):
588 return self.get_session(
589 request.get_cookie(
590 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500591
Brad Bishop87b63c12016-03-18 14:47:51 -0400592 def do_post(self, **kw):
593 if request.path == '/login':
594 return self.do_login(**kw)
595 else:
596 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500597
Brad Bishop87b63c12016-03-18 14:47:51 -0400598 def do_logout(self, **kw):
599 session = self.get_session_from_cookie()
600 if session is not None:
601 user = session['user']
602 self.invalidate_session(session)
603 response.delete_cookie('sid')
604 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500605
Brad Bishop87b63c12016-03-18 14:47:51 -0400606 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500607
Brad Bishop87b63c12016-03-18 14:47:51 -0400608 def do_login(self, **kw):
609 session = self.get_session_from_cookie()
610 if session is not None:
611 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 if len(request.parameter_list) != 2:
614 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500615
Brad Bishop87b63c12016-03-18 14:47:51 -0400616 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400617 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500618
Brad Bishop87b63c12016-03-18 14:47:51 -0400619 user = request.parameter_list[0]
620 session = self.new_session()
621 session['user'] = user
622 response.set_cookie(
623 'sid', session['sid'], secret=self.hmac_key,
624 secure=True,
625 httponly=True)
626 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500627
Brad Bishop87b63c12016-03-18 14:47:51 -0400628 def find(self, **kw):
629 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500630
Brad Bishop87b63c12016-03-18 14:47:51 -0400631 def setup(self, **kw):
632 pass
633
Brad Bishop2f428582015-12-02 10:56:11 -0500634
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500635class ImageUploadUtils:
636 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500637
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500638 file_loc = '/tmp/images'
639 file_prefix = 'img'
640 file_suffix = ''
641
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500642 @classmethod
643 def do_upload(cls, filename=''):
644 if not os.path.exists(cls.file_loc):
645 os.makedirs(cls.file_loc)
646 if not filename:
647 handle, filename = tempfile.mkstemp(cls.file_suffix,
648 cls.file_prefix, cls.file_loc)
649 os.close(handle)
650 else:
651 filename = os.path.join(cls.file_loc, filename)
652
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500653 try:
654 file_contents = request.body.read()
655 request.body.close()
656 with open(filename, "w") as fd:
657 fd.write(file_contents)
658 except (IOError, ValueError), e:
659 abort(400, str(e))
660 except:
661 abort(400, "Unexpected Error")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500662
663
664class ImagePostHandler(RouteHandler):
665 ''' Handles the /upload/image route. '''
666
667 verbs = ['POST']
668 rules = ['/upload/image']
669 content_type = 'application/octet-stream'
670
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500671 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500672 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500673 app, bus, self.verbs, self.rules, self.content_type)
674
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500675 def do_post(self, filename=''):
676 ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500677
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500678 def find(self, **kw):
679 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500680
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500681 def setup(self, **kw):
682 pass
683
684
685class ImagePutHandler(RouteHandler):
686 ''' Handles the /upload/image/<filename> route. '''
687
688 verbs = ['PUT']
689 rules = ['/upload/image/<filename>']
690 content_type = 'application/octet-stream'
691
692 def __init__(self, app, bus):
693 super(ImagePutHandler, self).__init__(
694 app, bus, self.verbs, self.rules, self.content_type)
695
696 def do_put(self, filename=''):
697 ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500698
699 def find(self, **kw):
700 pass
701
702 def setup(self, **kw):
703 pass
704
705
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500706class DownloadDumpHandler(RouteHandler):
707 ''' Handles the /download/dump route. '''
708
709 verbs = 'GET'
710 rules = ['/download/dump/<dumpid>']
711 content_type = 'application/octet-stream'
712 ''' TODO openbmc/issues #1795, Change dump path'''
713 dump_loc = '/tmp/dumps'
714
715 def __init__(self, app, bus):
716 super(DownloadDumpHandler, self).__init__(
717 app, bus, self.verbs, self.rules, self.content_type)
718
719 def do_get(self, dumpid):
720 return self.do_download(dumpid)
721
722 def find(self, **kw):
723 pass
724
725 def setup(self, **kw):
726 pass
727
728 def do_download(self, dumpid):
729 dump_loc = os.path.join(self.dump_loc, dumpid)
730 if not os.path.exists(dump_loc):
731 abort(404, "Path not found")
732
733 files = os.listdir(dump_loc)
734 num_files = len(files)
735 if num_files == 0:
736 abort(404, "Dump not found")
737
738 return static_file(os.path.basename(files[0]), root=dump_loc,
739 download=True, mimetype=self.content_type)
740
741
Brad Bishop2f428582015-12-02 10:56:11 -0500742class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400743 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500744
Brad Bishop87b63c12016-03-18 14:47:51 -0400745 name = 'authorization'
746 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500747
Brad Bishop87b63c12016-03-18 14:47:51 -0400748 class Compose:
749 def __init__(self, validators, callback, session_mgr):
750 self.validators = validators
751 self.callback = callback
752 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500753
Brad Bishop87b63c12016-03-18 14:47:51 -0400754 def __call__(self, *a, **kw):
755 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
756 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500757 if request.method != 'OPTIONS':
758 for x in self.validators:
759 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500760
Brad Bishop87b63c12016-03-18 14:47:51 -0400761 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500762
Brad Bishop87b63c12016-03-18 14:47:51 -0400763 def apply(self, callback, route):
764 undecorated = route.get_undecorated_callback()
765 if not isinstance(undecorated, RouteHandler):
766 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500767
Brad Bishop87b63c12016-03-18 14:47:51 -0400768 auth_types = getattr(
769 undecorated, '_require_auth', None)
770 if not auth_types:
771 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500772
Brad Bishop87b63c12016-03-18 14:47:51 -0400773 return self.Compose(
774 auth_types, callback, undecorated.app.session_handler)
775
Brad Bishop2f428582015-12-02 10:56:11 -0500776
Brad Bishopd0c404a2017-02-21 09:23:25 -0500777class CorsPlugin(object):
778 ''' Add CORS headers. '''
779
780 name = 'cors'
781 api = 2
782
783 @staticmethod
784 def process_origin():
785 origin = request.headers.get('Origin')
786 if origin:
787 response.add_header('Access-Control-Allow-Origin', origin)
788 response.add_header(
789 'Access-Control-Allow-Credentials', 'true')
790
791 @staticmethod
792 def process_method_and_headers(verbs):
793 method = request.headers.get('Access-Control-Request-Method')
794 headers = request.headers.get('Access-Control-Request-Headers')
795 if headers:
796 headers = [x.lower() for x in headers.split(',')]
797
798 if method in verbs \
799 and headers == ['content-type']:
800 response.add_header('Access-Control-Allow-Methods', method)
801 response.add_header(
802 'Access-Control-Allow-Headers', 'Content-Type')
803
804 def __init__(self, app):
805 app.install_error_callback(self.error_callback)
806
807 def apply(self, callback, route):
808 undecorated = route.get_undecorated_callback()
809 if not isinstance(undecorated, RouteHandler):
810 return callback
811
812 if not getattr(undecorated, '_enable_cors', None):
813 return callback
814
815 def wrap(*a, **kw):
816 self.process_origin()
817 self.process_method_and_headers(undecorated._verbs)
818 return callback(*a, **kw)
819
820 return wrap
821
822 def error_callback(self, **kw):
823 self.process_origin()
824
825
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500826class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400827 ''' Ensures request content satisfies the OpenBMC json api format. '''
828 name = 'json_api_request'
829 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500830
Brad Bishop87b63c12016-03-18 14:47:51 -0400831 error_str = "Expecting request format { 'data': <value> }, got '%s'"
832 type_error_str = "Unsupported Content-Type: '%s'"
833 json_type = "application/json"
834 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500835
Brad Bishop87b63c12016-03-18 14:47:51 -0400836 @staticmethod
837 def content_expected():
838 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500839
Brad Bishop87b63c12016-03-18 14:47:51 -0400840 def validate_request(self):
841 if request.content_length > 0 and \
842 request.content_type != self.json_type:
843 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500844
Brad Bishop87b63c12016-03-18 14:47:51 -0400845 try:
846 request.parameter_list = request.json.get('data')
847 except ValueError, e:
848 abort(400, str(e))
849 except (AttributeError, KeyError, TypeError):
850 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500851
Brad Bishop87b63c12016-03-18 14:47:51 -0400852 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500853 content_type = getattr(
854 route.get_undecorated_callback(), '_content_type', None)
855 if self.json_type != content_type:
856 return callback
857
Brad Bishop87b63c12016-03-18 14:47:51 -0400858 verbs = getattr(
859 route.get_undecorated_callback(), '_verbs', None)
860 if verbs is None:
861 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500862
Brad Bishop87b63c12016-03-18 14:47:51 -0400863 if not set(self.request_methods).intersection(verbs):
864 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500865
Brad Bishop87b63c12016-03-18 14:47:51 -0400866 def wrap(*a, **kw):
867 if self.content_expected():
868 self.validate_request()
869 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500870
Brad Bishop87b63c12016-03-18 14:47:51 -0400871 return wrap
872
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500873
874class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400875 ''' Ensures request content type satisfies the OpenBMC json api format. '''
876 name = 'json_api_method_request'
877 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500878
Brad Bishop87b63c12016-03-18 14:47:51 -0400879 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500880 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500881
Brad Bishop87b63c12016-03-18 14:47:51 -0400882 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500883 content_type = getattr(
884 route.get_undecorated_callback(), '_content_type', None)
885 if self.json_type != content_type:
886 return callback
887
Brad Bishop87b63c12016-03-18 14:47:51 -0400888 request_type = getattr(
889 route.get_undecorated_callback(), 'request_type', None)
890 if request_type is None:
891 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500892
Brad Bishop87b63c12016-03-18 14:47:51 -0400893 def validate_request():
894 if not isinstance(request.parameter_list, request_type):
895 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500896
Brad Bishop87b63c12016-03-18 14:47:51 -0400897 def wrap(*a, **kw):
898 if JsonApiRequestPlugin.content_expected():
899 validate_request()
900 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500901
Brad Bishop87b63c12016-03-18 14:47:51 -0400902 return wrap
903
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500904
Brad Bishop080a48e2017-02-21 22:34:43 -0500905class JsonErrorsPlugin(JSONPlugin):
906 ''' Extend the Bottle JSONPlugin such that it also encodes error
907 responses. '''
908
909 def __init__(self, app, **kw):
910 super(JsonErrorsPlugin, self).__init__(**kw)
911 self.json_opts = {
912 x: y for x, y in kw.iteritems()
913 if x in ['indent', 'sort_keys']}
914 app.install_error_callback(self.error_callback)
915
916 def error_callback(self, response_object, response_body, **kw):
917 response_body['body'] = json.dumps(response_object, **self.json_opts)
918 response.content_type = 'application/json'
919
920
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500921class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500922 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400923 name = 'json_api_response'
924 api = 2
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500925 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500926
Brad Bishopd4c1c552017-02-21 00:07:28 -0500927 @staticmethod
928 def has_body():
929 return request.method not in ['OPTIONS']
930
Brad Bishop080a48e2017-02-21 22:34:43 -0500931 def __init__(self, app):
932 app.install_error_callback(self.error_callback)
933
Brad Bishop87b63c12016-03-18 14:47:51 -0400934 def apply(self, callback, route):
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500935 content_type = getattr(
936 route.get_undecorated_callback(), '_content_type', None)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500937 if self.json_type != content_type :
Jayanth Othayoth1444fd82017-06-29 05:45:07 -0500938 return callback
939
Brad Bishop87b63c12016-03-18 14:47:51 -0400940 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500941 data = callback(*a, **kw)
942 if self.has_body():
943 resp = {'data': data}
944 resp['status'] = 'ok'
945 resp['message'] = response.status_line
946 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400947 return wrap
948
Brad Bishop080a48e2017-02-21 22:34:43 -0500949 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400950 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -0500951 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -0500952 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400953 if error.status_code == 500:
954 response_object['data']['exception'] = repr(error.exception)
955 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500956
Brad Bishop87b63c12016-03-18 14:47:51 -0400957
Brad Bishop080a48e2017-02-21 22:34:43 -0500958class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400959 ''' Json javascript wrapper. '''
960 name = 'jsonp'
961 api = 2
962
Brad Bishop080a48e2017-02-21 22:34:43 -0500963 def __init__(self, app, **kw):
964 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400965
966 @staticmethod
967 def to_jsonp(json):
968 jwrapper = request.query.callback or None
969 if(jwrapper):
970 response.set_header('Content-Type', 'application/javascript')
971 json = jwrapper + '(' + json + ');'
972 return json
973
974 def apply(self, callback, route):
975 def wrap(*a, **kw):
976 return self.to_jsonp(callback(*a, **kw))
977 return wrap
978
Brad Bishop080a48e2017-02-21 22:34:43 -0500979 def error_callback(self, response_body, **kw):
980 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400981
982
Deepak Kodihalli461367a2017-04-10 07:11:38 -0500983class ContentCheckerPlugin(object):
984 ''' Ensures that a route is associated with the expected content-type
985 header. '''
986 name = 'content_checker'
987 api = 2
988
989 class Checker:
990 def __init__(self, type, callback):
991 self.expected_type = type
992 self.callback = callback
993 self.error_str = "Expecting content type '%s', got '%s'"
994
995 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -0500996 if request.method in ['PUT', 'POST', 'PATCH'] and \
997 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -0500998 self.expected_type != request.content_type:
999 abort(415, self.error_str % (self.expected_type,
1000 request.content_type))
1001
1002 return self.callback(*a, **kw)
1003
1004 def apply(self, callback, route):
1005 content_type = getattr(
1006 route.get_undecorated_callback(), '_content_type', None)
1007
1008 return self.Checker(content_type, callback)
1009
1010
Brad Bishop2c6fc762016-08-29 15:53:25 -04001011class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -04001012 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001013 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -04001014 self.bus = dbus.SystemBus()
1015 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001016 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001017
Brad Bishop87b63c12016-03-18 14:47:51 -04001018 self.install_hooks()
1019 self.install_plugins()
1020 self.create_handlers()
1021 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001022
Brad Bishop87b63c12016-03-18 14:47:51 -04001023 def install_plugins(self):
1024 # install json api plugins
1025 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001026 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001027 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001028 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001029 self.install(JsonpPlugin(self, **json_kw))
1030 self.install(JsonErrorsPlugin(self, **json_kw))
1031 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001032 self.install(JsonApiRequestPlugin())
1033 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001034
Brad Bishop87b63c12016-03-18 14:47:51 -04001035 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001036 self.error_handler_type = type(self.default_error_handler)
1037 self.original_error_handler = self.default_error_handler
1038 self.default_error_handler = self.error_handler_type(
1039 self.custom_error_handler, self, Bottle)
1040
Brad Bishop87b63c12016-03-18 14:47:51 -04001041 self.real_router_match = self.router.match
1042 self.router.match = self.custom_router_match
1043 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001044
Brad Bishop87b63c12016-03-18 14:47:51 -04001045 def create_handlers(self):
1046 # create route handlers
1047 self.session_handler = SessionHandler(self, self.bus)
1048 self.directory_handler = DirectoryHandler(self, self.bus)
1049 self.list_names_handler = ListNamesHandler(self, self.bus)
1050 self.list_handler = ListHandler(self, self.bus)
1051 self.method_handler = MethodHandler(self, self.bus)
1052 self.property_handler = PropertyHandler(self, self.bus)
1053 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001054 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1055 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001056 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001057 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001058
Brad Bishop87b63c12016-03-18 14:47:51 -04001059 def install_handlers(self):
1060 self.session_handler.install()
1061 self.directory_handler.install()
1062 self.list_names_handler.install()
1063 self.list_handler.install()
1064 self.method_handler.install()
1065 self.property_handler.install()
1066 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001067 self.image_upload_post_handler.install()
1068 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001069 self.download_dump_get_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001070 # this has to come last, since it matches everything
1071 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001072
Brad Bishop080a48e2017-02-21 22:34:43 -05001073 def install_error_callback(self, callback):
1074 self.error_callbacks.insert(0, callback)
1075
Brad Bishop87b63c12016-03-18 14:47:51 -04001076 def custom_router_match(self, environ):
1077 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1078 needed doesn't work for us since the instance rules match
1079 everything. This monkey-patch lets the route handler figure
1080 out which response is needed. This could be accomplished
1081 with a hook but that would require calling the router match
1082 function twice.
1083 '''
1084 route, args = self.real_router_match(environ)
1085 if isinstance(route.callback, RouteHandler):
1086 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001087
Brad Bishop87b63c12016-03-18 14:47:51 -04001088 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001089
Brad Bishop080a48e2017-02-21 22:34:43 -05001090 def custom_error_handler(self, res, error):
1091 ''' Allow plugins to modify error reponses too via this custom
1092 error handler. '''
1093
1094 response_object = {}
1095 response_body = {}
1096 for x in self.error_callbacks:
1097 x(error=error,
1098 response_object=response_object,
1099 response_body=response_body)
1100
1101 return response_body.get('body', "")
1102
Brad Bishop87b63c12016-03-18 14:47:51 -04001103 @staticmethod
1104 def strip_extra_slashes():
1105 path = request.environ['PATH_INFO']
1106 trailing = ("", "/")[path[-1] == '/']
1107 parts = filter(bool, path.split('/'))
1108 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing