blob: 5eaffb7a6f7fe6472639b63f9f3e415c8ad8116f [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'
Jayashankar Padathbec10c22018-05-29 18:22:59 +053057WEBSOCKET_TIMEOUT = 45
Brad Bishop9ee57c42015-11-03 14:59:29 -050058
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050059_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040060
Matt Spinlerd41643e2018-02-02 13:51:38 -060061www_base_path = '/usr/share/www/'
62
Brad Bishop87b63c12016-03-18 14:47:51 -040063
Brad Bishop2f428582015-12-02 10:56:11 -050064def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040065 ''' Authorization plugin callback that checks
66 that the user is logged in. '''
67 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040068 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040069
Brad Bishop2f428582015-12-02 10:56:11 -050070
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050071def get_type_signature_by_introspection(bus, service, object_path,
72 property_name):
73 obj = bus.get_object(service, object_path)
74 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
75 xml_string = iface.Introspect()
76 for child in ElementTree.fromstring(xml_string):
77 # Iterate over each interfaces's properties to find
78 # matching property_name, and return its signature string
79 if child.tag == 'interface':
80 for i in child.iter():
81 if ('name' in i.attrib) and \
82 (i.attrib['name'] == property_name):
83 type_signature = i.attrib['type']
84 return type_signature
85
86
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053087def get_method_signature(bus, service, object_path, interface, method):
88 obj = bus.get_object(service, object_path)
89 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
90 xml_string = iface.Introspect()
91 arglist = []
92
93 root = ElementTree.fromstring(xml_string)
94 for dbus_intf in root.findall('interface'):
95 if (dbus_intf.get('name') == interface):
96 for dbus_method in dbus_intf.findall('method'):
97 if(dbus_method.get('name') == method):
98 for arg in dbus_method.findall('arg'):
99 arglist.append(arg.get('type'))
100 return arglist
101
102
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500103def split_struct_signature(signature):
104 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
105 struct_matches = re.findall(struct_regex, signature)
106 return struct_matches
107
108
109def convert_type(signature, value):
110 # Basic Types
111 converted_value = None
112 converted_container = None
CamVan Nguyen249d1322018-03-05 10:08:33 -0600113 # TODO: openbmc/openbmc#2994 remove python 2 support
114 try: # python 2
115 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
116 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
117 't': dbus.UInt64, 'd': float, 's': str}
118 except NameError: # python 3
119 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
120 'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32,
121 't': dbus.UInt64, 'd': float, 's': str}
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500122 array_matches = re.match(r'a\((\S+)\)', signature)
123 struct_matches = re.match(r'\((\S+)\)', signature)
124 dictionary_matches = re.match(r'a{(\S+)}', signature)
125 if signature in basic_types:
126 converted_value = basic_types[signature](value)
127 return converted_value
128 # Array
129 if array_matches:
130 element_type = array_matches.group(1)
131 converted_container = list()
132 # Test if value is a list
133 # to avoid iterating over each character in a string.
134 # Iterate over each item and convert type
135 if isinstance(value, list):
136 for i in value:
137 converted_element = convert_type(element_type, i)
138 converted_container.append(converted_element)
139 # Convert non-sequence to expected type, and append to list
140 else:
141 converted_element = convert_type(element_type, value)
142 converted_container.append(converted_element)
143 return converted_container
144 # Struct
145 if struct_matches:
146 element_types = struct_matches.group(1)
147 split_element_types = split_struct_signature(element_types)
148 converted_container = list()
149 # Test if value is a list
150 if isinstance(value, list):
151 for index, val in enumerate(value):
152 converted_element = convert_type(split_element_types[index],
153 value[index])
154 converted_container.append(converted_element)
155 else:
156 converted_element = convert_type(element_types, value)
157 converted_container.append(converted_element)
158 return tuple(converted_container)
159 # Dictionary
160 if dictionary_matches:
161 element_types = dictionary_matches.group(1)
162 split_element_types = split_struct_signature(element_types)
163 converted_container = dict()
164 # Convert each element of dict
CamVan Nguyen249d1322018-03-05 10:08:33 -0600165 for key, val in value.items():
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500166 converted_key = convert_type(split_element_types[0], key)
167 converted_val = convert_type(split_element_types[1], val)
168 converted_container[converted_key] = converted_val
169 return converted_container
170
171
Jayashankar Padathbec10c22018-05-29 18:22:59 +0530172def send_ws_ping(wsock, timeout) :
173 # Most webservers close websockets after 60 seconds of
174 # inactivity. Make sure to send a ping before that.
175 payload = "ping"
176 # the ping payload can be anything, the receiver has to just
177 # return the same back.
178 while True:
179 gevent.sleep(timeout)
180 try:
181 if wsock:
182 wsock.send_frame(payload, wsock.OPCODE_PING)
183 except Exception as e:
184 wsock.close()
185 return
186
187
Brad Bishop2f428582015-12-02 10:56:11 -0500188class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400189 ''' Authorization plugin callback that checks that the user is logged in
190 and a member of a group. '''
191 def __init__(self, group):
192 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500193
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 def __call__(self, session, *a, **kw):
195 valid_user(session, *a, **kw)
196 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500197
Brad Bishop87b63c12016-03-18 14:47:51 -0400198 try:
199 res = session['user'] in grp.getgrnam(self.group)[3]
200 except KeyError:
201 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500202
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 if not res:
204 abort(403, 'Insufficient access')
205
Brad Bishop2f428582015-12-02 10:56:11 -0500206
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500207class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400208 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500209 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400210
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500211 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400212 self.app = app
213 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500214 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400215 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400216 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500217 self._content_type = content_type
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400218
Brad Bishop88c76a42017-02-21 00:02:02 -0500219 if 'GET' in self._verbs:
220 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500221 if 'OPTIONS' not in self._verbs:
222 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500223
Brad Bishop87b63c12016-03-18 14:47:51 -0400224 def _setup(self, **kw):
225 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500226
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500228 if request.method != 'OPTIONS':
229 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500230
Brad Bishopd4c1c552017-02-21 00:07:28 -0500231 # Javascript implementations will not send credentials
232 # with an OPTIONS request. Don't help malicious clients
233 # by checking the path here and returning a 404 if the
234 # path doesn't exist.
235 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500236
Brad Bishopd4c1c552017-02-21 00:07:28 -0500237 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500238 raise HTTPError(
239 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400240
Brad Bishop87b63c12016-03-18 14:47:51 -0400241 def __call__(self, **kw):
242 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400243
Brad Bishop88c76a42017-02-21 00:02:02 -0500244 def do_head(self, **kw):
245 return self.do_get(**kw)
246
Brad Bishopd4c1c552017-02-21 00:07:28 -0500247 def do_options(self, **kw):
248 for v in self._verbs:
249 response.set_header(
250 'Allow',
251 ','.join(self._verbs))
252 return None
253
Brad Bishop87b63c12016-03-18 14:47:51 -0400254 def install(self):
255 self.app.route(
256 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500257 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400258
Brad Bishop87b63c12016-03-18 14:47:51 -0400259 @staticmethod
260 def try_mapper_call(f, callback=None, **kw):
261 try:
262 return f(**kw)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600263 except dbus.exceptions.DBusException as e:
Brad Bishopfce77562016-11-28 15:44:18 -0500264 if e.get_dbus_name() == \
265 'org.freedesktop.DBus.Error.ObjectPathInUse':
266 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500267 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400268 raise
269 if callback is None:
270 def callback(e, **kw):
271 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 callback(e, **kw)
274
275 @staticmethod
276 def try_properties_interface(f, *a):
277 try:
278 return f(*a)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600279 except dbus.exceptions.DBusException as e:
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -0600280 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name():
Brad Bishopf4e74982016-04-01 14:53:05 -0400281 # interface doesn't have any properties
282 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400283 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
284 # properties interface not implemented at all
285 return None
286 raise
287
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400288
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500289class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 verbs = 'GET'
291 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400292
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 def __init__(self, app, bus):
294 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400295 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400296
Brad Bishop87b63c12016-03-18 14:47:51 -0400297 def find(self, path='/'):
298 return self.try_mapper_call(
299 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400300
Brad Bishop87b63c12016-03-18 14:47:51 -0400301 def setup(self, path='/'):
302 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400303
Brad Bishop87b63c12016-03-18 14:47:51 -0400304 def do_get(self, path='/'):
305 return request.route_data['map']
306
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400307
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500308class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400309 verbs = 'GET'
310 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400311
Brad Bishop87b63c12016-03-18 14:47:51 -0400312 def __init__(self, app, bus):
313 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400314 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400315
Brad Bishop87b63c12016-03-18 14:47:51 -0400316 def find(self, path='/'):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600317 return list(self.try_mapper_call(
318 self.mapper.get_subtree, path=path).keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400319
Brad Bishop87b63c12016-03-18 14:47:51 -0400320 def setup(self, path='/'):
321 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400322
Brad Bishop87b63c12016-03-18 14:47:51 -0400323 def do_get(self, path='/'):
324 return request.route_data['map']
325
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400326
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500327class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400328 verbs = 'GET'
329 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400330
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 def __init__(self, app, bus):
332 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400333 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400334
Brad Bishop87b63c12016-03-18 14:47:51 -0400335 def find(self, path='/'):
336 return self.try_mapper_call(
337 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 def setup(self, path='/'):
340 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400341
Brad Bishop87b63c12016-03-18 14:47:51 -0400342 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400343 return {x: y for x, y in self.mapper.enumerate_subtree(
344 path,
345 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400346
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400347
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500348class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400349 verbs = 'POST'
350 rules = '<path:path>/action/<method>'
351 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500352 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400353
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 def __init__(self, app, bus):
355 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500356 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530357 self.service = ''
358 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400359
Brad Bishop87b63c12016-03-18 14:47:51 -0400360 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500361 method_list = []
Gunnar Mills313aadb2018-04-08 14:50:09 -0500362 buses = self.try_mapper_call(
Brad Bishop87b63c12016-03-18 14:47:51 -0400363 self.mapper.get_object, path=path)
Gunnar Mills313aadb2018-04-08 14:50:09 -0500364 for items in buses.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400365 m = self.find_method_on_bus(path, method, *items)
366 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500367 method_list.append(m)
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600368 if method_list:
369 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400370
Brad Bishop87b63c12016-03-18 14:47:51 -0400371 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400372
Brad Bishop87b63c12016-03-18 14:47:51 -0400373 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500374 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400375
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600376 def do_post(self, path, method, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400377 try:
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600378 args = []
379 if request.parameter_list:
380 args = request.parameter_list
381 # To see if the return type is capable of being merged
382 if len(request.route_data['map']) > 1:
383 results = None
384 for item in request.route_data['map']:
385 tmp = item(*args)
386 if not results:
387 if tmp is not None:
388 results = type(tmp)()
389 if isinstance(results, dict):
390 results = results.update(tmp)
391 elif isinstance(results, list):
392 results = results + tmp
393 elif isinstance(results, type(None)):
394 results = None
395 else:
396 abort(501, 'Don\'t know how to merge method call '
397 'results of {}'.format(type(tmp)))
398 return results
399 # There is only one method
400 return request.route_data['map'][0](*args)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400401
CamVan Nguyen249d1322018-03-05 10:08:33 -0600402 except dbus.exceptions.DBusException as e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530403 paramlist = []
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500404 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530405
406 signature_list = get_method_signature(self.bus, self.service,
407 path, self.interface,
408 method)
409 if not signature_list:
410 abort(400, "Failed to get method signature: %s" % str(e))
411 if len(signature_list) != len(request.parameter_list):
412 abort(400, "Invalid number of args")
413 converted_value = None
414 try:
415 for index, expected_type in enumerate(signature_list):
416 value = request.parameter_list[index]
417 converted_value = convert_type(expected_type, value)
418 paramlist.append(converted_value)
419 request.parameter_list = paramlist
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600420 self.do_post(path, method, False)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530421 return
422 except Exception as ex:
Nagaraju Gorugantiab404fa2017-12-14 10:24:40 -0600423 abort(400, "Bad Request/Invalid Args given")
Brad Bishop87b63c12016-03-18 14:47:51 -0400424 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530425
Brad Bishop87b63c12016-03-18 14:47:51 -0400426 if e.get_dbus_name() == DBUS_TYPE_ERROR:
427 abort(400, str(e))
428 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 @staticmethod
431 def find_method_in_interface(method, obj, interface, methods):
432 if methods is None:
433 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400434
CamVan Nguyen249d1322018-03-05 10:08:33 -0600435 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 if method is not None:
437 iface = dbus.Interface(obj, interface)
438 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400439
Brad Bishop87b63c12016-03-18 14:47:51 -0400440 def find_method_on_bus(self, path, method, bus, interfaces):
441 obj = self.bus.get_object(bus, path, introspect=False)
442 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
443 data = iface.Introspect()
444 parser = IntrospectionNodeParser(
445 ElementTree.fromstring(data),
Brad Bishopaeb995d2018-04-04 22:28:42 -0400446 intf_match=lambda x: x in interfaces)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600447 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400448 m = self.find_method_in_interface(
449 method, obj, x, y.get('method'))
450 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530451 self.service = bus
452 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400453 return m
454
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400455
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500456class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400457 verbs = ['PUT', 'GET']
458 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500459 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400460
Brad Bishop87b63c12016-03-18 14:47:51 -0400461 def __init__(self, app, bus):
462 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500463 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def find(self, path, prop):
466 self.app.instance_handler.setup(path)
467 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500468 real_name = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600469 prop, list(obj.keys()))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400470
Brad Bishop56ad87f2017-02-21 23:33:29 -0500471 if not real_name:
472 if request.method == 'PUT':
473 abort(403, _4034_msg % ('property', 'created', prop))
474 else:
475 abort(404, _4034_msg % ('property', 'found', prop))
476 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500477
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500479 name, obj = self.find(path, prop)
480 request.route_data['obj'] = obj
481 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500482
Brad Bishop87b63c12016-03-18 14:47:51 -0400483 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500484 name = request.route_data['name']
485 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500486
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600487 def do_put(self, path, prop, value=None, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 if value is None:
489 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500490
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 prop, iface, properties_iface = self.get_host_interface(
492 path, prop, request.route_data['map'][path])
493 try:
494 properties_iface.Set(iface, prop, value)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600495 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -0400496 abort(400, str(e))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600497 except dbus.exceptions.DBusException as e:
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500498 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500499 bus_name = properties_iface.bus_name
500 expected_type = get_type_signature_by_introspection(self.bus,
501 bus_name,
502 path,
503 prop)
504 if not expected_type:
505 abort(403, "Failed to get expected type: %s" % str(e))
506 converted_value = None
507 try:
508 converted_value = convert_type(expected_type, value)
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500509 except Exception as ex:
510 abort(403, "Failed to convert %s to type %s" %
511 (value, expected_type))
Lei YU1eea5c32018-07-12 15:32:37 +0800512 try:
513 self.do_put(path, prop, converted_value, False)
514 return
515 except Exception as ex:
516 abort(403, str(ex))
517
Brad Bishop87b63c12016-03-18 14:47:51 -0400518 abort(403, str(e))
519 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500520
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 def get_host_interface(self, path, prop, bus_info):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600522 for bus, interfaces in bus_info.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400523 obj = self.bus.get_object(bus, path, introspect=True)
524 properties_iface = dbus.Interface(
525 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500526
Brad Bishop87b63c12016-03-18 14:47:51 -0400527 info = self.get_host_interface_on_bus(
528 path, prop, properties_iface, bus, interfaces)
529 if info is not None:
530 prop, iface = info
531 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
534 for i in interfaces:
535 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500536 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400537 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500538 match = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600539 prop, list(properties.keys()))
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500540 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400541 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500542 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 return prop, i
544
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500545
Brad Bishop2503bd62015-12-16 17:56:12 -0500546class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400547 verbs = ['GET']
548 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500549
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 def __init__(self, app, bus):
551 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400552 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 def find(self, path):
555 return self.try_mapper_call(
556 self.mapper.get_object,
557 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 def setup(self, path):
560 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500561
Brad Bishop87b63c12016-03-18 14:47:51 -0400562 def do_get(self, path):
563 schema = {}
CamVan Nguyen249d1322018-03-05 10:08:33 -0600564 for x in request.route_data['map'].keys():
Brad Bishop87b63c12016-03-18 14:47:51 -0400565 obj = self.bus.get_object(x, path, introspect=False)
566 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
567 data = iface.Introspect()
568 parser = IntrospectionNodeParser(
569 ElementTree.fromstring(data))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600570 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400571 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500572
Brad Bishop87b63c12016-03-18 14:47:51 -0400573 return schema
574
Brad Bishop2503bd62015-12-16 17:56:12 -0500575
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500576class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400577 verbs = ['GET', 'PUT', 'DELETE']
578 rules = '<path:path>'
579 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500580
Brad Bishop87b63c12016-03-18 14:47:51 -0400581 def __init__(self, app, bus):
582 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400583 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500584
Brad Bishop87b63c12016-03-18 14:47:51 -0400585 def find(self, path, callback=None):
586 return {path: self.try_mapper_call(
587 self.mapper.get_object,
588 callback,
589 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500590
Brad Bishop87b63c12016-03-18 14:47:51 -0400591 def setup(self, path):
592 callback = None
593 if request.method == 'PUT':
594 def callback(e, **kw):
595 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500596
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 if request.route_data.get('map') is None:
598 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500599
Brad Bishop87b63c12016-03-18 14:47:51 -0400600 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400601 return self.mapper.enumerate_object(
602 path,
603 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500604
Brad Bishop87b63c12016-03-18 14:47:51 -0400605 def do_put(self, path):
606 # make sure all properties exist in the request
607 obj = set(self.do_get(path).keys())
608 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500609
Brad Bishop87b63c12016-03-18 14:47:51 -0400610 diff = list(obj.difference(req))
611 if diff:
612 abort(403, _4034_msg % (
613 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500614
Brad Bishop87b63c12016-03-18 14:47:51 -0400615 diff = list(req.difference(obj))
616 if diff:
617 abort(403, _4034_msg % (
618 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500619
CamVan Nguyen249d1322018-03-05 10:08:33 -0600620 for p, v in request.parameter_list.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400621 self.app.property_handler.do_put(
622 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500623
Brad Bishop87b63c12016-03-18 14:47:51 -0400624 def do_delete(self, path):
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500625 deleted = False
626 for bus, interfaces in request.route_data['map'][path].items():
627 if self.bus_has_delete(interfaces):
628 self.delete_on_bus(path, bus)
629 deleted = True
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500630
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500631 #It's OK if some objects didn't have a Delete, but not all
632 if not deleted:
633 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500634
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500635 def bus_has_delete(self, interfaces):
636 return DELETE_IFACE in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500637
Brad Bishop87b63c12016-03-18 14:47:51 -0400638 def delete_on_bus(self, path, bus):
639 obj = self.bus.get_object(bus, path, introspect=False)
640 delete_iface = dbus.Interface(
641 obj, dbus_interface=DELETE_IFACE)
642 delete_iface.Delete()
643
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500644
Brad Bishop2f428582015-12-02 10:56:11 -0500645class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400646 ''' Handles the /login and /logout routes, manages
647 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500648
Brad Bishop87b63c12016-03-18 14:47:51 -0400649 rules = ['/login', '/logout']
650 login_str = "User '%s' logged %s"
651 bad_passwd_str = "Invalid username or password"
652 no_user_str = "No user logged in"
653 bad_json_str = "Expecting request format { 'data': " \
654 "[<username>, <password>] }, got '%s'"
Alexander Filippovd08a4562018-03-20 12:02:23 +0300655 bmc_not_ready_str = "BMC is not ready (booting)"
Brad Bishop87b63c12016-03-18 14:47:51 -0400656 _require_auth = None
657 MAX_SESSIONS = 16
Alexander Filippovd08a4562018-03-20 12:02:23 +0300658 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
659 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
660 BMCSTATE_PROPERTY = 'CurrentBMCState'
661 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
Brad Bishop2f428582015-12-02 10:56:11 -0500662
Brad Bishop87b63c12016-03-18 14:47:51 -0400663 def __init__(self, app, bus):
664 super(SessionHandler, self).__init__(
665 app, bus)
666 self.hmac_key = os.urandom(128)
667 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500668
Brad Bishop87b63c12016-03-18 14:47:51 -0400669 @staticmethod
670 def authenticate(username, clear):
671 try:
672 encoded = spwd.getspnam(username)[1]
673 return encoded == crypt.crypt(clear, encoded)
674 except KeyError:
675 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500676
Brad Bishop87b63c12016-03-18 14:47:51 -0400677 def invalidate_session(self, session):
678 try:
679 self.session_store.remove(session)
680 except ValueError:
681 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500682
Brad Bishop87b63c12016-03-18 14:47:51 -0400683 def new_session(self):
684 sid = os.urandom(32)
685 if self.MAX_SESSIONS <= len(self.session_store):
686 self.session_store.pop()
687 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500688
Brad Bishop87b63c12016-03-18 14:47:51 -0400689 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500690
Brad Bishop87b63c12016-03-18 14:47:51 -0400691 def get_session(self, sid):
692 sids = [x['sid'] for x in self.session_store]
693 try:
694 return self.session_store[sids.index(sid)]
695 except ValueError:
696 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500697
Brad Bishop87b63c12016-03-18 14:47:51 -0400698 def get_session_from_cookie(self):
699 return self.get_session(
700 request.get_cookie(
701 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500702
Brad Bishop87b63c12016-03-18 14:47:51 -0400703 def do_post(self, **kw):
704 if request.path == '/login':
705 return self.do_login(**kw)
706 else:
707 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500708
Brad Bishop87b63c12016-03-18 14:47:51 -0400709 def do_logout(self, **kw):
710 session = self.get_session_from_cookie()
711 if session is not None:
712 user = session['user']
713 self.invalidate_session(session)
714 response.delete_cookie('sid')
715 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500716
Brad Bishop87b63c12016-03-18 14:47:51 -0400717 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500718
Brad Bishop87b63c12016-03-18 14:47:51 -0400719 def do_login(self, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400720 if len(request.parameter_list) != 2:
721 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500722
Brad Bishop87b63c12016-03-18 14:47:51 -0400723 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400724 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500725
Alexander Filippovd08a4562018-03-20 12:02:23 +0300726 force = False
727 try:
728 force = request.json.get('force')
729 except (ValueError, AttributeError, KeyError, TypeError):
730 force = False
731
732 if not force and not self.is_bmc_ready():
733 abort(503, self.bmc_not_ready_str)
734
Brad Bishop87b63c12016-03-18 14:47:51 -0400735 user = request.parameter_list[0]
736 session = self.new_session()
737 session['user'] = user
738 response.set_cookie(
739 'sid', session['sid'], secret=self.hmac_key,
740 secure=True,
741 httponly=True)
742 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500743
Alexander Filippovd08a4562018-03-20 12:02:23 +0300744 def is_bmc_ready(self):
745 if not self.app.with_bmc_check:
746 return True
747
748 try:
749 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
750 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
751 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
752 if state == self.BMCSTATE_READY:
753 return True
754
755 except dbus.exceptions.DBusException:
756 pass
757
758 return False
759
Brad Bishop87b63c12016-03-18 14:47:51 -0400760 def find(self, **kw):
761 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500762
Brad Bishop87b63c12016-03-18 14:47:51 -0400763 def setup(self, **kw):
764 pass
765
Brad Bishop2f428582015-12-02 10:56:11 -0500766
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500767class ImageUploadUtils:
768 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500769
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500770 file_loc = '/tmp/images'
771 file_prefix = 'img'
772 file_suffix = ''
Adriana Kobylak53693892018-03-12 13:05:50 -0500773 signal = None
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500774
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500775 @classmethod
776 def do_upload(cls, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500777 def cleanup():
778 os.close(handle)
779 if cls.signal:
780 cls.signal.remove()
781 cls.signal = None
782
783 def signal_callback(path, a, **kw):
784 # Just interested on the first Version interface created which is
785 # triggered when the file is uploaded. This helps avoid getting the
786 # wrong information for multiple upload requests in a row.
787 if "xyz.openbmc_project.Software.Version" in a and \
788 "xyz.openbmc_project.Software.Activation" not in a:
789 paths.append(path)
790
791 while cls.signal:
792 # Serialize uploads by waiting for the signal to be cleared.
793 # This makes it easier to ensure that the version information
794 # is the right one instead of the data from another upload request.
795 gevent.sleep(1)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500796 if not os.path.exists(cls.file_loc):
Gunnar Millsfb515792017-11-09 15:52:17 -0600797 abort(500, "Error Directory not found")
Adriana Kobylak53693892018-03-12 13:05:50 -0500798 paths = []
799 bus = dbus.SystemBus()
800 cls.signal = bus.add_signal_receiver(
801 signal_callback,
802 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
803 signal_name='InterfacesAdded',
804 path=SOFTWARE_PATH)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500805 if not filename:
806 handle, filename = tempfile.mkstemp(cls.file_suffix,
807 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500808 else:
809 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500810 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500811 try:
812 file_contents = request.body.read()
813 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500814 os.write(handle, file_contents)
Adriana Kobylak53693892018-03-12 13:05:50 -0500815 # Close file after writing, the image manager process watches for
816 # the close event to know the upload is complete.
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500817 os.close(handle)
Adriana Kobylak53693892018-03-12 13:05:50 -0500818 except (IOError, ValueError) as e:
819 cleanup()
820 abort(400, str(e))
821 except Exception:
822 cleanup()
823 abort(400, "Unexpected Error")
824 loop = gobject.MainLoop()
825 gcontext = loop.get_context()
826 count = 0
827 version_id = ''
828 while loop is not None:
829 try:
830 if gcontext.pending():
831 gcontext.iteration()
832 if not paths:
833 gevent.sleep(1)
834 else:
835 version_id = os.path.basename(paths.pop())
836 break
837 count += 1
838 if count == 10:
839 break
840 except Exception:
841 break
842 cls.signal.remove()
843 cls.signal = None
Adriana Kobylak97fe4352018-04-10 10:44:11 -0500844 if version_id:
845 return version_id
846 else:
847 abort(400, "Version already exists or failed to be extracted")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500848
849
850class ImagePostHandler(RouteHandler):
851 ''' Handles the /upload/image route. '''
852
853 verbs = ['POST']
854 rules = ['/upload/image']
855 content_type = 'application/octet-stream'
856
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500857 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500858 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500859 app, bus, self.verbs, self.rules, self.content_type)
860
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500861 def do_post(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500862 return ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500863
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500864 def find(self, **kw):
865 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500866
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500867 def setup(self, **kw):
868 pass
869
870
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500871class EventNotifier:
872 keyNames = {}
873 keyNames['event'] = 'event'
874 keyNames['path'] = 'path'
875 keyNames['intfMap'] = 'interfaces'
876 keyNames['propMap'] = 'properties'
877 keyNames['intf'] = 'interface'
878
879 def __init__(self, wsock, filters):
880 self.wsock = wsock
881 self.paths = filters.get("paths", [])
882 self.interfaces = filters.get("interfaces", [])
883 if not self.paths:
884 self.paths.append(None)
885 bus = dbus.SystemBus()
886 # Add a signal receiver for every path the client is interested in
887 for path in self.paths:
888 bus.add_signal_receiver(
889 self.interfaces_added_handler,
890 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
891 signal_name='InterfacesAdded',
892 path=path)
893 bus.add_signal_receiver(
894 self.properties_changed_handler,
895 dbus_interface=dbus.PROPERTIES_IFACE,
896 signal_name='PropertiesChanged',
897 path=path,
898 path_keyword='path')
899 loop = gobject.MainLoop()
900 # gobject's mainloop.run() will block the entire process, so the gevent
901 # scheduler and hence greenlets won't execute. The while-loop below
902 # works around this limitation by using gevent's sleep, instead of
903 # calling loop.run()
904 gcontext = loop.get_context()
905 while loop is not None:
906 try:
907 if gcontext.pending():
908 gcontext.iteration()
909 else:
910 # gevent.sleep puts only the current greenlet to sleep,
911 # not the entire process.
912 gevent.sleep(5)
913 except WebSocketError:
914 break
915
916 def interfaces_added_handler(self, path, iprops, **kw):
917 ''' If the client is interested in these changes, respond to the
918 client. This handles d-bus interface additions.'''
919 if (not self.interfaces) or \
920 (not set(iprops).isdisjoint(self.interfaces)):
921 response = {}
922 response[self.keyNames['event']] = "InterfacesAdded"
923 response[self.keyNames['path']] = path
924 response[self.keyNames['intfMap']] = iprops
925 try:
926 self.wsock.send(json.dumps(response))
927 except WebSocketError:
928 return
929
930 def properties_changed_handler(self, interface, new, old, **kw):
931 ''' If the client is interested in these changes, respond to the
932 client. This handles d-bus property changes. '''
933 if (not self.interfaces) or (interface in self.interfaces):
934 path = str(kw['path'])
935 response = {}
936 response[self.keyNames['event']] = "PropertiesChanged"
937 response[self.keyNames['path']] = path
938 response[self.keyNames['intf']] = interface
939 response[self.keyNames['propMap']] = new
940 try:
941 self.wsock.send(json.dumps(response))
942 except WebSocketError:
943 return
944
945
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500946class EventHandler(RouteHandler):
947 ''' Handles the /subscribe route, for clients to be able
948 to subscribe to BMC events. '''
949
950 verbs = ['GET']
951 rules = ['/subscribe']
952
953 def __init__(self, app, bus):
954 super(EventHandler, self).__init__(
955 app, bus, self.verbs, self.rules)
956
957 def find(self, **kw):
958 pass
959
960 def setup(self, **kw):
961 pass
962
963 def do_get(self):
964 wsock = request.environ.get('wsgi.websocket')
965 if not wsock:
966 abort(400, 'Expected WebSocket request.')
Jayashankar Padathbec10c22018-05-29 18:22:59 +0530967 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500968 filters = wsock.receive()
969 filters = json.loads(filters)
970 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500971
Deepak Kodihalli5c518f62018-04-23 03:26:38 -0500972class HostConsoleHandler(RouteHandler):
973 ''' Handles the /console route, for clients to be able
974 read/write the host serial console. The way this is
975 done is by exposing a websocket that's mirrored to an
976 abstract UNIX domain socket, which is the source for
977 the console data. '''
978
979 verbs = ['GET']
980 # Naming the route console0, because the numbering will help
981 # on multi-bmc/multi-host systems.
982 rules = ['/console0']
983
984 def __init__(self, app, bus):
985 super(HostConsoleHandler, self).__init__(
986 app, bus, self.verbs, self.rules)
987
988 def find(self, **kw):
989 pass
990
991 def setup(self, **kw):
992 pass
993
994 def read_wsock(self, wsock, sock):
995 while True:
996 try:
997 incoming = wsock.receive()
998 if incoming:
999 # Read websocket, write to UNIX socket
1000 sock.send(incoming)
1001 except Exception as e:
1002 sock.close()
1003 return
1004
1005 def read_sock(self, sock, wsock):
1006 max_sock_read_len = 4096
1007 while True:
1008 try:
1009 outgoing = sock.recv(max_sock_read_len)
1010 if outgoing:
1011 # Read UNIX socket, write to websocket
1012 wsock.send(outgoing)
1013 except Exception as e:
1014 wsock.close()
1015 return
1016
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001017 def do_get(self):
1018 wsock = request.environ.get('wsgi.websocket')
1019 if not wsock:
1020 abort(400, 'Expected WebSocket based request.')
1021
1022 # A UNIX domain socket structure defines a 108-byte pathname. The
1023 # server in this case, obmc-console-server, expects a 108-byte path.
1024 socket_name = "\0obmc-console"
1025 trailing_bytes = "\0" * (108 - len(socket_name))
1026 socket_path = socket_name + trailing_bytes
1027 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1028
1029 try:
1030 sock.connect(socket_path)
1031 except Exception as e:
1032 abort(500, str(e))
1033
1034 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1035 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
Jayashankar Padathbec10c22018-05-29 18:22:59 +05301036 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001037 gevent.joinall([wsock_reader, sock_reader, ping_sender])
1038
1039
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001040class ImagePutHandler(RouteHandler):
1041 ''' Handles the /upload/image/<filename> route. '''
1042
1043 verbs = ['PUT']
1044 rules = ['/upload/image/<filename>']
1045 content_type = 'application/octet-stream'
1046
1047 def __init__(self, app, bus):
1048 super(ImagePutHandler, self).__init__(
1049 app, bus, self.verbs, self.rules, self.content_type)
1050
1051 def do_put(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -05001052 return ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -05001053
1054 def find(self, **kw):
1055 pass
1056
1057 def setup(self, **kw):
1058 pass
1059
1060
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001061class DownloadDumpHandler(RouteHandler):
1062 ''' Handles the /download/dump route. '''
1063
1064 verbs = 'GET'
1065 rules = ['/download/dump/<dumpid>']
1066 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -05001067 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -04001068 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001069
1070 def __init__(self, app, bus):
1071 super(DownloadDumpHandler, self).__init__(
1072 app, bus, self.verbs, self.rules, self.content_type)
1073
1074 def do_get(self, dumpid):
1075 return self.do_download(dumpid)
1076
1077 def find(self, **kw):
1078 pass
1079
1080 def setup(self, **kw):
1081 pass
1082
1083 def do_download(self, dumpid):
1084 dump_loc = os.path.join(self.dump_loc, dumpid)
1085 if not os.path.exists(dump_loc):
1086 abort(404, "Path not found")
1087
1088 files = os.listdir(dump_loc)
1089 num_files = len(files)
1090 if num_files == 0:
1091 abort(404, "Dump not found")
1092
1093 return static_file(os.path.basename(files[0]), root=dump_loc,
1094 download=True, mimetype=self.content_type)
1095
1096
Matt Spinlerd41643e2018-02-02 13:51:38 -06001097class WebHandler(RouteHandler):
1098 ''' Handles the routes for the web UI files. '''
1099
1100 verbs = 'GET'
1101
1102 # Match only what we know are web files, so everything else
1103 # can get routed to the REST handlers.
1104 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1105 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1106 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1107 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1108 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1109 '/<filename:re:.+\.ico>']
1110
1111 # The mimetypes module knows about most types, but not these
1112 content_types = {
1113 '.eot': 'application/vnd.ms-fontobject',
1114 '.woff': 'application/x-font-woff',
1115 '.woff2': 'application/x-font-woff2',
1116 '.ttf': 'application/x-font-ttf',
1117 '.map': 'application/json'
1118 }
1119
1120 _require_auth = None
1121 suppress_json_resp = True
1122
1123 def __init__(self, app, bus):
1124 super(WebHandler, self).__init__(
1125 app, bus, self.verbs, self.rules)
1126
1127 def get_type(self, filename):
1128 ''' Returns the content type and encoding for a file '''
1129
1130 content_type, encoding = mimetypes.guess_type(filename)
1131
1132 # Try our own list if mimetypes didn't recognize it
1133 if content_type is None:
1134 if filename[-3:] == '.gz':
1135 filename = filename[:-3]
1136 extension = filename[filename.rfind('.'):]
1137 content_type = self.content_types.get(extension, None)
1138
1139 return content_type, encoding
1140
1141 def do_get(self, filename='index.html'):
1142
1143 # If a gzipped version exists, use that instead.
1144 # Possible future enhancement: if the client doesn't
1145 # accept compressed files, unzip it ourselves before sending.
1146 if not os.path.exists(os.path.join(www_base_path, filename)):
1147 filename = filename + '.gz'
1148
1149 # Though bottle should protect us, ensure path is valid
1150 realpath = os.path.realpath(filename)
1151 if realpath[0] == '/':
1152 realpath = realpath[1:]
1153 if not os.path.exists(os.path.join(www_base_path, realpath)):
1154 abort(404, "Path not found")
1155
1156 mimetype, encoding = self.get_type(filename)
1157
1158 # Couldn't find the type - let static_file() deal with it,
1159 # though this should never happen.
1160 if mimetype is None:
1161 print("Can't figure out content-type for %s" % filename)
1162 mimetype = 'auto'
1163
1164 # This call will set several header fields for us,
1165 # including the charset if the type is text.
1166 response = static_file(filename, www_base_path, mimetype)
1167
1168 # static_file() will only set the encoding if the
1169 # mimetype was auto, so set it here.
1170 if encoding is not None:
1171 response.set_header('Content-Encoding', encoding)
1172
1173 return response
1174
1175 def find(self, **kw):
1176 pass
1177
1178 def setup(self, **kw):
1179 pass
1180
1181
Brad Bishop2f428582015-12-02 10:56:11 -05001182class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001183 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -05001184
Brad Bishop87b63c12016-03-18 14:47:51 -04001185 name = 'authorization'
1186 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -05001187
Brad Bishop87b63c12016-03-18 14:47:51 -04001188 class Compose:
1189 def __init__(self, validators, callback, session_mgr):
1190 self.validators = validators
1191 self.callback = callback
1192 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -05001193
Brad Bishop87b63c12016-03-18 14:47:51 -04001194 def __call__(self, *a, **kw):
1195 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1196 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001197 if request.method != 'OPTIONS':
1198 for x in self.validators:
1199 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001200
Brad Bishop87b63c12016-03-18 14:47:51 -04001201 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001202
Brad Bishop87b63c12016-03-18 14:47:51 -04001203 def apply(self, callback, route):
1204 undecorated = route.get_undecorated_callback()
1205 if not isinstance(undecorated, RouteHandler):
1206 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001207
Brad Bishop87b63c12016-03-18 14:47:51 -04001208 auth_types = getattr(
1209 undecorated, '_require_auth', None)
1210 if not auth_types:
1211 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001212
Brad Bishop87b63c12016-03-18 14:47:51 -04001213 return self.Compose(
1214 auth_types, callback, undecorated.app.session_handler)
1215
Brad Bishop2f428582015-12-02 10:56:11 -05001216
Brad Bishopd0c404a2017-02-21 09:23:25 -05001217class CorsPlugin(object):
1218 ''' Add CORS headers. '''
1219
1220 name = 'cors'
1221 api = 2
1222
1223 @staticmethod
1224 def process_origin():
1225 origin = request.headers.get('Origin')
1226 if origin:
1227 response.add_header('Access-Control-Allow-Origin', origin)
1228 response.add_header(
1229 'Access-Control-Allow-Credentials', 'true')
1230
1231 @staticmethod
1232 def process_method_and_headers(verbs):
1233 method = request.headers.get('Access-Control-Request-Method')
1234 headers = request.headers.get('Access-Control-Request-Headers')
1235 if headers:
1236 headers = [x.lower() for x in headers.split(',')]
1237
1238 if method in verbs \
1239 and headers == ['content-type']:
1240 response.add_header('Access-Control-Allow-Methods', method)
1241 response.add_header(
1242 'Access-Control-Allow-Headers', 'Content-Type')
Ratan Gupta91b46f82018-01-14 12:52:23 +05301243 response.add_header('X-Frame-Options', 'deny')
1244 response.add_header('X-Content-Type-Options', 'nosniff')
1245 response.add_header('X-XSS-Protection', '1; mode=block')
1246 response.add_header(
1247 'Content-Security-Policy', "default-src 'self'")
1248 response.add_header(
1249 'Strict-Transport-Security',
1250 'max-age=31536000; includeSubDomains; preload')
Brad Bishopd0c404a2017-02-21 09:23:25 -05001251
1252 def __init__(self, app):
1253 app.install_error_callback(self.error_callback)
1254
1255 def apply(self, callback, route):
1256 undecorated = route.get_undecorated_callback()
1257 if not isinstance(undecorated, RouteHandler):
1258 return callback
1259
1260 if not getattr(undecorated, '_enable_cors', None):
1261 return callback
1262
1263 def wrap(*a, **kw):
1264 self.process_origin()
1265 self.process_method_and_headers(undecorated._verbs)
1266 return callback(*a, **kw)
1267
1268 return wrap
1269
1270 def error_callback(self, **kw):
1271 self.process_origin()
1272
1273
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001274class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001275 ''' Ensures request content satisfies the OpenBMC json api format. '''
1276 name = 'json_api_request'
1277 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001278
Brad Bishop87b63c12016-03-18 14:47:51 -04001279 error_str = "Expecting request format { 'data': <value> }, got '%s'"
1280 type_error_str = "Unsupported Content-Type: '%s'"
1281 json_type = "application/json"
1282 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001283
Brad Bishop87b63c12016-03-18 14:47:51 -04001284 @staticmethod
1285 def content_expected():
1286 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001287
Brad Bishop87b63c12016-03-18 14:47:51 -04001288 def validate_request(self):
1289 if request.content_length > 0 and \
1290 request.content_type != self.json_type:
1291 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001292
Brad Bishop87b63c12016-03-18 14:47:51 -04001293 try:
1294 request.parameter_list = request.json.get('data')
CamVan Nguyen249d1322018-03-05 10:08:33 -06001295 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -04001296 abort(400, str(e))
1297 except (AttributeError, KeyError, TypeError):
1298 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001299
Brad Bishop87b63c12016-03-18 14:47:51 -04001300 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001301 content_type = getattr(
1302 route.get_undecorated_callback(), '_content_type', None)
1303 if self.json_type != content_type:
1304 return callback
1305
Brad Bishop87b63c12016-03-18 14:47:51 -04001306 verbs = getattr(
1307 route.get_undecorated_callback(), '_verbs', None)
1308 if verbs is None:
1309 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001310
Brad Bishop87b63c12016-03-18 14:47:51 -04001311 if not set(self.request_methods).intersection(verbs):
1312 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001313
Brad Bishop87b63c12016-03-18 14:47:51 -04001314 def wrap(*a, **kw):
1315 if self.content_expected():
1316 self.validate_request()
1317 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001318
Brad Bishop87b63c12016-03-18 14:47:51 -04001319 return wrap
1320
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001321
1322class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001323 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1324 name = 'json_api_method_request'
1325 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001326
Brad Bishop87b63c12016-03-18 14:47:51 -04001327 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001328 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001329
Brad Bishop87b63c12016-03-18 14:47:51 -04001330 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001331 content_type = getattr(
1332 route.get_undecorated_callback(), '_content_type', None)
1333 if self.json_type != content_type:
1334 return callback
1335
Brad Bishop87b63c12016-03-18 14:47:51 -04001336 request_type = getattr(
1337 route.get_undecorated_callback(), 'request_type', None)
1338 if request_type is None:
1339 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001340
Brad Bishop87b63c12016-03-18 14:47:51 -04001341 def validate_request():
1342 if not isinstance(request.parameter_list, request_type):
1343 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001344
Brad Bishop87b63c12016-03-18 14:47:51 -04001345 def wrap(*a, **kw):
1346 if JsonApiRequestPlugin.content_expected():
1347 validate_request()
1348 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001349
Brad Bishop87b63c12016-03-18 14:47:51 -04001350 return wrap
1351
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001352
Brad Bishop080a48e2017-02-21 22:34:43 -05001353class JsonErrorsPlugin(JSONPlugin):
1354 ''' Extend the Bottle JSONPlugin such that it also encodes error
1355 responses. '''
1356
1357 def __init__(self, app, **kw):
1358 super(JsonErrorsPlugin, self).__init__(**kw)
1359 self.json_opts = {
CamVan Nguyen249d1322018-03-05 10:08:33 -06001360 x: y for x, y in kw.items()
Brad Bishop080a48e2017-02-21 22:34:43 -05001361 if x in ['indent', 'sort_keys']}
1362 app.install_error_callback(self.error_callback)
1363
1364 def error_callback(self, response_object, response_body, **kw):
1365 response_body['body'] = json.dumps(response_object, **self.json_opts)
1366 response.content_type = 'application/json'
1367
1368
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001369class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001370 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001371 name = 'json_api_response'
1372 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001373
Brad Bishopd4c1c552017-02-21 00:07:28 -05001374 @staticmethod
1375 def has_body():
1376 return request.method not in ['OPTIONS']
1377
Brad Bishop080a48e2017-02-21 22:34:43 -05001378 def __init__(self, app):
1379 app.install_error_callback(self.error_callback)
1380
Brad Bishop87b63c12016-03-18 14:47:51 -04001381 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001382 skip = getattr(
1383 route.get_undecorated_callback(), 'suppress_json_resp', None)
1384 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001385 return callback
1386
Brad Bishop87b63c12016-03-18 14:47:51 -04001387 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001388 data = callback(*a, **kw)
1389 if self.has_body():
1390 resp = {'data': data}
1391 resp['status'] = 'ok'
1392 resp['message'] = response.status_line
1393 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001394 return wrap
1395
Brad Bishop080a48e2017-02-21 22:34:43 -05001396 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001397 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001398 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001399 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001400 if error.status_code == 500:
1401 response_object['data']['exception'] = repr(error.exception)
1402 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001403
Brad Bishop87b63c12016-03-18 14:47:51 -04001404
Brad Bishop080a48e2017-02-21 22:34:43 -05001405class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001406 ''' Json javascript wrapper. '''
1407 name = 'jsonp'
1408 api = 2
1409
Brad Bishop080a48e2017-02-21 22:34:43 -05001410 def __init__(self, app, **kw):
1411 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001412
1413 @staticmethod
1414 def to_jsonp(json):
1415 jwrapper = request.query.callback or None
1416 if(jwrapper):
1417 response.set_header('Content-Type', 'application/javascript')
1418 json = jwrapper + '(' + json + ');'
1419 return json
1420
1421 def apply(self, callback, route):
1422 def wrap(*a, **kw):
1423 return self.to_jsonp(callback(*a, **kw))
1424 return wrap
1425
Brad Bishop080a48e2017-02-21 22:34:43 -05001426 def error_callback(self, response_body, **kw):
1427 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001428
1429
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001430class ContentCheckerPlugin(object):
1431 ''' Ensures that a route is associated with the expected content-type
1432 header. '''
1433 name = 'content_checker'
1434 api = 2
1435
1436 class Checker:
1437 def __init__(self, type, callback):
1438 self.expected_type = type
1439 self.callback = callback
1440 self.error_str = "Expecting content type '%s', got '%s'"
1441
1442 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001443 if request.method in ['PUT', 'POST', 'PATCH'] and \
1444 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001445 self.expected_type != request.content_type:
1446 abort(415, self.error_str % (self.expected_type,
1447 request.content_type))
1448
1449 return self.callback(*a, **kw)
1450
1451 def apply(self, callback, route):
1452 content_type = getattr(
1453 route.get_undecorated_callback(), '_content_type', None)
1454
1455 return self.Checker(content_type, callback)
1456
1457
Brad Bishop2c6fc762016-08-29 15:53:25 -04001458class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001459 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001460 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001461
1462 self.have_wsock = kw.get('have_wsock', False)
Alexander Filippovd08a4562018-03-20 12:02:23 +03001463 self.with_bmc_check = '--with-bmc-check' in sys.argv
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001464
Brad Bishop2ddfa002016-08-29 15:11:55 -04001465 self.bus = dbus.SystemBus()
1466 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001467 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001468
Brad Bishop87b63c12016-03-18 14:47:51 -04001469 self.install_hooks()
1470 self.install_plugins()
1471 self.create_handlers()
1472 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001473
Brad Bishop87b63c12016-03-18 14:47:51 -04001474 def install_plugins(self):
1475 # install json api plugins
1476 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001477 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001478 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001479 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001480 self.install(JsonpPlugin(self, **json_kw))
1481 self.install(JsonErrorsPlugin(self, **json_kw))
1482 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001483 self.install(JsonApiRequestPlugin())
1484 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001485
Brad Bishop87b63c12016-03-18 14:47:51 -04001486 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001487 self.error_handler_type = type(self.default_error_handler)
1488 self.original_error_handler = self.default_error_handler
1489 self.default_error_handler = self.error_handler_type(
1490 self.custom_error_handler, self, Bottle)
1491
Brad Bishop87b63c12016-03-18 14:47:51 -04001492 self.real_router_match = self.router.match
1493 self.router.match = self.custom_router_match
1494 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001495
Brad Bishop87b63c12016-03-18 14:47:51 -04001496 def create_handlers(self):
1497 # create route handlers
1498 self.session_handler = SessionHandler(self, self.bus)
Matt Spinlerd41643e2018-02-02 13:51:38 -06001499 self.web_handler = WebHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001500 self.directory_handler = DirectoryHandler(self, self.bus)
1501 self.list_names_handler = ListNamesHandler(self, self.bus)
1502 self.list_handler = ListHandler(self, self.bus)
1503 self.method_handler = MethodHandler(self, self.bus)
1504 self.property_handler = PropertyHandler(self, self.bus)
1505 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001506 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1507 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001508 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001509 if self.have_wsock:
1510 self.event_handler = EventHandler(self, self.bus)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001511 self.host_console_handler = HostConsoleHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001512 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001513
Brad Bishop87b63c12016-03-18 14:47:51 -04001514 def install_handlers(self):
1515 self.session_handler.install()
Matt Spinlerd41643e2018-02-02 13:51:38 -06001516 self.web_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001517 self.directory_handler.install()
1518 self.list_names_handler.install()
1519 self.list_handler.install()
1520 self.method_handler.install()
1521 self.property_handler.install()
1522 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001523 self.image_upload_post_handler.install()
1524 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001525 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001526 if self.have_wsock:
1527 self.event_handler.install()
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001528 self.host_console_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001529 # this has to come last, since it matches everything
1530 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001531
Brad Bishop080a48e2017-02-21 22:34:43 -05001532 def install_error_callback(self, callback):
1533 self.error_callbacks.insert(0, callback)
1534
Brad Bishop87b63c12016-03-18 14:47:51 -04001535 def custom_router_match(self, environ):
1536 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1537 needed doesn't work for us since the instance rules match
1538 everything. This monkey-patch lets the route handler figure
1539 out which response is needed. This could be accomplished
1540 with a hook but that would require calling the router match
1541 function twice.
1542 '''
1543 route, args = self.real_router_match(environ)
1544 if isinstance(route.callback, RouteHandler):
1545 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001546
Brad Bishop87b63c12016-03-18 14:47:51 -04001547 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001548
Brad Bishop080a48e2017-02-21 22:34:43 -05001549 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001550 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001551 error handler. '''
1552
1553 response_object = {}
1554 response_body = {}
1555 for x in self.error_callbacks:
1556 x(error=error,
1557 response_object=response_object,
1558 response_body=response_body)
1559
1560 return response_body.get('body', "")
1561
Brad Bishop87b63c12016-03-18 14:47:51 -04001562 @staticmethod
1563 def strip_extra_slashes():
1564 path = request.environ['PATH_INFO']
1565 trailing = ("", "/")[path[-1] == '/']
CamVan Nguyen249d1322018-03-05 10:08:33 -06001566 parts = list(filter(bool, path.split('/')))
Brad Bishop87b63c12016-03-18 14:47:51 -04001567 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing