blob: 3f84dddcb5258c3e2f28f3601841f7b06cbc09a5 [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):
331 busses = self.try_mapper_call(
332 self.mapper.get_object, path=path)
333 for items in busses.iteritems():
334 m = self.find_method_on_bus(path, method, *items)
335 if m:
336 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400337
Brad Bishop87b63c12016-03-18 14:47:51 -0400338 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400339
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 def setup(self, path, method):
341 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def do_post(self, path, method):
344 try:
345 if request.parameter_list:
346 return request.route_data['method'](*request.parameter_list)
347 else:
348 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400349
Brad Bishop87b63c12016-03-18 14:47:51 -0400350 except dbus.exceptions.DBusException, e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530351 paramlist = []
Brad Bishop87b63c12016-03-18 14:47:51 -0400352 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530353
354 signature_list = get_method_signature(self.bus, self.service,
355 path, self.interface,
356 method)
357 if not signature_list:
358 abort(400, "Failed to get method signature: %s" % str(e))
359 if len(signature_list) != len(request.parameter_list):
360 abort(400, "Invalid number of args")
361 converted_value = None
362 try:
363 for index, expected_type in enumerate(signature_list):
364 value = request.parameter_list[index]
365 converted_value = convert_type(expected_type, value)
366 paramlist.append(converted_value)
367 request.parameter_list = paramlist
368 self.do_post(path, method)
369 return
370 except Exception as ex:
371 abort(400, "Failed to convert the types")
Brad Bishop87b63c12016-03-18 14:47:51 -0400372 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530373
Brad Bishop87b63c12016-03-18 14:47:51 -0400374 if e.get_dbus_name() == DBUS_TYPE_ERROR:
375 abort(400, str(e))
376 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 @staticmethod
379 def find_method_in_interface(method, obj, interface, methods):
380 if methods is None:
381 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400382
Brad Bishop6d190602016-04-15 13:09:39 -0400383 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400384 if method is not None:
385 iface = dbus.Interface(obj, interface)
386 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400387
Brad Bishop87b63c12016-03-18 14:47:51 -0400388 def find_method_on_bus(self, path, method, bus, interfaces):
389 obj = self.bus.get_object(bus, path, introspect=False)
390 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
391 data = iface.Introspect()
392 parser = IntrospectionNodeParser(
393 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500394 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400395 for x, y in parser.get_interfaces().iteritems():
396 m = self.find_method_in_interface(
397 method, obj, x, y.get('method'))
398 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530399 self.service = bus
400 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400401 return m
402
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400403
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500404class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400405 verbs = ['PUT', 'GET']
406 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500407 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400408
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 def __init__(self, app, bus):
410 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500411 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 def find(self, path, prop):
414 self.app.instance_handler.setup(path)
415 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500416 real_name = obmc.utils.misc.find_case_insensitive(
417 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400418
Brad Bishop56ad87f2017-02-21 23:33:29 -0500419 if not real_name:
420 if request.method == 'PUT':
421 abort(403, _4034_msg % ('property', 'created', prop))
422 else:
423 abort(404, _4034_msg % ('property', 'found', prop))
424 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500425
Brad Bishop87b63c12016-03-18 14:47:51 -0400426 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500427 name, obj = self.find(path, prop)
428 request.route_data['obj'] = obj
429 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500430
Brad Bishop87b63c12016-03-18 14:47:51 -0400431 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500432 name = request.route_data['name']
433 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500434
Brad Bishop87b63c12016-03-18 14:47:51 -0400435 def do_put(self, path, prop, value=None):
436 if value is None:
437 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500438
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 prop, iface, properties_iface = self.get_host_interface(
440 path, prop, request.route_data['map'][path])
441 try:
442 properties_iface.Set(iface, prop, value)
443 except ValueError, e:
444 abort(400, str(e))
445 except dbus.exceptions.DBusException, e:
446 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500447 bus_name = properties_iface.bus_name
448 expected_type = get_type_signature_by_introspection(self.bus,
449 bus_name,
450 path,
451 prop)
452 if not expected_type:
453 abort(403, "Failed to get expected type: %s" % str(e))
454 converted_value = None
455 try:
456 converted_value = convert_type(expected_type, value)
457 self.do_put(path, prop, converted_value)
458 return
459 except Exception as ex:
460 abort(403, "Failed to convert %s to type %s" %
461 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 abort(403, str(e))
463 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def get_host_interface(self, path, prop, bus_info):
466 for bus, interfaces in bus_info.iteritems():
467 obj = self.bus.get_object(bus, path, introspect=True)
468 properties_iface = dbus.Interface(
469 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500470
Brad Bishop87b63c12016-03-18 14:47:51 -0400471 info = self.get_host_interface_on_bus(
472 path, prop, properties_iface, bus, interfaces)
473 if info is not None:
474 prop, iface = info
475 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500476
Brad Bishop87b63c12016-03-18 14:47:51 -0400477 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
478 for i in interfaces:
479 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500480 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400481 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500482 match = obmc.utils.misc.find_case_insensitive(
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500483 prop, properties.keys())
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500484 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400485 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500486 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400487 return prop, i
488
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500489
Brad Bishop2503bd62015-12-16 17:56:12 -0500490class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 verbs = ['GET']
492 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500493
Brad Bishop87b63c12016-03-18 14:47:51 -0400494 def __init__(self, app, bus):
495 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400496 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500497
Brad Bishop87b63c12016-03-18 14:47:51 -0400498 def find(self, path):
499 return self.try_mapper_call(
500 self.mapper.get_object,
501 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500502
Brad Bishop87b63c12016-03-18 14:47:51 -0400503 def setup(self, path):
504 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500505
Brad Bishop87b63c12016-03-18 14:47:51 -0400506 def do_get(self, path):
507 schema = {}
508 for x in request.route_data['map'].iterkeys():
509 obj = self.bus.get_object(x, path, introspect=False)
510 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
511 data = iface.Introspect()
512 parser = IntrospectionNodeParser(
513 ElementTree.fromstring(data))
514 for x, y in parser.get_interfaces().iteritems():
515 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500516
Brad Bishop87b63c12016-03-18 14:47:51 -0400517 return schema
518
Brad Bishop2503bd62015-12-16 17:56:12 -0500519
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500520class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 verbs = ['GET', 'PUT', 'DELETE']
522 rules = '<path:path>'
523 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500524
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 def __init__(self, app, bus):
526 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400527 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 def find(self, path, callback=None):
530 return {path: self.try_mapper_call(
531 self.mapper.get_object,
532 callback,
533 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500534
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 def setup(self, path):
536 callback = None
537 if request.method == 'PUT':
538 def callback(e, **kw):
539 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500540
Brad Bishop87b63c12016-03-18 14:47:51 -0400541 if request.route_data.get('map') is None:
542 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500543
Brad Bishop87b63c12016-03-18 14:47:51 -0400544 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400545 return self.mapper.enumerate_object(
546 path,
547 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500548
Brad Bishop87b63c12016-03-18 14:47:51 -0400549 def do_put(self, path):
550 # make sure all properties exist in the request
551 obj = set(self.do_get(path).keys())
552 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 diff = list(obj.difference(req))
555 if diff:
556 abort(403, _4034_msg % (
557 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 diff = list(req.difference(obj))
560 if diff:
561 abort(403, _4034_msg % (
562 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 for p, v in request.parameter_list.iteritems():
565 self.app.property_handler.do_put(
566 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500567
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 def do_delete(self, path):
569 for bus_info in request.route_data['map'][path].iteritems():
570 if self.bus_missing_delete(path, *bus_info):
571 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500572
Brad Bishop87b63c12016-03-18 14:47:51 -0400573 for bus in request.route_data['map'][path].iterkeys():
574 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 def bus_missing_delete(self, path, bus, interfaces):
577 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 def delete_on_bus(self, path, bus):
580 obj = self.bus.get_object(bus, path, introspect=False)
581 delete_iface = dbus.Interface(
582 obj, dbus_interface=DELETE_IFACE)
583 delete_iface.Delete()
584
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500585
Brad Bishop2f428582015-12-02 10:56:11 -0500586class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400587 ''' Handles the /login and /logout routes, manages
588 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500589
Brad Bishop87b63c12016-03-18 14:47:51 -0400590 rules = ['/login', '/logout']
591 login_str = "User '%s' logged %s"
592 bad_passwd_str = "Invalid username or password"
593 no_user_str = "No user logged in"
594 bad_json_str = "Expecting request format { 'data': " \
595 "[<username>, <password>] }, got '%s'"
596 _require_auth = None
597 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500598
Brad Bishop87b63c12016-03-18 14:47:51 -0400599 def __init__(self, app, bus):
600 super(SessionHandler, self).__init__(
601 app, bus)
602 self.hmac_key = os.urandom(128)
603 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500604
Brad Bishop87b63c12016-03-18 14:47:51 -0400605 @staticmethod
606 def authenticate(username, clear):
607 try:
608 encoded = spwd.getspnam(username)[1]
609 return encoded == crypt.crypt(clear, encoded)
610 except KeyError:
611 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 def invalidate_session(self, session):
614 try:
615 self.session_store.remove(session)
616 except ValueError:
617 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500618
Brad Bishop87b63c12016-03-18 14:47:51 -0400619 def new_session(self):
620 sid = os.urandom(32)
621 if self.MAX_SESSIONS <= len(self.session_store):
622 self.session_store.pop()
623 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 def get_session(self, sid):
628 sids = [x['sid'] for x in self.session_store]
629 try:
630 return self.session_store[sids.index(sid)]
631 except ValueError:
632 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500633
Brad Bishop87b63c12016-03-18 14:47:51 -0400634 def get_session_from_cookie(self):
635 return self.get_session(
636 request.get_cookie(
637 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500638
Brad Bishop87b63c12016-03-18 14:47:51 -0400639 def do_post(self, **kw):
640 if request.path == '/login':
641 return self.do_login(**kw)
642 else:
643 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500644
Brad Bishop87b63c12016-03-18 14:47:51 -0400645 def do_logout(self, **kw):
646 session = self.get_session_from_cookie()
647 if session is not None:
648 user = session['user']
649 self.invalidate_session(session)
650 response.delete_cookie('sid')
651 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500652
Brad Bishop87b63c12016-03-18 14:47:51 -0400653 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500654
Brad Bishop87b63c12016-03-18 14:47:51 -0400655 def do_login(self, **kw):
656 session = self.get_session_from_cookie()
657 if session is not None:
658 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500659
Brad Bishop87b63c12016-03-18 14:47:51 -0400660 if len(request.parameter_list) != 2:
661 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500662
Brad Bishop87b63c12016-03-18 14:47:51 -0400663 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400664 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500665
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 user = request.parameter_list[0]
667 session = self.new_session()
668 session['user'] = user
669 response.set_cookie(
670 'sid', session['sid'], secret=self.hmac_key,
671 secure=True,
672 httponly=True)
673 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500674
Brad Bishop87b63c12016-03-18 14:47:51 -0400675 def find(self, **kw):
676 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500677
Brad Bishop87b63c12016-03-18 14:47:51 -0400678 def setup(self, **kw):
679 pass
680
Brad Bishop2f428582015-12-02 10:56:11 -0500681
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500682class ImageUploadUtils:
683 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500684
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500685 file_loc = '/tmp/images'
686 file_prefix = 'img'
687 file_suffix = ''
688
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500689 @classmethod
690 def do_upload(cls, filename=''):
691 if not os.path.exists(cls.file_loc):
692 os.makedirs(cls.file_loc)
693 if not filename:
694 handle, filename = tempfile.mkstemp(cls.file_suffix,
695 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500696 else:
697 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500698 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500699 try:
700 file_contents = request.body.read()
701 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500702 os.write(handle, file_contents)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500703 except (IOError, ValueError), e:
704 abort(400, str(e))
705 except:
706 abort(400, "Unexpected Error")
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500707 finally:
708 os.close(handle)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500709
710
711class ImagePostHandler(RouteHandler):
712 ''' Handles the /upload/image route. '''
713
714 verbs = ['POST']
715 rules = ['/upload/image']
716 content_type = 'application/octet-stream'
717
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500718 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500719 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500720 app, bus, self.verbs, self.rules, self.content_type)
721
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500722 def do_post(self, filename=''):
723 ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500724
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500725 def find(self, **kw):
726 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500727
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500728 def setup(self, **kw):
729 pass
730
731
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500732class EventNotifier:
733 keyNames = {}
734 keyNames['event'] = 'event'
735 keyNames['path'] = 'path'
736 keyNames['intfMap'] = 'interfaces'
737 keyNames['propMap'] = 'properties'
738 keyNames['intf'] = 'interface'
739
740 def __init__(self, wsock, filters):
741 self.wsock = wsock
742 self.paths = filters.get("paths", [])
743 self.interfaces = filters.get("interfaces", [])
744 if not self.paths:
745 self.paths.append(None)
746 bus = dbus.SystemBus()
747 # Add a signal receiver for every path the client is interested in
748 for path in self.paths:
749 bus.add_signal_receiver(
750 self.interfaces_added_handler,
751 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
752 signal_name='InterfacesAdded',
753 path=path)
754 bus.add_signal_receiver(
755 self.properties_changed_handler,
756 dbus_interface=dbus.PROPERTIES_IFACE,
757 signal_name='PropertiesChanged',
758 path=path,
759 path_keyword='path')
760 loop = gobject.MainLoop()
761 # gobject's mainloop.run() will block the entire process, so the gevent
762 # scheduler and hence greenlets won't execute. The while-loop below
763 # works around this limitation by using gevent's sleep, instead of
764 # calling loop.run()
765 gcontext = loop.get_context()
766 while loop is not None:
767 try:
768 if gcontext.pending():
769 gcontext.iteration()
770 else:
771 # gevent.sleep puts only the current greenlet to sleep,
772 # not the entire process.
773 gevent.sleep(5)
774 except WebSocketError:
775 break
776
777 def interfaces_added_handler(self, path, iprops, **kw):
778 ''' If the client is interested in these changes, respond to the
779 client. This handles d-bus interface additions.'''
780 if (not self.interfaces) or \
781 (not set(iprops).isdisjoint(self.interfaces)):
782 response = {}
783 response[self.keyNames['event']] = "InterfacesAdded"
784 response[self.keyNames['path']] = path
785 response[self.keyNames['intfMap']] = iprops
786 try:
787 self.wsock.send(json.dumps(response))
788 except WebSocketError:
789 return
790
791 def properties_changed_handler(self, interface, new, old, **kw):
792 ''' If the client is interested in these changes, respond to the
793 client. This handles d-bus property changes. '''
794 if (not self.interfaces) or (interface in self.interfaces):
795 path = str(kw['path'])
796 response = {}
797 response[self.keyNames['event']] = "PropertiesChanged"
798 response[self.keyNames['path']] = path
799 response[self.keyNames['intf']] = interface
800 response[self.keyNames['propMap']] = new
801 try:
802 self.wsock.send(json.dumps(response))
803 except WebSocketError:
804 return
805
806
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500807class EventHandler(RouteHandler):
808 ''' Handles the /subscribe route, for clients to be able
809 to subscribe to BMC events. '''
810
811 verbs = ['GET']
812 rules = ['/subscribe']
813
814 def __init__(self, app, bus):
815 super(EventHandler, self).__init__(
816 app, bus, self.verbs, self.rules)
817
818 def find(self, **kw):
819 pass
820
821 def setup(self, **kw):
822 pass
823
824 def do_get(self):
825 wsock = request.environ.get('wsgi.websocket')
826 if not wsock:
827 abort(400, 'Expected WebSocket request.')
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500828 filters = wsock.receive()
829 filters = json.loads(filters)
830 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500831
832
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500833class ImagePutHandler(RouteHandler):
834 ''' Handles the /upload/image/<filename> route. '''
835
836 verbs = ['PUT']
837 rules = ['/upload/image/<filename>']
838 content_type = 'application/octet-stream'
839
840 def __init__(self, app, bus):
841 super(ImagePutHandler, self).__init__(
842 app, bus, self.verbs, self.rules, self.content_type)
843
844 def do_put(self, filename=''):
845 ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500846
847 def find(self, **kw):
848 pass
849
850 def setup(self, **kw):
851 pass
852
853
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500854class DownloadDumpHandler(RouteHandler):
855 ''' Handles the /download/dump route. '''
856
857 verbs = 'GET'
858 rules = ['/download/dump/<dumpid>']
859 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -0500860 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -0400861 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -0500862
863 def __init__(self, app, bus):
864 super(DownloadDumpHandler, self).__init__(
865 app, bus, self.verbs, self.rules, self.content_type)
866
867 def do_get(self, dumpid):
868 return self.do_download(dumpid)
869
870 def find(self, **kw):
871 pass
872
873 def setup(self, **kw):
874 pass
875
876 def do_download(self, dumpid):
877 dump_loc = os.path.join(self.dump_loc, dumpid)
878 if not os.path.exists(dump_loc):
879 abort(404, "Path not found")
880
881 files = os.listdir(dump_loc)
882 num_files = len(files)
883 if num_files == 0:
884 abort(404, "Dump not found")
885
886 return static_file(os.path.basename(files[0]), root=dump_loc,
887 download=True, mimetype=self.content_type)
888
889
Brad Bishop2f428582015-12-02 10:56:11 -0500890class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400891 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500892
Brad Bishop87b63c12016-03-18 14:47:51 -0400893 name = 'authorization'
894 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500895
Brad Bishop87b63c12016-03-18 14:47:51 -0400896 class Compose:
897 def __init__(self, validators, callback, session_mgr):
898 self.validators = validators
899 self.callback = callback
900 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500901
Brad Bishop87b63c12016-03-18 14:47:51 -0400902 def __call__(self, *a, **kw):
903 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
904 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500905 if request.method != 'OPTIONS':
906 for x in self.validators:
907 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500908
Brad Bishop87b63c12016-03-18 14:47:51 -0400909 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500910
Brad Bishop87b63c12016-03-18 14:47:51 -0400911 def apply(self, callback, route):
912 undecorated = route.get_undecorated_callback()
913 if not isinstance(undecorated, RouteHandler):
914 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500915
Brad Bishop87b63c12016-03-18 14:47:51 -0400916 auth_types = getattr(
917 undecorated, '_require_auth', None)
918 if not auth_types:
919 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500920
Brad Bishop87b63c12016-03-18 14:47:51 -0400921 return self.Compose(
922 auth_types, callback, undecorated.app.session_handler)
923
Brad Bishop2f428582015-12-02 10:56:11 -0500924
Brad Bishopd0c404a2017-02-21 09:23:25 -0500925class CorsPlugin(object):
926 ''' Add CORS headers. '''
927
928 name = 'cors'
929 api = 2
930
931 @staticmethod
932 def process_origin():
933 origin = request.headers.get('Origin')
934 if origin:
935 response.add_header('Access-Control-Allow-Origin', origin)
936 response.add_header(
937 'Access-Control-Allow-Credentials', 'true')
938
939 @staticmethod
940 def process_method_and_headers(verbs):
941 method = request.headers.get('Access-Control-Request-Method')
942 headers = request.headers.get('Access-Control-Request-Headers')
943 if headers:
944 headers = [x.lower() for x in headers.split(',')]
945
946 if method in verbs \
947 and headers == ['content-type']:
948 response.add_header('Access-Control-Allow-Methods', method)
949 response.add_header(
950 'Access-Control-Allow-Headers', 'Content-Type')
951
952 def __init__(self, app):
953 app.install_error_callback(self.error_callback)
954
955 def apply(self, callback, route):
956 undecorated = route.get_undecorated_callback()
957 if not isinstance(undecorated, RouteHandler):
958 return callback
959
960 if not getattr(undecorated, '_enable_cors', None):
961 return callback
962
963 def wrap(*a, **kw):
964 self.process_origin()
965 self.process_method_and_headers(undecorated._verbs)
966 return callback(*a, **kw)
967
968 return wrap
969
970 def error_callback(self, **kw):
971 self.process_origin()
972
973
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500974class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400975 ''' Ensures request content satisfies the OpenBMC json api format. '''
976 name = 'json_api_request'
977 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500978
Brad Bishop87b63c12016-03-18 14:47:51 -0400979 error_str = "Expecting request format { 'data': <value> }, got '%s'"
980 type_error_str = "Unsupported Content-Type: '%s'"
981 json_type = "application/json"
982 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500983
Brad Bishop87b63c12016-03-18 14:47:51 -0400984 @staticmethod
985 def content_expected():
986 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500987
Brad Bishop87b63c12016-03-18 14:47:51 -0400988 def validate_request(self):
989 if request.content_length > 0 and \
990 request.content_type != self.json_type:
991 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500992
Brad Bishop87b63c12016-03-18 14:47:51 -0400993 try:
994 request.parameter_list = request.json.get('data')
995 except ValueError, e:
996 abort(400, str(e))
997 except (AttributeError, KeyError, TypeError):
998 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500999
Brad Bishop87b63c12016-03-18 14:47:51 -04001000 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001001 content_type = getattr(
1002 route.get_undecorated_callback(), '_content_type', None)
1003 if self.json_type != content_type:
1004 return callback
1005
Brad Bishop87b63c12016-03-18 14:47:51 -04001006 verbs = getattr(
1007 route.get_undecorated_callback(), '_verbs', None)
1008 if verbs is None:
1009 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001010
Brad Bishop87b63c12016-03-18 14:47:51 -04001011 if not set(self.request_methods).intersection(verbs):
1012 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001013
Brad Bishop87b63c12016-03-18 14:47:51 -04001014 def wrap(*a, **kw):
1015 if self.content_expected():
1016 self.validate_request()
1017 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001018
Brad Bishop87b63c12016-03-18 14:47:51 -04001019 return wrap
1020
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001021
1022class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001023 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1024 name = 'json_api_method_request'
1025 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001026
Brad Bishop87b63c12016-03-18 14:47:51 -04001027 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001028 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001029
Brad Bishop87b63c12016-03-18 14:47:51 -04001030 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001031 content_type = getattr(
1032 route.get_undecorated_callback(), '_content_type', None)
1033 if self.json_type != content_type:
1034 return callback
1035
Brad Bishop87b63c12016-03-18 14:47:51 -04001036 request_type = getattr(
1037 route.get_undecorated_callback(), 'request_type', None)
1038 if request_type is None:
1039 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001040
Brad Bishop87b63c12016-03-18 14:47:51 -04001041 def validate_request():
1042 if not isinstance(request.parameter_list, request_type):
1043 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001044
Brad Bishop87b63c12016-03-18 14:47:51 -04001045 def wrap(*a, **kw):
1046 if JsonApiRequestPlugin.content_expected():
1047 validate_request()
1048 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001049
Brad Bishop87b63c12016-03-18 14:47:51 -04001050 return wrap
1051
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001052
Brad Bishop080a48e2017-02-21 22:34:43 -05001053class JsonErrorsPlugin(JSONPlugin):
1054 ''' Extend the Bottle JSONPlugin such that it also encodes error
1055 responses. '''
1056
1057 def __init__(self, app, **kw):
1058 super(JsonErrorsPlugin, self).__init__(**kw)
1059 self.json_opts = {
1060 x: y for x, y in kw.iteritems()
1061 if x in ['indent', 'sort_keys']}
1062 app.install_error_callback(self.error_callback)
1063
1064 def error_callback(self, response_object, response_body, **kw):
1065 response_body['body'] = json.dumps(response_object, **self.json_opts)
1066 response.content_type = 'application/json'
1067
1068
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001069class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001070 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001071 name = 'json_api_response'
1072 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001073
Brad Bishopd4c1c552017-02-21 00:07:28 -05001074 @staticmethod
1075 def has_body():
1076 return request.method not in ['OPTIONS']
1077
Brad Bishop080a48e2017-02-21 22:34:43 -05001078 def __init__(self, app):
1079 app.install_error_callback(self.error_callback)
1080
Brad Bishop87b63c12016-03-18 14:47:51 -04001081 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001082 skip = getattr(
1083 route.get_undecorated_callback(), 'suppress_json_resp', None)
1084 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001085 return callback
1086
Brad Bishop87b63c12016-03-18 14:47:51 -04001087 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001088 data = callback(*a, **kw)
1089 if self.has_body():
1090 resp = {'data': data}
1091 resp['status'] = 'ok'
1092 resp['message'] = response.status_line
1093 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001094 return wrap
1095
Brad Bishop080a48e2017-02-21 22:34:43 -05001096 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001097 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001098 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001099 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001100 if error.status_code == 500:
1101 response_object['data']['exception'] = repr(error.exception)
1102 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001103
Brad Bishop87b63c12016-03-18 14:47:51 -04001104
Brad Bishop080a48e2017-02-21 22:34:43 -05001105class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001106 ''' Json javascript wrapper. '''
1107 name = 'jsonp'
1108 api = 2
1109
Brad Bishop080a48e2017-02-21 22:34:43 -05001110 def __init__(self, app, **kw):
1111 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001112
1113 @staticmethod
1114 def to_jsonp(json):
1115 jwrapper = request.query.callback or None
1116 if(jwrapper):
1117 response.set_header('Content-Type', 'application/javascript')
1118 json = jwrapper + '(' + json + ');'
1119 return json
1120
1121 def apply(self, callback, route):
1122 def wrap(*a, **kw):
1123 return self.to_jsonp(callback(*a, **kw))
1124 return wrap
1125
Brad Bishop080a48e2017-02-21 22:34:43 -05001126 def error_callback(self, response_body, **kw):
1127 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001128
1129
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001130class ContentCheckerPlugin(object):
1131 ''' Ensures that a route is associated with the expected content-type
1132 header. '''
1133 name = 'content_checker'
1134 api = 2
1135
1136 class Checker:
1137 def __init__(self, type, callback):
1138 self.expected_type = type
1139 self.callback = callback
1140 self.error_str = "Expecting content type '%s', got '%s'"
1141
1142 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001143 if request.method in ['PUT', 'POST', 'PATCH'] and \
1144 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001145 self.expected_type != request.content_type:
1146 abort(415, self.error_str % (self.expected_type,
1147 request.content_type))
1148
1149 return self.callback(*a, **kw)
1150
1151 def apply(self, callback, route):
1152 content_type = getattr(
1153 route.get_undecorated_callback(), '_content_type', None)
1154
1155 return self.Checker(content_type, callback)
1156
1157
Brad Bishop2c6fc762016-08-29 15:53:25 -04001158class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001159 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001160 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001161
1162 self.have_wsock = kw.get('have_wsock', False)
1163
Brad Bishop2ddfa002016-08-29 15:11:55 -04001164 self.bus = dbus.SystemBus()
1165 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001166 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001167
Brad Bishop87b63c12016-03-18 14:47:51 -04001168 self.install_hooks()
1169 self.install_plugins()
1170 self.create_handlers()
1171 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001172
Brad Bishop87b63c12016-03-18 14:47:51 -04001173 def install_plugins(self):
1174 # install json api plugins
1175 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001176 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001177 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001178 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001179 self.install(JsonpPlugin(self, **json_kw))
1180 self.install(JsonErrorsPlugin(self, **json_kw))
1181 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001182 self.install(JsonApiRequestPlugin())
1183 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001184
Brad Bishop87b63c12016-03-18 14:47:51 -04001185 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001186 self.error_handler_type = type(self.default_error_handler)
1187 self.original_error_handler = self.default_error_handler
1188 self.default_error_handler = self.error_handler_type(
1189 self.custom_error_handler, self, Bottle)
1190
Brad Bishop87b63c12016-03-18 14:47:51 -04001191 self.real_router_match = self.router.match
1192 self.router.match = self.custom_router_match
1193 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001194
Brad Bishop87b63c12016-03-18 14:47:51 -04001195 def create_handlers(self):
1196 # create route handlers
1197 self.session_handler = SessionHandler(self, self.bus)
1198 self.directory_handler = DirectoryHandler(self, self.bus)
1199 self.list_names_handler = ListNamesHandler(self, self.bus)
1200 self.list_handler = ListHandler(self, self.bus)
1201 self.method_handler = MethodHandler(self, self.bus)
1202 self.property_handler = PropertyHandler(self, self.bus)
1203 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001204 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1205 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001206 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001207 if self.have_wsock:
1208 self.event_handler = EventHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001209 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001210
Brad Bishop87b63c12016-03-18 14:47:51 -04001211 def install_handlers(self):
1212 self.session_handler.install()
1213 self.directory_handler.install()
1214 self.list_names_handler.install()
1215 self.list_handler.install()
1216 self.method_handler.install()
1217 self.property_handler.install()
1218 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001219 self.image_upload_post_handler.install()
1220 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001221 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001222 if self.have_wsock:
1223 self.event_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001224 # this has to come last, since it matches everything
1225 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001226
Brad Bishop080a48e2017-02-21 22:34:43 -05001227 def install_error_callback(self, callback):
1228 self.error_callbacks.insert(0, callback)
1229
Brad Bishop87b63c12016-03-18 14:47:51 -04001230 def custom_router_match(self, environ):
1231 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1232 needed doesn't work for us since the instance rules match
1233 everything. This monkey-patch lets the route handler figure
1234 out which response is needed. This could be accomplished
1235 with a hook but that would require calling the router match
1236 function twice.
1237 '''
1238 route, args = self.real_router_match(environ)
1239 if isinstance(route.callback, RouteHandler):
1240 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001241
Brad Bishop87b63c12016-03-18 14:47:51 -04001242 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001243
Brad Bishop080a48e2017-02-21 22:34:43 -05001244 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001245 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001246 error handler. '''
1247
1248 response_object = {}
1249 response_body = {}
1250 for x in self.error_callbacks:
1251 x(error=error,
1252 response_object=response_object,
1253 response_body=response_body)
1254
1255 return response_body.get('body', "")
1256
Brad Bishop87b63c12016-03-18 14:47:51 -04001257 @staticmethod
1258 def strip_extra_slashes():
1259 path = request.environ['PATH_INFO']
1260 trailing = ("", "/")[path[-1] == '/']
1261 parts = filter(bool, path.split('/'))
1262 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing