restful: Prepare for api file split

The patch moves decorators into a separate file so they could be shared
amongst various files.

It also fixes key generation in werkzeug 0.10+ and fixes get input
method on various endpoints.

Signed-off-by: Boris Ranto <branto@redhat.com>
This commit is contained in:
Boris Ranto 2017-05-09 09:53:46 +02:00
parent cdd2079dbb
commit e58ba75bc0
3 changed files with 183 additions and 157 deletions

View File

@ -2,12 +2,11 @@ from pecan import expose, request, response
from pecan.rest import RestController
import common
import traceback
from base64 import b64decode
from functools import wraps
from collections import defaultdict
from decorators import auth, catch, lock
## We need this to access the instance of the module
#
# We can't use 'from module import instance' because
@ -15,65 +14,15 @@ from collections import defaultdict
import module
# Helper function to catch and log the exceptions
def catch(f):
@wraps(f)
def catcher(*args, **kwargs):
try:
return f(*args, **kwargs)
except:
module.instance.log.error(str(traceback.format_exc()))
response.status = 500
return {'message': str(traceback.format_exc()).split('\n')}
return catcher
# Handle authorization
def auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if not request.authorization:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: No HTTP username/password'}
username, password = b64decode(request.authorization[1]).split(':')
# Check that the username exists
if username not in module.instance.keys:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: No such user'}
# Check the password
if module.instance.keys[username] != password:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: Incorrect password'}
return f(*args, **kwargs)
return decorated
# Helper function to lock the function
def lock(f):
@wraps(f)
def locker(*args, **kwargs):
with module.instance.requests_lock:
return f(*args, **kwargs)
return locker
class ServerFqdn(RestController):
def __init__(self, fqdn):
self.fqdn = fqdn
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for the server fqdn
"""
@ -82,10 +31,10 @@ class ServerFqdn(RestController):
class Server(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for all the servers
"""
@ -103,10 +52,10 @@ class RequestId(RestController):
self.request_id = request_id
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for the request id
"""
@ -120,20 +69,20 @@ class RequestId(RestController):
return {'message': 'Unknown request id "%s"' % str(self.request_id)}
request = request[0]
return request.humanify()
return request
@expose('json')
@expose(template='json')
@catch
@auth
@lock
def delete(self):
def delete(self, **kwargs):
"""
Remove the request id from the database
"""
for index in range(len(module.instance.requests)):
if module.instance.requests[index].id == self.request_id:
return module.instance.requests.pop(index).humanify()
return module.instance.requests.pop(index)
# Failed to find the job to cancel
response.status = 500
@ -142,10 +91,10 @@ class RequestId(RestController):
class Request(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
List all the available requests and their state
"""
@ -156,11 +105,11 @@ class Request(RestController):
return states
@expose('json')
@expose(template='json')
@catch
@auth
@lock
def delete(self):
def delete(self, **kwargs):
"""
Remove all the finished requests
"""
@ -178,6 +127,16 @@ class Request(RestController):
}
@expose(template='json')
@catch
@auth
def post(self, **kwargs):
"""
Pass through method to create any request
"""
return module.instance.submit_request([[request.json]], **kwargs)
@expose()
def _lookup(self, request_id, *remainder):
return RequestId(request_id), remainder
@ -189,10 +148,10 @@ class PoolId(RestController):
self.pool_id = pool_id
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for the pool id
"""
@ -208,10 +167,10 @@ class PoolId(RestController):
return pool
@expose('json')
@expose(template='json')
@catch
@auth
def patch(self):
def patch(self, **kwargs):
"""
Modify the information for the pool id
"""
@ -230,13 +189,13 @@ class PoolId(RestController):
return {'message': 'Invalid arguments found: "%s"' % str(invalid)}
# Schedule the update request
return module.instance.submit_request(common.pool_update_commands(pool['pool_name'], args))
return module.instance.submit_request(common.pool_update_commands(pool['pool_name'], args), **kwargs)
@expose('json')
@expose(template='json')
@catch
@auth
def delete(self):
def delete(self, **kwargs):
"""
Remove the pool data for the pool id
"""
@ -251,15 +210,15 @@ class PoolId(RestController):
'pool': pool['pool_name'],
'pool2': pool['pool_name'],
'sure': '--yes-i-really-really-mean-it'
}]])
}]], **kwargs)
class Pool(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for all the pools
"""
@ -273,10 +232,10 @@ class Pool(RestController):
return pools
@expose('json')
@expose(template='json')
@catch
@auth
def post(self):
def post(self, **kwargs):
"""
Create a new pool
Requires name and pg_num dict arguments
@ -310,7 +269,8 @@ class Pool(RestController):
# Schedule the creation and update requests
return module.instance.submit_request(
[[create_command]] +
common.pool_update_commands(pool_name, args)
common.pool_update_commands(pool_name, args),
**kwargs
)
@ -325,10 +285,10 @@ class OsdIdCommand(RestController):
self.osd_id = osd_id
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show implemented commands for the OSD id
"""
@ -344,10 +304,10 @@ class OsdIdCommand(RestController):
return []
@expose('json')
@expose(template='json')
@catch
@auth
def post(self):
def post(self, **kwargs):
"""
Run the implemented command for the OSD id
"""
@ -366,7 +326,7 @@ class OsdIdCommand(RestController):
return module.instance.submit_request([[{
'prefix': 'osd ' + command,
'who': str(self.osd_id)
}]])
}]], **kwargs)
@ -376,14 +336,14 @@ class OsdId(RestController):
self.command = OsdIdCommand(osd_id)
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for the OSD id
"""
osd = module.instance.get_osds([str(self.osd_id)])
osd = module.instance.get_osds(ids=[str(self.osd_id)])
if len(osd) != 1:
response.status = 500
return {'message': 'Failed to identify the OSD id "%d"' % self.osd_id}
@ -391,10 +351,10 @@ class OsdId(RestController):
return osd[0]
@expose('json')
@expose(template='json')
@catch
@auth
def patch(self):
def patch(self, **kwargs):
"""
Modify the state (up, in) of the OSD id or reweight it
"""
@ -431,23 +391,23 @@ class OsdId(RestController):
'weight': args['reweight']
})
return module.instance.submit_request([commands])
return module.instance.submit_request([commands], **kwargs)
class Osd(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for all the OSDs
"""
# Parse request args
ids = request.GET.getall('id[]')
pool_id = request.GET.get('pool', None)
# TODO Filter by ids
pool_id = kwargs.get('pool', None)
return module.instance.get_osds(ids, pool_id)
return module.instance.get_osds(pool_id)
@expose()
@ -461,10 +421,10 @@ class MonName(RestController):
self.name = name
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for the monitor name
"""
@ -482,10 +442,10 @@ class MonName(RestController):
class Mon(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show the information for all the monitors
"""
@ -499,9 +459,9 @@ class Mon(RestController):
class Doc(RestController):
@expose('json')
@expose(template='json')
@catch
def get(self):
def get(self, **kwargs):
"""
Show documentation information
"""
@ -510,10 +470,10 @@ class Doc(RestController):
class CrushRuleset(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show crush rulesets
"""
@ -530,10 +490,10 @@ class CrushRuleset(RestController):
class CrushRule(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show crush rules
"""
@ -554,10 +514,10 @@ class Crush(RestController):
class ConfigOsd(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show OSD configuration options
"""
@ -569,14 +529,13 @@ class ConfigOsd(RestController):
return flags.split(',')
@expose('json')
@expose(template='json')
@catch
@auth
def patch(self):
def patch(self, **kwargs):
"""
Modify OSD configration options
"""
args = request.json
commands = []
@ -597,7 +556,7 @@ class ConfigOsd(RestController):
'key': flag,
})
return module.instance.submit_request([commands])
return module.instance.submit_request([commands], **kwargs)
@ -606,10 +565,10 @@ class ConfigClusterKey(RestController):
self.key = key
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show specific configuration option
"""
@ -618,10 +577,10 @@ class ConfigClusterKey(RestController):
class ConfigCluster(RestController):
@expose('json')
@expose(template='json')
@catch
@auth
def get(self):
def get(self, **kwargs):
"""
Show all cluster configuration options
"""
@ -650,9 +609,9 @@ class Root(RestController):
request = Request()
server = Server()
@expose('json')
@expose(template='json')
@catch
def get(self):
def get(self, **kwargs):
"""
Show the basic information for the REST API
This includes values like api version or auth method

View File

@ -0,0 +1,60 @@
from pecan import request, response
from base64 import b64decode
from functools import wraps
import traceback
## We need this to access the instance of the module
#
# We can't use 'from module import instance' because
# the instance is not ready, yet (would be None)
import module
# Handle authorization
def auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if not request.authorization:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: No HTTP username/password'}
username, password = b64decode(request.authorization[1]).split(':')
# Check that the username exists
if username not in module.instance.keys:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: No such user'}
# Check the password
if module.instance.keys[username] != password:
response.status = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return {'message': 'auth: Incorrect password'}
return f(*args, **kwargs)
return decorated
# Helper function to catch and log the exceptions
def catch(f):
@wraps(f)
def decorated(*args, **kwargs):
try:
return f(*args, **kwargs)
except:
module.instance.log.error(str(traceback.format_exc()))
response.status = 500
return {'message': str(traceback.format_exc()).split('\n')}
return decorated
# Helper function to lock the function
def lock(f):
@wraps(f)
def decorated(*args, **kwargs):
with module.instance.requests_lock:
return f(*args, **kwargs)
return decorated

View File

@ -3,18 +3,18 @@ A RESTful API for Ceph
"""
import json
import time
import errno
import inspect
import StringIO
import threading
import traceback
import ConfigParser
import common
from uuid import uuid4
from pecan import jsonify, make_app
from OpenSSL import SSL, crypto
from OpenSSL import crypto
from tempfile import NamedTemporaryFile
from pecan.rest import RestController
from werkzeug.serving import make_server
@ -152,7 +152,7 @@ class CommandsRequest(object):
return "success"
def humanify(self):
def __json__(self):
return {
'id': self.id,
'running': map(
@ -181,23 +181,21 @@ class CommandsRequest(object):
class Module(MgrModule):
COMMANDS = [
{
"cmd": "create_key "
"name=key_name,type=CephString",
"desc": "Create an API key with this name",
"perm": "rw"
},
{
"cmd": "delete_key "
"name=key_name,type=CephString",
"desc": "Delete an API key with this name",
"perm": "rw"
},
{
"cmd": "list_keys",
"desc": "List all API keys",
"perm": "rw"
},
{
"cmd": "create_key name=key_name,type=CephString",
"desc": "Create an API key with this name",
"perm": "rw"
},
{
"cmd": "delete_key name=key_name,type=CephString",
"desc": "Delete an API key with this name",
"perm": "rw"
},
{
"cmd": "list_keys",
"desc": "List all API keys",
"perm": "rw"
},
]
def __init__(self, *args, **kwargs):
@ -211,10 +209,11 @@ class Module(MgrModule):
self.keys = {}
self.disable_auth = False
self.shutdown_key = str(uuid4())
self.server = None
self.cert_file = None
self.pkey_file = None
def serve(self):
try:
@ -242,21 +241,20 @@ class Module(MgrModule):
self.set_config_json('cert', self.cert)
self.set_config_json('pkey', self.pkey)
# use SSL context for https
context = SSL.Context(SSL.TLSv1_METHOD)
context.use_certificate(
crypto.load_certificate(crypto.FILETYPE_PEM, self.cert)
)
context.use_privatekey(
crypto.load_privatekey(crypto.FILETYPE_PEM, self.pkey)
)
self.cert_file = NamedTemporaryFile()
self.cert_file.write(self.cert)
self.cert_file.flush()
self.pkey_file = NamedTemporaryFile()
self.pkey_file.write(self.pkey)
self.pkey_file.flush()
# Create the HTTPS werkzeug server serving pecan app
self.server = make_server(
host='0.0.0.0',
port=8002,
app=make_app('restful.api.Root'),
ssl_context=context
ssl_context=(self.cert_file.name, self.pkey_file.name),
)
self.server.serve_forever()
@ -264,7 +262,12 @@ class Module(MgrModule):
def shutdown(self):
try:
self.server.shutdown()
if self.server:
self.server.shutdown()
if self.cert_file:
self.cert_file.close()
if self.pkey_file:
self.pkey_file.close()
except:
self.log.error(str(traceback.format_exc()))
@ -336,7 +339,7 @@ class Module(MgrModule):
return (
-errno.EINVAL,
"",
"Command not found '{0}'".format(prefix)
"Command not found '{0}'".format(command['prefix'])
)
@ -423,7 +426,7 @@ class Module(MgrModule):
return osds
def get_osds(self, ids=[], pool_id=None):
def get_osds(self, pool_id=None, ids=None):
# Get data
osd_map = self.get('osd_map')
osd_metadata = self.get('osd_metadata')
@ -432,7 +435,7 @@ class Module(MgrModule):
osds = osd_map['osds']
# Filter by osd ids
if ids:
if ids is not None:
osds = filter(
lambda x: str(x['osd']) in ids,
osds
@ -494,10 +497,14 @@ class Module(MgrModule):
return pool[0]
def submit_request(self, _request):
def submit_request(self, _request, **kwargs):
request = CommandsRequest(_request)
self.requests.append(request)
return request.humanify()
with self.requests_lock:
self.requests.append(request)
if kwargs.get('wait', 0):
while not request.is_finished():
time.sleep(0.001)
return request
def run_command(self, command):