1
0
mirror of https://github.com/ceph/ceph synced 2025-03-30 23:40:09 +00:00

mgr/dashboard: clean-up controllers

Fixes: https://tracker.ceph.com/issues/52589
Signed-off-by: Ernesto Puerta <epuertat@redhat.com>
This commit is contained in:
Ernesto Puerta 2021-09-07 17:07:48 +02:00 committed by Avan Thakkar
parent ed8e8b7600
commit b36766ebd8
60 changed files with 1507 additions and 1333 deletions

View File

@ -9,7 +9,10 @@ if(WITH_MGR_DASHBOARD_FRONTEND)
add_subdirectory(frontend)
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()
else()
# prebuilt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
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

View File

@ -0,0 +1,18 @@
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'

View File

@ -0,0 +1,314 @@
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

View File

@ -0,0 +1,128 @@
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

View File

@ -0,0 +1,82 @@
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)

View File

@ -0,0 +1,127 @@
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

View File

@ -0,0 +1,60 @@
"""
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

View File

@ -0,0 +1,249 @@
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

View File

@ -0,0 +1,69 @@
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

View File

@ -0,0 +1,79 @@
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

View File

@ -0,0 +1,13 @@
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

View File

@ -8,8 +8,7 @@ from .. import mgr
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
@ -28,8 +27,8 @@ AUTH_CHECK_SCHEMA = {
}
@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.

View File

@ -11,8 +11,7 @@ from ..security import Scope
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, ''),
@ -20,11 +19,11 @@ GET_QUOTAS_SCHEMA = {
}
@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
@ -490,8 +489,8 @@ class CephFSClients(object):
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'

View File

@ -6,7 +6,7 @@ from .. import mgr
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'),
@ -27,8 +27,8 @@ FILTER_SCHEMA = [{
}]
@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):

View File

@ -1,23 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from cherrypy import NotFound
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
from . import ApiController, APIVersion, 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'),
"rule_name": (str, 'Rule Name'),
"ruleset": (int, 'RuleSet related to the rule'),
"type": (int, 'Type of Rule'),
"min_size": (int, 'Minimum size of Rule'),
"max_size": (int, 'Maximum size of Rule'),
'steps': ([{str}], 'Steps included in the rule')
}
@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})
@ -46,8 +51,8 @@ class CrushRule(RESTController):
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

View File

@ -6,14 +6,15 @@ import cherrypy
from .. import mgr
from ..api.doc import Schema, SchemaInput, SchemaType
from . import ENDPOINT_MAP, APIVersion, 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
@ -404,8 +405,6 @@ if __name__ == "__main__":
import yaml
from . import generate_routes
def fix_null_descr(obj):
"""
A hot fix for errors caused by null description values when generating
@ -415,7 +414,7 @@ if __name__ == "__main__":
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

View File

@ -5,8 +5,7 @@ from cherrypy import NotFound
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, ''),
@ -18,8 +17,8 @@ LIST_CODE__SCHEMA = {
}
@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
@ -46,8 +45,8 @@ class ErasureCodeProfile(RESTController):
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

View File

@ -5,16 +5,16 @@ from ..model.feedback import Feedback
from ..rest_client import RequestException
from ..security import Scope
from ..services import feedback
from . import ControllerDoc, RESTController, UiApiController
from . import APIDoc, APIRouter, RESTController
@UiApiController('/feedback', Scope.CONFIG_OPT)
@ControllerDoc("Feedback API", "Report")
@APIRouter('/feedback', Scope.CONFIG_OPT)
@APIDoc("Feedback API", "Report")
class FeedbackController(RESTController):
issueAPIkey = None
def __init__(self): # pragma: no cover
super(FeedbackController, self).__init__()
super().__init__()
self.tracker_client = feedback.CephTrackerClient()
def create(self, project, tracker, subject, description):

View File

@ -1,11 +1,11 @@
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')

View File

@ -4,16 +4,16 @@ from ..exceptions import DashboardException
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

View File

