]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
restful: Prepare for api file split
authorBoris Ranto <branto@redhat.com>
Tue, 9 May 2017 07:53:46 +0000 (09:53 +0200)
committerBoris Ranto <branto@redhat.com>
Mon, 22 May 2017 17:19:02 +0000 (19:19 +0200)
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>
src/pybind/mgr/restful/api.py
src/pybind/mgr/restful/decorators.py [new file with mode: 0644]
src/pybind/mgr/restful/module.py

index 3c09ad6a42753702b962f35e657e345f18d3934c..10de13de7e5b2ab9eabf2257d787f3216c1ddd9a 100644 (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
diff --git a/src/pybind/mgr/restful/decorators.py b/src/pybind/mgr/restful/decorators.py
new file mode 100644 (file)
index 0000000..8399449
--- /dev/null
@@ -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
index ba99cefcbea6c414ec4487248ece1fe8ca391a7b..0d286eb71ddb125bcb05d81fdf3db880bef256c1 100644 (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):