blob: c44a45d08df0feb6acfafe5a219d82297bc251d2 [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
Deepak Kodihalli639b5022017-10-13 06:40:26 -050032have_wsock = True
33try:
34 from geventwebsocket import WebSocketError
35except ImportError:
36 have_wsock = False
37if have_wsock:
38 from dbus.mainloop.glib import DBusGMainLoop
39 DBusGMainLoop(set_as_default=True)
40 import gobject
41 import gevent
Brad Bishopaa65f6e2015-10-27 16:28:51 -040042
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050043DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040044DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050045DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
46DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050047DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050048DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050049
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050050_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040051
Brad Bishop87b63c12016-03-18 14:47:51 -040052
Brad Bishop2f428582015-12-02 10:56:11 -050053def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040054 ''' Authorization plugin callback that checks
55 that the user is logged in. '''
56 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040057 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040058
Brad Bishop2f428582015-12-02 10:56:11 -050059
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050060def get_type_signature_by_introspection(bus, service, object_path,
61 property_name):
62 obj = bus.get_object(service, object_path)
63 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
64 xml_string = iface.Introspect()
65 for child in ElementTree.fromstring(xml_string):
66 # Iterate over each interfaces's properties to find
67 # matching property_name, and return its signature string
68 if child.tag == 'interface':
69 for i in child.iter():
70 if ('name' in i.attrib) and \
71 (i.attrib['name'] == property_name):
72 type_signature = i.attrib['type']
73 return type_signature
74
75
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053076def get_method_signature(bus, service, object_path, interface, method):
77 obj = bus.get_object(service, object_path)
78 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
79 xml_string = iface.Introspect()
80 arglist = []
81
82 root = ElementTree.fromstring(xml_string)
83 for dbus_intf in root.findall('interface'):
84 if (dbus_intf.get('name') == interface):
85 for dbus_method in dbus_intf.findall('method'):
86 if(dbus_method.get('name') == method):
87 for arg in dbus_method.findall('arg'):
88 arglist.append(arg.get('type'))
89 return arglist
90
91
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050092def split_struct_signature(signature):
93 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
94 struct_matches = re.findall(struct_regex, signature)
95 return struct_matches
96
97
98def convert_type(signature, value):
99 # Basic Types
100 converted_value = None
101 converted_container = None
102 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
103 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
104 't': dbus.UInt64, 'd': float, 's': str}
105 array_matches = re.match(r'a\((\S+)\)', signature)
106 struct_matches = re.match(r'\((\S+)\)', signature)
107 dictionary_matches = re.match(r'a{(\S+)}', signature)
108 if signature in basic_types:
109 converted_value = basic_types[signature](value)
110 return converted_value
111 # Array
112 if array_matches:
113 element_type = array_matches.group(1)
114 converted_container = list()
115 # Test if value is a list
116 # to avoid iterating over each character in a string.
117 # Iterate over each item and convert type
118 if isinstance(value, list):
119 for i in value:
120 converted_element = convert_type(element_type, i)
121 converted_container.append(converted_element)
122 # Convert non-sequence to expected type, and append to list
123 else:
124 converted_element = convert_type(element_type, value)
125 converted_container.append(converted_element)
126 return converted_container
127 # Struct
128 if struct_matches:
129 element_types = struct_matches.group(1)
130 split_element_types = split_struct_signature(element_types)
131 converted_container = list()
132 # Test if value is a list
133 if isinstance(value, list):
134 for index, val in enumerate(value):
135 converted_element = convert_type(split_element_types[index],
136 value[index])
137 converted_container.append(converted_element)
138 else:
139 converted_element = convert_type(element_types, value)
140 converted_container.append(converted_element)
141 return tuple(converted_container)
142 # Dictionary
143 if dictionary_matches:
144 element_types = dictionary_matches.group(1)
145 split_element_types = split_struct_signature(element_types)
146 converted_container = dict()
147 # Convert each element of dict
148 for key, val in value.iteritems():
149 converted_key = convert_type(split_element_types[0], key)
150 converted_val = convert_type(split_element_types[1], val)
151 converted_container[converted_key] = converted_val
152 return converted_container
153
154
Brad Bishop2f428582015-12-02 10:56:11 -0500155class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400156 ''' Authorization plugin callback that checks that the user is logged in
157 and a member of a group. '''
158 def __init__(self, group):
159 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500160
Brad Bishop87b63c12016-03-18 14:47:51 -0400161 def __call__(self, session, *a, **kw):
162 valid_user(session, *a, **kw)
163 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500164
Brad Bishop87b63c12016-03-18 14:47:51 -0400165 try:
166 res = session['user'] in grp.getgrnam(self.group)[3]
167 except KeyError:
168 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500169
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 if not res:
171 abort(403, 'Insufficient access')
172
Brad Bishop2f428582015-12-02 10:56:11 -0500173
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500174class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400175 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500176 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400177
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500178 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400179 self.app = app
180 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500181 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400182 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400183 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500184 self._content_type = content_type
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400185
Brad Bishop88c76a42017-02-21 00:02:02 -0500186 if 'GET' in self._verbs:
187 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500188 if 'OPTIONS' not in self._verbs:
189 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500190
Brad Bishop87b63c12016-03-18 14:47:51 -0400191 def _setup(self, **kw):
192 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500193
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500195 if request.method != 'OPTIONS':
196 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500197
Brad Bishopd4c1c552017-02-21 00:07:28 -0500198 # Javascript implementations will not send credentials
199 # with an OPTIONS request. Don't help malicious clients
200 # by checking the path here and returning a 404 if the
201 # path doesn't exist.
202 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500203
Brad Bishopd4c1c552017-02-21 00:07:28 -0500204 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500205 raise HTTPError(
206 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400207
Brad Bishop87b63c12016-03-18 14:47:51 -0400208 def __call__(self, **kw):
209 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400210
Brad Bishop88c76a42017-02-21 00:02:02 -0500211 def do_head(self, **kw):
212 return self.do_get(**kw)
213
Brad Bishopd4c1c552017-02-21 00:07:28 -0500214 def do_options(self, **kw):
215 for v in self._verbs:
216 response.set_header(
217 'Allow',
218 ','.join(self._verbs))
219 return None
220
Brad Bishop87b63c12016-03-18 14:47:51 -0400221 def install(self):
222 self.app.route(
223 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500224 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400225
Brad Bishop87b63c12016-03-18 14:47:51 -0400226 @staticmethod
227 def try_mapper_call(f, callback=None, **kw):
228 try:
229 return f(**kw)
230 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500231 if e.get_dbus_name() == \
232 'org.freedesktop.DBus.Error.ObjectPathInUse':
233 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500234 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400235 raise
236 if callback is None:
237 def callback(e, **kw):
238 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400239
Brad Bishop87b63c12016-03-18 14:47:51 -0400240 callback(e, **kw)
241
242 @staticmethod
243 def try_properties_interface(f, *a):
244 try:
245 return f(*a)
246 except dbus.exceptions.DBusException, e:
247 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
248 # interface doesn't have any properties
249 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400250 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
251 # interface doesn't have any properties
252 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400253 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
254 # properties interface not implemented at all
255 return None
256 raise
257
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400258
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500259class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400260 verbs = 'GET'
261 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400262
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 def __init__(self, app, bus):
264 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400265 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400266
Brad Bishop87b63c12016-03-18 14:47:51 -0400267 def find(self, path='/'):
268 return self.try_mapper_call(
269 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400270
Brad Bishop87b63c12016-03-18 14:47:51 -0400271 def setup(self, path='/'):
272 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishop87b63c12016-03-18 14:47:51 -0400274 def do_get(self, path='/'):
275 return request.route_data['map']
276
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400277
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500278class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 verbs = 'GET'
280 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400281
Brad Bishop87b63c12016-03-18 14:47:51 -0400282 def __init__(self, app, bus):
283 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400284 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 def find(self, path='/'):
287 return self.try_mapper_call(
288 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 def setup(self, path='/'):
291 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400292
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 def do_get(self, path='/'):
294 return request.route_data['map']
295
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400296
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500297class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400298 verbs = 'GET'
299 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400300
Brad Bishop87b63c12016-03-18 14:47:51 -0400301 def __init__(self, app, bus):
302 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400303 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400304
Brad Bishop87b63c12016-03-18 14:47:51 -0400305 def find(self, path='/'):
306 return self.try_mapper_call(
307 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400308
Brad Bishop87b63c12016-03-18 14:47:51 -0400309 def setup(self, path='/'):
310 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400311
Brad Bishop87b63c12016-03-18 14:47:51 -0400312 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400313 return {x: y for x, y in self.mapper.enumerate_subtree(
314 path,
315 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400316
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400317
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500318class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400319 verbs = 'POST'
320 rules = '<path:path>/action/<method>'
321 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500322 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400323
Brad Bishop87b63c12016-03-18 14:47:51 -0400324 def __init__(self, app, bus):
325 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500326 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530327 self.service = ''
328 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400329
Brad Bishop87b63c12016-03-18 14:47:51 -0400330 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500331 method_list = []
Brad Bishop87b63c12016-03-18 14:47:51 -0400332 busses = self.try_mapper_call(
333 self.mapper.get_object, path=path)
334 for items in busses.iteritems():
335 m = self.find_method_on_bus(path, method, *items)
336 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500337 method_list.append(m)
338 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400339
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400341
Brad Bishop87b63c12016-03-18 14:47:51 -0400342 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500343 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400344
Brad Bishop87b63c12016-03-18 14:47:51 -0400345 def do_post(self, path, method):
346 try:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500347 for item in request.route_data['map']:
348 if request.parameter_list:
349 item(*request.parameter_list)
350 else:
351 item()
352 return
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400353
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 except dbus.exceptions.DBusException, e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530355 paramlist = []
Brad Bishop87b63c12016-03-18 14:47:51 -0400356 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530357
358 signature_list = get_method_signature(self.bus, self.service,
359 path, self.interface,
360 method)
361 if not signature_list:
362 abort(400, "Failed to get method signature: %s" % str(e))
363 if len(signature_list) != len(request.parameter_list):
364 abort(400, "Invalid number of args")
365 converted_value = None
366 try:
367 for index, expected_type in enumerate(signature_list):
368 value = request.parameter_list[index]
369 converted_value = convert_type(expected_type, value)
370 paramlist.append(converted_value)
371 request.parameter_list = paramlist
372 self.do_post(path, method)
373 return
374 except Exception as ex:
375 abort(400, "Failed to convert the types")
Brad Bishop87b63c12016-03-18 14:47:51 -0400376 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 if e.get_dbus_name() == DBUS_TYPE_ERROR:
379 abort(400, str(e))
380 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400381
Brad Bishop87b63c12016-03-18 14:47:51 -0400382 @staticmethod
383 def find_method_in_interface(method, obj, interface, methods):
384 if methods is None:
385 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400386
Brad Bishop6d190602016-04-15 13:09:39 -0400387 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400388 if method is not None:
389 iface = dbus.Interface(obj, interface)
390 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400391
Brad Bishop87b63c12016-03-18 14:47:51 -0400392 def find_method_on_bus(self, path, method, bus, interfaces):
393 obj = self.bus.get_object(bus, path, introspect=False)
394 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
395 data = iface.Introspect()
396 parser = IntrospectionNodeParser(
397 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500398 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400399 for x, y in parser.get_interfaces().iteritems():
400 m = self.find_method_in_interface(
401 method, obj, x, y.get('method'))
402 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530403 self.service = bus
404 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400405 return m
406
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400407
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500408class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 verbs = ['PUT', 'GET']
410 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500411 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 def __init__(self, app, bus):
414 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500415 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400416
Brad Bishop87b63c12016-03-18 14:47:51 -0400417 def find(self, path, prop):
418 self.app.instance_handler.setup(path)
419 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500420 real_name = obmc.utils.misc.find_case_insensitive(
421 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400422
Brad Bishop56ad87f2017-02-21 23:33:29 -0500423 if not real_name:
424 if request.method == 'PUT':
425 abort(403, _4034_msg % ('property', 'created', prop))
426 else:
427 abort(404, _4034_msg % ('property', 'found', prop))
428 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500431 name, obj = self.find(path, prop)
432 request.route_data['obj'] = obj
433 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500434
Brad Bishop87b63c12016-03-18 14:47:51 -0400435 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500436 name = request.route_data['name']
437 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500438
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 def do_put(self, path, prop, value=None):
440 if value is None:
441 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500442
Brad Bishop87b63c12016-03-18 14:47:51 -0400443 prop, iface, properties_iface = self.get_host_interface(
444 path, prop, request.route_data['map'][path])
445 try:
446 properties_iface.Set(iface, prop, value)
447 except ValueError, e:
448 abort(400, str(e))
449 except dbus.exceptions.DBusException, e:
450 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500451 bus_name = properties_iface.bus_name
452 expected_type = get_type_signature_by_introspection(self.bus,
453 bus_name,
454 path,
455 prop)
456 if not expected_type:
457 abort(403, "Failed to get expected type: %s" % str(e))
458 converted_value = None
459 try:
460 converted_value = convert_type(expected_type, value)
461 self.do_put(path, prop, converted_value)
462 return
463 except Exception as ex:
464 abort(403, "Failed to convert %s to type %s" %
465 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400466 abort(403, str(e))
467 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500468
Brad Bishop87b63c12016-03-18 14:47:51 -0400469 def get_host_interface(self, path, prop, bus_info):
470 for bus, interfaces in bus_info.iteritems():
471 obj = self.bus.get_object(bus, path, introspect=True)
472 properties_iface = dbus.Interface(
473 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500474
Brad Bishop87b63c12016-03-18 14:47:51 -0400475 info = self.get_host_interface_on_bus(
476 path, prop, properties_iface, bus, interfaces)
477 if info is not None:
478 prop, iface = info
479 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500480
Brad Bishop87b63c12016-03-18 14:47:51 -0400481 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
482 for i in interfaces:
483 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500484 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400485 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500486 match = obmc.utils.misc.find_case_insensitive(
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500487 prop, properties.keys())
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500488 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400489 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500490 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 return prop, i
492
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500493
Brad Bishop2503bd62015-12-16 17:56:12 -0500494class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400495 verbs = ['GET']
496 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500497
Brad Bishop87b63c12016-03-18 14:47:51 -0400498 def __init__(self, app, bus):
499 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400500 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500501
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 def find(self, path):
503 return self.try_mapper_call(
504 self.mapper.get_object,
505 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500506
Brad Bishop87b63c12016-03-18 14:47:51 -0400507 def setup(self, path):
508 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500509
Brad Bishop87b63c12016-03-18 14:47:51 -0400510 def do_get(self, path):
511 schema = {}
512 for x in request.route_data['map'].iterkeys():
513 obj = self.bus.get_object(x, path, introspect=False)
514 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
515 data = iface.Introspect()
516 parser = IntrospectionNodeParser(
517 ElementTree.fromstring(data))
518 for x, y in parser.get_interfaces().iteritems():
519 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500520
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 return schema
522
Brad Bishop2503bd62015-12-16 17:56:12 -0500523
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500524class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 verbs = ['GET', 'PUT', 'DELETE']
526 rules = '<path:path>'
527 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 def __init__(self, app, bus):
530 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400531 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 def find(self, path, callback=None):
534 return {path: self.try_mapper_call(
535 self.mapper.get_object,
536 callback,
537 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500538
Brad Bishop87b63c12016-03-18 14:47:51 -0400539 def setup(self, path):
540 callback = None
541 if request.method == 'PUT':
542 def callback(e, **kw):
543 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500544
Brad Bishop87b63c12016-03-18 14:47:51 -0400545 if request.route_data.get('map') is None:
546 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500547
Brad Bishop87b63c12016-03-18 14:47:51 -0400548 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400549 return self.mapper.enumerate_object(
550 path,
551 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500552
Brad Bishop87b63c12016-03-18 14:47:51 -0400553 def do_put(self, path):
554 # make sure all properties exist in the request
555 obj = set(self.do_get(path).keys())
556 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500557
Brad Bishop87b63c12016-03-18 14:47:51 -0400558 diff = list(obj.difference(req))
559 if diff:
560 abort(403, _4034_msg % (
561 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500562
Brad Bishop87b63c12016-03-18 14:47:51 -0400563 diff = list(req.difference(obj))
564 if diff:
565 abort(403, _4034_msg % (
566 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500567
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 for p, v in request.parameter_list.iteritems():
569 self.app.property_handler.do_put(
570 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 def do_delete(self, path):
573 for bus_info in request.route_data['map'][path].iteritems():
574 if self.bus_missing_delete(path, *bus_info):
575 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500576
Brad Bishop87b63c12016-03-18 14:47:51 -0400577 for bus in request.route_data['map'][path].iterkeys():
578 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500579
Brad Bishop87b63c12016-03-18 14:47:51 -0400580 def bus_missing_delete(self, path, bus, interfaces):
581 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500582
Brad Bishop87b63c12016-03-18 14:47:51 -0400583 def delete_on_bus(self, path, bus):
584 obj = self.bus.get_object(bus, path, introspect=False)
585 delete_iface = dbus.Interface(
586 obj, dbus_interface=DELETE_IFACE)
587 delete_iface.Delete()
588
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500589
Brad Bishop2f428582015-12-02 10:56:11 -0500590class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400591 ''' Handles the /login and /logout routes, manages
592 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 rules = ['/login', '/logout']
595 login_str = "User '%s' logged %s"
596 bad_passwd_str = "Invalid username or password"
597 no_user_str = "No user logged in"
598 bad_json_str = "Expecting request format { 'data': " \
599 "[<username>, <password>] }, got '%s'"
600 _require_auth = None
601 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 def __init__(self, app, bus):
604 super(SessionHandler, self).__init__(
605 app, bus)
606 self.hmac_key = os.urandom(128)
607 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500608
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 @staticmethod
610 def authenticate(username, clear):
611 try:
612 encoded = spwd.getspnam(username)[1]
613 return encoded == crypt.crypt(clear, encoded)
614 except KeyError:
615 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500616
Brad Bishop87b63c12016-03-18 14:47:51 -0400617 def invalidate_session(self, session):
618 try:
619 self.session_store.remove(session)
620 except ValueError:
621 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500622
Brad Bishop87b63c12016-03-18 14:47:51 -0400623 def new_session(self):
624 sid = os.urandom(32)
625 if self.MAX_SESSIONS <= len(self.session_store):
626 self.session_store.pop()
627 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500628
Brad Bishop87b63c12016-03-18 14:47:51 -0400629 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500630
Brad Bishop87b63c12016-03-18 14:47:51 -0400631 def get_session(self, sid):
632 sids = [x['sid'] for x in self.session_store]
633 try:
634 return self.session_store[sids.index(sid)]
635 except ValueError:
636 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500637
Brad Bishop87b63c12016-03-18 14:47:51 -0400638 def get_session_from_cookie(self):
639 return self.get_session(
640 request.get_cookie(
641 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500642
Brad Bishop87b63c12016-03-18 14:47:51 -0400643 def do_post(self, **kw):
644 if request.path == '/login':
645 return self.do_login(**kw)
646 else:
647 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500648
Brad Bishop87b63c12016-03-18 14:47:51 -0400649 def do_logout(self, **kw):
650 session = self.get_session_from_cookie()
651 if session is not None:
652 user = session['user']
653 self.invalidate_session(session)
654 response.delete_cookie('sid')
655 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500656
Brad Bishop87b63c12016-03-18 14:47:51 -0400657 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500658
Brad Bishop87b63c12016-03-18 14:47:51 -0400659 def do_login(self, **kw):
660 session = self.get_session_from_cookie()
661 if session is not None:
662 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500663
Brad Bishop87b63c12016-03-18 14:47:51 -0400664 if len(request.parameter_list) != 2:
665 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500666
Brad Bishop87b63c12016-03-18 14:47:51 -0400667 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400668 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500669
Brad Bishop87b63c12016-03-18 14:47:51 -0400670 user = request.parameter_list[0]
671 session = self.new_session()
672 session['user'] = user
673 response.set_cookie(
674 'sid', session['sid'], secret=self.hmac_key,
675 secure=True,
676 httponly=True)
677 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500678
Brad Bishop87b63c12016-03-18 14:47:51 -0400679 def find(self, **kw):
680 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500681
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 def setup(self, **kw):
683 pass
684
Brad Bishop2f428582015-12-02 10:56:11 -0500685
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500686class ImageUploadUtils:
687 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500688
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500689 file_loc = '/tmp/images'
690 file_prefix = 'img'
691 file_suffix = ''
692
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500693 @classmethod
694 def do_upload(cls, filename=''):
695 if not os.path.exists(cls.file_loc):
696 os.makedirs(cls.file_loc)
697 if not filename:
698 handle, filename = tempfile.mkstemp(cls.file_suffix,
699 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500700 else:
701 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500702 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500703 try:
704 file_contents = request.body.read()
705 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500706 os.write(handle, file_contents)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500707 except (IOError, ValueError), e:
708 abort(400, str(e))
709 except:
710 abort(400, "Unexpected Error")
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500711 finally:
712 os.close(handle)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500713
714
715class ImagePostHandler(RouteHandler):
716 ''' Handles the /upload/image route. '''
717
718 verbs = ['POST']
719 rules = ['/upload/image']
720 content_type = 'application/octet-stream'
721
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500722 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500723 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500724 app, bus, self.verbs, self.rules, self.content_type)
725
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500726 def do_post(self, filename=''):
727 ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500728
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500729 def find(self, **kw):
730 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500731
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500732 def setup(self, **kw):
733 pass
734
735
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500736class EventNotifier:
737 keyNames = {}
738 keyNames['event'] = 'event'
739 keyNames['path'] = 'path'
740 keyNames['intfMap'] = 'interfaces'
741 keyNames['propMap'] = 'properties'
742 keyNames['intf'] = 'interface'
743
744 def __init__(self, wsock, filters):
745 self.wsock = wsock
746 self.paths = filters.get("paths", [])
747 self.interfaces = filters.get("interfaces", [])
748 if not self.paths:
749 self.paths.append(None)
750 bus = dbus.SystemBus()
751 # Add a signal receiver for every path the client is interested in
752 for path in self.paths:
753 bus.add_signal_receiver(
754 self.interfaces_added_handler,
755 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
756 signal_name='InterfacesAdded',
757 path=path)
758 bus.add_signal_receiver(
759 self.properties_changed_handler,
760 dbus_interface=dbus.PROPERTIES_IFACE,
761 signal_name='PropertiesChanged',
762 path=path,
763 path_keyword='path')
764 loop = gobject.MainLoop()
765 # gobject's mainloop.run() will block the entire process, so the gevent
766 # scheduler and hence greenlets won't execute. The while-loop below
767 # works around this limitation by using gevent's sleep, instead of
768 # calling loop.run()
769 gcontext = loop.get_context()
770 while loop is not None:
771 try:
772 if gcontext.pending():
773 gcontext.iteration()
774 else:
775 # gevent.sleep puts only the current greenlet to sleep,
776 # not the entire process.
777 gevent.sleep(5)
778 except WebSocketError:
779 break
780
781 def interfaces_added_handler(self, path, iprops, **kw):
782 ''' If the client is interested in these changes, respond to the
783 client. This handles d-bus interface additions.'''
784 if (not self.interfaces) or \
785 (not set(iprops).isdisjoint(self.interfaces)):
786 response = {}
787 response[self.keyNames['event']] = "InterfacesAdded"
788 response[self.keyNames['path']] = path
789 response[self.keyNames['intfMap']] = iprops
790 try:
791 self.wsock.send(json.dumps(response))
792 except WebSocketError:
793 return
794
795 def properties_changed_handler(self, interface, new, old, **kw):
796 ''' If the client is interested in these changes, respond to the
797 client. This handles d-bus property changes. '''
798 if (not self.interfaces) or (interface in self.interfaces):
799 path = str(kw['path'])
800 response = {}
801 response[self.keyNames['event']] = "PropertiesChanged"
802 response[self.keyNames['path']] = path
803 response[self.keyNames['intf']] = interface
804 response[self.keyNames['propMap']] = new
805 try:
806 self.wsock.send(json.dumps(response))
807 except WebSocketError:
808 return
809
810
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500811class EventHandler(RouteHandler):
812 ''' Handles the /subscribe route, for clients to be able
813 to subscribe to BMC events. '''
814
815 verbs = ['GET']
816 rules = ['/subscribe']
817
818 def __init__(self, app, bus):
819 super(EventHandler, self).__init__(
820 app, bus, self.verbs, self.rules)
821
822 def find(self, **kw):
823 pass
824
825 def setup(self, **kw):
826 pass
827
828 def do_get(self):
829 wsock = request.environ.get('wsgi.websocket')
830 if not wsock:
831 abort(400, 'Expected WebSocket request.')
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500832 filters = wsock.receive()
833 filters = json.loads(filters)
834 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500835
836
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500837class ImagePutHandler(RouteHandler):
838 ''' Handles the /upload/image/<filename> route. '''
839
840 verbs = ['PUT']
841 rules = ['/upload/image/<filename>']
842 content_type = 'application/octet-stream'
843
844 def __init__(self, app, bus):
845 super(ImagePutHandler, self).__init__(
846 app, bus, self.verbs, self.rules, self.content_type)
847
848 def do_put(self, filename=''):
849 ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500850
851 def find(self, **kw):
852 pass
853
854 def setup(self, **kw):
855 pass
856
857
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500858class DownloadDumpHandler(RouteHandler):
859 ''' Handles the /download/dump route. '''
860
861 verbs = 'GET'
862 rules = ['/download/dump/<dumpid>']
863 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -0500864 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -0400865 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500866
867 def __init__(self, app, bus):
868 super(DownloadDumpHandler, self).__init__(
869 app, bus, self.verbs, self.rules, self.content_type)
870
871 def do_get(self, dumpid):
872 return self.do_download(dumpid)
873
874 def find(self, **kw):
875 pass
876
877 def setup(self, **kw):
878 pass
879
880 def do_download(self, dumpid):
881 dump_loc = os.path.join(self.dump_loc, dumpid)
882 if not os.path.exists(dump_loc):
883 abort(404, "Path not found")
884
885 files = os.listdir(dump_loc)
886 num_files = len(files)
887 if num_files == 0:
888 abort(404, "Dump not found")
889
890 return static_file(os.path.basename(files[0]), root=dump_loc,
891 download=True, mimetype=self.content_type)
892
893
Brad Bishop2f428582015-12-02 10:56:11 -0500894class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400895 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500896
Brad Bishop87b63c12016-03-18 14:47:51 -0400897 name = 'authorization'
898 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500899
Brad Bishop87b63c12016-03-18 14:47:51 -0400900 class Compose:
901 def __init__(self, validators, callback, session_mgr):
902 self.validators = validators
903 self.callback = callback
904 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500905
Brad Bishop87b63c12016-03-18 14:47:51 -0400906 def __call__(self, *a, **kw):
907 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
908 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500909 if request.method != 'OPTIONS':
910 for x in self.validators:
911 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500912
Brad Bishop87b63c12016-03-18 14:47:51 -0400913 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500914
Brad Bishop87b63c12016-03-18 14:47:51 -0400915 def apply(self, callback, route):
916 undecorated = route.get_undecorated_callback()
917 if not isinstance(undecorated, RouteHandler):
918 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500919
Brad Bishop87b63c12016-03-18 14:47:51 -0400920 auth_types = getattr(
921 undecorated, '_require_auth', None)
922 if not auth_types:
923 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500924
Brad Bishop87b63c12016-03-18 14:47:51 -0400925 return self.Compose(
926 auth_types, callback, undecorated.app.session_handler)
927
Brad Bishop2f428582015-12-02 10:56:11 -0500928
Brad Bishopd0c404a2017-02-21 09:23:25 -0500929class CorsPlugin(object):
930 ''' Add CORS headers. '''
931
932 name = 'cors'
933 api = 2
934
935 @staticmethod
936 def process_origin():
937 origin = request.headers.get('Origin')
938 if origin:
939 response.add_header('Access-Control-Allow-Origin', origin)
940 response.add_header(
941 'Access-Control-Allow-Credentials', 'true')
942
943 @staticmethod
944 def process_method_and_headers(verbs):
945 method = request.headers.get('Access-Control-Request-Method')
946 headers = request.headers.get('Access-Control-Request-Headers')
947 if headers:
948 headers = [x.lower() for x in headers.split(',')]
949
950 if method in verbs \
951 and headers == ['content-type']:
952 response.add_header('Access-Control-Allow-Methods', method)
953 response.add_header(
954 'Access-Control-Allow-Headers', 'Content-Type')
955
956 def __init__(self, app):
957 app.install_error_callback(self.error_callback)
958
959 def apply(self, callback, route):
960 undecorated = route.get_undecorated_callback()
961 if not isinstance(undecorated, RouteHandler):
962 return callback
963
964 if not getattr(undecorated, '_enable_cors', None):
965 return callback
966
967 def wrap(*a, **kw):
968 self.process_origin()
969 self.process_method_and_headers(undecorated._verbs)
970 return callback(*a, **kw)
971
972 return wrap
973
974 def error_callback(self, **kw):
975 self.process_origin()
976
977
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500978class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400979 ''' Ensures request content satisfies the OpenBMC json api format. '''
980 name = 'json_api_request'
981 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500982
Brad Bishop87b63c12016-03-18 14:47:51 -0400983 error_str = "Expecting request format { 'data': <value> }, got '%s'"
984 type_error_str = "Unsupported Content-Type: '%s'"
985 json_type = "application/json"
986 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500987
Brad Bishop87b63c12016-03-18 14:47:51 -0400988 @staticmethod
989 def content_expected():
990 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500991
Brad Bishop87b63c12016-03-18 14:47:51 -0400992 def validate_request(self):
993 if request.content_length > 0 and \
994 request.content_type != self.json_type:
995 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500996
Brad Bishop87b63c12016-03-18 14:47:51 -0400997 try:
998 request.parameter_list = request.json.get('data')
999 except ValueError, e:
1000 abort(400, str(e))
1001 except (AttributeError, KeyError, TypeError):
1002 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001003
Brad Bishop87b63c12016-03-18 14:47:51 -04001004 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001005 content_type = getattr(
1006 route.get_undecorated_callback(), '_content_type', None)
1007 if self.json_type != content_type:
1008 return callback
1009
Brad Bishop87b63c12016-03-18 14:47:51 -04001010 verbs = getattr(
1011 route.get_undecorated_callback(), '_verbs', None)
1012 if verbs is None:
1013 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001014
Brad Bishop87b63c12016-03-18 14:47:51 -04001015 if not set(self.request_methods).intersection(verbs):
1016 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001017
Brad Bishop87b63c12016-03-18 14:47:51 -04001018 def wrap(*a, **kw):
1019 if self.content_expected():
1020 self.validate_request()
1021 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001022
Brad Bishop87b63c12016-03-18 14:47:51 -04001023 return wrap
1024
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001025
1026class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001027 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1028 name = 'json_api_method_request'
1029 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001030
Brad Bishop87b63c12016-03-18 14:47:51 -04001031 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001032 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001033
Brad Bishop87b63c12016-03-18 14:47:51 -04001034 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001035 content_type = getattr(
1036 route.get_undecorated_callback(), '_content_type', None)
1037 if self.json_type != content_type:
1038 return callback
1039
Brad Bishop87b63c12016-03-18 14:47:51 -04001040 request_type = getattr(
1041 route.get_undecorated_callback(), 'request_type', None)
1042 if request_type is None:
1043 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001044
Brad Bishop87b63c12016-03-18 14:47:51 -04001045 def validate_request():
1046 if not isinstance(request.parameter_list, request_type):
1047 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001048
Brad Bishop87b63c12016-03-18 14:47:51 -04001049 def wrap(*a, **kw):
1050 if JsonApiRequestPlugin.content_expected():
1051 validate_request()
1052 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001053
Brad Bishop87b63c12016-03-18 14:47:51 -04001054 return wrap
1055
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001056
Brad Bishop080a48e2017-02-21 22:34:43 -05001057class JsonErrorsPlugin(JSONPlugin):
1058 ''' Extend the Bottle JSONPlugin such that it also encodes error
1059 responses. '''
1060
1061 def __init__(self, app, **kw):
1062 super(JsonErrorsPlugin, self).__init__(**kw)
1063 self.json_opts = {
1064 x: y for x, y in kw.iteritems()
1065 if x in ['indent', 'sort_keys']}
1066 app.install_error_callback(self.error_callback)
1067
1068 def error_callback(self, response_object, response_body, **kw):
1069 response_body['body'] = json.dumps(response_object, **self.json_opts)
1070 response.content_type = 'application/json'
1071
1072
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001073class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001074 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001075 name = 'json_api_response'
1076 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001077
Brad Bishopd4c1c552017-02-21 00:07:28 -05001078 @staticmethod
1079 def has_body():
1080 return request.method not in ['OPTIONS']
1081
Brad Bishop080a48e2017-02-21 22:34:43 -05001082 def __init__(self, app):
1083 app.install_error_callback(self.error_callback)
1084
Brad Bishop87b63c12016-03-18 14:47:51 -04001085 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001086 skip = getattr(
1087 route.get_undecorated_callback(), 'suppress_json_resp', None)
1088 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001089 return callback
1090
Brad Bishop87b63c12016-03-18 14:47:51 -04001091 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001092 data = callback(*a, **kw)
1093 if self.has_body():
1094 resp = {'data': data}
1095 resp['status'] = 'ok'
1096 resp['message'] = response.status_line
1097 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001098 return wrap
1099
Brad Bishop080a48e2017-02-21 22:34:43 -05001100 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001101 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001102 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001103 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001104 if error.status_code == 500:
1105 response_object['data']['exception'] = repr(error.exception)
1106 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001107
Brad Bishop87b63c12016-03-18 14:47:51 -04001108
Brad Bishop080a48e2017-02-21 22:34:43 -05001109class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001110 ''' Json javascript wrapper. '''
1111 name = 'jsonp'
1112 api = 2
1113
Brad Bishop080a48e2017-02-21 22:34:43 -05001114 def __init__(self, app, **kw):
1115 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001116
1117 @staticmethod
1118 def to_jsonp(json):
1119 jwrapper = request.query.callback or None
1120 if(jwrapper):
1121 response.set_header('Content-Type', 'application/javascript')
1122 json = jwrapper + '(' + json + ');'
1123 return json
1124
1125 def apply(self, callback, route):
1126 def wrap(*a, **kw):
1127 return self.to_jsonp(callback(*a, **kw))
1128 return wrap
1129
Brad Bishop080a48e2017-02-21 22:34:43 -05001130 def error_callback(self, response_body, **kw):
1131 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001132
1133
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001134class ContentCheckerPlugin(object):
1135 ''' Ensures that a route is associated with the expected content-type
1136 header. '''
1137 name = 'content_checker'
1138 api = 2
1139
1140 class Checker:
1141 def __init__(self, type, callback):
1142 self.expected_type = type
1143 self.callback = callback
1144 self.error_str = "Expecting content type '%s', got '%s'"
1145
1146 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001147 if request.method in ['PUT', 'POST', 'PATCH'] and \
1148 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001149 self.expected_type != request.content_type:
1150 abort(415, self.error_str % (self.expected_type,
1151 request.content_type))
1152
1153 return self.callback(*a, **kw)
1154
1155 def apply(self, callback, route):
1156 content_type = getattr(
1157 route.get_undecorated_callback(), '_content_type', None)
1158
1159 return self.Checker(content_type, callback)
1160
1161
Brad Bishop2c6fc762016-08-29 15:53:25 -04001162class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001163 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001164 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001165
1166 self.have_wsock = kw.get('have_wsock', False)
1167
Brad Bishop2ddfa002016-08-29 15:11:55 -04001168 self.bus = dbus.SystemBus()
1169 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001170 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001171
Brad Bishop87b63c12016-03-18 14:47:51 -04001172 self.install_hooks()
1173 self.install_plugins()
1174 self.create_handlers()
1175 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001176
Brad Bishop87b63c12016-03-18 14:47:51 -04001177 def install_plugins(self):
1178 # install json api plugins
1179 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001180 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001181 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001182 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001183 self.install(JsonpPlugin(self, **json_kw))
1184 self.install(JsonErrorsPlugin(self, **json_kw))
1185 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001186 self.install(JsonApiRequestPlugin())
1187 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001188
Brad Bishop87b63c12016-03-18 14:47:51 -04001189 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001190 self.error_handler_type = type(self.default_error_handler)
1191 self.original_error_handler = self.default_error_handler
1192 self.default_error_handler = self.error_handler_type(
1193 self.custom_error_handler, self, Bottle)
1194
Brad Bishop87b63c12016-03-18 14:47:51 -04001195 self.real_router_match = self.router.match
1196 self.router.match = self.custom_router_match
1197 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001198
Brad Bishop87b63c12016-03-18 14:47:51 -04001199 def create_handlers(self):
1200 # create route handlers
1201 self.session_handler = SessionHandler(self, self.bus)
1202 self.directory_handler = DirectoryHandler(self, self.bus)
1203 self.list_names_handler = ListNamesHandler(self, self.bus)
1204 self.list_handler = ListHandler(self, self.bus)
1205 self.method_handler = MethodHandler(self, self.bus)
1206 self.property_handler = PropertyHandler(self, self.bus)
1207 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001208 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1209 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001210 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001211 if self.have_wsock:
1212 self.event_handler = EventHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001213 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001214
Brad Bishop87b63c12016-03-18 14:47:51 -04001215 def install_handlers(self):
1216 self.session_handler.install()
1217 self.directory_handler.install()
1218 self.list_names_handler.install()
1219 self.list_handler.install()
1220 self.method_handler.install()
1221 self.property_handler.install()
1222 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001223 self.image_upload_post_handler.install()
1224 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001225 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001226 if self.have_wsock:
1227 self.event_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001228 # this has to come last, since it matches everything
1229 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001230
Brad Bishop080a48e2017-02-21 22:34:43 -05001231 def install_error_callback(self, callback):
1232 self.error_callbacks.insert(0, callback)
1233
Brad Bishop87b63c12016-03-18 14:47:51 -04001234 def custom_router_match(self, environ):
1235 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1236 needed doesn't work for us since the instance rules match
1237 everything. This monkey-patch lets the route handler figure
1238 out which response is needed. This could be accomplished
1239 with a hook but that would require calling the router match
1240 function twice.
1241 '''
1242 route, args = self.real_router_match(environ)
1243 if isinstance(route.callback, RouteHandler):
1244 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001245
Brad Bishop87b63c12016-03-18 14:47:51 -04001246 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001247
Brad Bishop080a48e2017-02-21 22:34:43 -05001248 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001249 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001250 error handler. '''
1251
1252 response_object = {}
1253 response_body = {}
1254 for x in self.error_callbacks:
1255 x(error=error,
1256 response_object=response_object,
1257 response_body=response_body)
1258
1259 return response_body.get('body', "")
1260
Brad Bishop87b63c12016-03-18 14:47:51 -04001261 @staticmethod
1262 def strip_extra_slashes():
1263 path = request.environ['PATH_INFO']
1264 trailing = ("", "/")[path[-1] == '/']
1265 parts = filter(bool, path.split('/'))
1266 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing