blob: 3a36406c128d1df9ef5c62777a0c43891f48b934 [file] [log] [blame]
Brad Bishopaa65f6e2015-10-27 16:28:51 -04001#!/usr/bin/env python
2
Brad Bishop68caa1e2016-03-04 15:42:08 -05003# Contributors Listed Below - COPYRIGHT 2016
4# [+] International Business Machines Corp.
5#
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16# implied. See the License for the specific language governing
17# permissions and limitations under the License.
18
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import os
20import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -040021import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022import dbus.exceptions
23import json
24import logging
25from xml.etree import ElementTree
26from rocket import Rocket
27from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopb103d2d2016-03-04 16:19:14 -050028import obmc.utils.misc
29import obmc.utils.pathtree
30from obmc.dbuslib.introspection import IntrospectionNodeParser
31import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050032import spwd
33import grp
34import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040035
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050036DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
37DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
38DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050039DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Brad Bishopaac521c2015-11-25 09:16:35 -050040DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050041
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050042_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040043
Brad Bishop87b63c12016-03-18 14:47:51 -040044
Brad Bishop2f428582015-12-02 10:56:11 -050045def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040046 ''' Authorization plugin callback that checks
47 that the user is logged in. '''
48 if session is None:
49 abort(403, 'Login required')
50
Brad Bishop2f428582015-12-02 10:56:11 -050051
52class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -040053 ''' Authorization plugin callback that checks that the user is logged in
54 and a member of a group. '''
55 def __init__(self, group):
56 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -050057
Brad Bishop87b63c12016-03-18 14:47:51 -040058 def __call__(self, session, *a, **kw):
59 valid_user(session, *a, **kw)
60 res = False
Brad Bishop2f428582015-12-02 10:56:11 -050061
Brad Bishop87b63c12016-03-18 14:47:51 -040062 try:
63 res = session['user'] in grp.getgrnam(self.group)[3]
64 except KeyError:
65 pass
Brad Bishop2f428582015-12-02 10:56:11 -050066
Brad Bishop87b63c12016-03-18 14:47:51 -040067 if not res:
68 abort(403, 'Insufficient access')
69
Brad Bishop2f428582015-12-02 10:56:11 -050070
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050071def find_case_insensitive(value, lst):
Brad Bishop87b63c12016-03-18 14:47:51 -040072 return next((x for x in lst if x.lower() == value.lower()), None)
73
Brad Bishopaa65f6e2015-10-27 16:28:51 -040074
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050075def makelist(data):
Brad Bishop87b63c12016-03-18 14:47:51 -040076 if isinstance(data, list):
77 return data
78 elif data:
79 return [data]
80 else:
81 return []
82
Brad Bishopaa65f6e2015-10-27 16:28:51 -040083
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050084class RouteHandler(object):
Brad Bishop87b63c12016-03-18 14:47:51 -040085 _require_auth = makelist(valid_user)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040086
Brad Bishop87b63c12016-03-18 14:47:51 -040087 def __init__(self, app, bus, verbs, rules):
88 self.app = app
89 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -050090 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop87b63c12016-03-18 14:47:51 -040091 self._verbs = makelist(verbs)
92 self._rules = rules
Brad Bishop0f79e522016-03-18 13:33:17 -040093 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040094
Brad Bishop87b63c12016-03-18 14:47:51 -040095 def _setup(self, **kw):
96 request.route_data = {}
97 if request.method in self._verbs:
98 return self.setup(**kw)
99 else:
100 self.find(**kw)
101 raise HTTPError(
102 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400103
Brad Bishop87b63c12016-03-18 14:47:51 -0400104 def __call__(self, **kw):
105 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400106
Brad Bishop87b63c12016-03-18 14:47:51 -0400107 def install(self):
108 self.app.route(
109 self._rules, callback=self,
110 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400111
Brad Bishop87b63c12016-03-18 14:47:51 -0400112 @staticmethod
113 def try_mapper_call(f, callback=None, **kw):
114 try:
115 return f(**kw)
116 except dbus.exceptions.DBusException, e:
Brad Bishopb103d2d2016-03-04 16:19:14 -0500117 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400118 raise
119 if callback is None:
120 def callback(e, **kw):
121 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400122
Brad Bishop87b63c12016-03-18 14:47:51 -0400123 callback(e, **kw)
124
125 @staticmethod
126 def try_properties_interface(f, *a):
127 try:
128 return f(*a)
129 except dbus.exceptions.DBusException, e:
130 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
131 # interface doesn't have any properties
132 return None
133 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
134 # properties interface not implemented at all
135 return None
136 raise
137
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400138
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500139class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400140 verbs = 'GET'
141 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400142
Brad Bishop87b63c12016-03-18 14:47:51 -0400143 def __init__(self, app, bus):
144 super(DirectoryHandler, self).__init__(
145 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400146
Brad Bishop87b63c12016-03-18 14:47:51 -0400147 def find(self, path='/'):
148 return self.try_mapper_call(
149 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 def setup(self, path='/'):
152 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400153
Brad Bishop87b63c12016-03-18 14:47:51 -0400154 def do_get(self, path='/'):
155 return request.route_data['map']
156
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400157
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500158class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400159 verbs = 'GET'
160 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400161
Brad Bishop87b63c12016-03-18 14:47:51 -0400162 def __init__(self, app, bus):
163 super(ListNamesHandler, self).__init__(
164 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400165
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 def find(self, path='/'):
167 return self.try_mapper_call(
168 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400169
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 def setup(self, path='/'):
171 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400172
Brad Bishop87b63c12016-03-18 14:47:51 -0400173 def do_get(self, path='/'):
174 return request.route_data['map']
175
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400176
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500177class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400178 verbs = 'GET'
179 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400180
Brad Bishop87b63c12016-03-18 14:47:51 -0400181 def __init__(self, app, bus):
182 super(ListHandler, self).__init__(
183 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400184
Brad Bishop87b63c12016-03-18 14:47:51 -0400185 def find(self, path='/'):
186 return self.try_mapper_call(
187 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400188
Brad Bishop87b63c12016-03-18 14:47:51 -0400189 def setup(self, path='/'):
190 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400191
Brad Bishop87b63c12016-03-18 14:47:51 -0400192 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400193 return {x: y for x, y in self.mapper.enumerate_subtree(
194 path,
195 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400196
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400197
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500198class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400199 verbs = 'POST'
200 rules = '<path:path>/action/<method>'
201 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400202
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 def __init__(self, app, bus):
204 super(MethodHandler, self).__init__(
205 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400206
Brad Bishop87b63c12016-03-18 14:47:51 -0400207 def find(self, path, method):
208 busses = self.try_mapper_call(
209 self.mapper.get_object, path=path)
210 for items in busses.iteritems():
211 m = self.find_method_on_bus(path, method, *items)
212 if m:
213 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400214
Brad Bishop87b63c12016-03-18 14:47:51 -0400215 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400216
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 def setup(self, path, method):
218 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400219
Brad Bishop87b63c12016-03-18 14:47:51 -0400220 def do_post(self, path, method):
221 try:
222 if request.parameter_list:
223 return request.route_data['method'](*request.parameter_list)
224 else:
225 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400226
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 except dbus.exceptions.DBusException, e:
228 if e.get_dbus_name() == DBUS_INVALID_ARGS:
229 abort(400, str(e))
230 if e.get_dbus_name() == DBUS_TYPE_ERROR:
231 abort(400, str(e))
232 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400233
Brad Bishop87b63c12016-03-18 14:47:51 -0400234 @staticmethod
235 def find_method_in_interface(method, obj, interface, methods):
236 if methods is None:
237 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400238
Brad Bishop87b63c12016-03-18 14:47:51 -0400239 method = find_case_insensitive(method, methods.keys())
240 if method is not None:
241 iface = dbus.Interface(obj, interface)
242 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400243
Brad Bishop87b63c12016-03-18 14:47:51 -0400244 def find_method_on_bus(self, path, method, bus, interfaces):
245 obj = self.bus.get_object(bus, path, introspect=False)
246 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
247 data = iface.Introspect()
248 parser = IntrospectionNodeParser(
249 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500250 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400251 for x, y in parser.get_interfaces().iteritems():
252 m = self.find_method_in_interface(
253 method, obj, x, y.get('method'))
254 if m:
255 return m
256
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400257
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500258class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400259 verbs = ['PUT', 'GET']
260 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400261
Brad Bishop87b63c12016-03-18 14:47:51 -0400262 def __init__(self, app, bus):
263 super(PropertyHandler, self).__init__(
264 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400265
Brad Bishop87b63c12016-03-18 14:47:51 -0400266 def find(self, path, prop):
267 self.app.instance_handler.setup(path)
268 obj = self.app.instance_handler.do_get(path)
269 try:
270 obj[prop]
271 except KeyError, e:
272 if request.method == 'PUT':
273 abort(403, _4034_msg % ('property', 'created', str(e)))
274 else:
275 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400276
Brad Bishop87b63c12016-03-18 14:47:51 -0400277 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def setup(self, path, prop):
280 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500281
Brad Bishop87b63c12016-03-18 14:47:51 -0400282 def do_get(self, path, prop):
283 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500284
Brad Bishop87b63c12016-03-18 14:47:51 -0400285 def do_put(self, path, prop, value=None):
286 if value is None:
287 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500288
Brad Bishop87b63c12016-03-18 14:47:51 -0400289 prop, iface, properties_iface = self.get_host_interface(
290 path, prop, request.route_data['map'][path])
291 try:
292 properties_iface.Set(iface, prop, value)
293 except ValueError, e:
294 abort(400, str(e))
295 except dbus.exceptions.DBusException, e:
296 if e.get_dbus_name() == DBUS_INVALID_ARGS:
297 abort(403, str(e))
298 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500299
Brad Bishop87b63c12016-03-18 14:47:51 -0400300 def get_host_interface(self, path, prop, bus_info):
301 for bus, interfaces in bus_info.iteritems():
302 obj = self.bus.get_object(bus, path, introspect=True)
303 properties_iface = dbus.Interface(
304 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500305
Brad Bishop87b63c12016-03-18 14:47:51 -0400306 info = self.get_host_interface_on_bus(
307 path, prop, properties_iface, bus, interfaces)
308 if info is not None:
309 prop, iface = info
310 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500311
Brad Bishop87b63c12016-03-18 14:47:51 -0400312 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
313 for i in interfaces:
314 properties = self.try_properties_interface(iface.GetAll, i)
315 if properties is None:
316 continue
317 prop = find_case_insensitive(prop, properties.keys())
318 if prop is None:
319 continue
320 return prop, i
321
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500322
Brad Bishop2503bd62015-12-16 17:56:12 -0500323class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400324 verbs = ['GET']
325 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500326
Brad Bishop87b63c12016-03-18 14:47:51 -0400327 def __init__(self, app, bus):
328 super(SchemaHandler, self).__init__(
329 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500330
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 def find(self, path):
332 return self.try_mapper_call(
333 self.mapper.get_object,
334 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500335
Brad Bishop87b63c12016-03-18 14:47:51 -0400336 def setup(self, path):
337 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 def do_get(self, path):
340 schema = {}
341 for x in request.route_data['map'].iterkeys():
342 obj = self.bus.get_object(x, path, introspect=False)
343 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
344 data = iface.Introspect()
345 parser = IntrospectionNodeParser(
346 ElementTree.fromstring(data))
347 for x, y in parser.get_interfaces().iteritems():
348 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500349
Brad Bishop87b63c12016-03-18 14:47:51 -0400350 return schema
351
Brad Bishop2503bd62015-12-16 17:56:12 -0500352
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500353class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 verbs = ['GET', 'PUT', 'DELETE']
355 rules = '<path:path>'
356 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500357
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 def __init__(self, app, bus):
359 super(InstanceHandler, self).__init__(
360 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500361
Brad Bishop87b63c12016-03-18 14:47:51 -0400362 def find(self, path, callback=None):
363 return {path: self.try_mapper_call(
364 self.mapper.get_object,
365 callback,
366 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500367
Brad Bishop87b63c12016-03-18 14:47:51 -0400368 def setup(self, path):
369 callback = None
370 if request.method == 'PUT':
371 def callback(e, **kw):
372 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500373
Brad Bishop87b63c12016-03-18 14:47:51 -0400374 if request.route_data.get('map') is None:
375 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500376
Brad Bishop87b63c12016-03-18 14:47:51 -0400377 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400378 return self.mapper.enumerate_object(
379 path,
380 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500381
Brad Bishop87b63c12016-03-18 14:47:51 -0400382 def do_put(self, path):
383 # make sure all properties exist in the request
384 obj = set(self.do_get(path).keys())
385 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500386
Brad Bishop87b63c12016-03-18 14:47:51 -0400387 diff = list(obj.difference(req))
388 if diff:
389 abort(403, _4034_msg % (
390 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500391
Brad Bishop87b63c12016-03-18 14:47:51 -0400392 diff = list(req.difference(obj))
393 if diff:
394 abort(403, _4034_msg % (
395 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500396
Brad Bishop87b63c12016-03-18 14:47:51 -0400397 for p, v in request.parameter_list.iteritems():
398 self.app.property_handler.do_put(
399 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500400
Brad Bishop87b63c12016-03-18 14:47:51 -0400401 def do_delete(self, path):
402 for bus_info in request.route_data['map'][path].iteritems():
403 if self.bus_missing_delete(path, *bus_info):
404 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500405
Brad Bishop87b63c12016-03-18 14:47:51 -0400406 for bus in request.route_data['map'][path].iterkeys():
407 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500408
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 def bus_missing_delete(self, path, bus, interfaces):
410 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500411
Brad Bishop87b63c12016-03-18 14:47:51 -0400412 def delete_on_bus(self, path, bus):
413 obj = self.bus.get_object(bus, path, introspect=False)
414 delete_iface = dbus.Interface(
415 obj, dbus_interface=DELETE_IFACE)
416 delete_iface.Delete()
417
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500418
Brad Bishop2f428582015-12-02 10:56:11 -0500419class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400420 ''' Handles the /login and /logout routes, manages
421 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500422
Brad Bishop87b63c12016-03-18 14:47:51 -0400423 rules = ['/login', '/logout']
424 login_str = "User '%s' logged %s"
425 bad_passwd_str = "Invalid username or password"
426 no_user_str = "No user logged in"
427 bad_json_str = "Expecting request format { 'data': " \
428 "[<username>, <password>] }, got '%s'"
429 _require_auth = None
430 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500431
Brad Bishop87b63c12016-03-18 14:47:51 -0400432 def __init__(self, app, bus):
433 super(SessionHandler, self).__init__(
434 app, bus)
435 self.hmac_key = os.urandom(128)
436 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500437
Brad Bishop87b63c12016-03-18 14:47:51 -0400438 @staticmethod
439 def authenticate(username, clear):
440 try:
441 encoded = spwd.getspnam(username)[1]
442 return encoded == crypt.crypt(clear, encoded)
443 except KeyError:
444 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500445
Brad Bishop87b63c12016-03-18 14:47:51 -0400446 def invalidate_session(self, session):
447 try:
448 self.session_store.remove(session)
449 except ValueError:
450 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500451
Brad Bishop87b63c12016-03-18 14:47:51 -0400452 def new_session(self):
453 sid = os.urandom(32)
454 if self.MAX_SESSIONS <= len(self.session_store):
455 self.session_store.pop()
456 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500457
Brad Bishop87b63c12016-03-18 14:47:51 -0400458 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500459
Brad Bishop87b63c12016-03-18 14:47:51 -0400460 def get_session(self, sid):
461 sids = [x['sid'] for x in self.session_store]
462 try:
463 return self.session_store[sids.index(sid)]
464 except ValueError:
465 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500466
Brad Bishop87b63c12016-03-18 14:47:51 -0400467 def get_session_from_cookie(self):
468 return self.get_session(
469 request.get_cookie(
470 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500471
Brad Bishop87b63c12016-03-18 14:47:51 -0400472 def do_post(self, **kw):
473 if request.path == '/login':
474 return self.do_login(**kw)
475 else:
476 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500477
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 def do_logout(self, **kw):
479 session = self.get_session_from_cookie()
480 if session is not None:
481 user = session['user']
482 self.invalidate_session(session)
483 response.delete_cookie('sid')
484 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500485
Brad Bishop87b63c12016-03-18 14:47:51 -0400486 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500487
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 def do_login(self, **kw):
489 session = self.get_session_from_cookie()
490 if session is not None:
491 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500492
Brad Bishop87b63c12016-03-18 14:47:51 -0400493 if len(request.parameter_list) != 2:
494 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500495
Brad Bishop87b63c12016-03-18 14:47:51 -0400496 if not self.authenticate(*request.parameter_list):
497 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500498
Brad Bishop87b63c12016-03-18 14:47:51 -0400499 user = request.parameter_list[0]
500 session = self.new_session()
501 session['user'] = user
502 response.set_cookie(
503 'sid', session['sid'], secret=self.hmac_key,
504 secure=True,
505 httponly=True)
506 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500507
Brad Bishop87b63c12016-03-18 14:47:51 -0400508 def find(self, **kw):
509 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500510
Brad Bishop87b63c12016-03-18 14:47:51 -0400511 def setup(self, **kw):
512 pass
513
Brad Bishop2f428582015-12-02 10:56:11 -0500514
515class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400516 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500517
Brad Bishop87b63c12016-03-18 14:47:51 -0400518 name = 'authorization'
519 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500520
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 class Compose:
522 def __init__(self, validators, callback, session_mgr):
523 self.validators = validators
524 self.callback = callback
525 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500526
Brad Bishop87b63c12016-03-18 14:47:51 -0400527 def __call__(self, *a, **kw):
528 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
529 session = self.session_mgr.get_session(sid)
530 for x in self.validators:
531 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500534
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 def apply(self, callback, route):
536 undecorated = route.get_undecorated_callback()
537 if not isinstance(undecorated, RouteHandler):
538 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500539
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 auth_types = getattr(
541 undecorated, '_require_auth', None)
542 if not auth_types:
543 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500544
Brad Bishop87b63c12016-03-18 14:47:51 -0400545 return self.Compose(
546 auth_types, callback, undecorated.app.session_handler)
547
Brad Bishop2f428582015-12-02 10:56:11 -0500548
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500549class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 ''' Ensures request content satisfies the OpenBMC json api format. '''
551 name = 'json_api_request'
552 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 error_str = "Expecting request format { 'data': <value> }, got '%s'"
555 type_error_str = "Unsupported Content-Type: '%s'"
556 json_type = "application/json"
557 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 @staticmethod
560 def content_expected():
561 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500562
Brad Bishop87b63c12016-03-18 14:47:51 -0400563 def validate_request(self):
564 if request.content_length > 0 and \
565 request.content_type != self.json_type:
566 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500567
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 try:
569 request.parameter_list = request.json.get('data')
570 except ValueError, e:
571 abort(400, str(e))
572 except (AttributeError, KeyError, TypeError):
573 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500574
Brad Bishop87b63c12016-03-18 14:47:51 -0400575 def apply(self, callback, route):
576 verbs = getattr(
577 route.get_undecorated_callback(), '_verbs', None)
578 if verbs is None:
579 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500580
Brad Bishop87b63c12016-03-18 14:47:51 -0400581 if not set(self.request_methods).intersection(verbs):
582 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500583
Brad Bishop87b63c12016-03-18 14:47:51 -0400584 def wrap(*a, **kw):
585 if self.content_expected():
586 self.validate_request()
587 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500588
Brad Bishop87b63c12016-03-18 14:47:51 -0400589 return wrap
590
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500591
592class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400593 ''' Ensures request content type satisfies the OpenBMC json api format. '''
594 name = 'json_api_method_request'
595 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500596
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500598
Brad Bishop87b63c12016-03-18 14:47:51 -0400599 def apply(self, callback, route):
600 request_type = getattr(
601 route.get_undecorated_callback(), 'request_type', None)
602 if request_type is None:
603 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500604
Brad Bishop87b63c12016-03-18 14:47:51 -0400605 def validate_request():
606 if not isinstance(request.parameter_list, request_type):
607 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500608
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 def wrap(*a, **kw):
610 if JsonApiRequestPlugin.content_expected():
611 validate_request()
612 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500613
Brad Bishop87b63c12016-03-18 14:47:51 -0400614 return wrap
615
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500616
617class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 ''' Emits normal responses in the OpenBMC json api format. '''
619 name = 'json_api_response'
620 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500621
Brad Bishop87b63c12016-03-18 14:47:51 -0400622 def apply(self, callback, route):
623 def wrap(*a, **kw):
624 resp = {'data': callback(*a, **kw)}
625 resp['status'] = 'ok'
626 resp['message'] = response.status_line
627 return resp
628 return wrap
629
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500630
631class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400632 ''' Emits error responses in the OpenBMC json api format. '''
633 name = 'json_api_errors'
634 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500635
Brad Bishop87b63c12016-03-18 14:47:51 -0400636 def __init__(self, **kw):
637 self.app = None
638 self.function_type = None
639 self.original = None
640 self.json_opts = {
641 x: y for x, y in kw.iteritems()
642 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500643
Brad Bishop87b63c12016-03-18 14:47:51 -0400644 def setup(self, app):
645 self.app = app
646 self.function_type = type(app.default_error_handler)
647 self.original = app.default_error_handler
648 self.app.default_error_handler = self.function_type(
649 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500650
Brad Bishop87b63c12016-03-18 14:47:51 -0400651 def apply(self, callback, route):
652 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500653
Brad Bishop87b63c12016-03-18 14:47:51 -0400654 def close(self):
655 self.app.default_error_handler = self.function_type(
656 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500657
Brad Bishop87b63c12016-03-18 14:47:51 -0400658 def json_errors(self, res, error):
659 response_object = {'status': 'error', 'data': {}}
660 response_object['message'] = error.status_line
661 response_object['data']['description'] = str(error.body)
662 if error.status_code == 500:
663 response_object['data']['exception'] = repr(error.exception)
664 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500665
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 json_response = json.dumps(response_object, **self.json_opts)
667 response.content_type = 'application/json'
668 return json_response
669
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500670
Brad Bishop80fe37a2016-03-29 10:54:54 -0400671class JsonpPlugin(JsonApiErrorsPlugin):
672 ''' Json javascript wrapper. '''
673 name = 'jsonp'
674 api = 2
675
676 def __init__(self, **kw):
677 super(JsonpPlugin, self).__init__(**kw)
678
679 @staticmethod
680 def to_jsonp(json):
681 jwrapper = request.query.callback or None
682 if(jwrapper):
683 response.set_header('Content-Type', 'application/javascript')
684 json = jwrapper + '(' + json + ');'
685 return json
686
687 def apply(self, callback, route):
688 def wrap(*a, **kw):
689 return self.to_jsonp(callback(*a, **kw))
690 return wrap
691
692 def json_errors(self, res, error):
693 json = super(JsonpPlugin, self).json_errors(res, error)
694 return self.to_jsonp(json)
695
696
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500697class RestApp(Bottle):
Brad Bishop87b63c12016-03-18 14:47:51 -0400698 def __init__(self, bus):
699 super(RestApp, self).__init__(autojson=False)
700 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500701 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500702
Brad Bishop87b63c12016-03-18 14:47:51 -0400703 self.install_hooks()
704 self.install_plugins()
705 self.create_handlers()
706 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500707
Brad Bishop87b63c12016-03-18 14:47:51 -0400708 def install_plugins(self):
709 # install json api plugins
710 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400711 self.install(AuthorizationPlugin())
Brad Bishop80fe37a2016-03-29 10:54:54 -0400712 self.install(JsonpPlugin(**json_kw))
713 self.install(JSONPlugin(**json_kw))
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 self.install(JsonApiResponsePlugin())
715 self.install(JsonApiRequestPlugin())
716 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500717
Brad Bishop87b63c12016-03-18 14:47:51 -0400718 def install_hooks(self):
719 self.real_router_match = self.router.match
720 self.router.match = self.custom_router_match
721 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500722
Brad Bishop87b63c12016-03-18 14:47:51 -0400723 def create_handlers(self):
724 # create route handlers
725 self.session_handler = SessionHandler(self, self.bus)
726 self.directory_handler = DirectoryHandler(self, self.bus)
727 self.list_names_handler = ListNamesHandler(self, self.bus)
728 self.list_handler = ListHandler(self, self.bus)
729 self.method_handler = MethodHandler(self, self.bus)
730 self.property_handler = PropertyHandler(self, self.bus)
731 self.schema_handler = SchemaHandler(self, self.bus)
732 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500733
Brad Bishop87b63c12016-03-18 14:47:51 -0400734 def install_handlers(self):
735 self.session_handler.install()
736 self.directory_handler.install()
737 self.list_names_handler.install()
738 self.list_handler.install()
739 self.method_handler.install()
740 self.property_handler.install()
741 self.schema_handler.install()
742 # this has to come last, since it matches everything
743 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500744
Brad Bishop87b63c12016-03-18 14:47:51 -0400745 def custom_router_match(self, environ):
746 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
747 needed doesn't work for us since the instance rules match
748 everything. This monkey-patch lets the route handler figure
749 out which response is needed. This could be accomplished
750 with a hook but that would require calling the router match
751 function twice.
752 '''
753 route, args = self.real_router_match(environ)
754 if isinstance(route.callback, RouteHandler):
755 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500756
Brad Bishop87b63c12016-03-18 14:47:51 -0400757 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500758
Brad Bishop87b63c12016-03-18 14:47:51 -0400759 @staticmethod
760 def strip_extra_slashes():
761 path = request.environ['PATH_INFO']
762 trailing = ("", "/")[path[-1] == '/']
763 parts = filter(bool, path.split('/'))
764 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400765
766if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400767 log = logging.getLogger('Rocket.Errors')
768 log.setLevel(logging.INFO)
769 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500770
Brad Bishop87b63c12016-03-18 14:47:51 -0400771 bus = dbus.SystemBus()
772 app = RestApp(bus)
773 default_cert = os.path.join(
774 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500775
Brad Bishop87b63c12016-03-18 14:47:51 -0400776 server = Rocket(
777 ('0.0.0.0', 443, default_cert, default_cert),
778 'wsgi', {'wsgi_app': app},
779 min_threads=1,
780 max_threads=1)
781 server.start()