]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: controllers: @Endpoint annotation implementation
authorRicardo Dias <rdias@suse.com>
Thu, 24 May 2018 09:27:43 +0000 (10:27 +0100)
committerRicardo Dias <rdias@suse.com>
Mon, 28 May 2018 09:29:29 +0000 (10:29 +0100)
With these changes we now have a single implementation for both the
BaseController and RESTController classes, with the respective
overrides.

Signed-off-by: Ricardo Dias <rdias@suse.com>
20 files changed:
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/controllers/cluster_configuration.py
src/pybind/mgr/dashboard/controllers/dashboard.py
src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/monitor.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/controllers/summary.py
src/pybind/mgr/dashboard/controllers/task.py
src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/tests/test_exceptions.py
src/pybind/mgr/dashboard/tests/test_rest_tasks.py
src/pybind/mgr/dashboard/tests/test_tools.py

index 6dabe363dc5c5336629e4e4c191d64f94f51fe96..ff82a4c86ba82353fdf03a806d3496619b93a251 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# pylint: disable=protected-access
+# pylint: disable=protected-access,too-many-branches
 from __future__ import absolute_import
 
 import collections
@@ -21,16 +21,25 @@ from ..services.exception import serialize_dashboard_exception
 
 
 class Controller(object):
-    def __init__(self, path, base_url=""):
+    def __init__(self, path, base_url=None):
         self.path = path
         self.base_url = base_url
 
+        if self.path and self.path[0] != "/":
+            self.path = "/" + self.path
+
+        if self.base_url is None:
+            self.base_url = ""
+        elif self.base_url == "/":
+            self.base_url = ""
+
+        if self.base_url == "" and self.path == "":
+            self.base_url = "/"
+
     def __call__(self, cls):
         cls._cp_controller_ = True
-        if self.base_url:
-            cls._cp_path_ = "{}/{}".format(self.base_url, self.path)
-        else:
-            cls._cp_path_ = self.path
+        cls._cp_path_ = "{}{}".format(self.base_url, self.path)
+
         config = {
             'tools.sessions.on': True,
             'tools.sessions.name': Session.NAME,
@@ -46,17 +55,12 @@ class Controller(object):
 
 
 class ApiController(Controller):
-    def __init__(self, path, version=1):
-        if version == 1:
-            base_url = "api"
-        else:
-            base_url = "api/v" + str(version)
-        super(ApiController, self).__init__(path, base_url)
-        self.version = version
+    def __init__(self, path):
+        super(ApiController, self).__init__(path, base_url="/api")
 
     def __call__(self, cls):
         cls = super(ApiController, self).__call__(cls)
-        cls._api_version = self.version
+        cls._api_endpoint = True
         return cls
 
 
@@ -72,6 +76,72 @@ def AuthRequired(enabled=True):
     return decorate
 
 
+def Endpoint(method=None, path=None, path_params=None, query_params=None,
+             json_response=True, proxy=False):
+
+    if method is None:
+        method = 'GET'
+    elif not isinstance(method, str) or \
+            method.upper() not in ['GET', 'POST', 'DELETE', 'PUT']:
+        raise TypeError("Possible values for method are: 'GET', 'POST', "
+                        "'DELETE', or 'PUT'")
+
+    method = method.upper()
+
+    if method in ['GET', 'DELETE']:
+        if path_params is not None:
+            raise TypeError("path_params should not be used for {} "
+                            "endpoints. All function params are considered"
+                            " path parameters by default".format(method))
+
+    if path_params is None:
+        if method in ['POST', 'PUT']:
+            path_params = []
+
+    if query_params is None:
+        query_params = []
+
+    def _wrapper(func):
+        if method in ['POST', 'PUT']:
+            func_params = _get_function_params(func)
+            for param in func_params:
+                if param['name'] in path_params and not param['required']:
+                    raise TypeError("path_params can only reference "
+                                    "non-optional function parameters")
+
+        if func.__name__ == '__call__' and path is None:
+            e_path = ""
+        else:
+            e_path = path
+
+        if e_path is not None:
+            e_path = e_path.strip()
+            if e_path and e_path[0] != "/":
+                e_path = "/" + e_path
+            elif e_path == "/":
+                e_path = ""
+
+        func._endpoint = {
+            'method': method,
+            'path': e_path,
+            'path_params': path_params,
+            'query_params': query_params,
+            'json_response': json_response,
+            'proxy': proxy
+        }
+        return func
+    return _wrapper
+
+
+def Proxy(path=None):
+    if path is None:
+        path = ""
+    elif path == "/":
+        path = ""
+    path += "/{path:.*}"
+    return Endpoint(path=path, proxy=True)
+
+
 def load_controllers():
     # setting sys.path properly when not running under the mgr
     controllers_dir = os.path.dirname(os.path.realpath(__file__))
@@ -111,17 +181,26 @@ def generate_controller_routes(ctrl_class, mapper, base_url):
     endp_base_urls = set()
 
     for endpoint in ctrl_class.endpoints():
-        conditions = dict(method=endpoint.methods) if endpoint.methods else None
+        if endpoint.proxy:
+            conditions = None
+        else:
+            conditions = dict(method=[endpoint.method])
+
         endp_url = endpoint.url
-        if '/' in endp_url:
-            endp_base_urls.add(endp_url[:endp_url.find('/')])
+        if base_url == "/":
+            base_url = ""
+        if endp_url == "/" and base_url:
+            endp_url = ""
+        url = "{}{}".format(base_url, endp_url)
+
+        if '/' in url[len(base_url)+1:]:
+            endp_base_urls.add(url[:len(base_url)+1+endp_url[1:].find('/')])
         else:
-            endp_base_urls.add(endp_url)
-        url = "{}/{}".format(base_url, endp_url)
+            endp_base_urls.add(url)
 
         logger.debug("Mapped [%s] to %s:%s restricted to %s",
                      url, ctrl_class.__name__, endpoint.action,
-                     endpoint.methods)
+                     endpoint.method)
 
         ENDPOINT_MAP[endpoint.url].append(endpoint)
 
@@ -260,32 +339,51 @@ class BaseController(object):
         """
         An instance of this class represents an endpoint.
         """
-        def __init__(self, ctrl, func, methods=None):
+        def __init__(self, ctrl, func):
             self.ctrl = ctrl
-            self.func = self._unwrap(func)
-            if methods is None:
-                methods = []
-            self.methods = methods
+            self.func = func
 
-        @classmethod
-        def _unwrap(cls, func):
-            while hasattr(func, "__wrapped__"):
-                func = func.__wrapped__
-            return func
+            if not self.config['proxy']:
+                setattr(self.ctrl, func.__name__, self.function)
+
+        @property
+        def config(self):
+            func = self.func
+            while not hasattr(func, '_endpoint'):
+                if hasattr(func, "__wrapped__"):
+                    func = func.__wrapped__
+                else:
+                    return None
+            return func._endpoint
+
+        @property
+        def function(self):
+            return self.ctrl._request_wrapper(self.func, self.method,
+                                              self.config['json_response'])
+
+        @property
+        def method(self):
+            return self.config['method']
+
+        @property
+        def proxy(self):
+            return self.config['proxy']
 
         @property
         def url(self):
-            ctrl_path_params = self.ctrl.get_path_param_names()
-            if self.func.__name__ != '__call__':
-                url = "{}/{}".format(self.ctrl.get_path(), self.func.__name__)
+            if self.config['path'] is not None:
+                url = "{}{}".format(self.ctrl.get_path(), self.config['path'])
             else:
-                url = self.ctrl.get_path()
-            path_params = [
-                p['name'] for p in _get_function_params(self.func)
-                if p['required'] and p['name'] not in ctrl_path_params]
+                url = "{}/{}".format(self.ctrl.get_path(), self.func.__name__)
+
+            ctrl_path_params = self.ctrl.get_path_param_names(
+                self.config['path'])
+            path_params = [p['name'] for p in self.path_params
+                           if p['name'] not in ctrl_path_params]
             path_params = ["{{{}}}".format(p) for p in path_params]
             if path_params:
                 url += "/{}".format("/".join(path_params))
+
             return url
 
         @property
@@ -294,16 +392,41 @@ class BaseController(object):
 
         @property
         def path_params(self):
-            return [p for p in _get_function_params(self.func) if p['required']]
+            ctrl_path_params = self.ctrl.get_path_param_names(
+                self.config['path'])
+            func_params = _get_function_params(self.func)
+
+            if self.method in ['GET', 'DELETE']:
+                assert self.config['path_params'] is None
+
+                return [p for p in func_params if p['name'] in ctrl_path_params
+                        or (p['name'] not in self.config['query_params']
+                            and p['required'])]
+
+            # elif self.method in ['POST', 'PUT']:
+            return [p for p in func_params if p['name'] in ctrl_path_params
+                    or p['name'] in self.config['path_params']]
 
         @property
         def query_params(self):
-            return [p for p in _get_function_params(self.func)
-                    if not p['required']]
+            if self.method in ['GET', 'DELETE']:
+                func_params = _get_function_params(self.func)
+                path_params = [p['name'] for p in self.path_params]
+                return [p for p in func_params if p['name'] not in path_params]
+
+            # elif self.method in ['POST', 'PUT']:
+            func_params = _get_function_params(self.func)
+            return [p for p in func_params
+                    if p['name'] in self.config['query_params']]
 
         @property
         def body_params(self):
-            return []
+            func_params = _get_function_params(self.func)
+            path_params = [p['name'] for p in self.path_params]
+            query_params = [p['name'] for p in self.query_params]
+            return [p for p in func_params
+                    if p['name'] not in path_params and
+                    p['name'] not in query_params]
 
         @property
         def group(self):
@@ -311,25 +434,30 @@ class BaseController(object):
 
         @property
         def is_api(self):
-            return hasattr(self.ctrl, '_api_version')
+            return hasattr(self.ctrl, '_api_endpoint')
 
         @property
         def is_secure(self):
             return self.ctrl._cp_config['tools.authenticate.on']
 
         def __repr__(self):
-            return "Endpoint({}, {}, {})".format(self.url, self.methods,
+            return "Endpoint({}, {}, {})".format(self.url, self.method,
                                                  self.action)
 
     def __init__(self):
-        logger.info('Initializing controller: %s -> /%s',
+        logger.info('Initializing controller: %s -> %s',
                     self.__class__.__name__, self._cp_path_)
 
     @classmethod
-    def get_path_param_names(cls):
+    def get_path_param_names(cls, path_extension=None):
+        if path_extension is None:
+            path_extension = ""
+        full_path = cls._cp_path_[1:] + path_extension
         path_params = []
-        for step in cls._cp_path_.split('/'):
+        for step in full_path.split('/'):
             param = None
+            if not step:
+                continue
             if step[0] == ':':
                 param = step[1:]
             elif step[0] == '{' and step[-1] == '}':
@@ -352,12 +480,42 @@ class BaseController(object):
         :rtype: list[BaseController.Endpoint]
         """
         result = []
-
         for _, func in inspect.getmembers(cls, predicate=callable):
-            if hasattr(func, 'exposed') and func.exposed:
+            if hasattr(func, '_endpoint'):
                 result.append(cls.Endpoint(cls, func))
         return result
 
+    @staticmethod
+    def _request_wrapper(func, method, json_response):
+        @wraps(func)
+        def inner(*args, **kwargs):
+            if method in ['GET', 'DELETE']:
+                ret = func(*args, **kwargs)
+
+            elif cherrypy.request.headers.get('Content-Type', '') == \
+                    'application/x-www-form-urlencoded':
+                ret = func(*args, **kwargs)
+
+            else:
+                content_length = int(cherrypy.request.headers['Content-Length'])
+                body = cherrypy.request.body.read(content_length)
+                if not body:
+                    return func(*args, **kwargs)
+
+                try:
+                    data = json.loads(body.decode('utf-8'))
+                except Exception as e:
+                    raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
+                                             .format(str(e)))
+                kwargs.update(data.items())
+                ret = func(*args, **kwargs)
+
+            if json_response:
+                cherrypy.response.headers['Content-Type'] = 'application/json'
+                return json.dumps(ret).encode('utf8')
+            return ret
+        return inner
+
 
 class RESTController(BaseController):
     """
@@ -400,78 +558,6 @@ class RESTController(BaseController):
         ('set', {'method': 'PUT', 'resource': True, 'status': 200})
     ])
 
-    class RESTEndpoint(BaseController.Endpoint):
-        def __init__(self, ctrl, func):
-            if func.__name__ in ctrl._method_mapping:
-                methods = [ctrl._method_mapping[func.__name__]['method']]
-                status = ctrl._method_mapping[func.__name__]['status']
-            elif hasattr(func, "_resource_method_"):
-                methods = func._resource_method_
-                status = 200
-            elif hasattr(func, "_collection_method_"):
-                methods = func._collection_method_
-                status = 200
-            else:
-                assert False
-
-            wrapper = ctrl._rest_request_wrapper(func, status)
-            setattr(ctrl, func.__name__, wrapper)
-
-            super(RESTController.RESTEndpoint, self).__init__(
-                ctrl, func, methods)
-
-        def get_resource_id_params(self):
-            if self.func.__name__ in self.ctrl._method_mapping:
-                if self.ctrl._method_mapping[self.func.__name__]['resource']:
-                    resource_id_params = self.ctrl.infer_resource_id()
-                    if resource_id_params:
-                        return resource_id_params
-
-            if hasattr(self.func, '_resource_method_'):
-                resource_id_params = self.ctrl.infer_resource_id()
-                if resource_id_params:
-                    return resource_id_params
-
-            return []
-
-        @property
-        def url(self):
-            url = self.ctrl.get_path()
-
-            res_id_params = self.get_resource_id_params()
-            if res_id_params:
-                res_id_params = ["{{{}}}".format(p) for p in res_id_params]
-                url += "/{}".format("/".join(res_id_params))
-
-            if hasattr(self.func, "_collection_method_") \
-               or hasattr(self.func, "_resource_method_"):
-                url += "/{}".format(self.func.__name__)
-            return url
-
-        @property
-        def path_params(self):
-            params = [{'name': p, 'required': True}
-                      for p in self.ctrl.get_path_param_names()]
-            params.extend([{'name': p, 'required': True}
-                           for p in self.get_resource_id_params()])
-            return params
-
-        @property
-        def query_params(self):
-            path_params_names = [p['name'] for p in self.path_params]
-            if 'GET' in self.methods or 'DELETE' in self.methods:
-                return [p for p in _get_function_params(self.func)
-                        if p['name'] not in path_params_names]
-            return []
-
-        @property
-        def body_params(self):
-            path_params_names = [p['name'] for p in self.path_params]
-            if 'POST' in self.methods or 'PUT' in self.methods:
-                return [p for p in _get_function_params(self.func)
-                        if p['name'] not in path_params_names]
-            return []
-
     @classmethod
     def infer_resource_id(cls):
         if cls.RESOURCE_ID is not None:
@@ -489,84 +575,83 @@ class RESTController(BaseController):
 
     @classmethod
     def endpoints(cls):
-        result = []
-        for _, val in inspect.getmembers(cls, predicate=callable):
-            if val.__name__ in cls._method_mapping:
-                result.append(cls.RESTEndpoint(cls, val))
-            elif hasattr(val, "_collection_method_") \
-                    or hasattr(val, "_resource_method_"):
-                result.append(cls.RESTEndpoint(cls, val))
-            elif hasattr(val, 'exposed') and val.exposed:
-                result.append(cls.Endpoint(cls, val))
-        return result
+        result = super(RESTController, cls).endpoints()
+        for _, func in inspect.getmembers(cls, predicate=callable):
+            no_resource_id_params = False
+            status = 200
+            method = None
+            path = ""
+
+            if func.__name__ in cls._method_mapping:
+                meth = cls._method_mapping[func.__name__]
+
+                if meth['resource']:
+                    res_id_params = cls.infer_resource_id()
+                    if res_id_params is None:
+                        no_resource_id_params = True
+                    else:
+                        res_id_params = ["{{{}}}".format(p) for p in res_id_params]
+                        path += "/{}".format("/".join(res_id_params))
 
-    @classmethod
-    def _rest_request_wrapper(cls, func, status_code):
-        @wraps(func)
-        def wrapper(*vpath, **params):
-            method = func
-            if cherrypy.request.method not in ['GET', 'DELETE']:
-                method = RESTController._takes_json(method)
+                status = meth['status']
+                method = meth['method']
 
-            method = RESTController._returns_json(method)
+            elif hasattr(func, "_collection_method_"):
+                path = "/{}".format(func.__name__)
+                method = func._collection_method_
 
-            cherrypy.response.status = status_code
+            elif hasattr(func, "_resource_method_"):
+                res_id_params = cls.infer_resource_id()
+                if res_id_params is None:
+                    no_resource_id_params = True
+                else:
+                    res_id_params = ["{{{}}}".format(p) for p in res_id_params]
+                    path += "/{}".format("/".join(res_id_params))
+                    path += "/{}".format(func.__name__)
 
-            return method(*vpath, **params)
-        if not hasattr(wrapper, '__wrapped__'):
-            wrapper.__wrapped__ = func
-        return wrapper
+                method = func._resource_method_
 
-    @staticmethod
-    def _function_args(func):
-        return getargspec(func).args[1:]
+            else:
+                continue
 
-    @staticmethod
-    def _takes_json(func):
-        def inner(*args, **kwargs):
-            if cherrypy.request.headers.get('Content-Type', '') == \
-                    'application/x-www-form-urlencoded':
-                return func(*args, **kwargs)
+            if no_resource_id_params:
+                raise TypeError("Could not infer the resource ID parameters for"
+                                " method {}. "
+                                "Please specify the resource ID parameters "
+                                "using the RESOURCE_ID class property"
+                                .format(func.__name__))
 
-            content_length = int(cherrypy.request.headers['Content-Length'])
-            body = cherrypy.request.body.read(content_length)
-            if not body:
-                return func(*args, **kwargs)
+            func = cls._status_code_wrapper(func, status)
+            endp_func = Endpoint(method, path=path)(func)
+            result.append(cls.Endpoint(cls, endp_func))
 
-            try:
-                data = json.loads(body.decode('utf-8'))
-            except Exception as e:
-                raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
-                                         .format(str(e)))
+        return result
 
-            kwargs.update(data.items())
-            return func(*args, **kwargs)
-        return inner
+    @classmethod
+    def _status_code_wrapper(cls, func, status_code):
+        @wraps(func)
+        def wrapper(*vpath, **params):
+            cherrypy.response.status = status_code
+            return func(*vpath, **params)
 
