if(WITH_TESTS)
include(AddCephTest)
- add_tox_test(mgr-dashboard TOX_ENVS py3 lint check openapi-check)
+ add_tox_test(mgr-dashboard-py3 TOX_ENVS py3)
+ add_tox_test(mgr-dashboard-lint TOX_ENVS lint)
+ add_tox_test(mgr-dashboard-check TOX_ENVS check)
+ add_tox_test(mgr-dashboard-openapi TOX_ENVS openapi-check)
endif()
-# -*- coding: utf-8 -*-
-# pylint: disable=protected-access,too-many-branches,too-many-lines
-from __future__ import absolute_import
-
-import collections
-import importlib
-import inspect
-import json
-import logging
-import os
-import pkgutil
-import re
-import sys
-from functools import wraps
-from urllib.parse import unquote
-
-# pylint: disable=wrong-import-position
-import cherrypy
-# pylint: disable=import-error
-from ceph_argparse import ArgumentFormat # type: ignore
-
-from .. import DEFAULT_VERSION
-from ..api.doc import SchemaInput, SchemaType
-from ..exceptions import DashboardException, PermissionNotValid, ScopeNotValid
-from ..plugins import PLUGIN_MANAGER
-from ..security import Permission, Scope
-from ..services.auth import AuthManager, JwtManager
-from ..tools import TaskManager, get_request_body_params, getargspec
-
-try:
- from typing import Any, List, Optional
-except ImportError:
- pass # For typing only
-
-
-def EndpointDoc(description="", group="", parameters=None, responses=None): # noqa: N802
- if not isinstance(description, str):
- raise Exception("%s has been called with a description that is not a string: %s"
- % (EndpointDoc.__name__, description))
- if not isinstance(group, str):
- raise Exception("%s has been called with a groupname that is not a string: %s"
- % (EndpointDoc.__name__, group))
- if parameters and not isinstance(parameters, dict):
- raise Exception("%s has been called with parameters that is not a dict: %s"
- % (EndpointDoc.__name__, parameters))
- if responses and not isinstance(responses, dict):
- raise Exception("%s has been called with responses that is not a dict: %s"
- % (EndpointDoc.__name__, responses))
-
- if not parameters:
- parameters = {}
-
- def _split_param(name, p_type, description, optional=False, default_value=None, nested=False):
- param = {
- 'name': name,
- 'description': description,
- 'required': not optional,
- 'nested': nested,
- }
- if default_value:
- param['default'] = default_value
- if isinstance(p_type, type):
- param['type'] = p_type
- else:
- nested_params = _split_parameters(p_type, nested=True)
- if nested_params:
- param['type'] = type(p_type)
- param['nested_params'] = nested_params
- else:
- param['type'] = p_type
- return param
-
- # Optional must be set to True in order to set default value and parameters format must be:
- # 'name: (type or nested parameters, description, [optional], [default value])'
- def _split_dict(data, nested):
- splitted = []
- for name, props in data.items():
- if isinstance(name, str) and isinstance(props, tuple):
- if len(props) == 2:
- param = _split_param(name, props[0], props[1], nested=nested)
- elif len(props) == 3:
- param = _split_param(name, props[0], props[1], optional=props[2], nested=nested)
- if len(props) == 4:
- param = _split_param(name, props[0], props[1], props[2], props[3], nested)
- splitted.append(param)
- else:
- raise Exception(
- """Parameter %s in %s has not correct format. Valid formats are:
- <name>: (<type>, <description>, [optional], [default value])
- <name>: (<[type]>, <description>, [optional], [default value])
- <name>: (<[nested parameters]>, <description>, [optional], [default value])
- <name>: (<{nested parameters}>, <description>, [optional], [default value])"""
- % (name, EndpointDoc.__name__))
- return splitted
-
- def _split_list(data, nested):
- splitted = [] # type: List[Any]
- for item in data:
- splitted.extend(_split_parameters(item, nested))
- return splitted
-
- # nested = True means parameters are inside a dict or array
- def _split_parameters(data, nested=False):
- param_list = [] # type: List[Any]
- if isinstance(data, dict):
- param_list.extend(_split_dict(data, nested))
- elif isinstance(data, (list, tuple)):
- param_list.extend(_split_list(data, True))
- return param_list
-
- resp = {}
- if responses:
- for status_code, response_body in responses.items():
- schema_input = SchemaInput()
- schema_input.type = SchemaType.ARRAY if \
- isinstance(response_body, list) else SchemaType.OBJECT
- schema_input.params = _split_parameters(response_body)
-
- resp[str(status_code)] = schema_input
-
- def _wrapper(func):
- func.doc_info = {
- 'summary': description,
- 'tag': group,
- 'parameters': _split_parameters(parameters),
- 'response': resp
- }
- return func
-
- return _wrapper
-
-
-class ControllerDoc(object):
- def __init__(self, description="", group=""):
- self.tag = group
- self.tag_descr = description
-
- def __call__(self, cls):
- cls.doc_info = {
- 'tag': self.tag,
- 'tag_descr': self.tag_descr
- }
- return cls
-
-
-class Controller(object):
- def __init__(self, path, base_url=None, security_scope=None, secure=True):
- if security_scope and not Scope.valid_scope(security_scope):
- raise ScopeNotValid(security_scope)
- self.path = path
- self.base_url = base_url
- self.security_scope = security_scope
- self.secure = secure
-
- 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
- cls._cp_path_ = "{}{}".format(self.base_url, self.path)
- cls._security_scope = self.security_scope
-
- config = {
- 'tools.dashboard_exception_handler.on': True,
- 'tools.authenticate.on': self.secure,
- }
- if not hasattr(cls, '_cp_config'):
- cls._cp_config = {}
- cls._cp_config.update(config)
- return cls
-
-
-class ApiController(Controller):
- def __init__(self, path, security_scope=None, secure=True):
- super(ApiController, self).__init__(path, base_url="/api",
- security_scope=security_scope,
- secure=secure)
-
- def __call__(self, cls):
- cls = super(ApiController, self).__call__(cls)
- cls._api_endpoint = True
- return cls
-
-
-class UiApiController(Controller):
- def __init__(self, path, security_scope=None, secure=True):
- super(UiApiController, self).__init__(path, base_url="/ui-api",
- security_scope=security_scope,
- secure=secure)
-
- def __call__(self, cls):
- cls = super(UiApiController, self).__call__(cls)
- cls._api_endpoint = False
- return cls
-
-
-def Endpoint(method=None, path=None, path_params=None, query_params=None, # noqa: N802
- json_response=True, proxy=False, xml=False, version=DEFAULT_VERSION):
-
- 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,
- 'xml': xml,
- 'version': version
- }
- return func
- return _wrapper
-
-
-def Proxy(path=None): # noqa: N802
- if path is None:
- path = ""
- elif path == "/":
- path = ""
- path += "/{path:.*}"
- return Endpoint(path=path, proxy=True)
-
-
-def load_controllers():
- logger = logging.getLogger('controller.load')
- # setting sys.path properly when not running under the mgr
- controllers_dir = os.path.dirname(os.path.realpath(__file__))
- dashboard_dir = os.path.dirname(controllers_dir)
- mgr_dir = os.path.dirname(dashboard_dir)
- logger.debug("controllers_dir=%s", controllers_dir)
- logger.debug("dashboard_dir=%s", dashboard_dir)
- logger.debug("mgr_dir=%s", mgr_dir)
- if mgr_dir not in sys.path:
- sys.path.append(mgr_dir)
-
- controllers = []
- mods = [mod for _, mod, _ in pkgutil.iter_modules([controllers_dir])]
- logger.debug("mods=%s", mods)
- for mod_name in mods:
- mod = importlib.import_module('.controllers.{}'.format(mod_name),
- package='dashboard')
- for _, cls in mod.__dict__.items():
- # Controllers MUST be derived from the class BaseController.
- if inspect.isclass(cls) and issubclass(cls, BaseController) and \
- hasattr(cls, '_cp_controller_'):
- if cls._cp_path_.startswith(':'):
- # invalid _cp_path_ value
- logger.error("Invalid url prefix '%s' for controller '%s'",
- cls._cp_path_, cls.__name__)
- continue
- controllers.append(cls)
-
- for clist in PLUGIN_MANAGER.hook.get_controllers() or []:
- controllers.extend(clist)
-
- return controllers
-
-
-ENDPOINT_MAP = collections.defaultdict(list) # type: dict
-
-
-def generate_controller_routes(endpoint, mapper, base_url):
- inst = endpoint.inst
- ctrl_class = endpoint.ctrl
-
- if endpoint.proxy:
- conditions = None
- else:
- conditions = dict(method=[endpoint.method])
-
- # base_url can be empty or a URL path that starts with "/"
- # we will remove the trailing "/" if exists to help with the
- # concatenation with the endpoint url below
- if base_url.endswith("/"):
- base_url = base_url[:-1]
-
- endp_url = endpoint.url
-
- if endp_url.find("/", 1) == -1:
- parent_url = "{}{}".format(base_url, endp_url)
- else:
- parent_url = "{}{}".format(base_url, endp_url[:endp_url.find("/", 1)])
-
- # parent_url might be of the form "/.../{...}" where "{...}" is a path parameter
- # we need to remove the path parameter definition
- parent_url = re.sub(r'(?:/\{[^}]+\})$', '', parent_url)
- if not parent_url: # root path case
- parent_url = "/"
-
- url = "{}{}".format(base_url, endp_url)
-
- logger = logging.getLogger('controller')
- logger.debug("Mapped [%s] to %s:%s restricted to %s",
- url, ctrl_class.__name__, endpoint.action,
- endpoint.method)
-
- ENDPOINT_MAP[endpoint.url].append(endpoint)
-
- name = ctrl_class.__name__ + ":" + endpoint.action
- mapper.connect(name, url, controller=inst, action=endpoint.action,
- conditions=conditions)
-
- # adding route with trailing slash
- name += "/"
- url += "/"
- mapper.connect(name, url, controller=inst, action=endpoint.action,
- conditions=conditions)
-
- return parent_url
-
-
-def generate_routes(url_prefix):
- mapper = cherrypy.dispatch.RoutesDispatcher()
- ctrls = load_controllers()
-
- parent_urls = set()
-
- endpoint_list = []
- for ctrl in ctrls:
- inst = ctrl()
- for endpoint in ctrl.endpoints():
- endpoint.inst = inst
- endpoint_list.append(endpoint)
-
- endpoint_list = sorted(endpoint_list, key=lambda e: e.url)
- for endpoint in endpoint_list:
- parent_urls.add(generate_controller_routes(endpoint, mapper,
- "{}".format(url_prefix)))
-
- logger = logging.getLogger('controller')
- logger.debug("list of parent paths: %s", parent_urls)
- return mapper, parent_urls
-
-
-def json_error_page(status, message, traceback, version):
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps(dict(status=status, detail=message, traceback=traceback,
- version=version))
-
-
-def _get_function_params(func):
- """
- Retrieves the list of parameters declared in function.
- Each parameter is represented as dict with keys:
- * name (str): the name of the parameter
- * required (bool): whether the parameter is required or not
- * default (obj): the parameter's default value
- """
- fspec = getargspec(func)
-
- func_params = []
- nd = len(fspec.args) if not fspec.defaults else -len(fspec.defaults)
- for param in fspec.args[1:nd]:
- func_params.append({'name': param, 'required': True})
-
- if fspec.defaults:
- for param, val in zip(fspec.args[nd:], fspec.defaults):
- func_params.append({
- 'name': param,
- 'required': False,
- 'default': val
- })
-
- return func_params
-
-
-class Task(object):
- def __init__(self, name, metadata, wait_for=5.0, exception_handler=None):
- self.name = name
- if isinstance(metadata, list):
- self.metadata = {e[1:-1]: e for e in metadata}
- else:
- self.metadata = metadata
- self.wait_for = wait_for
- self.exception_handler = exception_handler
-
- def _gen_arg_map(self, func, args, kwargs):
- arg_map = {}
- params = _get_function_params(func)
-
- args = args[1:] # exclude self
- for idx, param in enumerate(params):
- if idx < len(args):
- arg_map[param['name']] = args[idx]
- else:
- if param['name'] in kwargs:
- arg_map[param['name']] = kwargs[param['name']]
- else:
- assert not param['required'], "{0} is required".format(param['name'])
- arg_map[param['name']] = param['default']
-
- if param['name'] in arg_map:
- # This is not a type error. We are using the index here.
- arg_map[idx+1] = arg_map[param['name']]
-
- return arg_map
-
- def __call__(self, func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- arg_map = self._gen_arg_map(func, args, kwargs)
- metadata = {}
- for k, v in self.metadata.items():
- if isinstance(v, str) and v and v[0] == '{' and v[-1] == '}':
- param = v[1:-1]
- try:
- pos = int(param)
- metadata[k] = arg_map[pos]
- except ValueError:
- if param.find('.') == -1:
- metadata[k] = arg_map[param]
- else:
- path = param.split('.')
- metadata[k] = arg_map[path[0]]
- for i in range(1, len(path)):
- metadata[k] = metadata[k][path[i]]
- else:
- metadata[k] = v
- task = TaskManager.run(self.name, metadata, func, args, kwargs,
- exception_handler=self.exception_handler)
- try:
- status, value = task.wait(self.wait_for)
- except Exception as ex:
- if task.ret_value:
- # exception was handled by task.exception_handler
- if 'status' in task.ret_value:
- status = task.ret_value['status']
- else:
- status = getattr(ex, 'status', 500)
- cherrypy.response.status = status
- return task.ret_value
- raise ex
- if status == TaskManager.VALUE_EXECUTING:
- cherrypy.response.status = 202
- return {'name': self.name, 'metadata': metadata}
- return value
- return wrapper
-
-
-class BaseController(object):
- """
- Base class for all controllers providing API endpoints.
- """
-
- class Endpoint(object):
- """
- An instance of this class represents an endpoint.
- """
-
- def __init__(self, ctrl, func):
- self.ctrl = ctrl
- self.inst = None
- self.func = 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'],
- self.config['xml'],
- self.config['version'])
-
- @property
- def method(self):
- return self.config['method']
-
- @property
- def proxy(self):
- return self.config['proxy']
-
- @property
- def url(self):
- ctrl_path = self.ctrl.get_path()
- if ctrl_path == "/":
- ctrl_path = ""
- if self.config['path'] is not None:
- url = "{}{}".format(ctrl_path, self.config['path'])
- else:
- url = "{}/{}".format(ctrl_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
- def action(self):
- return self.func.__name__
-
- @property
- def path_params(self):
- 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):
- 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):
- 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):
- return self.ctrl.__name__
-
- @property
- def is_api(self):
- # changed from hasattr to getattr: some ui-based api inherit _api_endpoint
- return getattr(self.ctrl, '_api_endpoint', False)
-
- @property
- def is_secure(self):
- return self.ctrl._cp_config['tools.authenticate.on']
-
- def __repr__(self):
- return "Endpoint({}, {}, {})".format(self.url, self.method,
- self.action)
-
- def __init__(self):
- logger = logging.getLogger('controller')
- logger.info('Initializing controller: %s -> %s',
- self.__class__.__name__, self._cp_path_) # type: ignore
- super(BaseController, self).__init__()
-
- def _has_permissions(self, permissions, scope=None):
- if not self._cp_config['tools.authenticate.on']: # type: ignore
- raise Exception("Cannot verify permission in non secured "
- "controllers")
-
- if not isinstance(permissions, list):
- permissions = [permissions]
-
- if scope is None:
- scope = getattr(self, '_security_scope', None)
- if scope is None:
- raise Exception("Cannot verify permissions without scope security"
- " defined")
- username = JwtManager.LOCAL_USER.username
- return AuthManager.authorize(username, scope, permissions)
-
- @classmethod
- def get_path_param_names(cls, path_extension=None):
- if path_extension is None:
- path_extension = ""
- full_path = cls._cp_path_[1:] + path_extension # type: ignore
- path_params = []
- for step in full_path.split('/'):
- param = None
- if not step:
- continue
- if step[0] == ':':
- param = step[1:]
- elif step[0] == '{' and step[-1] == '}':
- param, _, _ = step[1:-1].partition(':')
- if param:
- path_params.append(param)
- return path_params
-
- @classmethod
- def get_path(cls):
- return cls._cp_path_ # type: ignore
-
- @classmethod
- def endpoints(cls):
- """
- This method iterates over all the methods decorated with ``@endpoint``
- and creates an Endpoint object for each one of the methods.
-
- :return: A list of endpoint objects
- :rtype: list[BaseController.Endpoint]
- """
- result = []
- for _, func in inspect.getmembers(cls, predicate=callable):
- if hasattr(func, '_endpoint'):
- result.append(cls.Endpoint(cls, func))
- return result
-
- @staticmethod
- def _request_wrapper(func, method, json_response, xml, # pylint: disable=unused-argument
- version):
- @wraps(func)
- def inner(*args, **kwargs):
- req_version = None
- for key, value in kwargs.items():
- if isinstance(value, str):
- kwargs[key] = unquote(value)
-
- # Process method arguments.
- params = get_request_body_params(cherrypy.request)
- kwargs.update(params)
-
- if version is not None:
- accept_header = cherrypy.request.headers.get('Accept')
- if accept_header and accept_header.startswith('application/vnd.ceph.api.v'):
- req_match = re.search(r"\d\.\d", accept_header)
- if req_match:
- req_version = req_match[0]
- else:
- raise cherrypy.HTTPError(415, "Unable to find version in request header")
-
- if req_version and req_version == version:
- ret = func(*args, **kwargs)
- else:
- raise cherrypy.HTTPError(415,
- "Incorrect version: "
- "{} requested but {} is expected"
- "".format(req_version, version))
- else:
- ret = func(*args, **kwargs)
- if isinstance(ret, bytes):
- ret = ret.decode('utf-8')
- if xml:
- if version:
- cherrypy.response.headers['Content-Type'] = \
- 'application/vnd.ceph.api.v{}+xml'.format(version)
- else:
- cherrypy.response.headers['Content-Type'] = 'application/xml'
- return ret.encode('utf8')
- if json_response:
- if version:
- cherrypy.response.headers['Content-Type'] = \
- 'application/vnd.ceph.api.v{}+json'.format(version)
- else:
- cherrypy.response.headers['Content-Type'] = 'application/json'
- ret = json.dumps(ret).encode('utf8')
- return ret
- return inner
-
- @property
- def _request(self):
- return self.Request(cherrypy.request)
-
- class Request(object):
- def __init__(self, cherrypy_req):
- self._creq = cherrypy_req
-
- @property
- def scheme(self):
- return self._creq.scheme
-
- @property
- def host(self):
- base = self._creq.base
- base = base[len(self.scheme)+3:]
- return base[:base.find(":")] if ":" in base else base
-
- @property
- def port(self):
- base = self._creq.base
- base = base[len(self.scheme)+3:]
- default_port = 443 if self.scheme == 'https' else 80
- return int(base[base.find(":")+1:]) if ":" in base else default_port
-
- @property
- def path_info(self):
- return self._creq.path_info
-
-
-class RESTController(BaseController):
- """
- Base class for providing a RESTful interface to a resource.
-
- To use this class, simply derive a class from it and implement the methods
- you want to support. The list of possible methods are:
-
- * list()
- * bulk_set(data)
- * create(data)
- * bulk_delete()
- * get(key)
- * set(data, key)
- * singleton_set(data)
- * delete(key)
-
- Test with curl:
-
- curl -H "Content-Type: application/json" -X POST \
- -d '{"username":"xyz","password":"xyz"}' https://127.0.0.1:8443/foo
- curl https://127.0.0.1:8443/foo
- curl https://127.0.0.1:8443/foo/0
-
- """
-
- # resource id parameter for using in get, set, and delete methods
- # should be overridden by subclasses.
- # to specify a composite id (two parameters) use '/'. e.g., "param1/param2".
- # If subclasses don't override this property we try to infer the structure
- # of the resource ID.
- RESOURCE_ID = None # type: Optional[str]
-
- _permission_map = {
- 'GET': Permission.READ,
- 'POST': Permission.CREATE,
- 'PUT': Permission.UPDATE,
- 'DELETE': Permission.DELETE
- }
-
- _method_mapping = collections.OrderedDict([
- ('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}),
- ('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
- ('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
- ('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
- ('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': DEFAULT_VERSION}),
- ('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
- ('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': DEFAULT_VERSION}),
- ('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}) # noqa E501 #pylint: disable=line-too-long
- ])
-
- @classmethod
- def infer_resource_id(cls):
- if cls.RESOURCE_ID is not None:
- return cls.RESOURCE_ID.split('/')
- for k, v in cls._method_mapping.items():
- func = getattr(cls, k, None)
- while hasattr(func, "__wrapped__"):
- func = func.__wrapped__
- if v['resource'] and func:
- path_params = cls.get_path_param_names()
- params = _get_function_params(func)
- return [p['name'] for p in params
- if p['required'] and p['name'] not in path_params]
- return None
-
- @classmethod
- def endpoints(cls):
- result = super(RESTController, cls).endpoints()
- res_id_params = cls.infer_resource_id()
-
- for _, func in inspect.getmembers(cls, predicate=callable):
- no_resource_id_params = False
- status = 200
- method = None
- query_params = None
- path = ""
- version = DEFAULT_VERSION
- sec_permissions = hasattr(func, '_security_permissions')
- permission = None
-
- if func.__name__ in cls._method_mapping:
- meth = cls._method_mapping[func.__name__] # type: dict
-
- if meth['resource']:
- if not res_id_params:
- no_resource_id_params = True
- else:
- path_params = ["{{{}}}".format(p) for p in res_id_params]
- path += "/{}".format("/".join(path_params))
-
- status = meth['status']
- method = meth['method']
- if hasattr(func, "__method_map_method__"):
- version = func.__method_map_method__['version']
- if not sec_permissions:
- permission = cls._permission_map[method]
-
- elif hasattr(func, "__collection_method__"):
- if func.__collection_method__['path']:
- path = func.__collection_method__['path']
- else:
- path = "/{}".format(func.__name__)
- status = func.__collection_method__['status']
- method = func.__collection_method__['method']
- query_params = func.__collection_method__['query_params']
- version = func.__collection_method__['version']
- if not sec_permissions:
- permission = cls._permission_map[method]
-
- elif hasattr(func, "__resource_method__"):
- if not res_id_params:
- no_resource_id_params = True
- else:
- path_params = ["{{{}}}".format(p) for p in res_id_params]
- path += "/{}".format("/".join(path_params))
- if func.__resource_method__['path']:
- path += func.__resource_method__['path']
- else:
- path += "/{}".format(func.__name__)
- status = func.__resource_method__['status']
- method = func.__resource_method__['method']
- version = func.__resource_method__['version']
- query_params = func.__resource_method__['query_params']
- if not sec_permissions:
- permission = cls._permission_map[method]
-
- else:
- continue
-
- if no_resource_id_params:
- raise TypeError("Could not infer the resource ID parameters for"
- " method {} of controller {}. "
- "Please specify the resource ID parameters "
- "using the RESOURCE_ID class property"
- .format(func.__name__, cls.__name__))
-
- if method in ['GET', 'DELETE']:
- params = _get_function_params(func)
- if res_id_params is None:
- res_id_params = []
- if query_params is None:
- query_params = [p['name'] for p in params
- if p['name'] not in res_id_params]
-
- func = cls._status_code_wrapper(func, status)
- endp_func = Endpoint(method, path=path,
- query_params=query_params, version=version)(func)
- if permission:
- _set_func_permissions(endp_func, [permission])
- result.append(cls.Endpoint(cls, endp_func))
-
- return result
-
- @classmethod
- def _status_code_wrapper(cls, func, status_code):
- @wraps(func)
- def wrapper(*vpath, **params):
- cherrypy.response.status = status_code
- return func(*vpath, **params)
-
- return wrapper
-
- @staticmethod
- def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802
- version=DEFAULT_VERSION):
- if not method:
- method = 'GET'
-
- if status is None:
- status = 200
-
- def _wrapper(func):
- func.__resource_method__ = {
- 'method': method,
- 'path': path,
- 'status': status,
- 'query_params': query_params,
- 'version': version
- }
- return func
- return _wrapper
-
- @staticmethod
- def MethodMap(resource=False, status=None, version=DEFAULT_VERSION): # noqa: N802
-
- if status is None:
- status = 200
-
- def _wrapper(func):
- func.__method_map_method__ = {
- 'resource': resource,
- 'status': status,
- 'version': version
- }
- return func
- return _wrapper
-
- @staticmethod
- def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
- version=DEFAULT_VERSION):
- if not method:
- method = 'GET'
-
- if status is None:
- status = 200
-
- def _wrapper(func):
- func.__collection_method__ = {
- 'method': method,
- 'path': path,
- 'status': status,
- 'query_params': query_params,
- 'version': version
- }
- return func
- return _wrapper
-
-
-class ControllerAuthMixin(object):
- @staticmethod
- def _delete_token_cookie(token):
- cherrypy.response.cookie['token'] = token
- cherrypy.response.cookie['token']['expires'] = 0
- cherrypy.response.cookie['token']['max-age'] = 0
-
- @staticmethod
- def _set_token_cookie(url_prefix, token):
- cherrypy.response.cookie['token'] = token
- if url_prefix == 'https':
- cherrypy.response.cookie['token']['secure'] = True
- cherrypy.response.cookie['token']['HttpOnly'] = True
- cherrypy.response.cookie['token']['path'] = '/'
- cherrypy.response.cookie['token']['SameSite'] = 'Strict'
-
-
-# Role-based access permissions decorators
-
-def _set_func_permissions(func, permissions):
- if not isinstance(permissions, list):
- permissions = [permissions]
-
- for perm in permissions:
- if not Permission.valid_permission(perm):
- logger = logging.getLogger('controller.set_func_perms')
- logger.debug("Invalid security permission: %s\n "
- "Possible values: %s", perm,
- Permission.all_permissions())
- raise PermissionNotValid(perm)
-
- if not hasattr(func, '_security_permissions'):
- func._security_permissions = permissions
- else:
- permissions.extend(func._security_permissions)
- func._security_permissions = list(set(permissions))
-
-
-def ReadPermission(func): # noqa: N802
- """
- :raises PermissionNotValid: If the permission is missing.
- """
- _set_func_permissions(func, Permission.READ)
- return func
-
-
-def CreatePermission(func): # noqa: N802
- """
- :raises PermissionNotValid: If the permission is missing.
- """
- _set_func_permissions(func, Permission.CREATE)
- return func
-
-
-def DeletePermission(func): # noqa: N802
- """
- :raises PermissionNotValid: If the permission is missing.
- """
- _set_func_permissions(func, Permission.DELETE)
- return func
-
-
-def UpdatePermission(func): # noqa: N802
- """
- :raises PermissionNotValid: If the permission is missing.
- """
- _set_func_permissions(func, Permission.UPDATE)
- return func
-
-
-# Empty request body decorator
-
-def allow_empty_body(func): # noqa: N802
- """
- The POST/PUT request methods decorated with ``@allow_empty_body``
- are allowed to send empty request body.
- """
- try:
- func._cp_config['tools.json_in.force'] = False
- except (AttributeError, KeyError):
- func._cp_config = {'tools.json_in.force': False}
- return func
-
-
-def validate_ceph_type(validations, component=''):
- def decorator(func):
- @wraps(func)
- def validate_args(*args, **kwargs):
- input_values = kwargs
- for key, ceph_type in validations:
- try:
- ceph_type.valid(input_values[key])
- except ArgumentFormat as e:
- raise DashboardException(msg=e,
- code='ceph_type_not_valid',
- component=component)
- return func(*args, **kwargs)
- return validate_args
- return decorator
+from ._api_router import APIRouter
+from ._auth import ControllerAuthMixin
+from ._base_controller import BaseController
+from ._docs import APIDoc, EndpointDoc
+from ._endpoint import Endpoint, Proxy
+from ._helpers import ENDPOINT_MAP, allow_empty_body, \
+ generate_controller_routes, json_error_page, validate_ceph_type
+from ._permissions import CreatePermission, DeletePermission, ReadPermission, UpdatePermission
+from ._rest_controller import RESTController
+from ._router import Router
+from ._task import Task
+from ._ui_router import UIRouter
+
+__all__ = [
+ 'BaseController',
+ 'RESTController',
+ 'Router',
+ 'UIRouter',
+ 'APIRouter',
+ 'Endpoint',
+ 'Proxy',
+ 'Task',
+ 'ControllerAuthMixin',
+ 'EndpointDoc',
+ 'APIDoc',
+ 'allow_empty_body',
+ 'ENDPOINT_MAP',
+ 'generate_controller_routes',
+ 'json_error_page',
+ 'validate_ceph_type',
+ 'CreatePermission',
+ 'ReadPermission',
+ 'UpdatePermission',
+ 'DeletePermission'
+]
--- /dev/null
+from ._router import Router
+
+
+class APIRouter(Router):
+ def __init__(self, path, security_scope=None, secure=True):
+ super().__init__(path, base_url="/api",
+ security_scope=security_scope,
+ secure=secure)
+
+ def __call__(self, cls):
+ cls = super().__call__(cls)
+ cls._api_endpoint = True
+ return cls
--- /dev/null
+import cherrypy
+
+
+class ControllerAuthMixin:
+ @staticmethod
+ def _delete_token_cookie(token):
+ cherrypy.response.cookie['token'] = token
+ cherrypy.response.cookie['token']['expires'] = 0
+ cherrypy.response.cookie['token']['max-age'] = 0
+
+ @staticmethod
+ def _set_token_cookie(url_prefix, token):
+ cherrypy.response.cookie['token'] = token
+ if url_prefix == 'https':
+ cherrypy.response.cookie['token']['secure'] = True
+ cherrypy.response.cookie['token']['HttpOnly'] = True
+ cherrypy.response.cookie['token']['path'] = '/'
+ cherrypy.response.cookie['token']['SameSite'] = 'Strict'
--- /dev/null
+import inspect
+import json
+import logging
+from functools import wraps
+from typing import ClassVar, List, Optional, Type
+from urllib.parse import unquote
+
+import cherrypy
+
+from ..plugins import PLUGIN_MANAGER
+from ..services.auth import AuthManager, JwtManager
+from ..tools import get_request_body_params
+from ._helpers import _get_function_params
+from ._version import APIVersion
+
+logger = logging.getLogger(__name__)
+
+
+class BaseController:
+ """
+ Base class for all controllers providing API endpoints.
+ """
+
+ _registry: ClassVar[List[Type['BaseController']]] = []
+ _routed = False
+
+ def __init_subclass__(cls, skip_registry: bool = False, **kwargs) -> None:
+ super().__init_subclass__(**kwargs) # type: ignore
+ if not skip_registry:
+ BaseController._registry.append(cls)
+
+ @classmethod
+ def load_controllers(cls):
+ import importlib
+ from pathlib import Path
+
+ path = Path(__file__).parent
+ logger.debug('Controller import path: %s', path)
+ modules = [
+ f.stem for f in path.glob('*.py') if
+ not f.name.startswith('_') and f.is_file() and not f.is_symlink()]
+ logger.debug('Controller files found: %r', modules)
+
+ for module in modules:
+ importlib.import_module(f'{__package__}.{module}')
+
+ # pylint: disable=protected-access
+ controllers = [
+ controller for controller in BaseController._registry if
+ controller._routed
+ ]
+
+ for clist in PLUGIN_MANAGER.hook.get_controllers() or []:
+ controllers.extend(clist)
+
+ return controllers
+
+ class Endpoint:
+ """
+ An instance of this class represents an endpoint.
+ """
+
+ def __init__(self, ctrl, func):
+ self.ctrl = ctrl
+ self.inst = None
+ self.func = 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 # pylint: disable=protected-access
+
+ @property
+ def function(self):
+ # pylint: disable=protected-access
+ return self.ctrl._request_wrapper(self.func, self.method,
+ self.config['json_response'],
+ self.config['xml'],
+ self.config['version'])
+
+ @property
+ def method(self):
+ return self.config['method']
+
+ @property
+ def proxy(self):
+ return self.config['proxy']
+
+ @property
+ def url(self):
+ ctrl_path = self.ctrl.get_path()
+ if ctrl_path == "/":
+ ctrl_path = ""
+ if self.config['path'] is not None:
+ url = "{}{}".format(ctrl_path, self.config['path'])
+ else:
+ url = "{}/{}".format(ctrl_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
+ def action(self):
+ return self.func.__name__
+
+ @property
+ def path_params(self):
+ 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):
+ 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):
+ 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):
+ return self.ctrl.__name__
+
+ @property
+ def is_api(self):
+ # changed from hasattr to getattr: some ui-based api inherit _api_endpoint
+ return getattr(self.ctrl, '_api_endpoint', False)
+
+ @property
+ def is_secure(self):
+ return self.ctrl._cp_config['tools.authenticate.on'] # pylint: disable=protected-access
+
+ def __repr__(self):
+ return "Endpoint({}, {}, {})".format(self.url, self.method,
+ self.action)
+
+ def __init__(self):
+ logger.info('Initializing controller: %s -> %s',
+ self.__class__.__name__, self._cp_path_) # type: ignore
+ super().__init__()
+
+ def _has_permissions(self, permissions, scope=None):
+ if not self._cp_config['tools.authenticate.on']: # type: ignore
+ raise Exception("Cannot verify permission in non secured "
+ "controllers")
+
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ if scope is None:
+ scope = getattr(self, '_security_scope', None)
+ if scope is None:
+ raise Exception("Cannot verify permissions without scope security"
+ " defined")
+ username = JwtManager.LOCAL_USER.username
+ return AuthManager.authorize(username, scope, permissions)
+
+ @classmethod
+ def get_path_param_names(cls, path_extension=None):
+ if path_extension is None:
+ path_extension = ""
+ full_path = cls._cp_path_[1:] + path_extension # type: ignore
+ path_params = []
+ for step in full_path.split('/'):
+ param = None
+ if not step:
+ continue
+ if step[0] == ':':
+ param = step[1:]
+ elif step[0] == '{' and step[-1] == '}':
+ param, _, _ = step[1:-1].partition(':')
+ if param:
+ path_params.append(param)
+ return path_params
+
+ @classmethod
+ def get_path(cls):
+ return cls._cp_path_ # type: ignore
+
+ @classmethod
+ def endpoints(cls):
+ """
+ This method iterates over all the methods decorated with ``@endpoint``
+ and creates an Endpoint object for each one of the methods.
+
+ :return: A list of endpoint objects
+ :rtype: list[BaseController.Endpoint]
+ """
+ result = []
+ for _, func in inspect.getmembers(cls, predicate=callable):
+ if hasattr(func, '_endpoint'):
+ result.append(cls.Endpoint(cls, func))
+ return result
+
+ @staticmethod
+ def _request_wrapper(func, method, json_response, xml, # pylint: disable=unused-argument
+ version: Optional[APIVersion]):
+ # pylint: disable=too-many-branches
+ @wraps(func)
+ def inner(*args, **kwargs):
+ client_version = None
+ for key, value in kwargs.items():
+ if isinstance(value, str):
+ kwargs[key] = unquote(value)
+
+ # Process method arguments.
+ params = get_request_body_params(cherrypy.request)
+ kwargs.update(params)
+
+ if version is not None:
+ try:
+ client_version = APIVersion.from_mime_type(
+ cherrypy.request.headers['Accept'])
+ except Exception:
+ raise cherrypy.HTTPError(
+ 415, "Unable to find version in request header")
+
+ if version.supports(client_version):
+ ret = func(*args, **kwargs)
+ else:
+ raise cherrypy.HTTPError(
+ 415,
+ f"Incorrect version: endpoint is '{version!s}', "
+ f"client requested '{client_version!s}'"
+ )
+
+ else:
+ ret = func(*args, **kwargs)
+ if isinstance(ret, bytes):
+ ret = ret.decode('utf-8')
+ if xml:
+ if version:
+ cherrypy.response.headers['Content-Type'] = \
+ 'application/vnd.ceph.api.v{}+xml'.format(version)
+ else:
+ cherrypy.response.headers['Content-Type'] = 'application/xml'
+ return ret.encode('utf8')
+ if json_response:
+ if version:
+ cherrypy.response.headers['Content-Type'] = \
+ 'application/vnd.ceph.api.v{}+json'.format(version)
+ else:
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ ret = json.dumps(ret).encode('utf8')
+ return ret
+ return inner
+
+ @property
+ def _request(self):
+ return self.Request(cherrypy.request)
+
+ class Request(object):
+ def __init__(self, cherrypy_req):
+ self._creq = cherrypy_req
+
+ @property
+ def scheme(self):
+ return self._creq.scheme
+
+ @property
+ def host(self):
+ base = self._creq.base
+ base = base[len(self.scheme)+3:]
+ return base[:base.find(":")] if ":" in base else base
+
+ @property
+ def port(self):
+ base = self._creq.base
+ base = base[len(self.scheme)+3:]
+ default_port = 443 if self.scheme == 'https' else 80
+ return int(base[base.find(":")+1:]) if ":" in base else default_port
+
+ @property
+ def path_info(self):
+ return self._creq.path_info
--- /dev/null
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+from ..api.doc import SchemaInput, SchemaType
+
+
+class EndpointDoc: # noqa: N802
+ DICT_TYPE = Union[Dict[str, Any], Dict[int, Any]]
+
+ def __init__(self, description: str = "", group: str = "",
+ parameters: Optional[Union[DICT_TYPE, List[Any], Tuple[Any, ...]]] = None,
+ responses: Optional[DICT_TYPE] = None) -> None:
+ self.description = description
+ self.group = group
+ self.parameters = parameters
+ self.responses = responses
+
+ self.validate_args()
+
+ if not self.parameters:
+ self.parameters = {} # type: ignore
+
+ self.resp = {}
+ if self.responses:
+ for status_code, response_body in self.responses.items():
+ schema_input = SchemaInput()
+ schema_input.type = SchemaType.ARRAY if \
+ isinstance(response_body, list) else SchemaType.OBJECT
+ schema_input.params = self._split_parameters(response_body)
+
+ self.resp[str(status_code)] = schema_input
+
+ def validate_args(self) -> None:
+ if not isinstance(self.description, str):
+ raise Exception("%s has been called with a description that is not a string: %s"
+ % (EndpointDoc.__name__, self.description))
+ if not isinstance(self.group, str):
+ raise Exception("%s has been called with a groupname that is not a string: %s"
+ % (EndpointDoc.__name__, self.group))
+ if self.parameters and not isinstance(self.parameters, dict):
+ raise Exception("%s has been called with parameters that is not a dict: %s"
+ % (EndpointDoc.__name__, self.parameters))
+ if self.responses and not isinstance(self.responses, dict):
+ raise Exception("%s has been called with responses that is not a dict: %s"
+ % (EndpointDoc.__name__, self.responses))
+
+ def _split_param(self, name: str, p_type: Union[type, DICT_TYPE, List[Any], Tuple[Any, ...]],
+ description: str, optional: bool = False, default_value: Any = None,
+ nested: bool = False) -> Dict[str, Any]:
+ param = {
+ 'name': name,
+ 'description': description,
+ 'required': not optional,
+ 'nested': nested,
+ }
+ if default_value:
+ param['default'] = default_value
+ if isinstance(p_type, type):
+ param['type'] = p_type
+ else:
+ nested_params = self._split_parameters(p_type, nested=True)
+ if nested_params:
+ param['type'] = type(p_type)
+ param['nested_params'] = nested_params
+ else:
+ param['type'] = p_type
+ return param
+
+ # Optional must be set to True in order to set default value and parameters format must be:
+ # 'name: (type or nested parameters, description, [optional], [default value])'
+ def _split_dict(self, data: DICT_TYPE, nested: bool) -> List[Any]:
+ splitted = []
+ for name, props in data.items():
+ if isinstance(name, str) and isinstance(props, tuple):
+ if len(props) == 2:
+ param = self._split_param(name, props[0], props[1], nested=nested)
+ elif len(props) == 3:
+ param = self._split_param(
+ name, props[0], props[1], optional=props[2], nested=nested)
+ if len(props) == 4:
+ param = self._split_param(name, props[0], props[1], props[2], props[3], nested)
+ splitted.append(param)
+ else:
+ raise Exception(
+ """Parameter %s in %s has not correct format. Valid formats are:
+ <name>: (<type>, <description>, [optional], [default value])
+ <name>: (<[type]>, <description>, [optional], [default value])
+ <name>: (<[nested parameters]>, <description>, [optional], [default value])
+ <name>: (<{nested parameters}>, <description>, [optional], [default value])"""
+ % (name, EndpointDoc.__name__))
+ return splitted
+
+ def _split_list(self, data: Union[List[Any], Tuple[Any, ...]], nested: bool) -> List[Any]:
+ splitted = [] # type: List[Any]
+ for item in data:
+ splitted.extend(self._split_parameters(item, nested))
+ return splitted
+
+ # nested = True means parameters are inside a dict or array
+ def _split_parameters(self, data: Optional[Union[DICT_TYPE, List[Any], Tuple[Any, ...]]],
+ nested: bool = False) -> List[Any]:
+ param_list = [] # type: List[Any]
+ if isinstance(data, dict):
+ param_list.extend(self._split_dict(data, nested))
+ elif isinstance(data, (list, tuple)):
+ param_list.extend(self._split_list(data, True))
+ return param_list
+
+ def __call__(self, func: Any) -> Any:
+ func.doc_info = {
+ 'summary': self.description,
+ 'tag': self.group,
+ 'parameters': self._split_parameters(self.parameters),
+ 'response': self.resp
+ }
+ return func
+
+
+class APIDoc(object):
+ def __init__(self, description="", group=""):
+ self.tag = group
+ self.tag_descr = description
+
+ def __call__(self, cls):
+ cls.doc_info = {
+ 'tag': self.tag,
+ 'tag_descr': self.tag_descr
+ }
+ return cls
--- /dev/null
+from typing import Optional
+
+from ._helpers import _get_function_params
+from ._version import APIVersion
+
+
+class Endpoint:
+
+ def __init__(self, method=None, path=None, path_params=None, query_params=None, # noqa: N802
+ json_response=True, proxy=False, xml=False,
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ 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 = []
+
+ self.method = method
+ self.path = path
+ self.path_params = path_params
+ self.query_params = query_params
+ self.json_response = json_response
+ self.proxy = proxy
+ self.xml = xml
+ self.version = version
+
+ def __call__(self, func):
+ if self.method in ['POST', 'PUT']:
+ func_params = _get_function_params(func)
+ for param in func_params:
+ if param['name'] in self.path_params and not param['required']:
+ raise TypeError("path_params can only reference "
+ "non-optional function parameters")
+
+ if func.__name__ == '__call__' and self.path is None:
+ e_path = ""
+ else:
+ e_path = self.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': self.method,
+ 'path': e_path,
+ 'path_params': self.path_params,
+ 'query_params': self.query_params,
+ 'json_response': self.json_response,
+ 'proxy': self.proxy,
+ 'xml': self.xml,
+ 'version': self.version
+ }
+ return func
+
+
+def Proxy(path=None): # noqa: N802
+ if path is None:
+ path = ""
+ elif path == "/":
+ path = ""
+ path += "/{path:.*}"
+ return Endpoint(path=path, proxy=True)
--- /dev/null
+import collections
+import json
+import logging
+import re
+from functools import wraps
+
+import cherrypy
+from ceph_argparse import ArgumentFormat # pylint: disable=import-error
+
+from ..exceptions import DashboardException
+from ..tools import getargspec
+
+logger = logging.getLogger(__name__)
+
+
+ENDPOINT_MAP = collections.defaultdict(list) # type: dict
+
+
+def _get_function_params(func):
+ """
+ Retrieves the list of parameters declared in function.
+ Each parameter is represented as dict with keys:
+ * name (str): the name of the parameter
+ * required (bool): whether the parameter is required or not
+ * default (obj): the parameter's default value
+ """
+ fspec = getargspec(func)
+
+ func_params = []
+ nd = len(fspec.args) if not fspec.defaults else -len(fspec.defaults)
+ for param in fspec.args[1:nd]:
+ func_params.append({'name': param, 'required': True})
+
+ if fspec.defaults:
+ for param, val in zip(fspec.args[nd:], fspec.defaults):
+ func_params.append({
+ 'name': param,
+ 'required': False,
+ 'default': val
+ })
+
+ return func_params
+
+
+def generate_controller_routes(endpoint, mapper, base_url):
+ inst = endpoint.inst
+ ctrl_class = endpoint.ctrl
+
+ if endpoint.proxy:
+ conditions = None
+ else:
+ conditions = dict(method=[endpoint.method])
+
+ # base_url can be empty or a URL path that starts with "/"
+ # we will remove the trailing "/" if exists to help with the
+ # concatenation with the endpoint url below
+ if base_url.endswith("/"):
+ base_url = base_url[:-1]
+
+ endp_url = endpoint.url
+
+ if endp_url.find("/", 1) == -1:
+ parent_url = "{}{}".format(base_url, endp_url)
+ else:
+ parent_url = "{}{}".format(base_url, endp_url[:endp_url.find("/", 1)])
+
+ # parent_url might be of the form "/.../{...}" where "{...}" is a path parameter
+ # we need to remove the path parameter definition
+ parent_url = re.sub(r'(?:/\{[^}]+\})$', '', parent_url)
+ if not parent_url: # root path case
+ parent_url = "/"
+
+ url = "{}{}".format(base_url, endp_url)
+
+ logger.debug("Mapped [%s] to %s:%s restricted to %s",
+ url, ctrl_class.__name__, endpoint.action,
+ endpoint.method)
+
+ ENDPOINT_MAP[endpoint.url].append(endpoint)
+
+ name = ctrl_class.__name__ + ":" + endpoint.action
+ mapper.connect(name, url, controller=inst, action=endpoint.action,
+ conditions=conditions)
+
+ # adding route with trailing slash
+ name += "/"
+ url += "/"
+ mapper.connect(name, url, controller=inst, action=endpoint.action,
+ conditions=conditions)
+
+ return parent_url
+
+
+def json_error_page(status, message, traceback, version):
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ return json.dumps(dict(status=status, detail=message, traceback=traceback,
+ version=version))
+
+
+def allow_empty_body(func): # noqa: N802
+ """
+ The POST/PUT request methods decorated with ``@allow_empty_body``
+ are allowed to send empty request body.
+ """
+ # pylint: disable=protected-access
+ try:
+ func._cp_config['tools.json_in.force'] = False
+ except (AttributeError, KeyError):
+ func._cp_config = {'tools.json_in.force': False}
+ return func
+
+
+def validate_ceph_type(validations, component=''):
+ def decorator(func):
+ @wraps(func)
+ def validate_args(*args, **kwargs):
+ input_values = kwargs
+ for key, ceph_type in validations:
+ try:
+ ceph_type.valid(input_values[key])
+ except ArgumentFormat as e:
+ raise DashboardException(msg=e,
+ code='ceph_type_not_valid',
+ component=component)
+ return func(*args, **kwargs)
+ return validate_args
+ return decorator
--- /dev/null
+"""
+Role-based access permissions decorators
+"""
+import logging
+
+from ..exceptions import PermissionNotValid
+from ..security import Permission
+
+logger = logging.getLogger(__name__)
+
+
+def _set_func_permissions(func, permissions):
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ for perm in permissions:
+ if not Permission.valid_permission(perm):
+ logger.debug("Invalid security permission: %s\n "
+ "Possible values: %s", perm,
+ Permission.all_permissions())
+ raise PermissionNotValid(perm)
+
+ # pylint: disable=protected-access
+ if not hasattr(func, '_security_permissions'):
+ func._security_permissions = permissions
+ else:
+ permissions.extend(func._security_permissions)
+ func._security_permissions = list(set(permissions))
+
+
+def ReadPermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.READ)
+ return func
+
+
+def CreatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.CREATE)
+ return func
+
+
+def DeletePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.DELETE)
+ return func
+
+
+def UpdatePermission(func): # noqa: N802
+ """
+ :raises PermissionNotValid: If the permission is missing.
+ """
+ _set_func_permissions(func, Permission.UPDATE)
+ return func
--- /dev/null
+import collections
+import inspect
+from functools import wraps
+from typing import Optional
+
+import cherrypy
+
+from ..security import Permission
+from ._base_controller import BaseController
+from ._endpoint import Endpoint
+from ._helpers import _get_function_params
+from ._permissions import _set_func_permissions
+from ._version import APIVersion
+
+
+class RESTController(BaseController, skip_registry=True):
+ """
+ Base class for providing a RESTful interface to a resource.
+
+ To use this class, simply derive a class from it and implement the methods
+ you want to support. The list of possible methods are:
+
+ * list()
+ * bulk_set(data)
+ * create(data)
+ * bulk_delete()
+ * get(key)
+ * set(data, key)
+ * singleton_set(data)
+ * delete(key)
+
+ Test with curl:
+
+ curl -H "Content-Type: application/json" -X POST \
+ -d '{"username":"xyz","password":"xyz"}' https://127.0.0.1:8443/foo
+ curl https://127.0.0.1:8443/foo
+ curl https://127.0.0.1:8443/foo/0
+
+ """
+
+ # resource id parameter for using in get, set, and delete methods
+ # should be overridden by subclasses.
+ # to specify a composite id (two parameters) use '/'. e.g., "param1/param2".
+ # If subclasses don't override this property we try to infer the structure
+ # of the resource ID.
+ RESOURCE_ID: Optional[str] = None
+
+ _permission_map = {
+ 'GET': Permission.READ,
+ 'POST': Permission.CREATE,
+ 'PUT': Permission.UPDATE,
+ 'DELETE': Permission.DELETE
+ }
+
+ _method_mapping = collections.OrderedDict([
+ ('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
+ ('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
+ ('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
+ ('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}) # noqa E501 #pylint: disable=line-too-long
+ ])
+
+ @classmethod
+ def infer_resource_id(cls):
+ if cls.RESOURCE_ID is not None:
+ return cls.RESOURCE_ID.split('/')
+ for k, v in cls._method_mapping.items():
+ func = getattr(cls, k, None)
+ while hasattr(func, "__wrapped__"):
+ func = func.__wrapped__
+ if v['resource'] and func:
+ path_params = cls.get_path_param_names()
+ params = _get_function_params(func)
+ return [p['name'] for p in params
+ if p['required'] and p['name'] not in path_params]
+ return None
+
+ @classmethod
+ def endpoints(cls):
+ result = super().endpoints()
+ res_id_params = cls.infer_resource_id()
+
+ for _, func in inspect.getmembers(cls, predicate=callable):
+ endpoint_params = {
+ 'no_resource_id_params': False,
+ 'status': 200,
+ 'method': None,
+ 'query_params': None,
+ 'path': '',
+ 'version': APIVersion.DEFAULT,
+ 'sec_permissions': hasattr(func, '_security_permissions'),
+ 'permission': None,
+ }
+
+ if func.__name__ in cls._method_mapping:
+ cls._update_endpoint_params_method_map(
+ func, res_id_params, endpoint_params)
+
+ elif hasattr(func, "__collection_method__"):
+ cls._update_endpoint_params_collection_map(func, endpoint_params)
+
+ elif hasattr(func, "__resource_method__"):
+ cls._update_endpoint_params_resource_method(
+ res_id_params, endpoint_params, func)
+
+ else:
+ continue
+
+ if endpoint_params['no_resource_id_params']:
+ raise TypeError("Could not infer the resource ID parameters for"
+ " method {} of controller {}. "
+ "Please specify the resource ID parameters "
+ "using the RESOURCE_ID class property"
+ .format(func.__name__, cls.__name__))
+
+ if endpoint_params['method'] in ['GET', 'DELETE']:
+ params = _get_function_params(func)
+ if res_id_params is None:
+ res_id_params = []
+ if endpoint_params['query_params'] is None:
+ endpoint_params['query_params'] = [p['name'] for p in params # type: ignore
+ if p['name'] not in res_id_params]
+
+ func = cls._status_code_wrapper(func, endpoint_params['status'])
+ endp_func = Endpoint(endpoint_params['method'], path=endpoint_params['path'],
+ query_params=endpoint_params['query_params'],
+ version=endpoint_params['version'])(func) # type: ignore
+ if endpoint_params['permission']:
+ _set_func_permissions(endp_func, [endpoint_params['permission']])
+ result.append(cls.Endpoint(cls, endp_func))
+
+ return result
+
+ @classmethod
+ def _update_endpoint_params_resource_method(cls, res_id_params, endpoint_params, func):
+ if not res_id_params:
+ endpoint_params['no_resource_id_params'] = True
+ else:
+ path_params = ["{{{}}}".format(p) for p in res_id_params]
+ endpoint_params['path'] += "/{}".format("/".join(path_params))
+ if func.__resource_method__['path']:
+ endpoint_params['path'] += func.__resource_method__['path']
+ else:
+ endpoint_params['path'] += "/{}".format(func.__name__)
+ endpoint_params['status'] = func.__resource_method__['status']
+ endpoint_params['method'] = func.__resource_method__['method']
+ endpoint_params['version'] = func.__resource_method__['version']
+ endpoint_params['query_params'] = func.__resource_method__['query_params']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _update_endpoint_params_collection_map(cls, func, endpoint_params):
+ if func.__collection_method__['path']:
+ endpoint_params['path'] = func.__collection_method__['path']
+ else:
+ endpoint_params['path'] = "/{}".format(func.__name__)
+ endpoint_params['status'] = func.__collection_method__['status']
+ endpoint_params['method'] = func.__collection_method__['method']
+ endpoint_params['query_params'] = func.__collection_method__['query_params']
+ endpoint_params['version'] = func.__collection_method__['version']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _update_endpoint_params_method_map(cls, func, res_id_params, endpoint_params):
+ meth = cls._method_mapping[func.__name__] # type: dict
+
+ if meth['resource']:
+ if not res_id_params:
+ endpoint_params['no_resource_id_params'] = True
+ else:
+ path_params = ["{{{}}}".format(p) for p in res_id_params]
+ endpoint_params['path'] += "/{}".format("/".join(path_params))
+
+ endpoint_params['status'] = meth['status']
+ endpoint_params['method'] = meth['method']
+ if hasattr(func, "__method_map_method__"):
+ endpoint_params['version'] = func.__method_map_method__['version']
+ if not endpoint_params['sec_permissions']:
+ endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
+
+ @classmethod
+ def _status_code_wrapper(cls, func, status_code):
+ @wraps(func)
+ def wrapper(*vpath, **params):
+ cherrypy.response.status = status_code
+ return func(*vpath, **params)
+
+ return wrapper
+
+ @staticmethod
+ def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ if not method:
+ method = 'GET'
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__resource_method__ = {
+ 'method': method,
+ 'path': path,
+ 'status': status,
+ 'query_params': query_params,
+ 'version': version
+ }
+ return func
+ return _wrapper
+
+ @staticmethod
+ def MethodMap(resource=False, status=None,
+ version: Optional[APIVersion] = APIVersion.DEFAULT): # noqa: N802
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__method_map_method__ = {
+ 'resource': resource,
+ 'status': status,
+ 'version': version
+ }
+ return func
+ return _wrapper
+
+ @staticmethod
+ def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
+ version: Optional[APIVersion] = APIVersion.DEFAULT):
+ if not method:
+ method = 'GET'
+
+ if status is None:
+ status = 200
+
+ def _wrapper(func):
+ func.__collection_method__ = {
+ 'method': method,
+ 'path': path,
+ 'status': status,
+ 'query_params': query_params,
+ 'version': version
+ }
+ return func
+ return _wrapper
--- /dev/null
+import logging
+
+import cherrypy
+
+from ..exceptions import ScopeNotValid
+from ..security import Scope
+from ._base_controller import BaseController
+from ._helpers import generate_controller_routes
+
+logger = logging.getLogger(__name__)
+
+
+class Router(object):
+ def __init__(self, path, base_url=None, security_scope=None, secure=True):
+ if security_scope and not Scope.valid_scope(security_scope):
+ raise ScopeNotValid(security_scope)
+ self.path = path
+ self.base_url = base_url
+ self.security_scope = security_scope
+ self.secure = secure
+
+ 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._routed = True
+ cls._cp_path_ = "{}{}".format(self.base_url, self.path)
+ cls._security_scope = self.security_scope
+
+ config = {
+ 'tools.dashboard_exception_handler.on': True,
+ 'tools.authenticate.on': self.secure,
+ }
+ if not hasattr(cls, '_cp_config'):
+ cls._cp_config = {}
+ cls._cp_config.update(config)
+ return cls
+
+ @classmethod
+ def generate_routes(cls, url_prefix):
+ controllers = BaseController.load_controllers()
+ logger.debug("controllers=%r", controllers)
+
+ mapper = cherrypy.dispatch.RoutesDispatcher()
+
+ parent_urls = set()
+
+ endpoint_list = []
+ for ctrl in controllers:
+ inst = ctrl()
+ for endpoint in ctrl.endpoints():
+ endpoint.inst = inst
+ endpoint_list.append(endpoint)
+
+ endpoint_list = sorted(endpoint_list, key=lambda e: e.url)
+ for endpoint in endpoint_list:
+ parent_urls.add(generate_controller_routes(endpoint, mapper,
+ "{}".format(url_prefix)))
+
+ logger.debug("list of parent paths: %s", parent_urls)
+ return mapper, parent_urls
--- /dev/null
+from functools import wraps
+
+import cherrypy
+
+from ..tools import TaskManager
+from ._helpers import _get_function_params
+
+
+class Task:
+ def __init__(self, name, metadata, wait_for=5.0, exception_handler=None):
+ self.name = name
+ if isinstance(metadata, list):
+ self.metadata = {e[1:-1]: e for e in metadata}
+ else:
+ self.metadata = metadata
+ self.wait_for = wait_for
+ self.exception_handler = exception_handler
+
+ def _gen_arg_map(self, func, args, kwargs):
+ arg_map = {}
+ params = _get_function_params(func)
+
+ args = args[1:] # exclude self
+ for idx, param in enumerate(params):
+ if idx < len(args):
+ arg_map[param['name']] = args[idx]
+ else:
+ if param['name'] in kwargs:
+ arg_map[param['name']] = kwargs[param['name']]
+ else:
+ assert not param['required'], "{0} is required".format(param['name'])
+ arg_map[param['name']] = param['default']
+
+ if param['name'] in arg_map:
+ # This is not a type error. We are using the index here.
+ arg_map[idx+1] = arg_map[param['name']]
+
+ return arg_map
+
+ def __call__(self, func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ arg_map = self._gen_arg_map(func, args, kwargs)
+ metadata = {}
+ for k, v in self.metadata.items():
+ if isinstance(v, str) and v and v[0] == '{' and v[-1] == '}':
+ param = v[1:-1]
+ try:
+ pos = int(param)
+ metadata[k] = arg_map[pos]
+ except ValueError:
+ if param.find('.') == -1:
+ metadata[k] = arg_map[param]
+ else:
+ path = param.split('.')
+ metadata[k] = arg_map[path[0]]
+ for i in range(1, len(path)):
+ metadata[k] = metadata[k][path[i]]
+ else:
+ metadata[k] = v
+ task = TaskManager.run(self.name, metadata, func, args, kwargs,
+ exception_handler=self.exception_handler)
+ try:
+ status, value = task.wait(self.wait_for)
+ except Exception as ex:
+ if task.ret_value:
+ # exception was handled by task.exception_handler
+ if 'status' in task.ret_value:
+ status = task.ret_value['status']
+ else:
+ status = getattr(ex, 'status', 500)
+ cherrypy.response.status = status
+ return task.ret_value
+ raise ex
+ if status == TaskManager.VALUE_EXECUTING:
+ cherrypy.response.status = 202
+ return {'name': self.name, 'metadata': metadata}
+ return value
+ return wrapper
--- /dev/null
+from ._router import Router
+
+
+class UIRouter(Router):
+ def __init__(self, path, security_scope=None, secure=True):
+ super().__init__(path, base_url="/ui-api",
+ security_scope=security_scope,
+ secure=secure)
+
+ def __call__(self, cls):
+ cls = super().__call__(cls)
+ cls._api_endpoint = False
+ return cls
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
from ..services.auth import AuthManager, JwtManager
from ..settings import Settings
-from . import ApiController, ControllerAuthMixin, ControllerDoc, EndpointDoc, \
- RESTController, allow_empty_body
+from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
# Python 3.8 introduced `samesite` attribute:
# https://docs.python.org/3/library/http.cookies.html#morsel-objects
}
-@ApiController('/auth', secure=False)
-@ControllerDoc("Initiate a session with Ceph", "Auth")
+@APIRouter('/auth', secure=False)
+@APIDoc("Initiate a session with Ceph", "Auth")
class Auth(RESTController, ControllerAuthMixin):
"""
Provide authenticates and returns JWT token.
from ..services.ceph_service import CephService
from ..services.cephfs import CephFS as CephFS_
from ..tools import ViewCache
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController, \
- UiApiController, allow_empty_body
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, UIRouter, allow_empty_body
GET_QUOTAS_SCHEMA = {
'max_bytes': (int, ''),
}
-@ApiController('/cephfs', Scope.CEPHFS)
-@ControllerDoc("Cephfs Management API", "Cephfs")
+@APIRouter('/cephfs', Scope.CEPHFS)
+@APIDoc("Cephfs Management API", "Cephfs")
class CephFS(RESTController):
def __init__(self): # pragma: no cover
- super(CephFS, self).__init__()
+ super().__init__()
# Stateful instances of CephFSClients, hold cached results. Key to
# dict is FSCID
return CephService.send_command('mds', 'session ls', srv_spec='{0}:0'.format(self.fscid))
-@UiApiController('/cephfs', Scope.CEPHFS)
-@ControllerDoc("Dashboard UI helper function; not part of the public API", "CephFSUi")
+@UIRouter('/cephfs', Scope.CEPHFS)
+@APIDoc("Dashboard UI helper function; not part of the public API", "CephFSUi")
class CephFsUi(CephFS):
RESOURCE_ID = 'fs_id'
from ..exceptions import DashboardException
from ..security import Scope
from ..services.ceph_service import CephService
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
FILTER_SCHEMA = [{
"name": (str, 'Name of the config option'),
}]
-@ApiController('/cluster_conf', Scope.CONFIG_OPT)
-@ControllerDoc("Manage Cluster Configurations", "ClusterConfiguration")
+@APIRouter('/cluster_conf', Scope.CONFIG_OPT)
+@APIDoc("Manage Cluster Configurations", "ClusterConfiguration")
class ClusterConfiguration(RESTController):
def _append_config_option_values(self, options):
+
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
-from . import ApiController, ControllerDoc, Endpoint, EndpointDoc, \
- ReadPermission, RESTController, UiApiController
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
+from ._version import APIVersion
LIST_SCHEMA = {
"rule_id": (int, 'Rule ID'),
}
-@ApiController('/crush_rule', Scope.POOL)
-@ControllerDoc("Crush Rule Management API", "CrushRule")
+@APIRouter('/crush_rule', Scope.POOL)
+@APIDoc("Crush Rule Management API", "CrushRule")
class CrushRule(RESTController):
@EndpointDoc("List Crush Rule Configuration",
responses={200: LIST_SCHEMA})
CephService.send_command('mon', 'osd crush rule rm', name=name)
-@UiApiController('/crush_rule', Scope.POOL)
-@ControllerDoc("Dashboard UI helper function; not part of the public API", "CrushRuleUi")
+@UIRouter('/crush_rule', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "CrushRuleUi")
class CrushRuleUi(CrushRule):
@Endpoint()
@ReadPermission
from .. import DEFAULT_VERSION, mgr
from ..api.doc import Schema, SchemaInput, SchemaType
-from . import ENDPOINT_MAP, BaseController, Controller, Endpoint
+from . import ENDPOINT_MAP, BaseController, Endpoint, Router
+from ._version import APIVersion
NO_DESCRIPTION_AVAILABLE = "*No description available*"
logger = logging.getLogger('controllers.docs')
-@Controller('/docs', secure=False)
+@Router('/docs', secure=False)
class Docs(BaseController):
@classmethod
import yaml
- from . import generate_routes
-
def fix_null_descr(obj):
"""
A hot fix for errors caused by null description values when generating
return {k: fix_null_descr(v) for k, v in obj.items() if v is not None} \
if isinstance(obj, dict) else obj
- generate_routes("/api")
+ Router.generate_routes("/api")
try:
with open(sys.argv[1], 'w') as f:
# pylint: disable=protected-access
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
-from . import ApiController, ControllerDoc, Endpoint, EndpointDoc, \
- ReadPermission, RESTController, UiApiController
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
LIST_CODE__SCHEMA = {
"crush-failure-domain": (str, ''),
}
-@ApiController('/erasure_code_profile', Scope.POOL)
-@ControllerDoc("Erasure Code Profile Management API", "ErasureCodeProfile")
+@APIRouter('/erasure_code_profile', Scope.POOL)
+@APIDoc("Erasure Code Profile Management API", "ErasureCodeProfile")
class ErasureCodeProfile(RESTController):
"""
create() supports additional key-value arguments that are passed to the
CephService.send_command('mon', 'osd erasure-code-profile rm', name=name)
-@UiApiController('/erasure_code_profile', Scope.POOL)
-@ControllerDoc("Dashboard UI helper function; not part of the public API", "ErasureCodeProfileUi")
+@UIRouter('/erasure_code_profile', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "ErasureCodeProfileUi")
class ErasureCodeProfileUi(ErasureCodeProfile):
@Endpoint()
@ReadPermission
--- /dev/null
+# # -*- coding: utf-8 -*-
+
+from ..exceptions import DashboardException
+from ..model.feedback import Feedback
+from ..rest_client import RequestException
+from ..security import Scope
+from ..services import feedback
+from . import APIDoc, APIRouter, RESTController
+
+
+@APIRouter('/feedback', Scope.CONFIG_OPT)
+@APIDoc("Feedback API", "Report")
+class FeedbackController(RESTController):
+ issueAPIkey = None
+
+ def __init__(self): # pragma: no cover
+ super().__init__()
+ self.tracker_client = feedback.CephTrackerClient()
+
+ def create(self, project, tracker, subject, description):
+ """
+ Create an issue.
+ :param project: The affected ceph component.
+ :param tracker: The tracker type.
+ :param subject: The title of the issue.
+ :param description: The description of the issue.
+ """
+ try:
+ new_issue = Feedback(Feedback.Project[project].value,
+ Feedback.TrackerType[tracker].value, subject, description)
+ except KeyError:
+ raise DashboardException(msg=f'{"Invalid arguments"}', component='feedback')
+ try:
+ return self.tracker_client.create_issue(new_issue)
+ except RequestException as error:
+ if error.status_code == 401:
+ raise DashboardException(msg=f'{"Invalid API key"}',
+ http_status_code=error.status_code,
+ component='feedback')
+ raise error
+ except Exception:
+ raise DashboardException(msg=f'{"API key not set"}',
+ http_status_code=401,
+ component='feedback')
+
+ def get(self, issue_number):
+ """
+ Fetch issue details.
+ :param issueAPI: The issue tracker API access key.
+ """
+ try:
+ return self.tracker_client.get_issues(issue_number)
+ except RequestException as error:
+ if error.status_code == 404:
+ raise DashboardException(msg=f'Issue {issue_number} not found',
+ http_status_code=error.status_code,
+ component='feedback')
+ raise error
import logging
-from . import BaseController, Endpoint, UiApiController
+from . import BaseController, Endpoint, UIRouter
logger = logging.getLogger('frontend.error')
-@UiApiController('/logging', secure=False)
+@UIRouter('/logging', secure=False)
class FrontendLogging(BaseController):
@Endpoint('POST', path='js-error')
from ..grafana import GrafanaRestClient, push_local_dashboards
from ..security import Scope
from ..settings import Settings
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, UpdatePermission
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, UpdatePermission
URL_SCHEMA = {
"instance": (str, "grafana instance")
}
-@ApiController('/grafana', Scope.GRAFANA)
-@ControllerDoc("Grafana Management API", "Grafana")
+@APIRouter('/grafana', Scope.GRAFANA)
+@APIDoc("Grafana Management API", "Grafana")
class Grafana(BaseController):
@Endpoint()
@ReadPermission
from ..services.iscsi_cli import IscsiGatewaysConfig
from ..services.iscsi_client import IscsiClient
from ..tools import partial_dict
-from . import ApiController, BaseController, ControllerDoc, Endpoint, EndpointDoc
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc
from .host import get_hosts
HEALTH_MINIMAL_SCHEMA = ({
return CephService.get_scrub_status()
-@ApiController('/health')
-@ControllerDoc("Display Detailed Cluster health Status", "Health")
+@APIRouter('/health')
+@APIDoc("Display Detailed Cluster health Status", "Health")
class Health(BaseController):
def __init__(self):
- super(Health, self).__init__()
+ super().__init__()
self.health_full = HealthData(self._has_permissions, minimal=False)
self.health_minimal = HealthData(self._has_permissions, minimal=True)
from cherrypy.lib.static import serve_file
from .. import mgr
-from . import BaseController, Controller, Endpoint, Proxy, UiApiController
+from . import BaseController, Endpoint, Proxy, Router, UIRouter
logger = logging.getLogger("controllers.home")
self.DEFAULT_LANGUAGE = config['config']['locale']
self.DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(),
self.DEFAULT_LANGUAGE)
- super(LanguageMixin, self).__init__()
+ super().__init__()
-@Controller("/", secure=False)
+@Router("/", secure=False)
class HomeController(BaseController, LanguageMixin):
LANG_TAG_SEQ_RE = re.compile(r'\s*([^,]+)\s*,?\s*')
LANG_TAG_RE = re.compile(
return serve_file(full_path)
-@UiApiController("/langs", secure=False)
+@UIRouter("/langs", secure=False)
class LangsController(BaseController, LanguageMixin):
@Endpoint('GET')
def __call__(self):
from ..services.exception import handle_orchestrator_error
from ..services.orchestrator import OrchClient, OrchFeature
from ..tools import TaskManager, str_to_bool
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \
- UpdatePermission, allow_empty_body
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter, UpdatePermission, \
+ allow_empty_body
+from ._version import APIVersion
from .orchestrator import raise_if_no_orchestrator
LIST_HOST_SCHEMA = {
orch_client.hosts.enter_maintenance(hostname)
-@ApiController('/host', Scope.HOSTS)
-@ControllerDoc("Get Host Details", "Host")
+@APIRouter('/host', Scope.HOSTS)
+@APIDoc("Get Host Details", "Host")
class Host(RESTController):
@EndpointDoc("List Host Specifications",
parameters={
orch.hosts.add_label(hostname, label)
-@UiApiController('/host', Scope.HOSTS)
+@UIRouter('/host', Scope.HOSTS)
class HostUi(BaseController):
@Endpoint('GET')
@ReadPermission
from ..services.rbd import format_bitmask
from ..services.tcmu_service import TcmuService
from ..tools import TaskManager, str_to_bool
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \
- UpdatePermission
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter, UpdatePermission
try:
from typing import Any, Dict, List, no_type_check
}
-@UiApiController('/iscsi', Scope.ISCSI)
+@UIRouter('/iscsi', Scope.ISCSI)
class IscsiUi(BaseController):
REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
}
-@ApiController('/iscsi', Scope.ISCSI)
-@ControllerDoc("Iscsi Management API", "Iscsi")
+@APIRouter('/iscsi', Scope.ISCSI)
+@APIDoc("Iscsi Management API", "Iscsi")
class Iscsi(BaseController):
@Endpoint('GET', 'discoveryauth')
@ReadPermission
return Task("iscsi/target/{}".format(name), metadata, wait_for)
-@ApiController('/iscsi/target', Scope.ISCSI)
-@ControllerDoc("Get Iscsi Target Details", "IscsiTarget")
+@APIRouter('/iscsi/target', Scope.ISCSI)
+@APIDoc("Get Iscsi Target Details", "IscsiTarget")
class IscsiTarget(RESTController):
def list(self):
from ..security import Scope
from ..services.ceph_service import CephService
from ..tools import NotificationQueue
-from . import ApiController, BaseController, ControllerDoc, Endpoint, EndpointDoc, ReadPermission
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, ReadPermission
LOG_BUFFER_SIZE = 30
}
-@ApiController('/logs', Scope.LOG)
-@ControllerDoc("Logs Management API", "Logs")
+@APIRouter('/logs', Scope.LOG)
+@APIDoc("Logs Management API", "Logs")
class Logs(BaseController):
def __init__(self):
- super(Logs, self).__init__()
+ super().__init__()
self._log_initialized = False
self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
from ..services.ceph_service import CephService
from ..services.exception import handle_send_command_error
from ..tools import find_object_in_list, str_to_bool
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController, allow_empty_body
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body
MGR_MODULE_SCHEMA = ([{
"name": (str, "Module Name"),
}])
-@ApiController('/mgr/module', Scope.CONFIG_OPT)
-@ControllerDoc("Get details of MGR Module", "MgrModule")
+@APIRouter('/mgr/module', Scope.CONFIG_OPT)
+@APIDoc("Get details of MGR Module", "MgrModule")
class MgrModules(RESTController):
ignore_modules = ['selftest']
from .. import mgr
from ..security import Scope
-from . import ApiController, BaseController, ControllerDoc, Endpoint, EndpointDoc, ReadPermission
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, ReadPermission
MONITOR_SCHEMA = {
"mon_status": ({
}
-@ApiController('/monitor', Scope.MONITOR)
-@ControllerDoc("Get Monitor Details", "Monitor")
+@APIRouter('/monitor', Scope.MONITOR)
+@APIDoc("Get Monitor Details", "Monitor")
class Monitor(BaseController):
@Endpoint()
@ReadPermission
from ..services.ganesha import Ganesha, GaneshaConf, NFSException
from ..services.rgw_client import NoCredentialsException, \
NoRgwDaemonsException, RequestException, RgwClient
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, RESTController, Task, UiApiController
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UIRouter
logger = logging.getLogger('controllers.ganesha')
return composed_decorator
-@ApiController('/nfs-ganesha', Scope.NFS_GANESHA)
-@ControllerDoc("NFS-Ganesha Management API", "NFS-Ganesha")
+@APIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
+@APIDoc("NFS-Ganesha Management API", "NFS-Ganesha")
class NFSGanesha(RESTController):
@EndpointDoc("Status of NFS-Ganesha management feature",
return status
-@ApiController('/nfs-ganesha/export', Scope.NFS_GANESHA)
-@ControllerDoc(group="NFS-Ganesha")
+@APIRouter('/nfs-ganesha/export', Scope.NFS_GANESHA)
+@APIDoc(group="NFS-Ganesha")
class NFSGaneshaExports(RESTController):
RESOURCE_ID = "cluster_id/export_id"
ganesha_conf.reload_daemons(export.daemons)
-@ApiController('/nfs-ganesha/daemon', Scope.NFS_GANESHA)
-@ControllerDoc(group="NFS-Ganesha")
+@APIRouter('/nfs-ganesha/daemon', Scope.NFS_GANESHA)
+@APIDoc(group="NFS-Ganesha")
class NFSGaneshaService(RESTController):
@EndpointDoc("List NFS-Ganesha daemons information",
return result
-@UiApiController('/nfs-ganesha', Scope.NFS_GANESHA)
+@UIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
class NFSGaneshaUi(BaseController):
@Endpoint('GET', '/cephx/clients')
@ReadPermission
from ..exceptions import DashboardException
from ..services.orchestrator import OrchClient
-from . import ApiController, ControllerDoc, Endpoint, EndpointDoc, ReadPermission, RESTController
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController
STATUS_SCHEMA = {
"available": (bool, "Orchestrator status"),
return inner
-@ApiController('/orchestrator')
-@ControllerDoc("Orchestrator Management API", "Orchestrator")
+@APIRouter('/orchestrator')
+@APIDoc("Orchestrator Management API", "Orchestrator")
class Orchestrator(RESTController):
@Endpoint()
from ..services.exception import handle_orchestrator_error, handle_send_command_error
from ..services.orchestrator import OrchClient, OrchFeature
from ..tools import str_to_bool
-from . import ApiController, ControllerDoc, CreatePermission, \
- DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \
- Task, UpdatePermission, allow_empty_body
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+ EndpointDoc, ReadPermission, RESTController, Task, UpdatePermission, \
+ allow_empty_body
from .orchestrator import raise_if_no_orchestrator
logger = logging.getLogger('controllers.osd')
return Task("osd/{}".format(name), metadata, wait_for)
-@ApiController('/osd', Scope.OSD)
-@ControllerDoc('OSD management API', 'OSD')
+@APIRouter('/osd', Scope.OSD)
+@APIDoc('OSD management API', 'OSD')
class Osd(RESTController):
def list(self):
osds = self.get_osd_map()
return CephService.send_command('mon', 'device ls-by-daemon', who='osd.{}'.format(svc_id))
-@ApiController('/osd/flags', Scope.OSD)
-@ControllerDoc(group='OSD')
+@APIRouter('/osd/flags', Scope.OSD)
+@APIDoc(group='OSD')
class OsdFlagsController(RESTController):
@staticmethod
def _osd_flags():
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
PERF_SCHEMA = {
"mon.a": ({
raise cherrypy.HTTPError(404, "{0} not found".format(error))
-@ApiController('perf_counters/mds', Scope.CEPHFS)
-@ControllerDoc("Mds Perf Counters Management API", "MdsPerfCounter")
+@APIRouter('perf_counters/mds', Scope.CEPHFS)
+@APIDoc("Mds Perf Counters Management API", "MdsPerfCounter")
class MdsPerfCounter(PerfCounter):
service_type = 'mds'
-@ApiController('perf_counters/mon', Scope.MONITOR)
-@ControllerDoc("Mon Perf Counters Management API", "MonPerfCounter")
+@APIRouter('perf_counters/mon', Scope.MONITOR)
+@APIDoc("Mon Perf Counters Management API", "MonPerfCounter")
class MonPerfCounter(PerfCounter):
service_type = 'mon'
-@ApiController('perf_counters/osd', Scope.OSD)
-@ControllerDoc("OSD Perf Counters Management API", "OsdPerfCounter")
+@APIRouter('perf_counters/osd', Scope.OSD)
+@APIDoc("OSD Perf Counters Management API", "OsdPerfCounter")
class OsdPerfCounter(PerfCounter):
service_type = 'osd'
-@ApiController('perf_counters/rgw', Scope.RGW)
-@ControllerDoc("Rgw Perf Counters Management API", "RgwPerfCounter")
+@APIRouter('perf_counters/rgw', Scope.RGW)
+@APIDoc("Rgw Perf Counters Management API", "RgwPerfCounter")
class RgwPerfCounter(PerfCounter):
service_type = 'rgw'
-@ApiController('perf_counters/rbd-mirror', Scope.RBD_MIRRORING)
-@ControllerDoc("Rgw Mirroring Perf Counters Management API", "RgwMirrorPerfCounter")
+@APIRouter('perf_counters/rbd-mirror', Scope.RBD_MIRRORING)
+@APIDoc("Rgw Mirroring Perf Counters Management API", "RgwMirrorPerfCounter")
class RbdMirrorPerfCounter(PerfCounter):
service_type = 'rbd-mirror'
-@ApiController('perf_counters/mgr', Scope.MANAGER)
-@ControllerDoc("Mgr Perf Counters Management API", "MgrPerfCounter")
+@APIRouter('perf_counters/mgr', Scope.MANAGER)
+@APIDoc("Mgr Perf Counters Management API", "MgrPerfCounter")
class MgrPerfCounter(PerfCounter):
service_type = 'mgr'
-@ApiController('perf_counters/tcmu-runner', Scope.ISCSI)
-@ControllerDoc("Tcmu Runner Perf Counters Management API", "TcmuRunnerPerfCounter")
+@APIRouter('perf_counters/tcmu-runner', Scope.ISCSI)
+@APIDoc("Tcmu Runner Perf Counters Management API", "TcmuRunnerPerfCounter")
class TcmuRunnerPerfCounter(PerfCounter):
service_type = 'tcmu-runner'
-@ApiController('perf_counters')
-@ControllerDoc("Perf Counters Management API", "PerfCounters")
+@APIRouter('perf_counters')
+@APIDoc("Perf Counters Management API", "PerfCounters")
class PerfCounters(RESTController):
@EndpointDoc("Display Perf Counters",
responses={200: PERF_SCHEMA})
from ..services.exception import handle_send_command_error
from ..services.rbd import RbdConfiguration
from ..tools import TaskManager, str_to_bool
-from . import ApiController, ControllerDoc, Endpoint, EndpointDoc, \
- ReadPermission, RESTController, Task, UiApiController
+from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, \
+ RESTController, Task, UIRouter
POOL_SCHEMA = ([{
"pool": (int, "pool id"),
return Task("pool/{}".format(name), metadata, wait_for)
-@ApiController('/pool', Scope.POOL)
-@ControllerDoc("Get pool details by pool name", "Pool")
+@APIRouter('/pool', Scope.POOL)
+@APIDoc("Get pool details by pool name", "Pool")
class Pool(RESTController):
@staticmethod
return RbdConfiguration(pool_name).list()
-@UiApiController('/pool', Scope.POOL)
-@ControllerDoc("Dashboard UI helper function; not part of the public API", "PoolUi")
+@UIRouter('/pool', Scope.POOL)
+@APIDoc("Dashboard UI helper function; not part of the public API", "PoolUi")
class PoolUi(Pool):
@Endpoint()
@ReadPermission
from ..exceptions import DashboardException
from ..security import Scope
from ..settings import Settings
-from . import ApiController, BaseController, Controller, ControllerDoc, Endpoint, RESTController
+from . import APIDoc, APIRouter, BaseController, Endpoint, RESTController, Router
-@Controller('/api/prometheus_receiver', secure=False)
+@Router('/api/prometheus_receiver', secure=False)
class PrometheusReceiver(BaseController):
"""
The receiver is needed in order to receive alert notifications (reports)
raise DashboardException(content, http_status_code=400, component='prometheus')
-@ApiController('/prometheus', Scope.PROMETHEUS)
-@ControllerDoc("Prometheus Management API", "Prometheus")
+@APIRouter('/prometheus', Scope.PROMETHEUS)
+@APIDoc("Prometheus Management API", "Prometheus")
class Prometheus(PrometheusRESTController):
def list(self, **params):
return self.alert_proxy('GET', '/alerts', params)
return self.alert_proxy('DELETE', '/silence/' + s_id) if s_id else None
-@ApiController('/prometheus/notifications', Scope.PROMETHEUS)
-@ControllerDoc("Prometheus Notifications Management API", "PrometheusNotifications")
+@APIRouter('/prometheus/notifications', Scope.PROMETHEUS)
+@APIDoc("Prometheus Notifications Management API", "PrometheusNotifications")
class PrometheusNotifications(RESTController):
def list(self, **params):
format_bitmask, format_features, parse_image_spec, rbd_call, \
rbd_image_call
from ..tools import ViewCache, str_to_bool
-from . import ApiController, ControllerDoc, CreatePermission, \
- DeletePermission, EndpointDoc, RESTController, Task, UpdatePermission, \
- allow_empty_body
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, \
+ EndpointDoc, RESTController, Task, UpdatePermission, allow_empty_body
logger = logging.getLogger(__name__)
features.sort(key=key_func, reverse=not enable)
-@ApiController('/block/image', Scope.RBD_IMAGE)
-@ControllerDoc("RBD Management API", "Rbd")
+@APIRouter('/block/image', Scope.RBD_IMAGE)
+@APIDoc("RBD Management API", "Rbd")
class Rbd(RESTController):
# set of image features that can be enable on existing images
return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
-@ApiController('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
-@ControllerDoc("RBD Snapshot Management API", "RbdSnapshot")
+@APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
+@APIDoc("RBD Snapshot Management API", "RbdSnapshot")
class RbdSnapshot(RESTController):
RESOURCE_ID = "snapshot_name"
rbd_call(pool_name, namespace, _parent_clone)
-@ApiController('/block/image/trash', Scope.RBD_IMAGE)
-@ControllerDoc("RBD Trash Management API", "RbdTrash")
+@APIRouter('/block/image/trash', Scope.RBD_IMAGE)
+@APIDoc("RBD Trash Management API", "RbdTrash")
class RbdTrash(RESTController):
RESOURCE_ID = "image_id_spec"
int(str_to_bool(force)))
-@ApiController('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
-@ControllerDoc("RBD Namespace Management API", "RbdNamespace")
+@APIRouter('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE)
+@APIDoc("RBD Namespace Management API", "RbdNamespace")
class RbdNamespace(RESTController):
def __init__(self):
from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception
from ..services.rbd import rbd_call
from ..tools import ViewCache
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, RESTController, Task, UpdatePermission, \
- allow_empty_body
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, Task, UpdatePermission, allow_empty_body
try:
from typing import no_type_check
}
-@ApiController('/block/mirroring', Scope.RBD_MIRRORING)
-@ControllerDoc("RBD Mirroring Management API", "RbdMirroring")
+@APIRouter('/block/mirroring', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Management API", "RbdMirroring")
class RbdMirroring(BaseController):
@Endpoint(method='GET', path='site_name')
return {'site_name': rbd.RBD().mirror_site_name_get(mgr.rados)}
-@ApiController('/block/mirroring/summary', Scope.RBD_MIRRORING)
-@ControllerDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary")
+@APIRouter('/block/mirroring/summary', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Summary Management API", "RbdMirroringSummary")
class RbdMirroringSummary(BaseController):
@Endpoint()
'content_data': content_data}
-@ApiController('/block/mirroring/pool', Scope.RBD_MIRRORING)
-@ControllerDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode")
+@APIRouter('/block/mirroring/pool', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Mode Management API", "RbdMirroringPoolMode")
class RbdMirroringPoolMode(RESTController):
RESOURCE_ID = "pool_name"
return rbd_call(pool_name, None, _edit, mirror_mode)
-@ApiController('/block/mirroring/pool/{pool_name}/bootstrap',
- Scope.RBD_MIRRORING)
-@ControllerDoc("RBD Mirroring Pool Bootstrap Management API", "RbdMirroringPoolBootstrap")
+@APIRouter('/block/mirroring/pool/{pool_name}/bootstrap', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Bootstrap Management API", "RbdMirroringPoolBootstrap")
class RbdMirroringPoolBootstrap(BaseController):
@Endpoint(method='POST', path='token')
return {}
-@ApiController('/block/mirroring/pool/{pool_name}/peer', Scope.RBD_MIRRORING)
-@ControllerDoc("RBD Mirroring Pool Peer Management API", "RbdMirroringPoolPeer")
+@APIRouter('/block/mirroring/pool/{pool_name}/peer', Scope.RBD_MIRRORING)
+@APIDoc("RBD Mirroring Pool Peer Management API", "RbdMirroringPoolPeer")
class RbdMirroringPoolPeer(RESTController):
RESOURCE_ID = "peer_uuid"
from ..services.ceph_service import CephService
from ..services.rgw_client import NoRgwDaemonsException, RgwClient
from ..tools import json_str_to_object, str_to_bool
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, ReadPermission, RESTController, allow_empty_body
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ ReadPermission, RESTController, allow_empty_body
try:
from typing import Any, List, Optional
}
-@ApiController('/rgw', Scope.RGW)
-@ControllerDoc("RGW Management API", "Rgw")
+@APIRouter('/rgw', Scope.RGW)
+@APIDoc("RGW Management API", "Rgw")
class Rgw(BaseController):
@Endpoint()
@ReadPermission
return status
-@ApiController('/rgw/daemon', Scope.RGW)
-@ControllerDoc("RGW Daemon Management API", "RgwDaemon")
+@APIRouter('/rgw/daemon', Scope.RGW)
+@APIDoc("RGW Daemon Management API", "RgwDaemon")
class RgwDaemon(RESTController):
@EndpointDoc("Display RGW Daemons",
responses={200: [RGW_DAEMON_SCHEMA]})
raise DashboardException(e, http_status_code=http_status_code, component='rgw')
-@ApiController('/rgw/site', Scope.RGW)
-@ControllerDoc("RGW Site Management API", "RgwSite")
+@APIRouter('/rgw/site', Scope.RGW)
+@APIDoc("RGW Site Management API", "RgwSite")
class RgwSite(RgwRESTController):
def list(self, query=None, daemon_name=None):
if query == 'placement-targets':
raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
-@ApiController('/rgw/bucket', Scope.RGW)
-@ControllerDoc("RGW Bucket Management API", "RgwBucket")
+@APIRouter('/rgw/bucket', Scope.RGW)
+@APIDoc("RGW Bucket Management API", "RgwBucket")
class RgwBucket(RgwRESTController):
def _append_bid(self, bucket):
"""
}, json_response=False)
-@ApiController('/rgw/user', Scope.RGW)
-@ControllerDoc("RGW User Management API", "RgwUser")
+@APIRouter('/rgw/user', Scope.RGW)
+@APIDoc("RGW User Management API", "RgwUser")
class RgwUser(RgwRESTController):
def _append_uid(self, user):
"""
from ..security import Permission
from ..security import Scope as SecurityScope
from ..services.access_control import SYSTEM_ROLES
-from . import ApiController, ControllerDoc, CreatePermission, EndpointDoc, \
- RESTController, UiApiController
+from . import APIDoc, APIRouter, CreatePermission, EndpointDoc, RESTController, UIRouter
ROLE_SCHEMA = [{
"name": (str, "Role Name"),
}]
-@ApiController('/role', SecurityScope.USER)
-@ControllerDoc("Role Management API", "Role")
+@APIRouter('/role', SecurityScope.USER)
+@APIDoc("Role Management API", "Role")
class Role(RESTController):
@staticmethod
def _role_to_dict(role):
role.get('scopes_permissions'))
-@UiApiController('/scope', SecurityScope.USER)
+@UIRouter('/scope', SecurityScope.USER)
class Scope(RESTController):
def list(self):
return SecurityScope.all_scopes()
from ..exceptions import UserDoesNotExist
from ..services.auth import JwtManager
from ..tools import prepare_url_prefix
-from . import BaseController, Controller, ControllerAuthMixin, Endpoint, allow_empty_body
+from . import BaseController, ControllerAuthMixin, Endpoint, Router, allow_empty_body
-@Controller('/auth/saml2', secure=False)
+@Router('/auth/saml2', secure=False)
class Saml2(BaseController, ControllerAuthMixin):
@staticmethod
from ..security import Scope
from ..services.exception import handle_orchestrator_error
from ..services.orchestrator import OrchClient, OrchFeature
-from . import ApiController, ControllerDoc, CreatePermission, \
- DeletePermission, Endpoint, ReadPermission, RESTController, Task
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+ ReadPermission, RESTController, Task
from .orchestrator import raise_if_no_orchestrator
return Task("service/{}".format(name), metadata, wait_for)
-@ApiController('/service', Scope.HOSTS)
-@ControllerDoc("Service Management API", "Service")
+@APIRouter('/service', Scope.HOSTS)
+@APIDoc("Service Management API", "Service")
class Service(RESTController):
@Endpoint()
from ..security import Scope
from ..settings import Options
from ..settings import Settings as SettingsModule
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController, UiApiController
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, UIRouter
SETTINGS_SCHEMA = [{
"name": (str, 'Settings Name'),
}]
-@ApiController('/settings', Scope.CONFIG_OPT)
-@ControllerDoc("Settings Management API", "Settings")
+@APIRouter('/settings', Scope.CONFIG_OPT)
+@APIDoc("Settings Management API", "Settings")
class Settings(RESTController):
"""
Enables to manage the settings of the dashboard (not the Ceph cluster).
setattr(SettingsModule, self._to_native(name), value)
-@UiApiController('/standard_settings')
+@UIRouter('/standard_settings')
class StandardSettings(RESTController):
def list(self):
"""
from ..security import Permission, Scope
from ..services import progress
from ..tools import TaskManager
-from . import ApiController, BaseController, ControllerDoc, Endpoint, EndpointDoc
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc
SUMMARY_SCHEMA = {
"health_status": (str, ""),
}
-@ApiController('/summary')
-@ControllerDoc("Get Ceph Summary Details", "Summary")
+@APIRouter('/summary')
+@APIDoc("Get Ceph Summary Details", "Summary")
class Summary(BaseController):
def _health_status(self):
health_data = mgr.get("health")
from ..services import progress
from ..tools import TaskManager
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
TASK_SCHEMA = {
"executing_tasks": (str, "ongoing executing tasks"),
}
-@ApiController('/task')
-@ControllerDoc("Task Management API", "Task")
+@APIRouter('/task')
+@APIDoc("Task Management API", "Task")
class Task(RESTController):
@EndpointDoc("Display Tasks",
parameters={
from .. import mgr
from ..exceptions import DashboardException
from ..security import Scope
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController
+from . import APIDoc, APIRouter, EndpointDoc, RESTController
REPORT_SCHEMA = {
"report": ({
}
-@ApiController('/telemetry', Scope.CONFIG_OPT)
-@ControllerDoc("Display Telemetry Report", "Telemetry")
+@APIRouter('/telemetry', Scope.CONFIG_OPT)
+@APIDoc("Display Telemetry Report", "Telemetry")
class Telemetry(RESTController):
@RESTController.Collection('GET')
from ..security import Scope
from ..services.access_control import SYSTEM_ROLES, PasswordPolicy
from ..services.auth import JwtManager
-from . import ApiController, BaseController, ControllerDoc, Endpoint, \
- EndpointDoc, RESTController, allow_empty_body, validate_ceph_type
+from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
+ RESTController, allow_empty_body, validate_ceph_type
USER_SCHEMA = ([{
"username": (str, 'Username of the user'),
component='user')
-@ApiController('/user', Scope.USER)
-@ControllerDoc("Display User Details", "User")
+@APIRouter('/user', Scope.USER)
+@APIDoc("Display User Details", "User")
class User(RESTController):
@staticmethod
return User._user_to_dict(user)
-@ApiController('/user')
-@ControllerDoc("Get User Password Policy Details", "UserPasswordPolicy")
+@APIRouter('/user')
+@APIDoc("Get User Password Policy Details", "UserPasswordPolicy")
class UserPasswordPolicy(RESTController):
@Endpoint('POST')
return result
-@ApiController('/user/{username}')
-@ControllerDoc("Change User Password", "UserChangePassword")
+@APIRouter('/user/{username}')
+@APIDoc("Change User Password", "UserChangePassword")
class UserChangePassword(BaseController):
@Endpoint('POST')
create_self_signed_cert, get_default_addr, verify_tls_files
from . import mgr
-from .controllers import generate_routes, json_error_page
+from .controllers import Router, json_error_page
from .grafana import push_local_dashboards
from .services.auth import AuthManager, AuthManagerTool, JwtManager
from .services.exception import dashboard_exception_handler
# about to start serving
self.set_uri(uri)
- mapper, parent_urls = generate_routes(self.url_prefix)
+ mapper, parent_urls = Router.generate_routes(self.url_prefix)
config = {}
for purl in parent_urls:
summary: Get List Of Features
tags:
- FeatureTogglesEndpoint
+ /api/feedback:
+ post:
+ description: "\n Create an issue.\n :param project: The affected\
+ \ ceph component.\n :param tracker: The tracker type.\n :param\
+ \ subject: The title of the issue.\n :param description: The description\
+ \ of the issue.\n "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ description:
+ type: string
+ project:
+ type: string
+ subject:
+ type: string
+ tracker:
+ type: string
+ required:
+ - project
+ - tracker
+ - subject
+ - description
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
+ /api/feedback/{issue_number}:
+ get:
+ description: "\n Fetch issue details.\n :param issueAPI: The issue\
+ \ tracker API access key.\n "
+ parameters:
+ - in: path
+ name: issue_number
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Report
/api/grafana/dashboards:
post:
parameters: []
name: RbdSnapshot
- description: RBD Trash Management API
name: RbdTrash
+- description: Feedback API
+ name: Report
- description: RGW Management API
name: Rgw
- description: RGW Bucket Management API
@PM.add_hook
def get_controllers(self):
- from ..controllers import ApiController, ControllerDoc, EndpointDoc, RESTController
+ from ..controllers import APIDoc, APIRouter, EndpointDoc, RESTController
FEATURES_SCHEMA = {
"rbd": (bool, ''),
"nfs": (bool, '')
}
- @ApiController('/feature_toggles')
- @ControllerDoc("Manage Features API", "FeatureTogglesEndpoint")
+ @APIRouter('/feature_toggles')
+ @APIDoc("Manage Features API", "FeatureTogglesEndpoint")
class FeatureTogglesEndpoint(RESTController):
@EndpointDoc("Get List Of Features",
responses={200: FEATURES_SCHEMA})
@PM.add_hook
def get_controllers(self):
- from ..controllers import RESTController, UiApiController
+ from ..controllers import RESTController, UIRouter
- @UiApiController('/motd')
+ @UIRouter('/motd')
class MessageOfTheDay(RESTController):
def list(_) -> Optional[Dict]: # pylint: disable=no-self-argument
value: str = self.get_option(self.NAME)
from mgr_module import HandleCommandResult
from pyfakefs import fake_filesystem
-from .. import DEFAULT_VERSION, mgr
+from .. import mgr
from ..controllers import generate_controller_routes, json_error_page
+from ..controllers._version import APIVersion
from ..module import Module
from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa
from ..services.auth import AuthManagerTool
import unittest.mock as mock
from .. import mgr
-from ..controllers import Controller, RESTController
+from ..controllers import RESTController, Router
from . import ControllerTestCase, KVStoreMockMixin # pylint: disable=no-name-in-module
# pylint: disable=W0613
-@Controller('/foo', secure=False)
+@Router('/foo', secure=False)
class FooResource(RESTController):
def create(self, password):
pass
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-from ..controllers import ApiController, BaseController, Controller, Endpoint, RESTController
+from ..controllers import APIRouter, BaseController, Endpoint, RESTController, Router
from . import ControllerTestCase # pylint: disable=no-name-in-module
-@Controller("/btest/{key}", base_url="/ui", secure=False)
+@Router("/btest/{key}", base_url="/ui", secure=False)
class BTest(BaseController):
@Endpoint()
def test1(self, key, opt=1):
return {'key': key, 'opt': opt}
-@ApiController("/rtest/{key}", secure=False)
+@APIRouter("/rtest/{key}", secure=False)
class RTest(RESTController):
RESOURCE_ID = 'skey/ekey'
return {'key': key, 'skey': skey, 'ekey': ekey, 'opt': opt}
-@Controller("/", secure=False)
+@Router("/", secure=False)
class Root(BaseController):
@Endpoint(json_response=False)
def __call__(self):
from __future__ import absolute_import
from ..api.doc import SchemaType
-from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController
+from ..controllers import APIDoc, APIRouter, Endpoint, EndpointDoc, RESTController
+from ..controllers._version import APIVersion
from ..controllers.docs import Docs
from . import ControllerTestCase # pylint: disable=no-name-in-module
# Dummy controller and endpoint that can be assigned with @EndpointDoc and @GroupDoc
-@ControllerDoc("Group description", group="FooGroup")
-@ApiController("/doctest/", secure=False)
+@APIDoc("Group description", group="FooGroup")
+@APIRouter("/doctest/", secure=False)
class DecoratedController(RESTController):
RESOURCE_ID = 'doctest'
import rados
-from ..controllers import Controller, Endpoint, RESTController, Task
+from ..controllers import Endpoint, RESTController, Router, Task
from ..services.ceph_service import SendCommandError
from ..services.exception import handle_rados_error, \
handle_send_command_error, serialize_dashboard_exception
# pylint: disable=W0613
-@Controller('foo', secure=False)
+@Router('foo', secure=False)
class FooResource(RESTController):
@Endpoint()
cls.mgr = mgr
# Populate real endpoint map
- from ..controllers import load_controllers
- cls.controllers = load_controllers()
+ from ..controllers import BaseController
+ cls.controllers = BaseController.load_controllers()
# Initialize FeatureToggles plugin
cls.plugin = FeatureToggles()
from orchestrator import HostSpec, InventoryHost
from .. import mgr
+from ..controllers._version import APIVersion
from ..controllers.host import Host, HostUi, get_device_osd_map, get_hosts, get_inventories
from ..tools import NotificationQueue, TaskManager
from . import ControllerTestCase # pylint: disable=no-name-in-module
except ImportError:
import unittest.mock as mock
-from ..controllers import Controller, RESTController, Task
+from ..controllers import RESTController, Router, Task
from ..controllers.task import Task as TaskController
from ..services import progress
from ..tools import NotificationQueue, TaskManager
from . import ControllerTestCase # pylint: disable=no-name-in-module
-@Controller('/test/task', secure=False)
+@Router('/test/task', secure=False)
class TaskTest(RESTController):
sleep_time = 0.0
except ImportError:
from unittest.mock import patch
-from .. import DEFAULT_VERSION
-from ..controllers import ApiController, BaseController, Controller, Proxy, RESTController
+from ..controllers import APIRouter, BaseController, Proxy, RESTController, Router
+from ..controllers._version import APIVersion
from ..services.exception import handle_rados_error
from ..tools import dict_contains_path, dict_get, json_str_to_object, partial_dict
from . import ControllerTestCase # pylint: disable=no-name-in-module
# pylint: disable=W0613
-@Controller('/foo', secure=False)
+@Router('/foo', secure=False)
class FooResource(RESTController):
elems = []
return dict(key=key, newdata=newdata)
-@Controller('/foo/:key/:method', secure=False)
+@Router('/foo/:key/:method', secure=False)
class FooResourceDetail(RESTController):
def list(self, key, method):
return {'detail': (key, [method])}
-@ApiController('/rgw/proxy', secure=False)
+@APIRouter('/rgw/proxy', secure=False)
class GenerateControllerRoutesController(BaseController):
@Proxy()
def __call__(self, path, **params):
pass
-@ApiController('/fooargs', secure=False)
+@APIRouter('/fooargs', secure=False)
class FooArgs(RESTController):
def set(self, code, name=None, opt1=None, opt2=None):
return {'code': code, 'name': name, 'opt1': opt1, 'opt2': opt2}
import unittest
-from ..controllers import ApiController, RESTController
+from ..controllers._api_router import APIRouter
+from ..controllers._rest_controller import RESTController
+from ..controllers._version import APIVersion
from . import ControllerTestCase # pylint: disable=no-name-in-module
-@ApiController("/vtest", secure=False)
+@APIRouter("/vtest", secure=False)
class VTest(RESTController):
RESOURCE_ID = "vid"