blob: 605956cc888c7e977b50bc8647a177f494763117 [file] [log] [blame]
Brad Bishop68caa1e2016-03-04 15:42:08 -05001# Contributors Listed Below - COPYRIGHT 2016
2# [+] International Business Machines Corp.
3#
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied. See the License for the specific language governing
15# permissions and limitations under the License.
16
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050017import os
Brad Bishopaa65f6e2015-10-27 16:28:51 -040018import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import dbus.exceptions
20import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050021from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopb103d2d2016-03-04 16:19:14 -050023import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050024from obmc.dbuslib.introspection import IntrospectionNodeParser
25import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050026import spwd
27import grp
28import crypt
Deepak Kodihalli1af301a2017-04-11 07:29:01 -050029import tempfile
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050030import re
Brad Bishopaa65f6e2015-10-27 16:28:51 -040031
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050032DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040033DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050034DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
35DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050036DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050037DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050038
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050039_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040040
Brad Bishop87b63c12016-03-18 14:47:51 -040041
Brad Bishop2f428582015-12-02 10:56:11 -050042def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040043 ''' Authorization plugin callback that checks
44 that the user is logged in. '''
45 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040046 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040047
Brad Bishop2f428582015-12-02 10:56:11 -050048
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050049def get_type_signature_by_introspection(bus, service, object_path,
50 property_name):
51 obj = bus.get_object(service, object_path)
52 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
53 xml_string = iface.Introspect()
54 for child in ElementTree.fromstring(xml_string):
55 # Iterate over each interfaces's properties to find
56 # matching property_name, and return its signature string
57 if child.tag == 'interface':
58 for i in child.iter():
59 if ('name' in i.attrib) and \
60 (i.attrib['name'] == property_name):
61 type_signature = i.attrib['type']
62 return type_signature
63
64
65def split_struct_signature(signature):
66 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
67 struct_matches = re.findall(struct_regex, signature)
68 return struct_matches
69
70
71def convert_type(signature, value):
72 # Basic Types
73 converted_value = None
74 converted_container = None
75 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
76 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
77 't': dbus.UInt64, 'd': float, 's': str}
78 array_matches = re.match(r'a\((\S+)\)', signature)
79 struct_matches = re.match(r'\((\S+)\)', signature)
80 dictionary_matches = re.match(r'a{(\S+)}', signature)
81 if signature in basic_types:
82 converted_value = basic_types[signature](value)
83 return converted_value
84 # Array
85 if array_matches:
86 element_type = array_matches.group(1)
87 converted_container = list()
88 # Test if value is a list
89 # to avoid iterating over each character in a string.
90 # Iterate over each item and convert type
91 if isinstance(value, list):
92 for i in value:
93 converted_element = convert_type(element_type, i)
94 converted_container.append(converted_element)
95 # Convert non-sequence to expected type, and append to list
96 else:
97 converted_element = convert_type(element_type, value)
98 converted_container.append(converted_element)
99 return converted_container
100 # Struct
101 if struct_matches:
102 element_types = struct_matches.group(1)
103 split_element_types = split_struct_signature(element_types)
104 converted_container = list()
105 # Test if value is a list
106 if isinstance(value, list):
107 for index, val in enumerate(value):
108 converted_element = convert_type(split_element_types[index],
109 value[index])
110 converted_container.append(converted_element)
111 else:
112 converted_element = convert_type(element_types, value)
113 converted_container.append(converted_element)
114 return tuple(converted_container)
115 # Dictionary
116 if dictionary_matches:
117 element_types = dictionary_matches.group(1)
118 split_element_types = split_struct_signature(element_types)
119 converted_container = dict()
120 # Convert each element of dict
121 for key, val in value.iteritems():
122 converted_key = convert_type(split_element_types[0], key)
123 converted_val = convert_type(split_element_types[1], val)
124 converted_container[converted_key] = converted_val
125 return converted_container
126
127
Brad Bishop2f428582015-12-02 10:56:11 -0500128class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400129 ''' Authorization plugin callback that checks that the user is logged in
130 and a member of a group. '''
131 def __init__(self, group):
132 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500133
Brad Bishop87b63c12016-03-18 14:47:51 -0400134 def __call__(self, session, *a, **kw):
135 valid_user(session, *a, **kw)
136 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500137
Brad Bishop87b63c12016-03-18 14:47:51 -0400138 try:
139 res = session['user'] in grp.getgrnam(self.group)[3]
140 except KeyError:
141 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500142
Brad Bishop87b63c12016-03-18 14:47:51 -0400143 if not res:
144 abort(403, 'Insufficient access')
145
Brad Bishop2f428582015-12-02 10:56:11 -0500146
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500147class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400148 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500149 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500151 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400152 self.app = app
153 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500154 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400155 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400156 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500157 self._content_type = content_type
Brad Bishop0f79e522016-03-18 13:33:17 -0400158 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400159
Brad Bishop88c76a42017-02-21 00:02:02 -0500160 if 'GET' in self._verbs:
161 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500162 if 'OPTIONS' not in self._verbs:
163 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500164
Brad Bishop87b63c12016-03-18 14:47:51 -0400165 def _setup(self, **kw):
166 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500167
Brad Bishop87b63c12016-03-18 14:47:51 -0400168 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500169 if request.method != 'OPTIONS':
170 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500171
Brad Bishopd4c1c552017-02-21 00:07:28 -0500172 # Javascript implementations will not send credentials
173 # with an OPTIONS request. Don't help malicious clients
174 # by checking the path here and returning a 404 if the
175 # path doesn't exist.
176 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500177
Brad Bishopd4c1c552017-02-21 00:07:28 -0500178 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500179 raise HTTPError(
180 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400181
Brad Bishop87b63c12016-03-18 14:47:51 -0400182 def __call__(self, **kw):
183 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400184
Brad Bishop88c76a42017-02-21 00:02:02 -0500185 def do_head(self, **kw):
186 return self.do_get(**kw)
187
Brad Bishopd4c1c552017-02-21 00:07:28 -0500188 def do_options(self, **kw):
189 for v in self._verbs:
190 response.set_header(
191 'Allow',
192 ','.join(self._verbs))
193 return None
194
Brad Bishop87b63c12016-03-18 14:47:51 -0400195 def install(self):
196 self.app.route(
197 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500198 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400199
Brad Bishop87b63c12016-03-18 14:47:51 -0400200 @staticmethod
201 def try_mapper_call(f, callback=None, **kw):
202 try:
203 return f(**kw)
204 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500205 if e.get_dbus_name() == \
206 'org.freedesktop.DBus.Error.ObjectPathInUse':
207 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500208 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400209 raise
210 if callback is None:
211 def callback(e, **kw):
212 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400213
Brad Bishop87b63c12016-03-18 14:47:51 -0400214 callback(e, **kw)
215
216 @staticmethod
217 def try_properties_interface(f, *a):
218 try:
219 return f(*a)
220 except dbus.exceptions.DBusException, e:
221 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
222 # interface doesn't have any properties
223 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400224 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
225 # interface doesn't have any properties
226 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
228 # properties interface not implemented at all
229 return None
230 raise
231
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400232
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500233class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400234 verbs = 'GET'
235 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400236
Brad Bishop87b63c12016-03-18 14:47:51 -0400237 def __init__(self, app, bus):
238 super(DirectoryHandler, self).__init__(
239 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400240
Brad Bishop87b63c12016-03-18 14:47:51 -0400241 def find(self, path='/'):
242 return self.try_mapper_call(
243 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400244
Brad Bishop87b63c12016-03-18 14:47:51 -0400245 def setup(self, path='/'):
246 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400247
Brad Bishop87b63c12016-03-18 14:47:51 -0400248 def do_get(self, path='/'):
249 return request.route_data['map']
250
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400251
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500252class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400253 verbs = 'GET'
254 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400255
Brad Bishop87b63c12016-03-18 14:47:51 -0400256 def __init__(self, app, bus):
257 super(ListNamesHandler, self).__init__(
258 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400259
Brad Bishop87b63c12016-03-18 14:47:51 -0400260 def find(self, path='/'):
261 return self.try_mapper_call(
262 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400263
Brad Bishop87b63c12016-03-18 14:47:51 -0400264 def setup(self, path='/'):
265 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400266
Brad Bishop87b63c12016-03-18 14:47:51 -0400267 def do_get(self, path='/'):
268 return request.route_data['map']
269
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400270
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500271class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400272 verbs = 'GET'
273 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400274
Brad Bishop87b63c12016-03-18 14:47:51 -0400275 def __init__(self, app, bus):
276 super(ListHandler, self).__init__(
277 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def find(self, path='/'):
280 return self.try_mapper_call(
281 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400282
Brad Bishop87b63c12016-03-18 14:47:51 -0400283 def setup(self, path='/'):
284 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400287 return {x: y for x, y in self.mapper.enumerate_subtree(
288 path,
289 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400290
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400291
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500292class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 verbs = 'POST'
294 rules = '<path:path>/action/<method>'
295 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500296 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400297
Brad Bishop87b63c12016-03-18 14:47:51 -0400298 def __init__(self, app, bus):
299 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500300 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400301
Brad Bishop87b63c12016-03-18 14:47:51 -0400302 def find(self, path, method):
303 busses = self.try_mapper_call(
304 self.mapper.get_object, path=path)
305 for items in busses.iteritems():
306 m = self.find_method_on_bus(path, method, *items)
307 if m:
308 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400309
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400311
Brad Bishop87b63c12016-03-18 14:47:51 -0400312 def setup(self, path, method):
313 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400314
Brad Bishop87b63c12016-03-18 14:47:51 -0400315 def do_post(self, path, method):
316 try:
317 if request.parameter_list:
318 return request.route_data['method'](*request.parameter_list)
319 else:
320 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400321
Brad Bishop87b63c12016-03-18 14:47:51 -0400322 except dbus.exceptions.DBusException, e:
323 if e.get_dbus_name() == DBUS_INVALID_ARGS:
324 abort(400, str(e))
325 if e.get_dbus_name() == DBUS_TYPE_ERROR:
326 abort(400, str(e))
327 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400328
Brad Bishop87b63c12016-03-18 14:47:51 -0400329 @staticmethod
330 def find_method_in_interface(method, obj, interface, methods):
331 if methods is None:
332 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400333
Brad Bishop6d190602016-04-15 13:09:39 -0400334 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400335 if method is not None:
336 iface = dbus.Interface(obj, interface)
337 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 def find_method_on_bus(self, path, method, bus, interfaces):
340 obj = self.bus.get_object(bus, path, introspect=False)
341 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
342 data = iface.Introspect()
343 parser = IntrospectionNodeParser(
344 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500345 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400346 for x, y in parser.get_interfaces().iteritems():
347 m = self.find_method_in_interface(
348 method, obj, x, y.get('method'))
349 if m:
350 return m
351
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400352
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500353class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 verbs = ['PUT', 'GET']
355 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500356 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400357
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 def __init__(self, app, bus):
359 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500360 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400361
Brad Bishop87b63c12016-03-18 14:47:51 -0400362 def find(self, path, prop):
363 self.app.instance_handler.setup(path)
364 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500365 real_name = obmc.utils.misc.find_case_insensitive(
366 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400367
Brad Bishop56ad87f2017-02-21 23:33:29 -0500368 if not real_name:
369 if request.method == 'PUT':
370 abort(403, _4034_msg % ('property', 'created', prop))
371 else:
372 abort(404, _4034_msg % ('property', 'found', prop))
373 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500374
Brad Bishop87b63c12016-03-18 14:47:51 -0400375 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500376 name, obj = self.find(path, prop)
377 request.route_data['obj'] = obj
378 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500379
Brad Bishop87b63c12016-03-18 14:47:51 -0400380 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500381 name = request.route_data['name']
382 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500383
Brad Bishop87b63c12016-03-18 14:47:51 -0400384 def do_put(self, path, prop, value=None):
385 if value is None:
386 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500387
Brad Bishop87b63c12016-03-18 14:47:51 -0400388 prop, iface, properties_iface = self.get_host_interface(
389 path, prop, request.route_data['map'][path])
390 try:
391 properties_iface.Set(iface, prop, value)
392 except ValueError, e:
393 abort(400, str(e))
394 except dbus.exceptions.DBusException, e:
395 if e.get_dbus_name() == DBUS_INVALID_ARGS:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500396 bus_name = properties_iface.bus_name
397 expected_type = get_type_signature_by_introspection(self.bus,
398 bus_name,
399 path,
400 prop)
401 if not expected_type:
402 abort(403, "Failed to get expected type: %s" % str(e))
403 converted_value = None
404 try:
405 converted_value = convert_type(expected_type, value)
406 self.do_put(path, prop, converted_value)
407 return
408 except Exception as ex:
409 abort(403, "Failed to convert %s to type %s" %
410 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400411 abort(403, str(e))
412 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500413
Brad Bishop87b63c12016-03-18 14:47:51 -0400414 def get_host_interface(self, path, prop, bus_info):
415 for bus, interfaces in bus_info.iteritems():
416 obj = self.bus.get_object(bus, path, introspect=True)
417 properties_iface = dbus.Interface(
418 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500419
Brad Bishop87b63c12016-03-18 14:47:51 -0400420 info = self.get_host_interface_on_bus(
421 path, prop, properties_iface, bus, interfaces)
422 if info is not None:
423 prop, iface = info
424 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500425
Brad Bishop87b63c12016-03-18 14:47:51 -0400426 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
427 for i in interfaces:
428 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500429 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500431 match = obmc.utils.misc.find_case_insensitive(
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500432 prop, properties.keys())
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500433 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400434 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500435 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 return prop, i
437
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500438
Brad Bishop2503bd62015-12-16 17:56:12 -0500439class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400440 verbs = ['GET']
441 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500442
Brad Bishop87b63c12016-03-18 14:47:51 -0400443 def __init__(self, app, bus):
444 super(SchemaHandler, self).__init__(
445 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500446
Brad Bishop87b63c12016-03-18 14:47:51 -0400447 def find(self, path):
448 return self.try_mapper_call(
449 self.mapper.get_object,
450 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500451
Brad Bishop87b63c12016-03-18 14:47:51 -0400452 def setup(self, path):
453 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500454
Brad Bishop87b63c12016-03-18 14:47:51 -0400455 def do_get(self, path):
456 schema = {}
457 for x in request.route_data['map'].iterkeys():
458 obj = self.bus.get_object(x, path, introspect=False)
459 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
460 data = iface.Introspect()
461 parser = IntrospectionNodeParser(
462 ElementTree.fromstring(data))
463 for x, y in parser.get_interfaces().iteritems():
464 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500465
Brad Bishop87b63c12016-03-18 14:47:51 -0400466 return schema
467
Brad Bishop2503bd62015-12-16 17:56:12 -0500468
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500469class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 verbs = ['GET', 'PUT', 'DELETE']
471 rules = '<path:path>'
472 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500473
Brad Bishop87b63c12016-03-18 14:47:51 -0400474 def __init__(self, app, bus):
475 super(InstanceHandler, self).__init__(
476 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500477
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 def find(self, path, callback=None):
479 return {path: self.try_mapper_call(
480 self.mapper.get_object,
481 callback,
482 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 def setup(self, path):
485 callback = None
486 if request.method == 'PUT':
487 def callback(e, **kw):
488 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500489
Brad Bishop87b63c12016-03-18 14:47:51 -0400490 if request.route_data.get('map') is None:
491 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500492
Brad Bishop87b63c12016-03-18 14:47:51 -0400493 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400494 return self.mapper.enumerate_object(
495 path,
496 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500497
Brad Bishop87b63c12016-03-18 14:47:51 -0400498 def do_put(self, path):
499 # make sure all properties exist in the request
500 obj = set(self.do_get(path).keys())
501 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500502
Brad Bishop87b63c12016-03-18 14:47:51 -0400503 diff = list(obj.difference(req))
504 if diff:
505 abort(403, _4034_msg % (
506 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500507
Brad Bishop87b63c12016-03-18 14:47:51 -0400508 diff = list(req.difference(obj))
509 if diff:
510 abort(403, _4034_msg % (
511 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500512
Brad Bishop87b63c12016-03-18 14:47:51 -0400513 for p, v in request.parameter_list.iteritems():
514 self.app.property_handler.do_put(
515 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500516
Brad Bishop87b63c12016-03-18 14:47:51 -0400517 def do_delete(self, path):
518 for bus_info in request.route_data['map'][path].iteritems():
519 if self.bus_missing_delete(path, *bus_info):
520 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500521
Brad Bishop87b63c12016-03-18 14:47:51 -0400522 for bus in request.route_data['map'][path].iterkeys():
523 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500524
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 def bus_missing_delete(self, path, bus, interfaces):
526 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500527
Brad Bishop87b63c12016-03-18 14:47:51 -0400528 def delete_on_bus(self, path, bus):
529 obj = self.bus.get_object(bus, path, introspect=False)
530 delete_iface = dbus.Interface(
531 obj, dbus_interface=DELETE_IFACE)
532 delete_iface.Delete()
533
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500534
Brad Bishop2f428582015-12-02 10:56:11 -0500535class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400536 ''' Handles the /login and /logout routes, manages
537 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500538
Brad Bishop87b63c12016-03-18 14:47:51 -0400539 rules = ['/login', '/logout']
540 login_str = "User '%s' logged %s"
541 bad_passwd_str = "Invalid username or password"
542 no_user_str = "No user logged in"
543 bad_json_str = "Expecting request format { 'data': " \
544 "[<username>, <password>] }, got '%s'"
545 _require_auth = None
546 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500547
Brad Bishop87b63c12016-03-18 14:47:51 -0400548 def __init__(self, app, bus):
549 super(SessionHandler, self).__init__(
550 app, bus)
551 self.hmac_key = os.urandom(128)
552 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 @staticmethod
555 def authenticate(username, clear):
556 try:
557 encoded = spwd.getspnam(username)[1]
558 return encoded == crypt.crypt(clear, encoded)
559 except KeyError:
560 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500561
Brad Bishop87b63c12016-03-18 14:47:51 -0400562 def invalidate_session(self, session):
563 try:
564 self.session_store.remove(session)
565 except ValueError:
566 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500567
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 def new_session(self):
569 sid = os.urandom(32)
570 if self.MAX_SESSIONS <= len(self.session_store):
571 self.session_store.pop()
572 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500573
Brad Bishop87b63c12016-03-18 14:47:51 -0400574 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 def get_session(self, sid):
577 sids = [x['sid'] for x in self.session_store]
578 try:
579 return self.session_store[sids.index(sid)]
580 except ValueError:
581 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500582
Brad Bishop87b63c12016-03-18 14:47:51 -0400583 def get_session_from_cookie(self):
584 return self.get_session(
585 request.get_cookie(
586 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 def do_post(self, **kw):
589 if request.path == '/login':
590 return self.do_login(**kw)
591 else:
592 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 def do_logout(self, **kw):
595 session = self.get_session_from_cookie()
596 if session is not None:
597 user = session['user']
598 self.invalidate_session(session)
599 response.delete_cookie('sid')
600 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500601
Brad Bishop87b63c12016-03-18 14:47:51 -0400602 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500603
Brad Bishop87b63c12016-03-18 14:47:51 -0400604 def do_login(self, **kw):
605 session = self.get_session_from_cookie()
606 if session is not None:
607 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500608
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 if len(request.parameter_list) != 2:
610 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500611
Brad Bishop87b63c12016-03-18 14:47:51 -0400612 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400613 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500614
Brad Bishop87b63c12016-03-18 14:47:51 -0400615 user = request.parameter_list[0]
616 session = self.new_session()
617 session['user'] = user
618 response.set_cookie(
619 'sid', session['sid'], secret=self.hmac_key,
620 secure=True,
621 httponly=True)
622 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500623
Brad Bishop87b63c12016-03-18 14:47:51 -0400624 def find(self, **kw):
625 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 def setup(self, **kw):
628 pass
629
Brad Bishop2f428582015-12-02 10:56:11 -0500630
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500631class ImageUploadUtils:
632 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500633
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500634 file_loc = '/tmp/images'
635 file_prefix = 'img'
636 file_suffix = ''
637
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500638 @classmethod
639 def do_upload(cls, filename=''):
640 if not os.path.exists(cls.file_loc):
641 os.makedirs(cls.file_loc)
642 if not filename:
643 handle, filename = tempfile.mkstemp(cls.file_suffix,
644 cls.file_prefix, cls.file_loc)
645 os.close(handle)
646 else:
647 filename = os.path.join(cls.file_loc, filename)
648
649 with open(filename, "w") as fd:
650 fd.write(request.body.read())
651
652
653class ImagePostHandler(RouteHandler):
654 ''' Handles the /upload/image route. '''
655
656 verbs = ['POST']
657 rules = ['/upload/image']
658 content_type = 'application/octet-stream'
659
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500660 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500661 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500662 app, bus, self.verbs, self.rules, self.content_type)
663
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500664 def do_post(self, filename=''):
665 ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500666
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500667 def find(self, **kw):
668 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500669
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500670 def setup(self, **kw):
671 pass
672
673
674class ImagePutHandler(RouteHandler):
675 ''' Handles the /upload/image/<filename> route. '''
676
677 verbs = ['PUT']
678 rules = ['/upload/image/<filename>']
679 content_type = 'application/octet-stream'
680
681 def __init__(self, app, bus):
682 super(ImagePutHandler, self).__init__(
683 app, bus, self.verbs, self.rules, self.content_type)
684
685 def do_put(self, filename=''):
686 ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500687
688 def find(self, **kw):
689 pass
690
691 def setup(self, **kw):
692 pass
693
694
Brad Bishop2f428582015-12-02 10:56:11 -0500695class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400696 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500697
Brad Bishop87b63c12016-03-18 14:47:51 -0400698 name = 'authorization'
699 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500700
Brad Bishop87b63c12016-03-18 14:47:51 -0400701 class Compose:
702 def __init__(self, validators, callback, session_mgr):
703 self.validators = validators
704 self.callback = callback
705 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500706
Brad Bishop87b63c12016-03-18 14:47:51 -0400707 def __call__(self, *a, **kw):
708 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
709 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500710 if request.method != 'OPTIONS':
711 for x in self.validators:
712 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500715
Brad Bishop87b63c12016-03-18 14:47:51 -0400716 def apply(self, callback, route):
717 undecorated = route.get_undecorated_callback()
718 if not isinstance(undecorated, RouteHandler):
719 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500720
Brad Bishop87b63c12016-03-18 14:47:51 -0400721 auth_types = getattr(
722 undecorated, '_require_auth', None)
723 if not auth_types:
724 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500725
Brad Bishop87b63c12016-03-18 14:47:51 -0400726 return self.Compose(
727 auth_types, callback, undecorated.app.session_handler)
728
Brad Bishop2f428582015-12-02 10:56:11 -0500729
Brad Bishopd0c404a2017-02-21 09:23:25 -0500730class CorsPlugin(object):
731 ''' Add CORS headers. '''
732
733 name = 'cors'
734 api = 2
735
736 @staticmethod
737 def process_origin():
738 origin = request.headers.get('Origin')
739 if origin:
740 response.add_header('Access-Control-Allow-Origin', origin)
741 response.add_header(
742 'Access-Control-Allow-Credentials', 'true')
743
744 @staticmethod
745 def process_method_and_headers(verbs):
746 method = request.headers.get('Access-Control-Request-Method')
747 headers = request.headers.get('Access-Control-Request-Headers')
748 if headers:
749 headers = [x.lower() for x in headers.split(',')]
750
751 if method in verbs \
752 and headers == ['content-type']:
753 response.add_header('Access-Control-Allow-Methods', method)
754 response.add_header(
755 'Access-Control-Allow-Headers', 'Content-Type')
756
757 def __init__(self, app):
758 app.install_error_callback(self.error_callback)
759
760 def apply(self, callback, route):
761 undecorated = route.get_undecorated_callback()
762 if not isinstance(undecorated, RouteHandler):
763 return callback
764
765 if not getattr(undecorated, '_enable_cors', None):
766 return callback
767
768 def wrap(*a, **kw):
769 self.process_origin()
770 self.process_method_and_headers(undecorated._verbs)
771 return callback(*a, **kw)
772
773 return wrap
774
775 def error_callback(self, **kw):
776 self.process_origin()
777
778
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500779class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400780 ''' Ensures request content satisfies the OpenBMC json api format. '''
781 name = 'json_api_request'
782 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500783
Brad Bishop87b63c12016-03-18 14:47:51 -0400784 error_str = "Expecting request format { 'data': <value> }, got '%s'"
785 type_error_str = "Unsupported Content-Type: '%s'"
786 json_type = "application/json"
787 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500788
Brad Bishop87b63c12016-03-18 14:47:51 -0400789 @staticmethod
790 def content_expected():
791 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500792
Brad Bishop87b63c12016-03-18 14:47:51 -0400793 def validate_request(self):
794 if request.content_length > 0 and \
795 request.content_type != self.json_type:
796 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500797
Brad Bishop87b63c12016-03-18 14:47:51 -0400798 try:
799 request.parameter_list = request.json.get('data')
800 except ValueError, e:
801 abort(400, str(e))
802 except (AttributeError, KeyError, TypeError):
803 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500804
Brad Bishop87b63c12016-03-18 14:47:51 -0400805 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500806 content_type = getattr(
807 route.get_undecorated_callback(), '_content_type', None)
808 if self.json_type != content_type:
809 return callback
810
Brad Bishop87b63c12016-03-18 14:47:51 -0400811 verbs = getattr(
812 route.get_undecorated_callback(), '_verbs', None)
813 if verbs is None:
814 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500815
Brad Bishop87b63c12016-03-18 14:47:51 -0400816 if not set(self.request_methods).intersection(verbs):
817 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500818
Brad Bishop87b63c12016-03-18 14:47:51 -0400819 def wrap(*a, **kw):
820 if self.content_expected():
821 self.validate_request()
822 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500823
Brad Bishop87b63c12016-03-18 14:47:51 -0400824 return wrap
825
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500826
827class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400828 ''' Ensures request content type satisfies the OpenBMC json api format. '''
829 name = 'json_api_method_request'
830 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500831
Brad Bishop87b63c12016-03-18 14:47:51 -0400832 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500833 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500834
Brad Bishop87b63c12016-03-18 14:47:51 -0400835 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -0500836 content_type = getattr(
837 route.get_undecorated_callback(), '_content_type', None)
838 if self.json_type != content_type:
839 return callback
840
Brad Bishop87b63c12016-03-18 14:47:51 -0400841 request_type = getattr(
842 route.get_undecorated_callback(), 'request_type', None)
843 if request_type is None:
844 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500845
Brad Bishop87b63c12016-03-18 14:47:51 -0400846 def validate_request():
847 if not isinstance(request.parameter_list, request_type):
848 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500849
Brad Bishop87b63c12016-03-18 14:47:51 -0400850 def wrap(*a, **kw):
851 if JsonApiRequestPlugin.content_expected():
852 validate_request()
853 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500854
Brad Bishop87b63c12016-03-18 14:47:51 -0400855 return wrap
856
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500857
Brad Bishop080a48e2017-02-21 22:34:43 -0500858class JsonErrorsPlugin(JSONPlugin):
859 ''' Extend the Bottle JSONPlugin such that it also encodes error
860 responses. '''
861
862 def __init__(self, app, **kw):
863 super(JsonErrorsPlugin, self).__init__(**kw)
864 self.json_opts = {
865 x: y for x, y in kw.iteritems()
866 if x in ['indent', 'sort_keys']}
867 app.install_error_callback(self.error_callback)
868
869 def error_callback(self, response_object, response_body, **kw):
870 response_body['body'] = json.dumps(response_object, **self.json_opts)
871 response.content_type = 'application/json'
872
873
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500874class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500875 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400876 name = 'json_api_response'
877 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500878
Brad Bishopd4c1c552017-02-21 00:07:28 -0500879 @staticmethod
880 def has_body():
881 return request.method not in ['OPTIONS']
882
Brad Bishop080a48e2017-02-21 22:34:43 -0500883 def __init__(self, app):
884 app.install_error_callback(self.error_callback)
885
Brad Bishop87b63c12016-03-18 14:47:51 -0400886 def apply(self, callback, route):
887 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500888 data = callback(*a, **kw)
889 if self.has_body():
890 resp = {'data': data}
891 resp['status'] = 'ok'
892 resp['message'] = response.status_line
893 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400894 return wrap
895
Brad Bishop080a48e2017-02-21 22:34:43 -0500896 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400897 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -0500898 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -0500899 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400900 if error.status_code == 500:
901 response_object['data']['exception'] = repr(error.exception)
902 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500903
Brad Bishop87b63c12016-03-18 14:47:51 -0400904
Brad Bishop080a48e2017-02-21 22:34:43 -0500905class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400906 ''' Json javascript wrapper. '''
907 name = 'jsonp'
908 api = 2
909
Brad Bishop080a48e2017-02-21 22:34:43 -0500910 def __init__(self, app, **kw):
911 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400912
913 @staticmethod
914 def to_jsonp(json):
915 jwrapper = request.query.callback or None
916 if(jwrapper):
917 response.set_header('Content-Type', 'application/javascript')
918 json = jwrapper + '(' + json + ');'
919 return json
920
921 def apply(self, callback, route):
922 def wrap(*a, **kw):
923 return self.to_jsonp(callback(*a, **kw))
924 return wrap
925
Brad Bishop080a48e2017-02-21 22:34:43 -0500926 def error_callback(self, response_body, **kw):
927 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400928
929
Deepak Kodihalli461367a2017-04-10 07:11:38 -0500930class ContentCheckerPlugin(object):
931 ''' Ensures that a route is associated with the expected content-type
932 header. '''
933 name = 'content_checker'
934 api = 2
935
936 class Checker:
937 def __init__(self, type, callback):
938 self.expected_type = type
939 self.callback = callback
940 self.error_str = "Expecting content type '%s', got '%s'"
941
942 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -0500943 if request.method in ['PUT', 'POST', 'PATCH'] and \
944 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -0500945 self.expected_type != request.content_type:
946 abort(415, self.error_str % (self.expected_type,
947 request.content_type))
948
949 return self.callback(*a, **kw)
950
951 def apply(self, callback, route):
952 content_type = getattr(
953 route.get_undecorated_callback(), '_content_type', None)
954
955 return self.Checker(content_type, callback)
956
957
Brad Bishop2c6fc762016-08-29 15:53:25 -0400958class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400959 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400960 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400961 self.bus = dbus.SystemBus()
962 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -0500963 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500964
Brad Bishop87b63c12016-03-18 14:47:51 -0400965 self.install_hooks()
966 self.install_plugins()
967 self.create_handlers()
968 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500969
Brad Bishop87b63c12016-03-18 14:47:51 -0400970 def install_plugins(self):
971 # install json api plugins
972 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400973 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -0500974 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -0500975 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -0500976 self.install(JsonpPlugin(self, **json_kw))
977 self.install(JsonErrorsPlugin(self, **json_kw))
978 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -0400979 self.install(JsonApiRequestPlugin())
980 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500981
Brad Bishop87b63c12016-03-18 14:47:51 -0400982 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -0500983 self.error_handler_type = type(self.default_error_handler)
984 self.original_error_handler = self.default_error_handler
985 self.default_error_handler = self.error_handler_type(
986 self.custom_error_handler, self, Bottle)
987
Brad Bishop87b63c12016-03-18 14:47:51 -0400988 self.real_router_match = self.router.match
989 self.router.match = self.custom_router_match
990 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500991
Brad Bishop87b63c12016-03-18 14:47:51 -0400992 def create_handlers(self):
993 # create route handlers
994 self.session_handler = SessionHandler(self, self.bus)
995 self.directory_handler = DirectoryHandler(self, self.bus)
996 self.list_names_handler = ListNamesHandler(self, self.bus)
997 self.list_handler = ListHandler(self, self.bus)
998 self.method_handler = MethodHandler(self, self.bus)
999 self.property_handler = PropertyHandler(self, self.bus)
1000 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001001 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1002 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001003 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001004
Brad Bishop87b63c12016-03-18 14:47:51 -04001005 def install_handlers(self):
1006 self.session_handler.install()
1007 self.directory_handler.install()
1008 self.list_names_handler.install()
1009 self.list_handler.install()
1010 self.method_handler.install()
1011 self.property_handler.install()
1012 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001013 self.image_upload_post_handler.install()
1014 self.image_upload_put_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001015 # this has to come last, since it matches everything
1016 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001017
Brad Bishop080a48e2017-02-21 22:34:43 -05001018 def install_error_callback(self, callback):
1019 self.error_callbacks.insert(0, callback)
1020
Brad Bishop87b63c12016-03-18 14:47:51 -04001021 def custom_router_match(self, environ):
1022 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1023 needed doesn't work for us since the instance rules match
1024 everything. This monkey-patch lets the route handler figure
1025 out which response is needed. This could be accomplished
1026 with a hook but that would require calling the router match
1027 function twice.
1028 '''
1029 route, args = self.real_router_match(environ)
1030 if isinstance(route.callback, RouteHandler):
1031 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001032
Brad Bishop87b63c12016-03-18 14:47:51 -04001033 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001034
Brad Bishop080a48e2017-02-21 22:34:43 -05001035 def custom_error_handler(self, res, error):
1036 ''' Allow plugins to modify error reponses too via this custom
1037 error handler. '''
1038
1039 response_object = {}
1040 response_body = {}
1041 for x in self.error_callbacks:
1042 x(error=error,
1043 response_object=response_object,
1044 response_body=response_body)
1045
1046 return response_body.get('body', "")
1047
Brad Bishop87b63c12016-03-18 14:47:51 -04001048 @staticmethod
1049 def strip_extra_slashes():
1050 path = request.environ['PATH_INFO']
1051 trailing = ("", "/")[path[-1] == '/']
1052 parts = filter(bool, path.split('/'))
1053 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing