blob: 949a67c6209f155ab28777fbc87809bcd2397cef [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 Bishopaa65f6e2015-10-27 16:28:51 -040093
Brad Bishop87b63c12016-03-18 14:47:51 -040094 def _setup(self, **kw):
95 request.route_data = {}
96 if request.method in self._verbs:
97 return self.setup(**kw)
98 else:
99 self.find(**kw)
100 raise HTTPError(
101 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400102
Brad Bishop87b63c12016-03-18 14:47:51 -0400103 def __call__(self, **kw):
104 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400105
Brad Bishop87b63c12016-03-18 14:47:51 -0400106 def install(self):
107 self.app.route(
108 self._rules, callback=self,
109 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400110
Brad Bishop87b63c12016-03-18 14:47:51 -0400111 @staticmethod
112 def try_mapper_call(f, callback=None, **kw):
113 try:
114 return f(**kw)
115 except dbus.exceptions.DBusException, e:
Brad Bishopb103d2d2016-03-04 16:19:14 -0500116 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400117 raise
118 if callback is None:
119 def callback(e, **kw):
120 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400121
Brad Bishop87b63c12016-03-18 14:47:51 -0400122 callback(e, **kw)
123
124 @staticmethod
125 def try_properties_interface(f, *a):
126 try:
127 return f(*a)
128 except dbus.exceptions.DBusException, e:
129 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
130 # interface doesn't have any properties
131 return None
132 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
133 # properties interface not implemented at all
134 return None
135 raise
136
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400137
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500138class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400139 verbs = 'GET'
140 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400141
Brad Bishop87b63c12016-03-18 14:47:51 -0400142 def __init__(self, app, bus):
143 super(DirectoryHandler, self).__init__(
144 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400145
Brad Bishop87b63c12016-03-18 14:47:51 -0400146 def find(self, path='/'):
147 return self.try_mapper_call(
148 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400149
Brad Bishop87b63c12016-03-18 14:47:51 -0400150 def setup(self, path='/'):
151 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400152
Brad Bishop87b63c12016-03-18 14:47:51 -0400153 def do_get(self, path='/'):
154 return request.route_data['map']
155
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400156
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500157class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400158 verbs = 'GET'
159 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400160
Brad Bishop87b63c12016-03-18 14:47:51 -0400161 def __init__(self, app, bus):
162 super(ListNamesHandler, self).__init__(
163 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400164
Brad Bishop87b63c12016-03-18 14:47:51 -0400165 def find(self, path='/'):
166 return self.try_mapper_call(
167 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400168
Brad Bishop87b63c12016-03-18 14:47:51 -0400169 def setup(self, path='/'):
170 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400171
Brad Bishop87b63c12016-03-18 14:47:51 -0400172 def do_get(self, path='/'):
173 return request.route_data['map']
174
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400175
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500176class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400177 verbs = 'GET'
178 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400179
Brad Bishop87b63c12016-03-18 14:47:51 -0400180 def __init__(self, app, bus):
181 super(ListHandler, self).__init__(
182 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400183
Brad Bishop87b63c12016-03-18 14:47:51 -0400184 def find(self, path='/'):
185 return self.try_mapper_call(
186 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400187
Brad Bishop87b63c12016-03-18 14:47:51 -0400188 def setup(self, path='/'):
189 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400190
Brad Bishop87b63c12016-03-18 14:47:51 -0400191 def do_get(self, path='/'):
192 objs = {}
193 mapper_data = request.route_data['map']
Brad Bishopb103d2d2016-03-04 16:19:14 -0500194 tree = obmc.utils.pathreee.PathTree()
Brad Bishop87b63c12016-03-18 14:47:51 -0400195 for x, y in mapper_data.iteritems():
196 tree[x] = y
Brad Bishop936f5fe2015-11-03 15:10:11 -0500197
Brad Bishop87b63c12016-03-18 14:47:51 -0400198 try:
199 # Check to see if the root path implements
200 # enumerate in addition to any sub tree
201 # objects.
202 root = self.try_mapper_call(
203 self.mapper.get_object, path=path)
204 mapper_data[path] = root
205 except:
206 pass
Brad Bishop936f5fe2015-11-03 15:10:11 -0500207
Brad Bishop87b63c12016-03-18 14:47:51 -0400208 have_enumerate = [
209 (x[0], self.enumerate_capable(*x))
210 for x in mapper_data.iteritems() if self.enumerate_capable(*x)]
Brad Bishop936f5fe2015-11-03 15:10:11 -0500211
Brad Bishop87b63c12016-03-18 14:47:51 -0400212 for x, y in have_enumerate:
213 objs.update(self.call_enumerate(x, y))
214 tmp = tree[x]
215 # remove the subtree
216 del tree[x]
217 # add the new leaf back since enumerate results don't
218 # include the object enumerate is being invoked on
219 tree[x] = tmp
Brad Bishop936f5fe2015-11-03 15:10:11 -0500220
Brad Bishop87b63c12016-03-18 14:47:51 -0400221 # make dbus calls for any remaining objects
222 for x, y in tree.dataitems():
223 objs[x] = self.app.instance_handler.do_get(x)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400224
Brad Bishop87b63c12016-03-18 14:47:51 -0400225 return objs
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400226
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 @staticmethod
228 def enumerate_capable(path, bus_data):
229 busses = []
230 for name, ifaces in bus_data.iteritems():
Brad Bishopb103d2d2016-03-04 16:19:14 -0500231 if obmc.mapper.ENUMERATE_IFACE in ifaces:
Brad Bishop87b63c12016-03-18 14:47:51 -0400232 busses.append(name)
233 return busses
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400234
Brad Bishop87b63c12016-03-18 14:47:51 -0400235 def call_enumerate(self, path, busses):
236 objs = {}
237 for b in busses:
238 obj = self.bus.get_object(b, path, introspect=False)
Brad Bishopb103d2d2016-03-04 16:19:14 -0500239 iface = dbus.Interface(obj, obmc.mapper.ENUMERATE_IFACE)
Brad Bishop87b63c12016-03-18 14:47:51 -0400240 objs.update(iface.enumerate())
241 return objs
242
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400243
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500244class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400245 verbs = 'POST'
246 rules = '<path:path>/action/<method>'
247 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400248
Brad Bishop87b63c12016-03-18 14:47:51 -0400249 def __init__(self, app, bus):
250 super(MethodHandler, self).__init__(
251 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400252
Brad Bishop87b63c12016-03-18 14:47:51 -0400253 def find(self, path, method):
254 busses = self.try_mapper_call(
255 self.mapper.get_object, path=path)
256 for items in busses.iteritems():
257 m = self.find_method_on_bus(path, method, *items)
258 if m:
259 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400260
Brad Bishop87b63c12016-03-18 14:47:51 -0400261 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400262
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 def setup(self, path, method):
264 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400265
Brad Bishop87b63c12016-03-18 14:47:51 -0400266 def do_post(self, path, method):
267 try:
268 if request.parameter_list:
269 return request.route_data['method'](*request.parameter_list)
270 else:
271 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 except dbus.exceptions.DBusException, e:
274 if e.get_dbus_name() == DBUS_INVALID_ARGS:
275 abort(400, str(e))
276 if e.get_dbus_name() == DBUS_TYPE_ERROR:
277 abort(400, str(e))
278 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400279
Brad Bishop87b63c12016-03-18 14:47:51 -0400280 @staticmethod
281 def find_method_in_interface(method, obj, interface, methods):
282 if methods is None:
283 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400284
Brad Bishop87b63c12016-03-18 14:47:51 -0400285 method = find_case_insensitive(method, methods.keys())
286 if method is not None:
287 iface = dbus.Interface(obj, interface)
288 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 def find_method_on_bus(self, path, method, bus, interfaces):
291 obj = self.bus.get_object(bus, path, introspect=False)
292 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
293 data = iface.Introspect()
294 parser = IntrospectionNodeParser(
295 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500296 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400297 for x, y in parser.get_interfaces().iteritems():
298 m = self.find_method_in_interface(
299 method, obj, x, y.get('method'))
300 if m:
301 return m
302
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400303
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500304class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400305 verbs = ['PUT', 'GET']
306 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400307
Brad Bishop87b63c12016-03-18 14:47:51 -0400308 def __init__(self, app, bus):
309 super(PropertyHandler, self).__init__(
310 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400311
Brad Bishop87b63c12016-03-18 14:47:51 -0400312 def find(self, path, prop):
313 self.app.instance_handler.setup(path)
314 obj = self.app.instance_handler.do_get(path)
315 try:
316 obj[prop]
317 except KeyError, e:
318 if request.method == 'PUT':
319 abort(403, _4034_msg % ('property', 'created', str(e)))
320 else:
321 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400322
Brad Bishop87b63c12016-03-18 14:47:51 -0400323 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500324
Brad Bishop87b63c12016-03-18 14:47:51 -0400325 def setup(self, path, prop):
326 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500327
Brad Bishop87b63c12016-03-18 14:47:51 -0400328 def do_get(self, path, prop):
329 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500330
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 def do_put(self, path, prop, value=None):
332 if value is None:
333 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500334
Brad Bishop87b63c12016-03-18 14:47:51 -0400335 prop, iface, properties_iface = self.get_host_interface(
336 path, prop, request.route_data['map'][path])
337 try:
338 properties_iface.Set(iface, prop, value)
339 except ValueError, e:
340 abort(400, str(e))
341 except dbus.exceptions.DBusException, e:
342 if e.get_dbus_name() == DBUS_INVALID_ARGS:
343 abort(403, str(e))
344 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500345
Brad Bishop87b63c12016-03-18 14:47:51 -0400346 def get_host_interface(self, path, prop, bus_info):
347 for bus, interfaces in bus_info.iteritems():
348 obj = self.bus.get_object(bus, path, introspect=True)
349 properties_iface = dbus.Interface(
350 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500351
Brad Bishop87b63c12016-03-18 14:47:51 -0400352 info = self.get_host_interface_on_bus(
353 path, prop, properties_iface, bus, interfaces)
354 if info is not None:
355 prop, iface = info
356 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500357
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
359 for i in interfaces:
360 properties = self.try_properties_interface(iface.GetAll, i)
361 if properties is None:
362 continue
363 prop = find_case_insensitive(prop, properties.keys())
364 if prop is None:
365 continue
366 return prop, i
367
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500368
Brad Bishop2503bd62015-12-16 17:56:12 -0500369class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400370 verbs = ['GET']
371 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500372
Brad Bishop87b63c12016-03-18 14:47:51 -0400373 def __init__(self, app, bus):
374 super(SchemaHandler, self).__init__(
375 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500376
Brad Bishop87b63c12016-03-18 14:47:51 -0400377 def find(self, path):
378 return self.try_mapper_call(
379 self.mapper.get_object,
380 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500381
Brad Bishop87b63c12016-03-18 14:47:51 -0400382 def setup(self, path):
383 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500384
Brad Bishop87b63c12016-03-18 14:47:51 -0400385 def do_get(self, path):
386 schema = {}
387 for x in request.route_data['map'].iterkeys():
388 obj = self.bus.get_object(x, path, introspect=False)
389 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
390 data = iface.Introspect()
391 parser = IntrospectionNodeParser(
392 ElementTree.fromstring(data))
393 for x, y in parser.get_interfaces().iteritems():
394 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500395
Brad Bishop87b63c12016-03-18 14:47:51 -0400396 return schema
397
Brad Bishop2503bd62015-12-16 17:56:12 -0500398
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500399class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400400 verbs = ['GET', 'PUT', 'DELETE']
401 rules = '<path:path>'
402 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500403
Brad Bishop87b63c12016-03-18 14:47:51 -0400404 def __init__(self, app, bus):
405 super(InstanceHandler, self).__init__(
406 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500407
Brad Bishop87b63c12016-03-18 14:47:51 -0400408 def find(self, path, callback=None):
409 return {path: self.try_mapper_call(
410 self.mapper.get_object,
411 callback,
412 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500413
Brad Bishop87b63c12016-03-18 14:47:51 -0400414 def setup(self, path):
415 callback = None
416 if request.method == 'PUT':
417 def callback(e, **kw):
418 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500419
Brad Bishop87b63c12016-03-18 14:47:51 -0400420 if request.route_data.get('map') is None:
421 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500422
Brad Bishop87b63c12016-03-18 14:47:51 -0400423 def do_get(self, path):
424 properties = {}
425 for item in request.route_data['map'][path].iteritems():
426 properties.update(self.get_properties_on_bus(
427 path, *item))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500428
Brad Bishop87b63c12016-03-18 14:47:51 -0400429 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500430
Brad Bishop87b63c12016-03-18 14:47:51 -0400431 @staticmethod
432 def get_properties_on_iface(properties_iface, iface):
433 properties = InstanceHandler.try_properties_interface(
434 properties_iface.GetAll, iface)
435 if properties is None:
436 return {}
437 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500438
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 def get_properties_on_bus(self, path, bus, interfaces):
440 properties = {}
441 obj = self.bus.get_object(bus, path, introspect=False)
442 properties_iface = dbus.Interface(
443 obj, dbus_interface=dbus.PROPERTIES_IFACE)
444 for i in interfaces:
445 properties.update(self.get_properties_on_iface(
446 properties_iface, i))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500447
Brad Bishop87b63c12016-03-18 14:47:51 -0400448 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500449
Brad Bishop87b63c12016-03-18 14:47:51 -0400450 def do_put(self, path):
451 # make sure all properties exist in the request
452 obj = set(self.do_get(path).keys())
453 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500454
Brad Bishop87b63c12016-03-18 14:47:51 -0400455 diff = list(obj.difference(req))
456 if diff:
457 abort(403, _4034_msg % (
458 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500459
Brad Bishop87b63c12016-03-18 14:47:51 -0400460 diff = list(req.difference(obj))
461 if diff:
462 abort(403, _4034_msg % (
463 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 for p, v in request.parameter_list.iteritems():
466 self.app.property_handler.do_put(
467 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500468
Brad Bishop87b63c12016-03-18 14:47:51 -0400469 def do_delete(self, path):
470 for bus_info in request.route_data['map'][path].iteritems():
471 if self.bus_missing_delete(path, *bus_info):
472 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500473
Brad Bishop87b63c12016-03-18 14:47:51 -0400474 for bus in request.route_data['map'][path].iterkeys():
475 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500476
Brad Bishop87b63c12016-03-18 14:47:51 -0400477 def bus_missing_delete(self, path, bus, interfaces):
478 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500479
Brad Bishop87b63c12016-03-18 14:47:51 -0400480 def delete_on_bus(self, path, bus):
481 obj = self.bus.get_object(bus, path, introspect=False)
482 delete_iface = dbus.Interface(
483 obj, dbus_interface=DELETE_IFACE)
484 delete_iface.Delete()
485
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500486
Brad Bishop2f428582015-12-02 10:56:11 -0500487class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 ''' Handles the /login and /logout routes, manages
489 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500490
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 rules = ['/login', '/logout']
492 login_str = "User '%s' logged %s"
493 bad_passwd_str = "Invalid username or password"
494 no_user_str = "No user logged in"
495 bad_json_str = "Expecting request format { 'data': " \
496 "[<username>, <password>] }, got '%s'"
497 _require_auth = None
498 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500499
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 def __init__(self, app, bus):
501 super(SessionHandler, self).__init__(
502 app, bus)
503 self.hmac_key = os.urandom(128)
504 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500505
Brad Bishop87b63c12016-03-18 14:47:51 -0400506 @staticmethod
507 def authenticate(username, clear):
508 try:
509 encoded = spwd.getspnam(username)[1]
510 return encoded == crypt.crypt(clear, encoded)
511 except KeyError:
512 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500513
Brad Bishop87b63c12016-03-18 14:47:51 -0400514 def invalidate_session(self, session):
515 try:
516 self.session_store.remove(session)
517 except ValueError:
518 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500519
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 def new_session(self):
521 sid = os.urandom(32)
522 if self.MAX_SESSIONS <= len(self.session_store):
523 self.session_store.pop()
524 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500525
Brad Bishop87b63c12016-03-18 14:47:51 -0400526 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500527
Brad Bishop87b63c12016-03-18 14:47:51 -0400528 def get_session(self, sid):
529 sids = [x['sid'] for x in self.session_store]
530 try:
531 return self.session_store[sids.index(sid)]
532 except ValueError:
533 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500534
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 def get_session_from_cookie(self):
536 return self.get_session(
537 request.get_cookie(
538 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500539
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 def do_post(self, **kw):
541 if request.path == '/login':
542 return self.do_login(**kw)
543 else:
544 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500545
Brad Bishop87b63c12016-03-18 14:47:51 -0400546 def do_logout(self, **kw):
547 session = self.get_session_from_cookie()
548 if session is not None:
549 user = session['user']
550 self.invalidate_session(session)
551 response.delete_cookie('sid')
552 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500555
Brad Bishop87b63c12016-03-18 14:47:51 -0400556 def do_login(self, **kw):
557 session = self.get_session_from_cookie()
558 if session is not None:
559 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500560
Brad Bishop87b63c12016-03-18 14:47:51 -0400561 if len(request.parameter_list) != 2:
562 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 if not self.authenticate(*request.parameter_list):
565 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500566
Brad Bishop87b63c12016-03-18 14:47:51 -0400567 user = request.parameter_list[0]
568 session = self.new_session()
569 session['user'] = user
570 response.set_cookie(
571 'sid', session['sid'], secret=self.hmac_key,
572 secure=True,
573 httponly=True)
574 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 def find(self, **kw):
577 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 def setup(self, **kw):
580 pass
581
Brad Bishop2f428582015-12-02 10:56:11 -0500582
583class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400584 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500585
Brad Bishop87b63c12016-03-18 14:47:51 -0400586 name = 'authorization'
587 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500588
Brad Bishop87b63c12016-03-18 14:47:51 -0400589 class Compose:
590 def __init__(self, validators, callback, session_mgr):
591 self.validators = validators
592 self.callback = callback
593 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500594
Brad Bishop87b63c12016-03-18 14:47:51 -0400595 def __call__(self, *a, **kw):
596 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
597 session = self.session_mgr.get_session(sid)
598 for x in self.validators:
599 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500600
Brad Bishop87b63c12016-03-18 14:47:51 -0400601 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 def apply(self, callback, route):
604 undecorated = route.get_undecorated_callback()
605 if not isinstance(undecorated, RouteHandler):
606 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500607
Brad Bishop87b63c12016-03-18 14:47:51 -0400608 auth_types = getattr(
609 undecorated, '_require_auth', None)
610 if not auth_types:
611 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 return self.Compose(
614 auth_types, callback, undecorated.app.session_handler)
615
Brad Bishop2f428582015-12-02 10:56:11 -0500616
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500617class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 ''' Ensures request content satisfies the OpenBMC json api format. '''
619 name = 'json_api_request'
620 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500621
Brad Bishop87b63c12016-03-18 14:47:51 -0400622 error_str = "Expecting request format { 'data': <value> }, got '%s'"
623 type_error_str = "Unsupported Content-Type: '%s'"
624 json_type = "application/json"
625 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 @staticmethod
628 def content_expected():
629 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500630
Brad Bishop87b63c12016-03-18 14:47:51 -0400631 def validate_request(self):
632 if request.content_length > 0 and \
633 request.content_type != self.json_type:
634 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500635
Brad Bishop87b63c12016-03-18 14:47:51 -0400636 try:
637 request.parameter_list = request.json.get('data')
638 except ValueError, e:
639 abort(400, str(e))
640 except (AttributeError, KeyError, TypeError):
641 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500642
Brad Bishop87b63c12016-03-18 14:47:51 -0400643 def apply(self, callback, route):
644 verbs = getattr(
645 route.get_undecorated_callback(), '_verbs', None)
646 if verbs is None:
647 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500648
Brad Bishop87b63c12016-03-18 14:47:51 -0400649 if not set(self.request_methods).intersection(verbs):
650 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500651
Brad Bishop87b63c12016-03-18 14:47:51 -0400652 def wrap(*a, **kw):
653 if self.content_expected():
654 self.validate_request()
655 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500656
Brad Bishop87b63c12016-03-18 14:47:51 -0400657 return wrap
658
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500659
660class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400661 ''' Ensures request content type satisfies the OpenBMC json api format. '''
662 name = 'json_api_method_request'
663 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500664
Brad Bishop87b63c12016-03-18 14:47:51 -0400665 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500666
Brad Bishop87b63c12016-03-18 14:47:51 -0400667 def apply(self, callback, route):
668 request_type = getattr(
669 route.get_undecorated_callback(), 'request_type', None)
670 if request_type is None:
671 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500672
Brad Bishop87b63c12016-03-18 14:47:51 -0400673 def validate_request():
674 if not isinstance(request.parameter_list, request_type):
675 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500676
Brad Bishop87b63c12016-03-18 14:47:51 -0400677 def wrap(*a, **kw):
678 if JsonApiRequestPlugin.content_expected():
679 validate_request()
680 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500681
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 return wrap
683
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500684
685class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400686 ''' Emits normal responses in the OpenBMC json api format. '''
687 name = 'json_api_response'
688 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500689
Brad Bishop87b63c12016-03-18 14:47:51 -0400690 def apply(self, callback, route):
691 def wrap(*a, **kw):
692 resp = {'data': callback(*a, **kw)}
693 resp['status'] = 'ok'
694 resp['message'] = response.status_line
695 return resp
696 return wrap
697
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500698
699class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400700 ''' Emits error responses in the OpenBMC json api format. '''
701 name = 'json_api_errors'
702 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500703
Brad Bishop87b63c12016-03-18 14:47:51 -0400704 def __init__(self, **kw):
705 self.app = None
706 self.function_type = None
707 self.original = None
708 self.json_opts = {
709 x: y for x, y in kw.iteritems()
710 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500711
Brad Bishop87b63c12016-03-18 14:47:51 -0400712 def setup(self, app):
713 self.app = app
714 self.function_type = type(app.default_error_handler)
715 self.original = app.default_error_handler
716 self.app.default_error_handler = self.function_type(
717 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500718
Brad Bishop87b63c12016-03-18 14:47:51 -0400719 def apply(self, callback, route):
720 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500721
Brad Bishop87b63c12016-03-18 14:47:51 -0400722 def close(self):
723 self.app.default_error_handler = self.function_type(
724 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500725
Brad Bishop87b63c12016-03-18 14:47:51 -0400726 def json_errors(self, res, error):
727 response_object = {'status': 'error', 'data': {}}
728 response_object['message'] = error.status_line
729 response_object['data']['description'] = str(error.body)
730 if error.status_code == 500:
731 response_object['data']['exception'] = repr(error.exception)
732 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500733
Brad Bishop87b63c12016-03-18 14:47:51 -0400734 json_response = json.dumps(response_object, **self.json_opts)
735 response.content_type = 'application/json'
736 return json_response
737
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500738
739class RestApp(Bottle):
Brad Bishop87b63c12016-03-18 14:47:51 -0400740 def __init__(self, bus):
741 super(RestApp, self).__init__(autojson=False)
742 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500743 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500744
Brad Bishop87b63c12016-03-18 14:47:51 -0400745 self.install_hooks()
746 self.install_plugins()
747 self.create_handlers()
748 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500749
Brad Bishop87b63c12016-03-18 14:47:51 -0400750 def install_plugins(self):
751 # install json api plugins
752 json_kw = {'indent': 2, 'sort_keys': True}
753 self.install(JSONPlugin(**json_kw))
754 self.install(JsonApiErrorsPlugin(**json_kw))
755 self.install(AuthorizationPlugin())
756 self.install(JsonApiResponsePlugin())
757 self.install(JsonApiRequestPlugin())
758 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500759
Brad Bishop87b63c12016-03-18 14:47:51 -0400760 def install_hooks(self):
761 self.real_router_match = self.router.match
762 self.router.match = self.custom_router_match
763 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500764
Brad Bishop87b63c12016-03-18 14:47:51 -0400765 def create_handlers(self):
766 # create route handlers
767 self.session_handler = SessionHandler(self, self.bus)
768 self.directory_handler = DirectoryHandler(self, self.bus)
769 self.list_names_handler = ListNamesHandler(self, self.bus)
770 self.list_handler = ListHandler(self, self.bus)
771 self.method_handler = MethodHandler(self, self.bus)
772 self.property_handler = PropertyHandler(self, self.bus)
773 self.schema_handler = SchemaHandler(self, self.bus)
774 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500775
Brad Bishop87b63c12016-03-18 14:47:51 -0400776 def install_handlers(self):
777 self.session_handler.install()
778 self.directory_handler.install()
779 self.list_names_handler.install()
780 self.list_handler.install()
781 self.method_handler.install()
782 self.property_handler.install()
783 self.schema_handler.install()
784 # this has to come last, since it matches everything
785 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500786
Brad Bishop87b63c12016-03-18 14:47:51 -0400787 def custom_router_match(self, environ):
788 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
789 needed doesn't work for us since the instance rules match
790 everything. This monkey-patch lets the route handler figure
791 out which response is needed. This could be accomplished
792 with a hook but that would require calling the router match
793 function twice.
794 '''
795 route, args = self.real_router_match(environ)
796 if isinstance(route.callback, RouteHandler):
797 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500798
Brad Bishop87b63c12016-03-18 14:47:51 -0400799 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500800
Brad Bishop87b63c12016-03-18 14:47:51 -0400801 @staticmethod
802 def strip_extra_slashes():
803 path = request.environ['PATH_INFO']
804 trailing = ("", "/")[path[-1] == '/']
805 parts = filter(bool, path.split('/'))
806 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400807
808if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400809 log = logging.getLogger('Rocket.Errors')
810 log.setLevel(logging.INFO)
811 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500812
Brad Bishop87b63c12016-03-18 14:47:51 -0400813 bus = dbus.SystemBus()
814 app = RestApp(bus)
815 default_cert = os.path.join(
816 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500817
Brad Bishop87b63c12016-03-18 14:47:51 -0400818 server = Rocket(
819 ('0.0.0.0', 443, default_cert, default_cert),
820 'wsgi', {'wsgi_app': app},
821 min_threads=1,
822 max_threads=1)
823 server.start()