blob: bf4fe7cc4ae33b75479359588165040d5779fb29 [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
Alexander Filippovd08a4562018-03-20 12:02:23 +030018import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -040019import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050020import dbus.exceptions
21import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050023from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Jayanth Othayoth9bc94992017-06-29 06:30:40 -050024from bottle import static_file
Brad Bishopb103d2d2016-03-04 16:19:14 -050025import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050026from obmc.dbuslib.introspection import IntrospectionNodeParser
27import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050028import spwd
29import grp
30import crypt
Deepak Kodihalli1af301a2017-04-11 07:29:01 -050031import tempfile
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050032import re
Matt Spinlerd41643e2018-02-02 13:51:38 -060033import mimetypes
Deepak Kodihalli639b5022017-10-13 06:40:26 -050034have_wsock = True
35try:
36 from geventwebsocket import WebSocketError
37except ImportError:
38 have_wsock = False
39if have_wsock:
40 from dbus.mainloop.glib import DBusGMainLoop
41 DBusGMainLoop(set_as_default=True)
CamVan Nguyen249d1322018-03-05 10:08:33 -060042 # TODO: openbmc/openbmc#2994 remove python 2 support
43 try: # python 2
44 import gobject
45 except ImportError: # python 3
46 from gi.repository import GObject as gobject
Deepak Kodihalli639b5022017-10-13 06:40:26 -050047 import gevent
Deepak Kodihalli5c518f62018-04-23 03:26:38 -050048 from gevent import socket
49 from gevent import Greenlet
Brad Bishopaa65f6e2015-10-27 16:28:51 -040050
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -060051DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050052DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
53DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050054DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050055DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Adriana Kobylak53693892018-03-12 13:05:50 -050056SOFTWARE_PATH = '/xyz/openbmc_project/software'
Brad Bishop9ee57c42015-11-03 14:59:29 -050057
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050058_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040059
Matt Spinlerd41643e2018-02-02 13:51:38 -060060www_base_path = '/usr/share/www/'
61
Brad Bishop87b63c12016-03-18 14:47:51 -040062
Brad Bishop2f428582015-12-02 10:56:11 -050063def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040064 ''' Authorization plugin callback that checks
65 that the user is logged in. '''
66 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040067 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040068
Brad Bishop2f428582015-12-02 10:56:11 -050069
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050070def get_type_signature_by_introspection(bus, service, object_path,
71 property_name):
72 obj = bus.get_object(service, object_path)
73 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
74 xml_string = iface.Introspect()
75 for child in ElementTree.fromstring(xml_string):
76 # Iterate over each interfaces's properties to find
77 # matching property_name, and return its signature string
78 if child.tag == 'interface':
79 for i in child.iter():
80 if ('name' in i.attrib) and \
81 (i.attrib['name'] == property_name):
82 type_signature = i.attrib['type']
83 return type_signature
84
85
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053086def get_method_signature(bus, service, object_path, interface, method):
87 obj = bus.get_object(service, object_path)
88 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
89 xml_string = iface.Introspect()
90 arglist = []
91
92 root = ElementTree.fromstring(xml_string)
93 for dbus_intf in root.findall('interface'):
94 if (dbus_intf.get('name') == interface):
95 for dbus_method in dbus_intf.findall('method'):
96 if(dbus_method.get('name') == method):
97 for arg in dbus_method.findall('arg'):
98 arglist.append(arg.get('type'))
99 return arglist
100
101
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500102def split_struct_signature(signature):
103 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
104 struct_matches = re.findall(struct_regex, signature)
105 return struct_matches
106
107
108def convert_type(signature, value):
109 # Basic Types
110 converted_value = None
111 converted_container = None
CamVan Nguyen249d1322018-03-05 10:08:33 -0600112 # TODO: openbmc/openbmc#2994 remove python 2 support
113 try: # python 2
114 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
115 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
116 't': dbus.UInt64, 'd': float, 's': str}
117 except NameError: # python 3
118 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
119 'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32,
120 't': dbus.UInt64, 'd': float, 's': str}
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500121 array_matches = re.match(r'a\((\S+)\)', signature)
122 struct_matches = re.match(r'\((\S+)\)', signature)
123 dictionary_matches = re.match(r'a{(\S+)}', signature)
124 if signature in basic_types:
125 converted_value = basic_types[signature](value)
126 return converted_value
127 # Array
128 if array_matches:
129 element_type = array_matches.group(1)
130 converted_container = list()
131 # Test if value is a list
132 # to avoid iterating over each character in a string.
133 # Iterate over each item and convert type
134 if isinstance(value, list):
135 for i in value:
136 converted_element = convert_type(element_type, i)
137 converted_container.append(converted_element)
138 # Convert non-sequence to expected type, and append to list
139 else:
140 converted_element = convert_type(element_type, value)
141 converted_container.append(converted_element)
142 return converted_container
143 # Struct
144 if struct_matches:
145 element_types = struct_matches.group(1)
146 split_element_types = split_struct_signature(element_types)
147 converted_container = list()
148 # Test if value is a list
149 if isinstance(value, list):
150 for index, val in enumerate(value):
151 converted_element = convert_type(split_element_types[index],
152 value[index])
153 converted_container.append(converted_element)
154 else:
155 converted_element = convert_type(element_types, value)
156 converted_container.append(converted_element)
157 return tuple(converted_container)
158 # Dictionary
159 if dictionary_matches:
160 element_types = dictionary_matches.group(1)
161 split_element_types = split_struct_signature(element_types)
162 converted_container = dict()
163 # Convert each element of dict
CamVan Nguyen249d1322018-03-05 10:08:33 -0600164 for key, val in value.items():
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500165 converted_key = convert_type(split_element_types[0], key)
166 converted_val = convert_type(split_element_types[1], val)
167 converted_container[converted_key] = converted_val
168 return converted_container
169
170
Brad Bishop2f428582015-12-02 10:56:11 -0500171class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400172 ''' Authorization plugin callback that checks that the user is logged in
173 and a member of a group. '''
174 def __init__(self, group):
175 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500176
Brad Bishop87b63c12016-03-18 14:47:51 -0400177 def __call__(self, session, *a, **kw):
178 valid_user(session, *a, **kw)
179 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500180
Brad Bishop87b63c12016-03-18 14:47:51 -0400181 try:
182 res = session['user'] in grp.getgrnam(self.group)[3]
183 except KeyError:
184 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500185
Brad Bishop87b63c12016-03-18 14:47:51 -0400186 if not res:
187 abort(403, 'Insufficient access')
188
Brad Bishop2f428582015-12-02 10:56:11 -0500189
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500190class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400191 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500192 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400193
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500194 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400195 self.app = app
196 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500197 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400198 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400199 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500200 self._content_type = content_type
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400201
Brad Bishop88c76a42017-02-21 00:02:02 -0500202 if 'GET' in self._verbs:
203 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500204 if 'OPTIONS' not in self._verbs:
205 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500206
Brad Bishop87b63c12016-03-18 14:47:51 -0400207 def _setup(self, **kw):
208 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500209
Brad Bishop87b63c12016-03-18 14:47:51 -0400210 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500211 if request.method != 'OPTIONS':
212 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500213
Brad Bishopd4c1c552017-02-21 00:07:28 -0500214 # Javascript implementations will not send credentials
215 # with an OPTIONS request. Don't help malicious clients
216 # by checking the path here and returning a 404 if the
217 # path doesn't exist.
218 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500219
Brad Bishopd4c1c552017-02-21 00:07:28 -0500220 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500221 raise HTTPError(
222 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400223
Brad Bishop87b63c12016-03-18 14:47:51 -0400224 def __call__(self, **kw):
225 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400226
Brad Bishop88c76a42017-02-21 00:02:02 -0500227 def do_head(self, **kw):
228 return self.do_get(**kw)
229
Brad Bishopd4c1c552017-02-21 00:07:28 -0500230 def do_options(self, **kw):
231 for v in self._verbs:
232 response.set_header(
233 'Allow',
234 ','.join(self._verbs))
235 return None
236
Brad Bishop87b63c12016-03-18 14:47:51 -0400237 def install(self):
238 self.app.route(
239 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500240 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400241
Brad Bishop87b63c12016-03-18 14:47:51 -0400242 @staticmethod
243 def try_mapper_call(f, callback=None, **kw):
244 try:
245 return f(**kw)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600246 except dbus.exceptions.DBusException as e:
Brad Bishopfce77562016-11-28 15:44:18 -0500247 if e.get_dbus_name() == \
248 'org.freedesktop.DBus.Error.ObjectPathInUse':
249 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500250 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400251 raise
252 if callback is None:
253 def callback(e, **kw):
254 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400255
Brad Bishop87b63c12016-03-18 14:47:51 -0400256 callback(e, **kw)
257
258 @staticmethod
259 def try_properties_interface(f, *a):
260 try:
261 return f(*a)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600262 except dbus.exceptions.DBusException as e:
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -0600263 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name():
Brad Bishopf4e74982016-04-01 14:53:05 -0400264 # interface doesn't have any properties
265 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400266 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
267 # properties interface not implemented at all
268 return None
269 raise
270
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400271
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500272class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 verbs = 'GET'
274 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400275
Brad Bishop87b63c12016-03-18 14:47:51 -0400276 def __init__(self, app, bus):
277 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400278 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400279
Brad Bishop87b63c12016-03-18 14:47:51 -0400280 def find(self, path='/'):
281 return self.try_mapper_call(
282 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400283
Brad Bishop87b63c12016-03-18 14:47:51 -0400284 def setup(self, path='/'):
285 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400286
Brad Bishop87b63c12016-03-18 14:47:51 -0400287 def do_get(self, path='/'):
288 return request.route_data['map']
289
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400290
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500291class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400292 verbs = 'GET'
293 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400294
Brad Bishop87b63c12016-03-18 14:47:51 -0400295 def __init__(self, app, bus):
296 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400297 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400298
Brad Bishop87b63c12016-03-18 14:47:51 -0400299 def find(self, path='/'):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600300 return list(self.try_mapper_call(
301 self.mapper.get_subtree, path=path).keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def setup(self, path='/'):
304 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400305
Brad Bishop87b63c12016-03-18 14:47:51 -0400306 def do_get(self, path='/'):
307 return request.route_data['map']
308
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400309
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500310class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400311 verbs = 'GET'
312 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400313
Brad Bishop87b63c12016-03-18 14:47:51 -0400314 def __init__(self, app, bus):
315 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400316 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400317
Brad Bishop87b63c12016-03-18 14:47:51 -0400318 def find(self, path='/'):
319 return self.try_mapper_call(
320 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400321
Brad Bishop87b63c12016-03-18 14:47:51 -0400322 def setup(self, path='/'):
323 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400324
Brad Bishop87b63c12016-03-18 14:47:51 -0400325 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400326 return {x: y for x, y in self.mapper.enumerate_subtree(
327 path,
328 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400329
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400330
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500331class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400332 verbs = 'POST'
333 rules = '<path:path>/action/<method>'
334 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500335 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400336
Brad Bishop87b63c12016-03-18 14:47:51 -0400337 def __init__(self, app, bus):
338 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500339 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530340 self.service = ''
341 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500344 method_list = []
Gunnar Mills313aadb2018-04-08 14:50:09 -0500345 buses = self.try_mapper_call(
Brad Bishop87b63c12016-03-18 14:47:51 -0400346 self.mapper.get_object, path=path)
Gunnar Mills313aadb2018-04-08 14:50:09 -0500347 for items in buses.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400348 m = self.find_method_on_bus(path, method, *items)
349 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500350 method_list.append(m)
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600351 if method_list:
352 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400353
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400355
Brad Bishop87b63c12016-03-18 14:47:51 -0400356 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500357 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400358
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600359 def do_post(self, path, method, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400360 try:
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600361 args = []
362 if request.parameter_list:
363 args = request.parameter_list
364 # To see if the return type is capable of being merged
365 if len(request.route_data['map']) > 1:
366 results = None
367 for item in request.route_data['map']:
368 tmp = item(*args)
369 if not results:
370 if tmp is not None:
371 results = type(tmp)()
372 if isinstance(results, dict):
373 results = results.update(tmp)
374 elif isinstance(results, list):
375 results = results + tmp
376 elif isinstance(results, type(None)):
377 results = None
378 else:
379 abort(501, 'Don\'t know how to merge method call '
380 'results of {}'.format(type(tmp)))
381 return results
382 # There is only one method
383 return request.route_data['map'][0](*args)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400384
CamVan Nguyen249d1322018-03-05 10:08:33 -0600385 except dbus.exceptions.DBusException as e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530386 paramlist = []
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500387 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530388
389 signature_list = get_method_signature(self.bus, self.service,
390 path, self.interface,
391 method)
392 if not signature_list:
393 abort(400, "Failed to get method signature: %s" % str(e))
394 if len(signature_list) != len(request.parameter_list):
395 abort(400, "Invalid number of args")
396 converted_value = None
397 try:
398 for index, expected_type in enumerate(signature_list):
399 value = request.parameter_list[index]
400 converted_value = convert_type(expected_type, value)
401 paramlist.append(converted_value)
402 request.parameter_list = paramlist
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600403 self.do_post(path, method, False)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530404 return
405 except Exception as ex:
Nagaraju Gorugantiab404fa2017-12-14 10:24:40 -0600406 abort(400, "Bad Request/Invalid Args given")
Brad Bishop87b63c12016-03-18 14:47:51 -0400407 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530408
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 if e.get_dbus_name() == DBUS_TYPE_ERROR:
410 abort(400, str(e))
411 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 @staticmethod
414 def find_method_in_interface(method, obj, interface, methods):
415 if methods is None:
416 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400417
CamVan Nguyen249d1322018-03-05 10:08:33 -0600418 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
Brad Bishop87b63c12016-03-18 14:47:51 -0400419 if method is not None:
420 iface = dbus.Interface(obj, interface)
421 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400422
Brad Bishop87b63c12016-03-18 14:47:51 -0400423 def find_method_on_bus(self, path, method, bus, interfaces):
424 obj = self.bus.get_object(bus, path, introspect=False)
425 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
426 data = iface.Introspect()
427 parser = IntrospectionNodeParser(
428 ElementTree.fromstring(data),
Brad Bishopaeb995d2018-04-04 22:28:42 -0400429 intf_match=lambda x: x in interfaces)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600430 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400431 m = self.find_method_in_interface(
432 method, obj, x, y.get('method'))
433 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530434 self.service = bus
435 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 return m
437
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400438
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500439class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400440 verbs = ['PUT', 'GET']
441 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500442 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400443
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 def __init__(self, app, bus):
445 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500446 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400447
Brad Bishop87b63c12016-03-18 14:47:51 -0400448 def find(self, path, prop):
449 self.app.instance_handler.setup(path)
450 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500451 real_name = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600452 prop, list(obj.keys()))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400453
Brad Bishop56ad87f2017-02-21 23:33:29 -0500454 if not real_name:
455 if request.method == 'PUT':
456 abort(403, _4034_msg % ('property', 'created', prop))
457 else:
458 abort(404, _4034_msg % ('property', 'found', prop))
459 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500460
Brad Bishop87b63c12016-03-18 14:47:51 -0400461 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500462 name, obj = self.find(path, prop)
463 request.route_data['obj'] = obj
464 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500465
Brad Bishop87b63c12016-03-18 14:47:51 -0400466 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500467 name = request.route_data['name']
468 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500469
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600470 def do_put(self, path, prop, value=None, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400471 if value is None:
472 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500473
Brad Bishop87b63c12016-03-18 14:47:51 -0400474 prop, iface, properties_iface = self.get_host_interface(
475 path, prop, request.route_data['map'][path])
476 try:
477 properties_iface.Set(iface, prop, value)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600478 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -0400479 abort(400, str(e))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600480 except dbus.exceptions.DBusException as e:
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500481 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500482 bus_name = properties_iface.bus_name
483 expected_type = get_type_signature_by_introspection(self.bus,
484 bus_name,
485 path,
486 prop)
487 if not expected_type:
488 abort(403, "Failed to get expected type: %s" % str(e))
489 converted_value = None
490 try:
491 converted_value = convert_type(expected_type, value)
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600492 self.do_put(path, prop, converted_value, False)
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500493 return
494 except Exception as ex:
495 abort(403, "Failed to convert %s to type %s" %
496 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 abort(403, str(e))
498 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500499
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 def get_host_interface(self, path, prop, bus_info):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600501 for bus, interfaces in bus_info.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 obj = self.bus.get_object(bus, path, introspect=True)
503 properties_iface = dbus.Interface(
504 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500505
Brad Bishop87b63c12016-03-18 14:47:51 -0400506 info = self.get_host_interface_on_bus(
507 path, prop, properties_iface, bus, interfaces)
508 if info is not None:
509 prop, iface = info
510 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
513 for i in interfaces:
514 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500515 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400516 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500517 match = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600518 prop, list(properties.keys()))
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500519 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500521 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400522 return prop, i
523
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500524
Brad Bishop2503bd62015-12-16 17:56:12 -0500525class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400526 verbs = ['GET']
527 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 def __init__(self, app, bus):
530 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400531 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 def find(self, path):
534 return self.try_mapper_call(
535 self.mapper.get_object,
536 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500537
Brad Bishop87b63c12016-03-18 14:47:51 -0400538 def setup(self, path):
539 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500540
Brad Bishop87b63c12016-03-18 14:47:51 -0400541 def do_get(self, path):
542 schema = {}
CamVan Nguyen249d1322018-03-05 10:08:33 -0600543 for x in request.route_data['map'].keys():
Brad Bishop87b63c12016-03-18 14:47:51 -0400544 obj = self.bus.get_object(x, path, introspect=False)
545 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
546 data = iface.Introspect()
547 parser = IntrospectionNodeParser(
548 ElementTree.fromstring(data))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600549 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500551
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 return schema
553
Brad Bishop2503bd62015-12-16 17:56:12 -0500554
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500555class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400556 verbs = ['GET', 'PUT', 'DELETE']
557 rules = '<path:path>'
558 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500559
Brad Bishop87b63c12016-03-18 14:47:51 -0400560 def __init__(self, app, bus):
561 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400562 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 def find(self, path, callback=None):
565 return {path: self.try_mapper_call(
566 self.mapper.get_object,
567 callback,
568 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500569
Brad Bishop87b63c12016-03-18 14:47:51 -0400570 def setup(self, path):
571 callback = None
572 if request.method == 'PUT':
573 def callback(e, **kw):
574 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 if request.route_data.get('map') is None:
577 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400580 return self.mapper.enumerate_object(
581 path,
582 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500583
Brad Bishop87b63c12016-03-18 14:47:51 -0400584 def do_put(self, path):
585 # make sure all properties exist in the request
586 obj = set(self.do_get(path).keys())
587 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500588
Brad Bishop87b63c12016-03-18 14:47:51 -0400589 diff = list(obj.difference(req))
590 if diff:
591 abort(403, _4034_msg % (
592 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 diff = list(req.difference(obj))
595 if diff:
596 abort(403, _4034_msg % (
597 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500598
CamVan Nguyen249d1322018-03-05 10:08:33 -0600599 for p, v in request.parameter_list.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400600 self.app.property_handler.do_put(
601 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 def do_delete(self, path):
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500604 deleted = False
605 for bus, interfaces in request.route_data['map'][path].items():
606 if self.bus_has_delete(interfaces):
607 self.delete_on_bus(path, bus)
608 deleted = True
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500609
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500610 #It's OK if some objects didn't have a Delete, but not all
611 if not deleted:
612 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500613
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500614 def bus_has_delete(self, interfaces):
615 return DELETE_IFACE in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500616
Brad Bishop87b63c12016-03-18 14:47:51 -0400617 def delete_on_bus(self, path, bus):
618 obj = self.bus.get_object(bus, path, introspect=False)
619 delete_iface = dbus.Interface(
620 obj, dbus_interface=DELETE_IFACE)
621 delete_iface.Delete()
622
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500623
Brad Bishop2f428582015-12-02 10:56:11 -0500624class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 ''' Handles the /login and /logout routes, manages
626 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500627
Brad Bishop87b63c12016-03-18 14:47:51 -0400628 rules = ['/login', '/logout']
629 login_str = "User '%s' logged %s"
630 bad_passwd_str = "Invalid username or password"
631 no_user_str = "No user logged in"
632 bad_json_str = "Expecting request format { 'data': " \
633 "[<username>, <password>] }, got '%s'"
Alexander Filippovd08a4562018-03-20 12:02:23 +0300634 bmc_not_ready_str = "BMC is not ready (booting)"
Brad Bishop87b63c12016-03-18 14:47:51 -0400635 _require_auth = None
636 MAX_SESSIONS = 16
Alexander Filippovd08a4562018-03-20 12:02:23 +0300637 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
638 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
639 BMCSTATE_PROPERTY = 'CurrentBMCState'
640 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
Brad Bishop2f428582015-12-02 10:56:11 -0500641
Brad Bishop87b63c12016-03-18 14:47:51 -0400642 def __init__(self, app, bus):
643 super(SessionHandler, self).__init__(
644 app, bus)
645 self.hmac_key = os.urandom(128)
646 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500647
Brad Bishop87b63c12016-03-18 14:47:51 -0400648 @staticmethod
649 def authenticate(username, clear):
650 try:
651 encoded = spwd.getspnam(username)[1]
652 return encoded == crypt.crypt(clear, encoded)
653 except KeyError:
654 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500655
Brad Bishop87b63c12016-03-18 14:47:51 -0400656 def invalidate_session(self, session):
657 try:
658 self.session_store.remove(session)
659 except ValueError:
660 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500661
Brad Bishop87b63c12016-03-18 14:47:51 -0400662 def new_session(self):
663 sid = os.urandom(32)
664 if self.MAX_SESSIONS <= len(self.session_store):
665 self.session_store.pop()
666 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500667
Brad Bishop87b63c12016-03-18 14:47:51 -0400668 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500669
Brad Bishop87b63c12016-03-18 14:47:51 -0400670 def get_session(self, sid):
671 sids = [x['sid'] for x in self.session_store]
672 try:
673 return self.session_store[sids.index(sid)]
674 except ValueError:
675 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500676
Brad Bishop87b63c12016-03-18 14:47:51 -0400677 def get_session_from_cookie(self):
678 return self.get_session(
679 request.get_cookie(
680 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500681
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 def do_post(self, **kw):
683 if request.path == '/login':
684 return self.do_login(**kw)
685 else:
686 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500687
Brad Bishop87b63c12016-03-18 14:47:51 -0400688 def do_logout(self, **kw):
689 session = self.get_session_from_cookie()
690 if session is not None:
691 user = session['user']
692 self.invalidate_session(session)
693 response.delete_cookie('sid')
694 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500695
Brad Bishop87b63c12016-03-18 14:47:51 -0400696 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500697
Brad Bishop87b63c12016-03-18 14:47:51 -0400698 def do_login(self, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400699 if len(request.parameter_list) != 2:
700 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500701
Brad Bishop87b63c12016-03-18 14:47:51 -0400702 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400703 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500704
Alexander Filippovd08a4562018-03-20 12:02:23 +0300705 force = False
706 try:
707 force = request.json.get('force')
708 except (ValueError, AttributeError, KeyError, TypeError):
709 force = False
710
711 if not force and not self.is_bmc_ready():
712 abort(503, self.bmc_not_ready_str)
713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 user = request.parameter_list[0]
715 session = self.new_session()
716 session['user'] = user
717 response.set_cookie(
718 'sid', session['sid'], secret=self.hmac_key,
719 secure=True,
720 httponly=True)
721 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500722
Alexander Filippovd08a4562018-03-20 12:02:23 +0300723 def is_bmc_ready(self):
724 if not self.app.with_bmc_check:
725 return True
726
727 try:
728 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
729 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
730 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
731 if state == self.BMCSTATE_READY:
732 return True
733
734 except dbus.exceptions.DBusException:
735 pass
736
737 return False
738
Brad Bishop87b63c12016-03-18 14:47:51 -0400739 def find(self, **kw):
740 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500741
Brad Bishop87b63c12016-03-18 14:47:51 -0400742 def setup(self, **kw):
743 pass
744
Brad Bishop2f428582015-12-02 10:56:11 -0500745
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500746class ImageUploadUtils:
747 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500748
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500749 file_loc = '/tmp/images'
750 file_prefix = 'img'
751 file_suffix = ''
Adriana Kobylak53693892018-03-12 13:05:50 -0500752 signal = None
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500753
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500754 @classmethod
755 def do_upload(cls, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500756 def cleanup():
757 os.close(handle)
758 if cls.signal:
759 cls.signal.remove()
760 cls.signal = None
761
762 def signal_callback(path, a, **kw):
763 # Just interested on the first Version interface created which is
764 # triggered when the file is uploaded. This helps avoid getting the
765 # wrong information for multiple upload requests in a row.
766 if "xyz.openbmc_project.Software.Version" in a and \
767 "xyz.openbmc_project.Software.Activation" not in a:
768 paths.append(path)
769
770 while cls.signal:
771 # Serialize uploads by waiting for the signal to be cleared.
772 # This makes it easier to ensure that the version information
773 # is the right one instead of the data from another upload request.
774 gevent.sleep(1)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500775 if not os.path.exists(cls.file_loc):
Gunnar Millsfb515792017-11-09 15:52:17 -0600776 abort(500, "Error Directory not found")
Adriana Kobylak53693892018-03-12 13:05:50 -0500777 paths = []
778 bus = dbus.SystemBus()
779 cls.signal = bus.add_signal_receiver(
780 signal_callback,
781 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
782 signal_name='InterfacesAdded',
783 path=SOFTWARE_PATH)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500784 if not filename:
785 handle, filename = tempfile.mkstemp(cls.file_suffix,
786 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500787 else:
788 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500789 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500790 try:
791 file_contents = request.body.read()
792 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500793 os.write(handle, file_contents)
Adriana Kobylak53693892018-03-12 13:05:50 -0500794 # Close file after writing, the image manager process watches for
795 # the close event to know the upload is complete.
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500796 os.close(handle)
Adriana Kobylak53693892018-03-12 13:05:50 -0500797 except (IOError, ValueError) as e:
798 cleanup()
799 abort(400, str(e))
800 except Exception:
801 cleanup()
802 abort(400, "Unexpected Error")
803 loop = gobject.MainLoop()
804 gcontext = loop.get_context()
805 count = 0
806 version_id = ''
807 while loop is not None:
808 try:
809 if gcontext.pending():
810 gcontext.iteration()
811 if not paths:
812 gevent.sleep(1)
813 else:
814 version_id = os.path.basename(paths.pop())
815 break
816 count += 1
817 if count == 10:
818 break
819 except Exception:
820 break
821 cls.signal.remove()
822 cls.signal = None
Adriana Kobylak97fe4352018-04-10 10:44:11 -0500823 if version_id:
824 return version_id
825 else:
826 abort(400, "Version already exists or failed to be extracted")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500827
828
829class ImagePostHandler(RouteHandler):
830 ''' Handles the /upload/image route. '''
831
832 verbs = ['POST']
833 rules = ['/upload/image']
834 content_type = 'application/octet-stream'
835
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500836 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500837 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500838 app, bus, self.verbs, self.rules, self.content_type)
839
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500840 def do_post(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500841 return ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500842
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500843 def find(self, **kw):
844 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500845
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500846 def setup(self, **kw):
847 pass
848
849
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500850class EventNotifier:
851 keyNames = {}
852 keyNames['event'] = 'event'
853 keyNames['path'] = 'path'
854 keyNames['intfMap'] = 'interfaces'
855 keyNames['propMap'] = 'properties'
856 keyNames['intf'] = 'interface'
857
858 def __init__(self, wsock, filters):
859 self.wsock = wsock
860 self.paths = filters.get("paths", [])
861 self.interfaces = filters.get("interfaces", [])
862 if not self.paths:
863 self.paths.append(None)
864 bus = dbus.SystemBus()
865 # Add a signal receiver for every path the client is interested in
866 for path in self.paths:
867 bus.add_signal_receiver(
868 self.interfaces_added_handler,
869 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
870 signal_name='InterfacesAdded',
871 path=path)
872 bus.add_signal_receiver(
873 self.properties_changed_handler,
874 dbus_interface=dbus.PROPERTIES_IFACE,
875 signal_name='PropertiesChanged',
876 path=path,
877 path_keyword='path')
878 loop = gobject.MainLoop()
879 # gobject's mainloop.run() will block the entire process, so the gevent
880 # scheduler and hence greenlets won't execute. The while-loop below
881 # works around this limitation by using gevent's sleep, instead of
882 # calling loop.run()
883 gcontext = loop.get_context()
884 while loop is not None:
885 try:
886 if gcontext.pending():
887 gcontext.iteration()
888 else:
889 # gevent.sleep puts only the current greenlet to sleep,
890 # not the entire process.
891 gevent.sleep(5)
892 except WebSocketError:
893 break
894
895 def interfaces_added_handler(self, path, iprops, **kw):
896 ''' If the client is interested in these changes, respond to the
897 client. This handles d-bus interface additions.'''
898 if (not self.interfaces) or \
899 (not set(iprops).isdisjoint(self.interfaces)):
900 response = {}
901 response[self.keyNames['event']] = "InterfacesAdded"
902 response[self.keyNames['path']] = path
903 response[self.keyNames['intfMap']] = iprops
904 try:
905 self.wsock.send(json.dumps(response))
906 except WebSocketError:
907 return
908
909 def properties_changed_handler(self, interface, new, old, **kw):
910 ''' If the client is interested in these changes, respond to the
911 client. This handles d-bus property changes. '''
912 if (not self.interfaces) or (interface in self.interfaces):
913 path = str(kw['path'])
914 response = {}
915 response[self.keyNames['event']] = "PropertiesChanged"
916 response[self.keyNames['path']] = path
917 response[self.keyNames['intf']] = interface
918 response[self.keyNames['propMap']] = new
919 try:
920 self.wsock.send(json.dumps(response))
921 except WebSocketError:
922 return
923
924
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500925class EventHandler(RouteHandler):
926 ''' Handles the /subscribe route, for clients to be able
927 to subscribe to BMC events. '''
928
929 verbs = ['GET']
930 rules = ['/subscribe']
931
932 def __init__(self, app, bus):
933 super(EventHandler, self).__init__(
934 app, bus, self.verbs, self.rules)
935
936 def find(self, **kw):
937 pass
938
939 def setup(self, **kw):
940 pass
941
942 def do_get(self):
943 wsock = request.environ.get('wsgi.websocket')
944 if not wsock:
945 abort(400, 'Expected WebSocket request.')
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500946 filters = wsock.receive()
947 filters = json.loads(filters)
948 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500949
950
Deepak Kodihalli5c518f62018-04-23 03:26:38 -0500951class HostConsoleHandler(RouteHandler):
952 ''' Handles the /console route, for clients to be able
953 read/write the host serial console. The way this is
954 done is by exposing a websocket that's mirrored to an
955 abstract UNIX domain socket, which is the source for
956 the console data. '''
957
958 verbs = ['GET']
959 # Naming the route console0, because the numbering will help
960 # on multi-bmc/multi-host systems.
961 rules = ['/console0']
962
963 def __init__(self, app, bus):
964 super(HostConsoleHandler, self).__init__(
965 app, bus, self.verbs, self.rules)
966
967 def find(self, **kw):
968 pass
969
970 def setup(self, **kw):
971 pass
972
973 def read_wsock(self, wsock, sock):
974 while True:
975 try:
976 incoming = wsock.receive()
977 if incoming:
978 # Read websocket, write to UNIX socket
979 sock.send(incoming)
980 except Exception as e:
981 sock.close()
982 return
983
984 def read_sock(self, sock, wsock):
985 max_sock_read_len = 4096
986 while True:
987 try:
988 outgoing = sock.recv(max_sock_read_len)
989 if outgoing:
990 # Read UNIX socket, write to websocket
991 wsock.send(outgoing)
992 except Exception as e:
993 wsock.close()
994 return
995
996 def send_ping(self, wsock) :
997 # Most webservers close websockets after 60 seconds of
998 # inactivity. Make sure to send a ping before that.
999 timeout = 45
1000 payload = "ping"
1001 # the ping payload can be anything, the receiver has to just
1002 # return the same back.
1003 while True:
1004 gevent.sleep(timeout)
1005 wsock.send_frame(payload, wsock.OPCODE_PING)
1006
1007 def do_get(self):
1008 wsock = request.environ.get('wsgi.websocket')
1009 if not wsock:
1010 abort(400, 'Expected WebSocket based request.')
1011
1012 # A UNIX domain socket structure defines a 108-byte pathname. The
1013 # server in this case, obmc-console-server, expects a 108-byte path.
1014 socket_name = "\0obmc-console"
1015 trailing_bytes = "\0" * (108 - len(socket_name))
1016 socket_path = socket_name + trailing_bytes
1017 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1018
1019 try:
1020 sock.connect(socket_path)
1021 except Exception as e:
1022 abort(500, str(e))
1023
1024 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1025 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
1026 ping_sender = Greenlet.spawn(self.send_ping, wsock)
1027 gevent.joinall([wsock_reader, sock_reader, ping_sender])
1028
1029
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001030class ImagePutHandler(RouteHandler):
1031 ''' Handles the /upload/image/<filename> route. '''
1032
1033 verbs = ['PUT']
1034 rules = ['/upload/image/<filename>']
1035 content_type = 'application/octet-stream'
1036
1037 def __init__(self, app, bus):
1038 super(ImagePutHandler, self).__init__(
1039 app, bus, self.verbs, self.rules, self.content_type)
1040
1041 def do_put(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -05001042 return ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -05001043
1044 def find(self, **kw):
1045 pass
1046
1047 def setup(self, **kw):
1048 pass
1049
1050
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001051class DownloadDumpHandler(RouteHandler):
1052 ''' Handles the /download/dump route. '''
1053
1054 verbs = 'GET'
1055 rules = ['/download/dump/<dumpid>']
1056 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -05001057 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -04001058 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001059
1060 def __init__(self, app, bus):
1061 super(DownloadDumpHandler, self).__init__(
1062 app, bus, self.verbs, self.rules, self.content_type)
1063
1064 def do_get(self, dumpid):
1065 return self.do_download(dumpid)
1066
1067 def find(self, **kw):
1068 pass
1069
1070 def setup(self, **kw):
1071 pass
1072
1073 def do_download(self, dumpid):
1074 dump_loc = os.path.join(self.dump_loc, dumpid)
1075 if not os.path.exists(dump_loc):
1076 abort(404, "Path not found")
1077
1078 files = os.listdir(dump_loc)
1079 num_files = len(files)
1080 if num_files == 0:
1081 abort(404, "Dump not found")
1082
1083 return static_file(os.path.basename(files[0]), root=dump_loc,
1084 download=True, mimetype=self.content_type)
1085
1086
Matt Spinlerd41643e2018-02-02 13:51:38 -06001087class WebHandler(RouteHandler):
1088 ''' Handles the routes for the web UI files. '''
1089
1090 verbs = 'GET'
1091
1092 # Match only what we know are web files, so everything else
1093 # can get routed to the REST handlers.
1094 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1095 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1096 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1097 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1098 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1099 '/<filename:re:.+\.ico>']
1100
1101 # The mimetypes module knows about most types, but not these
1102 content_types = {
1103 '.eot': 'application/vnd.ms-fontobject',
1104 '.woff': 'application/x-font-woff',
1105 '.woff2': 'application/x-font-woff2',
1106 '.ttf': 'application/x-font-ttf',
1107 '.map': 'application/json'
1108 }
1109
1110 _require_auth = None
1111 suppress_json_resp = True
1112
1113 def __init__(self, app, bus):
1114 super(WebHandler, self).__init__(
1115 app, bus, self.verbs, self.rules)
1116
1117 def get_type(self, filename):
1118 ''' Returns the content type and encoding for a file '''
1119
1120 content_type, encoding = mimetypes.guess_type(filename)
1121
1122 # Try our own list if mimetypes didn't recognize it
1123 if content_type is None:
1124 if filename[-3:] == '.gz':
1125 filename = filename[:-3]
1126 extension = filename[filename.rfind('.'):]
1127 content_type = self.content_types.get(extension, None)
1128
1129 return content_type, encoding
1130
1131 def do_get(self, filename='index.html'):
1132
1133 # If a gzipped version exists, use that instead.
1134 # Possible future enhancement: if the client doesn't
1135 # accept compressed files, unzip it ourselves before sending.
1136 if not os.path.exists(os.path.join(www_base_path, filename)):
1137 filename = filename + '.gz'
1138
1139 # Though bottle should protect us, ensure path is valid
1140 realpath = os.path.realpath(filename)
1141 if realpath[0] == '/':
1142 realpath = realpath[1:]
1143 if not os.path.exists(os.path.join(www_base_path, realpath)):
1144 abort(404, "Path not found")
1145
1146 mimetype, encoding = self.get_type(filename)
1147
1148 # Couldn't find the type - let static_file() deal with it,
1149 # though this should never happen.
1150 if mimetype is None:
1151 print("Can't figure out content-type for %s" % filename)
1152 mimetype = 'auto'
1153
1154 # This call will set several header fields for us,
1155 # including the charset if the type is text.
1156 response = static_file(filename, www_base_path, mimetype)
1157
1158 # static_file() will only set the encoding if the
1159 # mimetype was auto, so set it here.
1160 if encoding is not None:
1161 response.set_header('Content-Encoding', encoding)
1162
1163 return response
1164
1165 def find(self, **kw):
1166 pass
1167
1168 def setup(self, **kw):
1169 pass
1170
1171
Brad Bishop2f428582015-12-02 10:56:11 -05001172class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001173 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -05001174
Brad Bishop87b63c12016-03-18 14:47:51 -04001175 name = 'authorization'
1176 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -05001177
Brad Bishop87b63c12016-03-18 14:47:51 -04001178 class Compose:
1179 def __init__(self, validators, callback, session_mgr):
1180 self.validators = validators
1181 self.callback = callback
1182 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -05001183
Brad Bishop87b63c12016-03-18 14:47:51 -04001184 def __call__(self, *a, **kw):
1185 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1186 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001187 if request.method != 'OPTIONS':
1188 for x in self.validators:
1189 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001190
Brad Bishop87b63c12016-03-18 14:47:51 -04001191 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001192
Brad Bishop87b63c12016-03-18 14:47:51 -04001193 def apply(self, callback, route):
1194 undecorated = route.get_undecorated_callback()
1195 if not isinstance(undecorated, RouteHandler):
1196 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001197
Brad Bishop87b63c12016-03-18 14:47:51 -04001198 auth_types = getattr(
1199 undecorated, '_require_auth', None)
1200 if not auth_types:
1201 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001202
Brad Bishop87b63c12016-03-18 14:47:51 -04001203 return self.Compose(
1204 auth_types, callback, undecorated.app.session_handler)
1205
Brad Bishop2f428582015-12-02 10:56:11 -05001206
Brad Bishopd0c404a2017-02-21 09:23:25 -05001207class CorsPlugin(object):
1208 ''' Add CORS headers. '''
1209
1210 name = 'cors'
1211 api = 2
1212
1213 @staticmethod
1214 def process_origin():
1215 origin = request.headers.get('Origin')
1216 if origin:
1217 response.add_header('Access-Control-Allow-Origin', origin)
1218 response.add_header(
1219 'Access-Control-Allow-Credentials', 'true')
1220
1221 @staticmethod
1222 def process_method_and_headers(verbs):
1223 method = request.headers.get('Access-Control-Request-Method')
1224 headers = request.headers.get('Access-Control-Request-Headers')
1225 if headers:
1226 headers = [x.lower() for x in headers.split(',')]
1227
1228 if method in verbs \
1229 and headers == ['content-type']:
1230 response.add_header('Access-Control-Allow-Methods', method)
1231 response.add_header(
1232 'Access-Control-Allow-Headers', 'Content-Type')
Ratan Gupta91b46f82018-01-14 12:52:23 +05301233 response.add_header('X-Frame-Options', 'deny')
1234 response.add_header('X-Content-Type-Options', 'nosniff')
1235 response.add_header('X-XSS-Protection', '1; mode=block')
1236 response.add_header(
1237 'Content-Security-Policy', "default-src 'self'")
1238 response.add_header(
1239 'Strict-Transport-Security',
1240 'max-age=31536000; includeSubDomains; preload')
Brad Bishopd0c404a2017-02-21 09:23:25 -05001241
1242 def __init__(self, app):
1243 app.install_error_callback(self.error_callback)
1244
1245 def apply(self, callback, route):
1246 undecorated = route.get_undecorated_callback()
1247 if not isinstance(undecorated, RouteHandler):
1248 return callback
1249
1250 if not getattr(undecorated, '_enable_cors', None):
1251 return callback
1252
1253 def wrap(*a, **kw):
1254 self.process_origin()
1255 self.process_method_and_headers(undecorated._verbs)
1256 return callback(*a, **kw)
1257
1258 return wrap
1259
1260 def error_callback(self, **kw):
1261 self.process_origin()
1262
1263
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001264class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001265 ''' Ensures request content satisfies the OpenBMC json api format. '''
1266 name = 'json_api_request'
1267 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001268
Brad Bishop87b63c12016-03-18 14:47:51 -04001269 error_str = "Expecting request format { 'data': <value> }, got '%s'"
1270 type_error_str = "Unsupported Content-Type: '%s'"
1271 json_type = "application/json"
1272 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001273
Brad Bishop87b63c12016-03-18 14:47:51 -04001274 @staticmethod
1275 def content_expected():
1276 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001277
Brad Bishop87b63c12016-03-18 14:47:51 -04001278 def validate_request(self):
1279 if request.content_length > 0 and \
1280 request.content_type != self.json_type:
1281 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001282
Brad Bishop87b63c12016-03-18 14:47:51 -04001283 try:
1284 request.parameter_list = request.json.get('data')
CamVan Nguyen249d1322018-03-05 10:08:33 -06001285 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -04001286 abort(400, str(e))
1287 except (AttributeError, KeyError, TypeError):
1288 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001289
Brad Bishop87b63c12016-03-18 14:47:51 -04001290 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001291 content_type = getattr(
1292 route.get_undecorated_callback(), '_content_type', None)
1293 if self.json_type != content_type:
1294 return callback
1295
Brad Bishop87b63c12016-03-18 14:47:51 -04001296 verbs = getattr(
1297 route.get_undecorated_callback(), '_verbs', None)
1298 if verbs is None:
1299 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001300
Brad Bishop87b63c12016-03-18 14:47:51 -04001301 if not set(self.request_methods).intersection(verbs):
1302 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001303
Brad Bishop87b63c12016-03-18 14:47:51 -04001304 def wrap(*a, **kw):
1305 if self.content_expected():
1306 self.validate_request()
1307 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001308
Brad Bishop87b63c12016-03-18 14:47:51 -04001309 return wrap
1310
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001311
1312class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001313 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1314 name = 'json_api_method_request'
1315 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001316
Brad Bishop87b63c12016-03-18 14:47:51 -04001317 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001318 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001319
Brad Bishop87b63c12016-03-18 14:47:51 -04001320 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001321 content_type = getattr(
1322 route.get_undecorated_callback(), '_content_type', None)
1323 if self.json_type != content_type:
1324 return callback
1325
Brad Bishop87b63c12016-03-18 14:47:51 -04001326 request_type = getattr(
1327 route.get_undecorated_callback(), 'request_type', None)
1328 if request_type is None:
1329 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001330
Brad Bishop87b63c12016-03-18 14:47:51 -04001331 def validate_request():
1332 if not isinstance(request.parameter_list, request_type):
1333 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001334
Brad Bishop87b63c12016-03-18 14:47:51 -04001335 def wrap(*a, **kw):
1336 if JsonApiRequestPlugin.content_expected():
1337 validate_request()
1338 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001339
Brad Bishop87b63c12016-03-18 14:47:51 -04001340 return wrap
1341
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001342
Brad Bishop080a48e2017-02-21 22:34:43 -05001343class JsonErrorsPlugin(JSONPlugin):
1344 ''' Extend the Bottle JSONPlugin such that it also encodes error
1345 responses. '''
1346
1347 def __init__(self, app, **kw):
1348 super(JsonErrorsPlugin, self).__init__(**kw)
1349 self.json_opts = {
CamVan Nguyen249d1322018-03-05 10:08:33 -06001350 x: y for x, y in kw.items()
Brad Bishop080a48e2017-02-21 22:34:43 -05001351 if x in ['indent', 'sort_keys']}
1352 app.install_error_callback(self.error_callback)
1353
1354 def error_callback(self, response_object, response_body, **kw):
1355 response_body['body'] = json.dumps(response_object, **self.json_opts)
1356 response.content_type = 'application/json'
1357
1358
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001359class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001360 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001361 name = 'json_api_response'
1362 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001363
Brad Bishopd4c1c552017-02-21 00:07:28 -05001364 @staticmethod
1365 def has_body():
1366 return request.method not in ['OPTIONS']
1367
Brad Bishop080a48e2017-02-21 22:34:43 -05001368 def __init__(self, app):
1369 app.install_error_callback(self.error_callback)
1370
Brad Bishop87b63c12016-03-18 14:47:51 -04001371 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001372 skip = getattr(
1373 route.get_undecorated_callback(), 'suppress_json_resp', None)
1374 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001375 return callback
1376
Brad Bishop87b63c12016-03-18 14:47:51 -04001377 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001378 data = callback(*a, **kw)
1379 if self.has_body():
1380 resp = {'data': data}
1381 resp['status'] = 'ok'
1382 resp['message'] = response.status_line
1383 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001384 return wrap
1385
Brad Bishop080a48e2017-02-21 22:34:43 -05001386 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001387 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001388 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001389 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001390 if error.status_code == 500:
1391 response_object['data']['exception'] = repr(error.exception)
1392 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001393
Brad Bishop87b63c12016-03-18 14:47:51 -04001394
Brad Bishop080a48e2017-02-21 22:34:43 -05001395class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001396 ''' Json javascript wrapper. '''
1397 name = 'jsonp'
1398 api = 2
1399
Brad Bishop080a48e2017-02-21 22:34:43 -05001400 def __init__(self, app, **kw):
1401 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001402
1403 @staticmethod
1404 def to_jsonp(json):
1405 jwrapper = request.query.callback or None
1406 if(jwrapper):
1407 response.set_header('Content-Type', 'application/javascript')
1408 json = jwrapper + '(' + json + ');'
1409 return json
1410
1411 def apply(self, callback, route):
1412 def wrap(*a, **kw):
1413 return self.to_jsonp(callback(*a, **kw))
1414 return wrap
1415
Brad Bishop080a48e2017-02-21 22:34:43 -05001416 def error_callback(self, response_body, **kw):
1417 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001418
1419
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001420class ContentCheckerPlugin(object):
1421 ''' Ensures that a route is associated with the expected content-type
1422 header. '''
1423 name = 'content_checker'
1424 api = 2
1425
1426 class Checker:
1427 def __init__(self, type, callback):
1428 self.expected_type = type
1429 self.callback = callback
1430 self.error_str = "Expecting content type '%s', got '%s'"
1431
1432 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001433 if request.method in ['PUT', 'POST', 'PATCH'] and \
1434 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001435 self.expected_type != request.content_type:
1436 abort(415, self.error_str % (self.expected_type,
1437 request.content_type))
1438
1439 return self.callback(*a, **kw)
1440
1441 def apply(self, callback, route):
1442 content_type = getattr(
1443 route.get_undecorated_callback(), '_content_type', None)
1444
1445 return self.Checker(content_type, callback)
1446
1447
Brad Bishop2c6fc762016-08-29 15:53:25 -04001448class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001449 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001450 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001451
1452 self.have_wsock = kw.get('have_wsock', False)
Alexander Filippovd08a4562018-03-20 12:02:23 +03001453 self.with_bmc_check = '--with-bmc-check' in sys.argv
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001454
Brad Bishop2ddfa002016-08-29 15:11:55 -04001455 self.bus = dbus.SystemBus()
1456 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001457 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001458
Brad Bishop87b63c12016-03-18 14:47:51 -04001459 self.install_hooks()
1460 self.install_plugins()
1461 self.create_handlers()
1462 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001463
Brad Bishop87b63c12016-03-18 14:47:51 -04001464 def install_plugins(self):
1465 # install json api plugins
1466 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001467 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001468 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001469 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001470 self.install(JsonpPlugin(self, **json_kw))
1471 self.install(JsonErrorsPlugin(self, **json_kw))
1472 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001473 self.install(JsonApiRequestPlugin())
1474 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001475
Brad Bishop87b63c12016-03-18 14:47:51 -04001476 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001477 self.error_handler_type = type(self.default_error_handler)
1478 self.original_error_handler = self.default_error_handler
1479 self.default_error_handler = self.error_handler_type(
1480 self.custom_error_handler, self, Bottle)
1481
Brad Bishop87b63c12016-03-18 14:47:51 -04001482 self.real_router_match = self.router.match
1483 self.router.match = self.custom_router_match
1484 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001485
Brad Bishop87b63c12016-03-18 14:47:51 -04001486 def create_handlers(self):
1487 # create route handlers
1488 self.session_handler = SessionHandler(self, self.bus)
Matt Spinlerd41643e2018-02-02 13:51:38 -06001489 self.web_handler = WebHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001490 self.directory_handler = DirectoryHandler(self, self.bus)
1491 self.list_names_handler = ListNamesHandler(self, self.bus)
1492 self.list_handler = ListHandler(self, self.bus)
1493 self.method_handler = MethodHandler(self, self.bus)
1494 self.property_handler = PropertyHandler(self, self.bus)
1495 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001496 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1497 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001498 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001499 if self.have_wsock:
1500 self.event_handler = EventHandler(self, self.bus)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001501 self.host_console_handler = HostConsoleHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001502 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001503
Brad Bishop87b63c12016-03-18 14:47:51 -04001504 def install_handlers(self):
1505 self.session_handler.install()
Matt Spinlerd41643e2018-02-02 13:51:38 -06001506 self.web_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001507 self.directory_handler.install()
1508 self.list_names_handler.install()
1509 self.list_handler.install()
1510 self.method_handler.install()
1511 self.property_handler.install()
1512 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001513 self.image_upload_post_handler.install()
1514 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001515 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001516 if self.have_wsock:
1517 self.event_handler.install()
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001518 self.host_console_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001519 # this has to come last, since it matches everything
1520 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001521
Brad Bishop080a48e2017-02-21 22:34:43 -05001522 def install_error_callback(self, callback):
1523 self.error_callbacks.insert(0, callback)
1524
Brad Bishop87b63c12016-03-18 14:47:51 -04001525 def custom_router_match(self, environ):
1526 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1527 needed doesn't work for us since the instance rules match
1528 everything. This monkey-patch lets the route handler figure
1529 out which response is needed. This could be accomplished
1530 with a hook but that would require calling the router match
1531 function twice.
1532 '''
1533 route, args = self.real_router_match(environ)
1534 if isinstance(route.callback, RouteHandler):
1535 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001536
Brad Bishop87b63c12016-03-18 14:47:51 -04001537 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001538
Brad Bishop080a48e2017-02-21 22:34:43 -05001539 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001540 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001541 error handler. '''
1542
1543 response_object = {}
1544 response_body = {}
1545 for x in self.error_callbacks:
1546 x(error=error,
1547 response_object=response_object,
1548 response_body=response_body)
1549
1550 return response_body.get('body', "")
1551
Brad Bishop87b63c12016-03-18 14:47:51 -04001552 @staticmethod
1553 def strip_extra_slashes():
1554 path = request.environ['PATH_INFO']
1555 trailing = ("", "/")[path[-1] == '/']
CamVan Nguyen249d1322018-03-05 10:08:33 -06001556 parts = list(filter(bool, path.split('/')))
Brad Bishop87b63c12016-03-18 14:47:51 -04001557 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing