blob: 0bc67ef5f186a7621c9a52ae30eda23f6ddc6f8c [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='/'):
193 objs = {}
194 mapper_data = request.route_data['map']
Brad Bishop0f79e522016-03-18 13:33:17 -0400195 managers = {}
196 owners = []
Brad Bishop936f5fe2015-11-03 15:10:11 -0500197
Brad Bishop0f79e522016-03-18 13:33:17 -0400198 # look for objectmanager implementations as they result
199 # in fewer dbus calls
200 for path, bus_data in mapper_data.iteritems():
201 for owner, interfaces in bus_data.iteritems():
202 owners.append(owner)
203 if dbus.BUS_DAEMON_IFACE + '.ObjectManager' in interfaces:
204 managers[owner] = path
205
206 # also look in the parent objects
207 ancestors = self.mapper.get_ancestors(path)
208
209 # finally check the root for one too
Brad Bishop87b63c12016-03-18 14:47:51 -0400210 try:
Brad Bishop0f79e522016-03-18 13:33:17 -0400211 ancestors.update({path: self.mapper.get_object(path)})
212 except dbus.exceptions.DBusException, e:
213 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
214 raise
Brad Bishop936f5fe2015-11-03 15:10:11 -0500215
Brad Bishop0f79e522016-03-18 13:33:17 -0400216 for path, bus_data in ancestors.iteritems():
217 for owner, interfaces in bus_data.iteritems():
218 if dbus.BUS_DAEMON_IFACE + '.ObjectManager' in interfaces:
219 managers[owner] = path
Brad Bishop936f5fe2015-11-03 15:10:11 -0500220
Brad Bishop0f79e522016-03-18 13:33:17 -0400221 # make all the manager gmo (get managed objects) calls
222 results = {}
223 for owner, path in managers.iteritems():
224 if owner not in owners:
225 continue
226 obj = self.bus.get_object(owner, path, introspect=False)
227 iface = dbus.Interface(
228 obj, dbus.BUS_DAEMON_IFACE + '.ObjectManager')
229
230 # flatten (remove interface names) gmo results
231 for path, interfaces in iface.GetManagedObjects().iteritems():
232 if path not in mapper_data.iterkeys():
233 continue
234 properties = {}
235 for iface, props in interfaces.iteritems():
236 properties.update(props)
237 results.setdefault(path, {}).setdefault(owner, properties)
Brad Bishop936f5fe2015-11-03 15:10:11 -0500238
Brad Bishop87b63c12016-03-18 14:47:51 -0400239 # make dbus calls for any remaining objects
Brad Bishop0f79e522016-03-18 13:33:17 -0400240 for path, bus_data in mapper_data.iteritems():
241 for owner, interfaces in bus_data.iteritems():
242 if results.setdefault(path, {}).setdefault(owner, {}):
243 continue
244 results.setdefault(path, {}).setdefault(
245 owner,
246 self.app.instance_handler.get_properties_on_bus(
247 path, owner, interfaces))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400248
Brad Bishop87b63c12016-03-18 14:47:51 -0400249 objs = {}
Brad Bishop0f79e522016-03-18 13:33:17 -0400250 for path, owners in results.iteritems():
251 for owner, properties in owners.iteritems():
252 objs.setdefault(path, {}).update(properties)
253
Brad Bishop87b63c12016-03-18 14:47:51 -0400254 return objs
255
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400256
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500257class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400258 verbs = 'POST'
259 rules = '<path:path>/action/<method>'
260 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400261
Brad Bishop87b63c12016-03-18 14:47:51 -0400262 def __init__(self, app, bus):
263 super(MethodHandler, 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, method):
267 busses = self.try_mapper_call(
268 self.mapper.get_object, path=path)
269 for items in busses.iteritems():
270 m = self.find_method_on_bus(path, method, *items)
271 if m:
272 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishop87b63c12016-03-18 14:47:51 -0400274 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400275
Brad Bishop87b63c12016-03-18 14:47:51 -0400276 def setup(self, path, method):
277 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def do_post(self, path, method):
280 try:
281 if request.parameter_list:
282 return request.route_data['method'](*request.parameter_list)
283 else:
284 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 except dbus.exceptions.DBusException, e:
287 if e.get_dbus_name() == DBUS_INVALID_ARGS:
288 abort(400, str(e))
289 if e.get_dbus_name() == DBUS_TYPE_ERROR:
290 abort(400, str(e))
291 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400292
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 @staticmethod
294 def find_method_in_interface(method, obj, interface, methods):
295 if methods is None:
296 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400297
Brad Bishop87b63c12016-03-18 14:47:51 -0400298 method = find_case_insensitive(method, methods.keys())
299 if method is not None:
300 iface = dbus.Interface(obj, interface)
301 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def find_method_on_bus(self, path, method, bus, interfaces):
304 obj = self.bus.get_object(bus, path, introspect=False)
305 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
306 data = iface.Introspect()
307 parser = IntrospectionNodeParser(
308 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500309 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 for x, y in parser.get_interfaces().iteritems():
311 m = self.find_method_in_interface(
312 method, obj, x, y.get('method'))
313 if m:
314 return m
315
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400316
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500317class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400318 verbs = ['PUT', 'GET']
319 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400320
Brad Bishop87b63c12016-03-18 14:47:51 -0400321 def __init__(self, app, bus):
322 super(PropertyHandler, self).__init__(
323 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400324
Brad Bishop87b63c12016-03-18 14:47:51 -0400325 def find(self, path, prop):
326 self.app.instance_handler.setup(path)
327 obj = self.app.instance_handler.do_get(path)
328 try:
329 obj[prop]
330 except KeyError, e:
331 if request.method == 'PUT':
332 abort(403, _4034_msg % ('property', 'created', str(e)))
333 else:
334 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400335
Brad Bishop87b63c12016-03-18 14:47:51 -0400336 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500337
Brad Bishop87b63c12016-03-18 14:47:51 -0400338 def setup(self, path, prop):
339 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500340
Brad Bishop87b63c12016-03-18 14:47:51 -0400341 def do_get(self, path, prop):
342 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500343
Brad Bishop87b63c12016-03-18 14:47:51 -0400344 def do_put(self, path, prop, value=None):
345 if value is None:
346 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500347
Brad Bishop87b63c12016-03-18 14:47:51 -0400348 prop, iface, properties_iface = self.get_host_interface(
349 path, prop, request.route_data['map'][path])
350 try:
351 properties_iface.Set(iface, prop, value)
352 except ValueError, e:
353 abort(400, str(e))
354 except dbus.exceptions.DBusException, e:
355 if e.get_dbus_name() == DBUS_INVALID_ARGS:
356 abort(403, str(e))
357 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500358
Brad Bishop87b63c12016-03-18 14:47:51 -0400359 def get_host_interface(self, path, prop, bus_info):
360 for bus, interfaces in bus_info.iteritems():
361 obj = self.bus.get_object(bus, path, introspect=True)
362 properties_iface = dbus.Interface(
363 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500364
Brad Bishop87b63c12016-03-18 14:47:51 -0400365 info = self.get_host_interface_on_bus(
366 path, prop, properties_iface, bus, interfaces)
367 if info is not None:
368 prop, iface = info
369 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500370
Brad Bishop87b63c12016-03-18 14:47:51 -0400371 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
372 for i in interfaces:
373 properties = self.try_properties_interface(iface.GetAll, i)
374 if properties is None:
375 continue
376 prop = find_case_insensitive(prop, properties.keys())
377 if prop is None:
378 continue
379 return prop, i
380
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500381
Brad Bishop2503bd62015-12-16 17:56:12 -0500382class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400383 verbs = ['GET']
384 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500385
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 def __init__(self, app, bus):
387 super(SchemaHandler, self).__init__(
388 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500389
Brad Bishop87b63c12016-03-18 14:47:51 -0400390 def find(self, path):
391 return self.try_mapper_call(
392 self.mapper.get_object,
393 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500394
Brad Bishop87b63c12016-03-18 14:47:51 -0400395 def setup(self, path):
396 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500397
Brad Bishop87b63c12016-03-18 14:47:51 -0400398 def do_get(self, path):
399 schema = {}
400 for x in request.route_data['map'].iterkeys():
401 obj = self.bus.get_object(x, path, introspect=False)
402 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
403 data = iface.Introspect()
404 parser = IntrospectionNodeParser(
405 ElementTree.fromstring(data))
406 for x, y in parser.get_interfaces().iteritems():
407 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500408
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 return schema
410
Brad Bishop2503bd62015-12-16 17:56:12 -0500411
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500412class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 verbs = ['GET', 'PUT', 'DELETE']
414 rules = '<path:path>'
415 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500416
Brad Bishop87b63c12016-03-18 14:47:51 -0400417 def __init__(self, app, bus):
418 super(InstanceHandler, self).__init__(
419 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500420
Brad Bishop87b63c12016-03-18 14:47:51 -0400421 def find(self, path, callback=None):
422 return {path: self.try_mapper_call(
423 self.mapper.get_object,
424 callback,
425 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500426
Brad Bishop87b63c12016-03-18 14:47:51 -0400427 def setup(self, path):
428 callback = None
429 if request.method == 'PUT':
430 def callback(e, **kw):
431 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500432
Brad Bishop87b63c12016-03-18 14:47:51 -0400433 if request.route_data.get('map') is None:
434 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500435
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 def do_get(self, path):
437 properties = {}
438 for item in request.route_data['map'][path].iteritems():
439 properties.update(self.get_properties_on_bus(
440 path, *item))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500441
Brad Bishop87b63c12016-03-18 14:47:51 -0400442 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500443
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 @staticmethod
445 def get_properties_on_iface(properties_iface, iface):
446 properties = InstanceHandler.try_properties_interface(
447 properties_iface.GetAll, iface)
448 if properties is None:
449 return {}
450 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500451
Brad Bishop87b63c12016-03-18 14:47:51 -0400452 def get_properties_on_bus(self, path, bus, interfaces):
453 properties = {}
454 obj = self.bus.get_object(bus, path, introspect=False)
455 properties_iface = dbus.Interface(
456 obj, dbus_interface=dbus.PROPERTIES_IFACE)
457 for i in interfaces:
Brad Bishop0f79e522016-03-18 13:33:17 -0400458 if not self.intf_match(i):
459 continue
Brad Bishop87b63c12016-03-18 14:47:51 -0400460 properties.update(self.get_properties_on_iface(
461 properties_iface, i))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500462
Brad Bishop87b63c12016-03-18 14:47:51 -0400463 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def do_put(self, path):
466 # make sure all properties exist in the request
467 obj = set(self.do_get(path).keys())
468 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500469
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 diff = list(obj.difference(req))
471 if diff:
472 abort(403, _4034_msg % (
473 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500474
Brad Bishop87b63c12016-03-18 14:47:51 -0400475 diff = list(req.difference(obj))
476 if diff:
477 abort(403, _4034_msg % (
478 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500479
Brad Bishop87b63c12016-03-18 14:47:51 -0400480 for p, v in request.parameter_list.iteritems():
481 self.app.property_handler.do_put(
482 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 def do_delete(self, path):
485 for bus_info in request.route_data['map'][path].iteritems():
486 if self.bus_missing_delete(path, *bus_info):
487 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500488
Brad Bishop87b63c12016-03-18 14:47:51 -0400489 for bus in request.route_data['map'][path].iterkeys():
490 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500491
Brad Bishop87b63c12016-03-18 14:47:51 -0400492 def bus_missing_delete(self, path, bus, interfaces):
493 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500494
Brad Bishop87b63c12016-03-18 14:47:51 -0400495 def delete_on_bus(self, path, bus):
496 obj = self.bus.get_object(bus, path, introspect=False)
497 delete_iface = dbus.Interface(
498 obj, dbus_interface=DELETE_IFACE)
499 delete_iface.Delete()
500
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500501
Brad Bishop2f428582015-12-02 10:56:11 -0500502class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400503 ''' Handles the /login and /logout routes, manages
504 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500505
Brad Bishop87b63c12016-03-18 14:47:51 -0400506 rules = ['/login', '/logout']
507 login_str = "User '%s' logged %s"
508 bad_passwd_str = "Invalid username or password"
509 no_user_str = "No user logged in"
510 bad_json_str = "Expecting request format { 'data': " \
511 "[<username>, <password>] }, got '%s'"
512 _require_auth = None
513 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500514
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 def __init__(self, app, bus):
516 super(SessionHandler, self).__init__(
517 app, bus)
518 self.hmac_key = os.urandom(128)
519 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500520
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 @staticmethod
522 def authenticate(username, clear):
523 try:
524 encoded = spwd.getspnam(username)[1]
525 return encoded == crypt.crypt(clear, encoded)
526 except KeyError:
527 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 def invalidate_session(self, session):
530 try:
531 self.session_store.remove(session)
532 except ValueError:
533 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500534
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 def new_session(self):
536 sid = os.urandom(32)
537 if self.MAX_SESSIONS <= len(self.session_store):
538 self.session_store.pop()
539 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500540
Brad Bishop87b63c12016-03-18 14:47:51 -0400541 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 def get_session(self, sid):
544 sids = [x['sid'] for x in self.session_store]
545 try:
546 return self.session_store[sids.index(sid)]
547 except ValueError:
548 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500549
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 def get_session_from_cookie(self):
551 return self.get_session(
552 request.get_cookie(
553 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500554
Brad Bishop87b63c12016-03-18 14:47:51 -0400555 def do_post(self, **kw):
556 if request.path == '/login':
557 return self.do_login(**kw)
558 else:
559 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500560
Brad Bishop87b63c12016-03-18 14:47:51 -0400561 def do_logout(self, **kw):
562 session = self.get_session_from_cookie()
563 if session is not None:
564 user = session['user']
565 self.invalidate_session(session)
566 response.delete_cookie('sid')
567 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500568
Brad Bishop87b63c12016-03-18 14:47:51 -0400569 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500570
Brad Bishop87b63c12016-03-18 14:47:51 -0400571 def do_login(self, **kw):
572 session = self.get_session_from_cookie()
573 if session is not None:
574 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 if len(request.parameter_list) != 2:
577 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 if not self.authenticate(*request.parameter_list):
580 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500581
Brad Bishop87b63c12016-03-18 14:47:51 -0400582 user = request.parameter_list[0]
583 session = self.new_session()
584 session['user'] = user
585 response.set_cookie(
586 'sid', session['sid'], secret=self.hmac_key,
587 secure=True,
588 httponly=True)
589 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500590
Brad Bishop87b63c12016-03-18 14:47:51 -0400591 def find(self, **kw):
592 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 def setup(self, **kw):
595 pass
596
Brad Bishop2f428582015-12-02 10:56:11 -0500597
598class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400599 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500600
Brad Bishop87b63c12016-03-18 14:47:51 -0400601 name = 'authorization'
602 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500603
Brad Bishop87b63c12016-03-18 14:47:51 -0400604 class Compose:
605 def __init__(self, validators, callback, session_mgr):
606 self.validators = validators
607 self.callback = callback
608 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500609
Brad Bishop87b63c12016-03-18 14:47:51 -0400610 def __call__(self, *a, **kw):
611 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
612 session = self.session_mgr.get_session(sid)
613 for x in self.validators:
614 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500615
Brad Bishop87b63c12016-03-18 14:47:51 -0400616 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500617
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 def apply(self, callback, route):
619 undecorated = route.get_undecorated_callback()
620 if not isinstance(undecorated, RouteHandler):
621 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500622
Brad Bishop87b63c12016-03-18 14:47:51 -0400623 auth_types = getattr(
624 undecorated, '_require_auth', None)
625 if not auth_types:
626 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500627
Brad Bishop87b63c12016-03-18 14:47:51 -0400628 return self.Compose(
629 auth_types, callback, undecorated.app.session_handler)
630
Brad Bishop2f428582015-12-02 10:56:11 -0500631
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500632class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400633 ''' Ensures request content satisfies the OpenBMC json api format. '''
634 name = 'json_api_request'
635 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500636
Brad Bishop87b63c12016-03-18 14:47:51 -0400637 error_str = "Expecting request format { 'data': <value> }, got '%s'"
638 type_error_str = "Unsupported Content-Type: '%s'"
639 json_type = "application/json"
640 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500641
Brad Bishop87b63c12016-03-18 14:47:51 -0400642 @staticmethod
643 def content_expected():
644 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500645
Brad Bishop87b63c12016-03-18 14:47:51 -0400646 def validate_request(self):
647 if request.content_length > 0 and \
648 request.content_type != self.json_type:
649 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500650
Brad Bishop87b63c12016-03-18 14:47:51 -0400651 try:
652 request.parameter_list = request.json.get('data')
653 except ValueError, e:
654 abort(400, str(e))
655 except (AttributeError, KeyError, TypeError):
656 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500657
Brad Bishop87b63c12016-03-18 14:47:51 -0400658 def apply(self, callback, route):
659 verbs = getattr(
660 route.get_undecorated_callback(), '_verbs', None)
661 if verbs is None:
662 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500663
Brad Bishop87b63c12016-03-18 14:47:51 -0400664 if not set(self.request_methods).intersection(verbs):
665 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500666
Brad Bishop87b63c12016-03-18 14:47:51 -0400667 def wrap(*a, **kw):
668 if self.content_expected():
669 self.validate_request()
670 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500671
Brad Bishop87b63c12016-03-18 14:47:51 -0400672 return wrap
673
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500674
675class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400676 ''' Ensures request content type satisfies the OpenBMC json api format. '''
677 name = 'json_api_method_request'
678 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500679
Brad Bishop87b63c12016-03-18 14:47:51 -0400680 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500681
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 def apply(self, callback, route):
683 request_type = getattr(
684 route.get_undecorated_callback(), 'request_type', None)
685 if request_type is None:
686 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500687
Brad Bishop87b63c12016-03-18 14:47:51 -0400688 def validate_request():
689 if not isinstance(request.parameter_list, request_type):
690 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500691
Brad Bishop87b63c12016-03-18 14:47:51 -0400692 def wrap(*a, **kw):
693 if JsonApiRequestPlugin.content_expected():
694 validate_request()
695 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500696
Brad Bishop87b63c12016-03-18 14:47:51 -0400697 return wrap
698
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500699
700class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400701 ''' Emits normal responses in the OpenBMC json api format. '''
702 name = 'json_api_response'
703 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500704
Brad Bishop87b63c12016-03-18 14:47:51 -0400705 def apply(self, callback, route):
706 def wrap(*a, **kw):
707 resp = {'data': callback(*a, **kw)}
708 resp['status'] = 'ok'
709 resp['message'] = response.status_line
710 return resp
711 return wrap
712
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500713
714class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400715 ''' Emits error responses in the OpenBMC json api format. '''
716 name = 'json_api_errors'
717 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500718
Brad Bishop87b63c12016-03-18 14:47:51 -0400719 def __init__(self, **kw):
720 self.app = None
721 self.function_type = None
722 self.original = None
723 self.json_opts = {
724 x: y for x, y in kw.iteritems()
725 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500726
Brad Bishop87b63c12016-03-18 14:47:51 -0400727 def setup(self, app):
728 self.app = app
729 self.function_type = type(app.default_error_handler)
730 self.original = app.default_error_handler
731 self.app.default_error_handler = self.function_type(
732 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500733
Brad Bishop87b63c12016-03-18 14:47:51 -0400734 def apply(self, callback, route):
735 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500736
Brad Bishop87b63c12016-03-18 14:47:51 -0400737 def close(self):
738 self.app.default_error_handler = self.function_type(
739 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500740
Brad Bishop87b63c12016-03-18 14:47:51 -0400741 def json_errors(self, res, error):
742 response_object = {'status': 'error', 'data': {}}
743 response_object['message'] = error.status_line
744 response_object['data']['description'] = str(error.body)
745 if error.status_code == 500:
746 response_object['data']['exception'] = repr(error.exception)
747 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500748
Brad Bishop87b63c12016-03-18 14:47:51 -0400749 json_response = json.dumps(response_object, **self.json_opts)
750 response.content_type = 'application/json'
751 return json_response
752
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500753
Brad Bishop80fe37a2016-03-29 10:54:54 -0400754class JsonpPlugin(JsonApiErrorsPlugin):
755 ''' Json javascript wrapper. '''
756 name = 'jsonp'
757 api = 2
758
759 def __init__(self, **kw):
760 super(JsonpPlugin, self).__init__(**kw)
761
762 @staticmethod
763 def to_jsonp(json):
764 jwrapper = request.query.callback or None
765 if(jwrapper):
766 response.set_header('Content-Type', 'application/javascript')
767 json = jwrapper + '(' + json + ');'
768 return json
769
770 def apply(self, callback, route):
771 def wrap(*a, **kw):
772 return self.to_jsonp(callback(*a, **kw))
773 return wrap
774
775 def json_errors(self, res, error):
776 json = super(JsonpPlugin, self).json_errors(res, error)
777 return self.to_jsonp(json)
778
779
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500780class RestApp(Bottle):
Brad Bishop87b63c12016-03-18 14:47:51 -0400781 def __init__(self, bus):
782 super(RestApp, self).__init__(autojson=False)
783 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500784 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500785
Brad Bishop87b63c12016-03-18 14:47:51 -0400786 self.install_hooks()
787 self.install_plugins()
788 self.create_handlers()
789 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500790
Brad Bishop87b63c12016-03-18 14:47:51 -0400791 def install_plugins(self):
792 # install json api plugins
793 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400794 self.install(AuthorizationPlugin())
Brad Bishop80fe37a2016-03-29 10:54:54 -0400795 self.install(JsonpPlugin(**json_kw))
796 self.install(JSONPlugin(**json_kw))
Brad Bishop87b63c12016-03-18 14:47:51 -0400797 self.install(JsonApiResponsePlugin())
798 self.install(JsonApiRequestPlugin())
799 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500800
Brad Bishop87b63c12016-03-18 14:47:51 -0400801 def install_hooks(self):
802 self.real_router_match = self.router.match
803 self.router.match = self.custom_router_match
804 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500805
Brad Bishop87b63c12016-03-18 14:47:51 -0400806 def create_handlers(self):
807 # create route handlers
808 self.session_handler = SessionHandler(self, self.bus)
809 self.directory_handler = DirectoryHandler(self, self.bus)
810 self.list_names_handler = ListNamesHandler(self, self.bus)
811 self.list_handler = ListHandler(self, self.bus)
812 self.method_handler = MethodHandler(self, self.bus)
813 self.property_handler = PropertyHandler(self, self.bus)
814 self.schema_handler = SchemaHandler(self, self.bus)
815 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500816
Brad Bishop87b63c12016-03-18 14:47:51 -0400817 def install_handlers(self):
818 self.session_handler.install()
819 self.directory_handler.install()
820 self.list_names_handler.install()
821 self.list_handler.install()
822 self.method_handler.install()
823 self.property_handler.install()
824 self.schema_handler.install()
825 # this has to come last, since it matches everything
826 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500827
Brad Bishop87b63c12016-03-18 14:47:51 -0400828 def custom_router_match(self, environ):
829 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
830 needed doesn't work for us since the instance rules match
831 everything. This monkey-patch lets the route handler figure
832 out which response is needed. This could be accomplished
833 with a hook but that would require calling the router match
834 function twice.
835 '''
836 route, args = self.real_router_match(environ)
837 if isinstance(route.callback, RouteHandler):
838 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500839
Brad Bishop87b63c12016-03-18 14:47:51 -0400840 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500841
Brad Bishop87b63c12016-03-18 14:47:51 -0400842 @staticmethod
843 def strip_extra_slashes():
844 path = request.environ['PATH_INFO']
845 trailing = ("", "/")[path[-1] == '/']
846 parts = filter(bool, path.split('/'))
847 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400848
849if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400850 log = logging.getLogger('Rocket.Errors')
851 log.setLevel(logging.INFO)
852 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500853
Brad Bishop87b63c12016-03-18 14:47:51 -0400854 bus = dbus.SystemBus()
855 app = RestApp(bus)
856 default_cert = os.path.join(
857 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500858
Brad Bishop87b63c12016-03-18 14:47:51 -0400859 server = Rocket(
860 ('0.0.0.0', 443, default_cert, default_cert),
861 'wsgi', {'wsgi_app': app},
862 min_threads=1,
863 max_threads=1)
864 server.start()