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:
parent
ed8e8b7600
commit
b36766ebd8
src/pybind/mgr/dashboard
CMakeLists.txt
controllers
__init__.py_api_router.py_auth.py_base_controller.py_docs.py_endpoint.py_helpers.py_permissions.py_rest_controller.py_router.py_task.py_ui_router.pyauth.pycephfs.pycluster_configuration.pycrush_rule.pydocs.pyerasure_code_profile.pyfeedback.pyfrontend_logging.pygrafana.pyhealth.pyhome.pyhost.pyiscsi.pylogs.pymgr_modules.pymonitor.pynfsganesha.pyorchestrator.pyosd.pyperf_counters.pypool.pyprometheus.pyrbd.pyrbd_mirroring.pyrgw.pyrole.pysaml2.pyservice.pysettings.pysummary.pytask.pytelemetry.pyuser.py
module.pyopenapi.yamlplugins
tests
@ -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
13
src/pybind/mgr/dashboard/controllers/_api_router.py
Normal file
13
src/pybind/mgr/dashboard/controllers/_api_router.py
Normal 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
|
18
src/pybind/mgr/dashboard/controllers/_auth.py
Normal file
18
src/pybind/mgr/dashboard/controllers/_auth.py
Normal 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'
|
314
src/pybind/mgr/dashboard/controllers/_base_controller.py
Normal file
314
src/pybind/mgr/dashboard/controllers/_base_controller.py
Normal 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
|
128
src/pybind/mgr/dashboard/controllers/_docs.py
Normal file
128
src/pybind/mgr/dashboard/controllers/_docs.py
Normal 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
|
82
src/pybind/mgr/dashboard/controllers/_endpoint.py
Normal file
82
src/pybind/mgr/dashboard/controllers/_endpoint.py
Normal 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)
|
127
src/pybind/mgr/dashboard/controllers/_helpers.py
Normal file
127
src/pybind/mgr/dashboard/controllers/_helpers.py
Normal 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
|
60
src/pybind/mgr/dashboard/controllers/_permissions.py
Normal file
60
src/pybind/mgr/dashboard/controllers/_permissions.py
Normal 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
|
249
src/pybind/mgr/dashboard/controllers/_rest_controller.py
Normal file
249
src/pybind/mgr/dashboard/controllers/_rest_controller.py
Normal 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
|
69
src/pybind/mgr/dashboard/controllers/_router.py
Normal file
69
src/pybind/mgr/dashboard/controllers/_router.py
Normal 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
|
79
src/pybind/mgr/dashboard/controllers/_task.py
Normal file
79
src/pybind/mgr/dashboard/controllers/_task.py
Normal 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
|
13
src/pybind/mgr/dashboard/controllers/_ui_router.py
Normal file
13
src/pybind/mgr/dashboard/controllers/_ui_router.py
Normal 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
|
@ -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.
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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']
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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():
|
||||
|
@ -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})
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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")
|
||||
|
@ -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={
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user