From 75f669ad454ef2202139ff70f0b2164e37eadf79 Mon Sep 17 00:00:00 2001 From: Ricardo Dias Date: Thu, 24 May 2018 10:27:43 +0100 Subject: [PATCH] mgr/dashboard: controllers: @Endpoint annotation implementation 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 --- .../mgr/dashboard/controllers/__init__.py | 443 +++++++++++------- src/pybind/mgr/dashboard/controllers/auth.py | 2 +- .../mgr/dashboard/controllers/cephfs.py | 13 +- .../controllers/cluster_configuration.py | 2 +- .../mgr/dashboard/controllers/dashboard.py | 9 +- .../controllers/erasure_code_profile.py | 2 +- src/pybind/mgr/dashboard/controllers/host.py | 2 +- .../mgr/dashboard/controllers/monitor.py | 9 +- src/pybind/mgr/dashboard/controllers/osd.py | 2 +- src/pybind/mgr/dashboard/controllers/pool.py | 7 +- src/pybind/mgr/dashboard/controllers/rbd.py | 14 +- .../dashboard/controllers/rbd_mirroring.py | 8 +- src/pybind/mgr/dashboard/controllers/rgw.py | 18 +- .../mgr/dashboard/controllers/summary.py | 9 +- src/pybind/mgr/dashboard/controllers/task.py | 2 +- .../mgr/dashboard/controllers/tcmu_iscsi.py | 2 +- src/pybind/mgr/dashboard/module.py | 2 +- .../mgr/dashboard/tests/test_exceptions.py | 31 +- .../mgr/dashboard/tests/test_rest_tasks.py | 4 +- src/pybind/mgr/dashboard/tests/test_tools.py | 12 +- 20 files changed, 326 insertions(+), 267 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index 6dabe363dc5..ff82a4c86ba 100644 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -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 diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index 4f37a133036..1cbad91cb86 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -11,7 +11,7 @@ from .. import logger, mgr from ..tools import Session -@ApiController('auth') +@ApiController('/auth') class Auth(RESTController): """ Provide login and logout actions. diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 22acf8d60fc..40f367850ff 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -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 diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py index fd8388d2b87..35b5ca3778f 100644 --- a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py +++ b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/dashboard.py b/src/pybind/mgr/dashboard/controllers/dashboard.py index 9a409b36dff..4e21810b4b4 100644 --- a/src/pybind/mgr/dashboard/controllers/dashboard.py +++ b/src/pybind/mgr/dashboard/controllers/dashboard.py @@ -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 diff --git a/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py index 16929081d6d..b5178ea9bbb 100644 --- a/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py +++ b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py @@ -15,7 +15,7 @@ def _serialize_ecp(name, ecp): return ecp -@ApiController('erasure_code_profile') +@ApiController('/erasure_code_profile') @AuthRequired() class ErasureCodeProfile(RESTController): """ diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index f93d943b664..ff08112afa1 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -5,7 +5,7 @@ from . import ApiController, AuthRequired, RESTController from .. import mgr -@ApiController('host') +@ApiController('/host') @AuthRequired() class Host(RESTController): def list(self): diff --git a/src/pybind/mgr/dashboard/controllers/monitor.py b/src/pybind/mgr/dashboard/controllers/monitor.py index d429e7c303c..12bba726b86 100644 --- a/src/pybind/mgr/dashboard/controllers/monitor.py +++ b/src/pybind/mgr/dashboard/controllers/monitor.py @@ -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 = [], [] diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index d5a25e20c4b..44295d6ef92 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py index 402225ad880..84c2b7a9a0c 100644 --- a/src/pybind/mgr/dashboard/controllers/pool.py +++ b/src/pybind/mgr/dashboard/controllers/pool.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index daa3e4937fc..05409fd9d6d 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py index c218a518f82..ceb299170ed 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -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() diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 13f177a6069..7d02d11d1a5 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/summary.py b/src/pybind/mgr/dashboard/controllers/summary.py index da570ceae1e..0bf21c881ba 100644 --- a/src/pybind/mgr/dashboard/controllers/summary.py +++ b/src/pybind/mgr/dashboard/controllers/summary.py @@ -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 { diff --git a/src/pybind/mgr/dashboard/controllers/task.py b/src/pybind/mgr/dashboard/controllers/task.py index b986b172aec..537f34158f2 100644 --- a/src/pybind/mgr/dashboard/controllers/task.py +++ b/src/pybind/mgr/dashboard/controllers/task.py @@ -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): diff --git a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py index 942f1c60c70..89e7c8ed890 100644 --- a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py +++ b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py @@ -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 diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index 5ed9525f3e2..81af10ac6cb 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -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) diff --git a/src/pybind/mgr/dashboard/tests/test_exceptions.py b/src/pybind/mgr/dashboard/tests/test_exceptions.py index 98f80265ef8..8998cd38d81 100644 --- a/src/pybind/mgr/dashboard/tests/test_exceptions.py +++ b/src/pybind/mgr/dashboard/tests/test_exceptions.py @@ -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)) diff --git a/src/pybind/mgr/dashboard/tests/test_rest_tasks.py b/src/pybind/mgr/dashboard/tests/test_rest_tasks.py index 3a23c115566..ea7bba23171 100644 --- a/src/pybind/mgr/dashboard/tests/test_rest_tasks.py +++ b/src/pybind/mgr/dashboard/tests/test_rest_tasks.py @@ -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} diff --git a/src/pybind/mgr/dashboard/tests/test_tools.py b/src/pybind/mgr/dashboard/tests/test_tools.py index 69c8b697b14..aaadd1e8af5 100644 --- a/src/pybind/mgr/dashboard/tests/test_tools.py +++ b/src/pybind/mgr/dashboard/tests/test_tools.py @@ -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} -- 2.39.5