@ -9,7 +9,7 @@ from ..services.ceph_service import CephService
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 = ({
@ -274,11 +274,11 @@ class HealthData(object):
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)

View File

@ -14,7 +14,7 @@ import cherrypy
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")
@ -51,10 +51,10 @@ class LanguageMixin(object):
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(
@ -134,7 +134,7 @@ class HomeController(BaseController, LanguageMixin):
return serve_file(full_path)
@UiApiController("/langs", secure=False)
@UIRouter("/langs", secure=False)
class LangsController(BaseController, LanguageMixin):
@Endpoint('GET')
def __call__(self):

View File

@ -16,9 +16,10 @@ from ..services.ceph_service import CephService
from ..services.exception import handle_orchestrator_error
from ..services.orchestrator import OrchClient, OrchFeature
from ..tools import TaskManager, str_to_bool
from . import ApiController, APIVersion, 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 = {
@ -266,8 +267,8 @@ def add_host(hostname: str, addr: Optional[str] = None,
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={
@ -448,7 +449,7 @@ class Host(RESTController):
orch.hosts.add_label(hostname, label)
@UiApiController('/host', Scope.HOSTS)
@UIRouter('/host', Scope.HOSTS)
class HostUi(BaseController):
@Endpoint('GET')
@ReadPermission

View File

@ -23,9 +23,8 @@ from ..services.iscsi_config import IscsiGatewayDoesNotExist
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
ISCSI_SCHEMA = {
'user': (str, 'username'),
@ -35,7 +34,7 @@ ISCSI_SCHEMA = {
}
@UiApiController('/iscsi', Scope.ISCSI)
@UIRouter('/iscsi', Scope.ISCSI)
class IscsiUi(BaseController):
REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
@ -199,8 +198,8 @@ class IscsiUi(BaseController):
return result_gateways
@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
@ -256,8 +255,8 @@ def iscsi_target_task(name, metadata, wait_for=2.0):
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):

View File

@ -5,7 +5,7 @@ import collections
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
@ -30,11 +30,11 @@ LOGS_SCHEMA = {
}
@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)

View File

@ -5,7 +5,7 @@ from ..security import Scope
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"),
@ -30,8 +30,8 @@ MGR_MODULE_SCHEMA = ([{
}])
@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']

View File

@ -4,7 +4,7 @@ import json
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": ({
@ -98,8 +98,8 @@ MONITOR_SCHEMA = {
}
@ApiController('/monitor', Scope.MONITOR)
@ControllerDoc("Get Monitor Details", "Monitor")
@APIRouter('/monitor', Scope.MONITOR)
@APIDoc("Get Monitor Details", "Monitor")
class Monitor(BaseController):
@Endpoint()
@ReadPermission

View File

@ -14,8 +14,8 @@ from ..services.exception import DashboardException, serialize_dashboard_excepti
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')
@ -86,8 +86,8 @@ def NfsTask(name, metadata, wait_for): # noqa: N802
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",
@ -108,8 +108,8 @@ class NFSGanesha(RESTController):
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"
@ -233,8 +233,8 @@ class NFSGaneshaExports(RESTController):
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",
@ -252,7 +252,7 @@ class NFSGaneshaService(RESTController):
return result
@UiApiController('/nfs-ganesha', Scope.NFS_GANESHA)
@UIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
class NFSGaneshaUi(BaseController):
@Endpoint('GET', '/cephx/clients')
@ReadPermission

View File

@ -4,7 +4,7 @@ from functools import wraps
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"),
@ -35,8 +35,8 @@ def raise_if_no_orchestrator(features=None):
return inner
@ApiController('/orchestrator')
@ControllerDoc("Orchestrator Management API", "Orchestrator")
@APIRouter('/orchestrator')
@APIDoc("Orchestrator Management API", "Orchestrator")
class Orchestrator(RESTController):
@Endpoint()

View File

@ -15,9 +15,9 @@ from ..services.ceph_service import CephService, SendCommandError
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')
@ -50,8 +50,8 @@ def osd_task(name, metadata, wait_for=2.0):
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()
@ -395,8 +395,8 @@ class Osd(RESTController):
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():

View File

@ -5,7 +5,7 @@ import cherrypy
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": ({
@ -31,50 +31,50 @@ class PerfCounter(RESTController):
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})

View File

@ -11,8 +11,8 @@ from ..services.ceph_service import CephService
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"),
@ -89,8 +89,8 @@ def pool_task(name, metadata, wait_for=2.0):
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
@ -283,8 +283,8 @@ class Pool(RESTController):
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

View File

@ -8,10 +8,10 @@ import requests
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)
@ -59,8 +59,8 @@ class PrometheusRESTController(RESTController):
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)
@ -82,8 +82,8 @@ class Prometheus(PrometheusRESTController):
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):

View File

@ -18,9 +18,8 @@ from ..services.rbd import RbdConfiguration, RbdService, RbdSnapshotService, \
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__)
@ -66,8 +65,8 @@ def _sort_features(features, enable=True):
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
@ -264,8 +263,8 @@ class Rbd(RESTController):
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"
@ -355,8 +354,8 @@ class RbdSnapshot(RESTController):
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"
@ -447,8 +446,8 @@ class RbdTrash(RESTController):
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):

View File

@ -15,9 +15,8 @@ from ..services.ceph_service import CephService
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
logger = logging.getLogger('controllers.rbd_mirror')
@ -367,8 +366,8 @@ RBDM_SUMMARY_SCHEMA = {
}
@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')
@ -390,8 +389,8 @@ class RbdMirroring(BaseController):
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()
@ -408,8 +407,8 @@ class RbdMirroringSummary(BaseController):
'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"
@ -450,9 +449,8 @@ class RbdMirroringPoolMode(RESTController):
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')
@ -484,8 +482,8 @@ class RbdMirroringPoolBootstrap(BaseController):
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"

View File

@ -12,8 +12,8 @@ from ..services.auth import AuthManager, JwtManager
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
@ -40,8 +40,8 @@ RGW_USER_SCHEMA = {
}
@ApiController('/rgw', Scope.RGW)
@ControllerDoc("RGW Management API", "Rgw")
@APIRouter('/rgw', Scope.RGW)
@APIDoc("RGW Management API", "Rgw")
class Rgw(BaseController):
@Endpoint()
@ReadPermission
@ -78,8 +78,8 @@ class Rgw(BaseController):
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]})
@ -149,8 +149,8 @@ class RgwRESTController(RESTController):
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':
@ -162,8 +162,8 @@ class RgwSite(RgwRESTController):
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):
"""
@ -324,8 +324,8 @@ class RgwBucket(RgwRESTController):
}, 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):
"""

View File

@ -8,8 +8,7 @@ from ..exceptions import DashboardException, RoleAlreadyExists, \
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"),
@ -21,8 +20,8 @@ ROLE_SCHEMA = [{
}]
@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):
@ -138,7 +137,7 @@ class Role(RESTController):
role.get('scopes_permissions'))
@UiApiController('/scope', SecurityScope.USER)
@UIRouter('/scope', SecurityScope.USER)
class Scope(RESTController):
def list(self):
return SecurityScope.all_scopes()

View File

@ -15,10 +15,10 @@ from .. import mgr
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

View File

@ -7,8 +7,8 @@ from ..exceptions import DashboardException
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
@ -16,8 +16,8 @@ def service_task(name, metadata, wait_for=2.0):
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()

View File

@ -6,7 +6,7 @@ import cherrypy
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'),
@ -16,8 +16,8 @@ SETTINGS_SCHEMA = [{
}]
@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).
@ -102,7 +102,7 @@ class Settings(RESTController):
setattr(SettingsModule, self._to_native(name), value)
@UiApiController('/standard_settings')
@UIRouter('/standard_settings')
class StandardSettings(RESTController):
def list(self):
"""

View File

@ -8,7 +8,7 @@ from ..exceptions import ViewCacheNoDataException
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, ""),
@ -37,8 +37,8 @@ SUMMARY_SCHEMA = {
}
@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")

View File

@ -2,7 +2,7 @@
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"),
@ -22,8 +22,8 @@ TASK_SCHEMA = {
}
@ApiController('/task')
@ControllerDoc("Task Management API", "Task")
@APIRouter('/task')
@APIDoc("Task Management API", "Task")
class Task(RESTController):
@EndpointDoc("Display Tasks",
parameters={

View File

@ -3,7 +3,7 @@
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": ({
@ -200,8 +200,8 @@ REPORT_SCHEMA = {
}
@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')

View File

@ -12,8 +12,8 @@ from ..exceptions import DashboardException, PasswordPolicyException, \
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'),
@ -46,8 +46,8 @@ def validate_password_policy(password, username=None, old_password=None):
component='user')
@ApiController('/user', Scope.USER)
@ControllerDoc("Display User Details", "User")
@APIRouter('/user', Scope.USER)
@APIDoc("Display User Details", "User")
class User(RESTController):
@staticmethod
@ -157,8 +157,8 @@ class User(RESTController):
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')
@ -190,8 +190,8 @@ class UserPasswordPolicy(RESTController):
return result
@ApiController('/user/{username}')
@ControllerDoc("Change User Password", "UserChangePassword")
@APIRouter('/user/{username}')
@APIDoc("Change User Password", "UserChangePassword")
class UserChangePassword(BaseController):
@Endpoint('POST')

View File

@ -25,7 +25,7 @@ from mgr_util import ServerConfigException, build_url, \
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 .model.feedback import Feedback
from .rest_client import RequestException
@ -339,7 +339,7 @@ class Module(MgrModule, CherryPyConfig):
# 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:

View File

@ -2311,12 +2311,21 @@ paths:
application/vnd.ceph.api.v2.0+json:
schema:
properties:
max_size:
description: Maximum size of Rule
type: integer
min_size:
description: Minimum size of Rule
type: integer
rule_id:
description: Rule ID
type: integer
rule_name:
description: Rule Name
type: string
ruleset:
description: RuleSet related to the rule
type: integer
steps:
description: Steps included in the rule
items:
@ -2328,7 +2337,10 @@ paths:
required:
- rule_id
- rule_name
- ruleset
- type
- min_size
- max_size
- steps
type: object
description: OK
@ -2644,6 +2656,85 @@ paths:
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: []
@ -10361,6 +10452,8 @@ tags:
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

View File

@ -132,7 +132,7 @@ class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions,
@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, ''),
@ -143,8 +143,8 @@ class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions,
"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})

View File

@ -79,9 +79,9 @@ class Motd(SP):
@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)

View File

@ -14,7 +14,8 @@ from mgr_module import HandleCommandResult
from pyfakefs import fake_filesystem
from .. import mgr
from ..controllers import APIVersion, generate_controller_routes, json_error_page
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

View File

@ -9,12 +9,12 @@ except ImportError:
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

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
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):
@ -36,7 +36,7 @@ class BTest(BaseController):
return {'key': key, 'opt': opt}
@ApiController("/rtest/{key}", secure=False)
@APIRouter("/rtest/{key}", secure=False)
class RTest(RESTController):
RESOURCE_ID = 'skey/ekey'
@ -70,7 +70,7 @@ class RTest(RESTController):
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):

View File

@ -3,15 +3,15 @@
import unittest
from ..api.doc import SchemaType
from ..controllers import ApiController, APIVersion, 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'

View File

@ -4,7 +4,7 @@ import time
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
@ -13,7 +13,7 @@ from . import ControllerTestCase # pylint: disable=no-name-in-module
# pylint: disable=W0613
@Controller('foo', secure=False)
@Router('foo', secure=False)
class FooResource(RESTController):
@Endpoint()

View File

@ -22,8 +22,8 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin):
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()

View File

@ -6,7 +6,7 @@ from unittest import mock
from orchestrator import HostSpec, InventoryHost
from .. import mgr
from ..controllers import APIVersion
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

View File

@ -7,14 +7,14 @@ try:
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

View File

@ -10,15 +10,15 @@ try:
except ImportError:
from unittest.mock import patch
from ..controllers import ApiController, APIVersion, 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 = []
@ -43,20 +43,20 @@ class FooResource(RESTController):
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}

View File

@ -2,11 +2,13 @@
import unittest
from ..controllers import ApiController, APIVersion, 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"