-    @staticmethod
-    def _returns_json(func):
-        def inner(*args, **kwargs):
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            ret = func(*args, **kwargs)
-            return json.dumps(ret).encode('utf8')
-        return inner
+        return wrapper
 
     @staticmethod
-    def resource(methods=None):
-        if not methods:
-            methods = ['GET']
+    def Resource(method=None):
+        if not method:
+            method = 'GET'
 
         def _wrapper(func):
-            func._resource_method_ = methods
+            func._resource_method_ = method
             return func
         return _wrapper
 
     @staticmethod
-    def collection(methods=None):
-        if not methods:
-            methods = ['GET']
+    def Collection(method=None):
+        if not method:
+            method = 'GET'
 
         def _wrapper(func):
-            func._collection_method_ = methods
+            func._collection_method_ = method
             return func
         return _wrapper
index 4f37a133036e072fcfe0f8d275d06cdc4836b39a..1cbad91cb86b483e5f1146c06237c9d279f89930 100644 (file)
@@ -11,7 +11,7 @@ from .. import logger, mgr
 from ..tools import Session
 
 
-@ApiController('auth')
+@ApiController('/auth')
 class Auth(RESTController):
     """
     Provide login and logout actions.
index 22acf8d60fc2f8f035b175296bfd5257be8e471d..40f367850fff904a0c0e9bc83efd1b8a9bdeb39c 100644 (file)
@@ -6,13 +6,13 @@ from collections import defaultdict
 import cherrypy
 
 from ..exceptions import DashboardException
-from . import ApiController, AuthRequired, BaseController
+from . import ApiController, AuthRequired, Endpoint, BaseController
 from .. import mgr
 from ..services.ceph_service import CephService
 from ..tools import ViewCache
 
 
-@ApiController('cephfs')
+@ApiController('/cephfs')
 @AuthRequired()
 class CephFS(BaseController):
     def __init__(self):
@@ -22,22 +22,19 @@ class CephFS(BaseController):
         # dict is FSCID
         self.cephfs_clients = {}
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def clients(self, fs_id):
         fs_id = self.fs_id_to_int(fs_id)
 
         return self._clients(fs_id)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def data(self, fs_id):
         fs_id = self.fs_id_to_int(fs_id)
 
         return self.fs_status(fs_id)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def mds_counters(self, fs_id):
         """
         Result format: map of daemon name to map of counter to list of datapoints
index fd8388d2b8765aca76588a8383d55bd872d36162..35b5ca3778f44d0316568b2356265d97c092d9bc 100644 (file)
@@ -7,7 +7,7 @@ from .. import mgr
 from . import ApiController, RESTController, AuthRequired
 
 
-@ApiController('cluster_conf')
+@ApiController('/cluster_conf')
 @AuthRequired()
 class ClusterConfiguration(RESTController):
     def list(self):
index 9a409b36dff7fa4d44030eee09e60aa7f66a4cc3..4e21810b4b496c1ec64e728be7e7f5610cff7427 100644 (file)
@@ -4,9 +4,7 @@ from __future__ import absolute_import
 import collections
 import json
 
-import cherrypy
-
-from . import ApiController, AuthRequired, BaseController
+from . import ApiController, AuthRequired, Endpoint, BaseController
 from .. import mgr
 from ..services.ceph_service import CephService
 from ..tools import NotificationQueue
@@ -15,7 +13,7 @@ from ..tools import NotificationQueue
 LOG_BUFFER_SIZE = 30
 
 
-@ApiController('dashboard')
+@ApiController('/dashboard')
 @AuthRequired()
 class Dashboard(BaseController):
     def __init__(self):
@@ -38,8 +36,7 @@ class Dashboard(BaseController):
         for l in lines:
             buf.appendleft(l)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def health(self):
         if not self._log_initialized:
             self._log_initialized = True
index 16929081d6d372706ecef39e1fbf728ac600fef1..b5178ea9bbb1443a4494c63cbc3e70d5e740a5e9 100644 (file)
@@ -15,7 +15,7 @@ def _serialize_ecp(name, ecp):
     return ecp
 
 
-@ApiController('erasure_code_profile')
+@ApiController('/erasure_code_profile')
 @AuthRequired()
 class ErasureCodeProfile(RESTController):
     """
index f93d943b66485bf9df27227900ab3ab91a40df52..ff08112afa1a7da94dcec405175650f96dcdd579 100644 (file)
@@ -5,7 +5,7 @@ from . import ApiController, AuthRequired, RESTController
 from .. import mgr
 
 
-@ApiController('host')
+@ApiController('/host')
 @AuthRequired()
 class Host(RESTController):
     def list(self):
index d429e7c303cdd4121a071c2ecbb53c92707698ad..12bba726b86763770f653497fa2c63bcb1592840 100644 (file)
@@ -3,17 +3,14 @@ from __future__ import absolute_import
 
 import json
 
-import cherrypy
-
-from . import ApiController, AuthRequired, BaseController
+from . import ApiController, AuthRequired, Endpoint, BaseController
 from .. import mgr
 
 
-@ApiController('monitor')
+@ApiController('/monitor')
 @AuthRequired()
 class Monitor(BaseController):
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def __call__(self):
         in_quorum, out_quorum = [], []
 
index d5a25e20c4bfee87ffda87c0d0ab011c0fd729a4..44295d6ef9291854fcb051ca86e9820b464918df 100644 (file)
@@ -7,7 +7,7 @@ from ..services.ceph_service import CephService
 from ..services.exception import handle_send_command_error
 
 
-@ApiController('osd')
+@ApiController('/osd')
 @AuthRequired()
 class Osd(RESTController):
     def list(self):
index 402225ad88098ce7ef2736042e5ea41bdadd8acb..84c2b7a9a0cd05ba54bbb29925a8dbbbb99d857e 100644 (file)
@@ -3,13 +3,13 @@ from __future__ import absolute_import
 
 import cherrypy
 
-from . import ApiController, RESTController, AuthRequired
+from . import ApiController, RESTController, Endpoint, AuthRequired
 from .. import mgr
 from ..services.ceph_service import CephService
 from ..services.exception import handle_send_command_error
 
 
-@ApiController('pool')
+@ApiController('/pool')
 @AuthRequired()
 class Pool(RESTController):
 
@@ -88,8 +88,7 @@ class Pool(RESTController):
         for key, value in kwargs.items():
             CephService.send_command('mon', 'osd pool set', pool=pool, var=key, val=value)
 
-    @cherrypy.tools.json_out()
-    @cherrypy.expose
+    @Endpoint()
     def _info(self):
         """Used by the create-pool dialog"""
         def rules(pool_type):
index daa3e4937fc9d53caaf5b768a57e68e0e5984803..05409fd9d6d6de4c6e35d1bdb72c4c65f013c7a7 100644 (file)
@@ -112,7 +112,7 @@ def _sort_features(features, enable=True):
     features.sort(key=key_func, reverse=not enable)
 
 
-@ApiController('block/image')
+@ApiController('/block/image')
 @AuthRequired()
 class Rbd(RESTController):
 
@@ -334,7 +334,7 @@ class Rbd(RESTController):
               'src_image_name': '{image_name}',
               'dest_pool_name': '{dest_pool_name}',
               'dest_image_name': '{dest_image_name}'}, 2.0)
-    @RESTController.resource(['POST'])
+    @RESTController.Resource('POST')
     def copy(self, pool_name, image_name, dest_pool_name, dest_image_name,
              snapshot_name=None, obj_size=None, features=None, stripe_unit=None,
              stripe_count=None, data_pool=None):
@@ -360,7 +360,7 @@ class Rbd(RESTController):
         return _rbd_image_call(pool_name, image_name, _src_copy)
 
     @RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0)
-    @RESTController.resource(['POST'])
+    @RESTController.Resource('POST')
     def flatten(self, pool_name, image_name):
 
         def _flatten(ioctx, image):
@@ -368,13 +368,13 @@ class Rbd(RESTController):
 
         return _rbd_image_call(pool_name, image_name, _flatten)
 
-    @RESTController.collection(['GET'])
+    @RESTController.Collection('GET')
     def default_features(self):
         rbd_default_features = mgr.get('config')['rbd_default_features']
         return _format_bitmask(int(rbd_default_features))
 
 
-@ApiController('block/image/:pool_name/:image_name/snap')
+@ApiController('/block/image/:pool_name/:image_name/snap')
 @AuthRequired()
 class RbdSnapshot(RESTController):
 
@@ -417,7 +417,7 @@ class RbdSnapshot(RESTController):
 
     @RbdTask('snap/rollback',
              ['{pool_name}', '{image_name}', '{snapshot_name}'], 5.0)
-    @RESTController.resource(['POST'])
+    @RESTController.Resource('POST')
     def rollback(self, pool_name, image_name, snapshot_name):
         def _rollback(ioctx, img, snapshot_name):
             img.rollback_to_snap(snapshot_name)
@@ -429,7 +429,7 @@ class RbdSnapshot(RESTController):
               'parent_snap_name': '{snapshot_name}',
               'child_pool_name': '{child_pool_name}',
               'child_image_name': '{child_image_name}'}, 2.0)
-    @RESTController.resource(['POST'])
+    @RESTController.Resource('POST')
     def clone(self, pool_name, image_name, snapshot_name, child_pool_name,
               child_image_name, obj_size=None, features=None,
               stripe_unit=None, stripe_count=None, data_pool=None):
index c218a518f82ffaa07e3f4b5709282e1a28dc8ce2..ceb299170ed2322d417f0f32d1b801754d9dc3be 100644 (file)
@@ -6,10 +6,9 @@ import re
 
 from functools import partial
 
-import cherrypy
 import rbd
 
-from . import ApiController, AuthRequired, BaseController
+from . import ApiController, AuthRequired, Endpoint, BaseController
 from .. import logger, mgr
 from ..services.ceph_service import CephService
 from ..tools import ViewCache
@@ -155,7 +154,7 @@ def get_daemons_and_pools():  # pylint: disable=R0915
     }
 
 
-@ApiController('rbdmirror')
+@ApiController('/rbdmirror')
 @AuthRequired()
 class RbdMirror(BaseController):
 
@@ -163,8 +162,7 @@ class RbdMirror(BaseController):
         super(RbdMirror, self).__init__()
         self.pool_data = {}
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     @handle_rbd_error()
     def __call__(self):
         status, content_data = self._get_content_data()
index 13f177a60698f5992120903b4f7694985f24123a..7d02d11d1a53922746a1baada24bb0768226b12a 100644 (file)
@@ -4,19 +4,19 @@ from __future__ import absolute_import
 import json
 import cherrypy
 
-from . import ApiController, BaseController, RESTController, AuthRequired
+from . import ApiController, BaseController, RESTController, AuthRequired, \
+              Endpoint, Proxy
 from .. import logger
 from ..services.ceph_service import CephService
 from ..services.rgw_client import RgwClient
 from ..rest_client import RequestException
 
 
-@ApiController('rgw')
+@ApiController('/rgw')
 @AuthRequired()
-class Rgw(RESTController):
+class Rgw(BaseController):
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def status(self):
         status = {'available': False, 'message': None}
         try:
@@ -37,7 +37,7 @@ class Rgw(RESTController):
         return status
 
 
-@ApiController('rgw/daemon')
+@ApiController('/rgw/daemon')
 @AuthRequired()
 class RgwDaemon(RESTController):
 
@@ -84,11 +84,11 @@ class RgwDaemon(RESTController):
         return daemon
 
 
-@ApiController('rgw/proxy/{path:.*}')
+@ApiController('/rgw/proxy')
 @AuthRequired()
 class RgwProxy(BaseController):
 
-    @cherrypy.expose
+    @Proxy()
     def __call__(self, path, **params):
         try:
             rgw_client = RgwClient.admin_instance()
@@ -109,7 +109,7 @@ class RgwProxy(BaseController):
             return json.dumps({'detail': str(e)}).encode('utf-8')
 
 
-@ApiController('rgw/bucket')
+@ApiController('/rgw/bucket')
 @AuthRequired()
 class RgwBucket(RESTController):
 
index da570ceae1e4918f53a57b0afd246eb3865aec2d..0bf21c881ba6e2a29d99e92a402028bd2d2dea5a 100644 (file)
@@ -3,17 +3,15 @@ from __future__ import absolute_import
 
 import json
 
-import cherrypy
-
 from .. import mgr
-from . import AuthRequired, ApiController, BaseController
+from . import AuthRequired, ApiController, Endpoint, BaseController
 from ..controllers.rbd_mirroring import get_daemons_and_pools
 from ..tools import ViewCacheNoDataException
 from ..services.ceph_service import CephService
 from ..tools import TaskManager
 
 
-@ApiController('summary')
+@ApiController('/summary')
 @AuthRequired()
 class Summary(BaseController):
     def _rbd_pool_data(self):
@@ -57,8 +55,7 @@ class Summary(BaseController):
                 warnings += 1
         return {'warnings': warnings, 'errors': errors}
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def __call__(self):
         executing_t, finished_t = TaskManager.list_serializable()
         return {
index b986b172aec4c0a7d138bae7483868b59b0d9e1d..537f34158f2b58290ffa3301a4dbab5b65d7b4a1 100644 (file)
@@ -5,7 +5,7 @@ from . import ApiController, AuthRequired, RESTController
 from ..tools import TaskManager
 
 
-@ApiController('task')
+@ApiController('/task')
 @AuthRequired()
 class Task(RESTController):
     def list(self, name=None):
index 942f1c60c70b0d4d646d2b91b3d54d9dacd98aa0..89e7c8ed890a05d1944ed28bcf87aab2178a85f6 100644 (file)
@@ -8,7 +8,7 @@ from ..services.ceph_service import CephService
 SERVICE_TYPE = 'tcmu-runner'
 
 
-@ApiController('tcmuiscsi')
+@ApiController('/tcmuiscsi')
 @AuthRequired()
 class TcmuIscsi(RESTController):
     # pylint: disable=too-many-nested-blocks
index 5ed9525f3e26c665c50fd5d8ae3e3c5f5a964416..81af10ac6cbf9ae53d1fcb41f6ae894677fe7ccc 100644 (file)
@@ -286,7 +286,7 @@ class Module(MgrModule, SSLCherryPyConfig):
             }
         }
         for purl in parent_urls:
-            config['{}/{}'.format(self.url_prefix, purl)] = {
+            config[purl] = {
                 'request.dispatch': mapper
             }
         cherrypy.tree.mount(None, config=config)
index 98f80265ef8ee22290f8aef7435a40cb10a8609d..8998cd38d81c1d43282972630f953a82a0f90fc8 100644 (file)
@@ -3,11 +3,9 @@ from __future__ import absolute_import
 
 import time
 
-import cherrypy
-
 import rados
 from ..services.ceph_service import SendCommandError
-from ..controllers import RESTController, Controller, Task
+from ..controllers import RESTController, Controller, Task, Endpoint
 from .helper import ControllerTestCase
 from ..services.exception import handle_rados_error, handle_send_command_error, \
     serialize_dashboard_exception
@@ -18,31 +16,26 @@ from ..tools import ViewCache, TaskManager, NotificationQueue
 @Controller('foo')
 class FooResource(RESTController):
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     @handle_rados_error('foo')
     def no_exception(self, param1, param2):
         return [param1, param2]
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     @handle_rados_error('foo')
     def error_foo_controller(self):
         raise rados.OSError('hi', errno=-42)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     @handle_send_command_error('foo')
     def error_send_command(self):
         raise SendCommandError('hi', 'prefix', {}, -42)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def error_generic(self):
         raise rados.Error('hi')
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def vc_no_data(self):
         @ViewCache(timeout=0)
         def _no_data():
@@ -52,8 +45,7 @@ class FooResource(RESTController):
         assert False
 
     @handle_rados_error('foo')
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def vc_exception(self):
         @ViewCache(timeout=10)
         def _raise():
@@ -62,8 +54,7 @@ class FooResource(RESTController):
         _raise()
         assert False
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def internal_server_error(self):
         return 1/0
 
@@ -71,16 +62,14 @@ class FooResource(RESTController):
     def list(self):
         raise SendCommandError('list', 'prefix', {}, -42)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     @Task('task_exceptions/task_exception', {1: 2}, 1.0,
           exception_handler=serialize_dashboard_exception)
     @handle_rados_error('foo')
     def task_exception(self):
         raise rados.OSError('hi', errno=-42)
 
-    @cherrypy.expose
-    @cherrypy.tools.json_out()
+    @Endpoint()
     def wait_task_exception(self):
         ex, _ = TaskManager.list('task_exceptions/task_exception')
         return bool(len(ex))
index 3a23c11556618739a6a5513fbb36ad7f9dfd3510..ea7bba23171debd88ca7d483febc2d6425f58598 100644 (file)
@@ -29,12 +29,12 @@ class TaskTest(RESTController):
         time.sleep(TaskTest.sleep_time)
 
     @Task('task/foo', ['{param}'])
-    @RESTController.collection(['POST'])
+    @RESTController.Collection('POST')
     def foo(self, param):
         return {'my_param': param}
 
     @Task('task/bar', ['{key}', '{param}'])
-    @RESTController.resource(['PUT'])
+    @RESTController.Resource('PUT')
     def bar(self, key, param=None):
         return {'my_param': param, 'key': key}
 
index 69c8b697b14a9e317c74a7b6583572584dee5d6b..aaadd1e8af53a6d486919c79d7d463039bed4a92 100644 (file)
@@ -10,12 +10,12 @@ from mock import patch
 from ..services.exception import handle_rados_error
 from .helper import ControllerTestCase
 from ..controllers import RESTController, ApiController, Controller, \
-                          BaseController
+                          BaseController, Proxy
 from ..tools import is_valid_ipv6_address, dict_contains_path
 
 
 # pylint: disable=W0613
-@Controller('foo')
+@Controller('/foo')
 class FooResource(RESTController):
     elems = []
 
@@ -40,20 +40,20 @@ class FooResource(RESTController):
         return dict(key=key, newdata=newdata)
 
 
-@Controller('foo/:key/:method')
+@Controller('/foo/:key/:method')
 class FooResourceDetail(RESTController):
     def list(self, key, method):
         return {'detail': (key, [method])}
 
 
-@ApiController('rgw/proxy/{path:.*}')
+@ApiController('/rgw/proxy')
 class GenerateControllerRoutesController(BaseController):
-    @cherrypy.expose
+    @Proxy()
     def __call__(self, path, **params):
         pass
 
 
-@ApiController('fooargs')
+@ApiController('/fooargs')
 class FooArgs(RESTController):
     def set(self, code, name=None, opt1=None, opt2=None):
         return {'code': code, 'name': name, 'opt1': opt1, 'opt2': opt2}