From 357f79ff2e2187403d496dcbb5af5b36169a3b1e Mon Sep 17 00:00:00 2001 From: Ernesto Puerta Date: Fri, 24 Sep 2021 17:46:42 +0200 Subject: [PATCH] mgr/dashboard: replace string version with class * APIVersion: * Moved to a separate file * Added doctests * Added sentinel values: * DEFAULT = 1.0 * EXPERIMENTAL = 0.1 * NONE = 0.0 * Added to_mime_type() helper method * Controllers.__init__: * Added type hints * Replaced string versions with APIVersions * Feedback controller: * Replaced with EXPERIMENTAL (probably it should be NONE) Fixes: https://tracker.ceph.com/issues/52480 Signed-off-by: Ernesto Puerta --- src/pybind/mgr/dashboard/__init__.py | 27 ------- .../mgr/dashboard/controllers/__init__.py | 64 +++++++-------- .../mgr/dashboard/controllers/_version.py | 75 +++++++++++++++++ .../mgr/dashboard/controllers/crush_rule.py | 6 +- src/pybind/mgr/dashboard/controllers/docs.py | 32 ++++---- .../mgr/dashboard/controllers/feedback.py | 6 +- src/pybind/mgr/dashboard/controllers/host.py | 10 +-- src/pybind/mgr/dashboard/openapi.yaml | 81 ------------------- src/pybind/mgr/dashboard/tests/__init__.py | 26 +++--- src/pybind/mgr/dashboard/tests/test_docs.py | 13 +-- src/pybind/mgr/dashboard/tests/test_host.py | 24 +++--- src/pybind/mgr/dashboard/tests/test_tools.py | 16 ++-- .../mgr/dashboard/tests/test_versioning.py | 39 ++++----- 13 files changed, 196 insertions(+), 223 deletions(-) create mode 100644 src/pybind/mgr/dashboard/controllers/_version.py diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py index 46273971ea0aa..653474c6304db 100644 --- a/src/pybind/mgr/dashboard/__init__.py +++ b/src/pybind/mgr/dashboard/__init__.py @@ -5,36 +5,9 @@ ceph dashboard module """ import os -import re -from typing import NamedTuple import cherrypy -DEFAULT_API_VERSION = '1.0' - - -class APIVersion(NamedTuple): - major: int = 1 - minor: int = 0 - - @classmethod - def from_string(cls, version_string): - result = re.match(r'application/vnd\.ceph\.api\.v(\d+)\.(\d+)\+json', version_string) - if result: - return cls(*(int(s) for s in result.groups())) - return None - - def __str__(self): - return f'{self.major}.{self.minor}' - - def supports(self, client_version): - return self.major == client_version.major and client_version.minor <= self.minor - - @staticmethod - def to_tuple(version: str): - return APIVersion(int(version.split('.')[0]), int(version.split('.')[1])) - - if 'COVERAGE_ENABLED' in os.environ: import coverage # pylint: disable=import-error __cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)), diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index d83a034482ee6..25fd383fc1574 100755 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -18,13 +18,13 @@ import cherrypy # pylint: disable=import-error from ceph_argparse import ArgumentFormat -from .. import DEFAULT_API_VERSION, APIVersion from ..api.doc import SchemaInput, SchemaType from ..exceptions import DashboardException, PermissionNotValid, ScopeNotValid from ..plugins import PLUGIN_MANAGER from ..security import Permission, Scope from ..services.auth import AuthManager, JwtManager from ..tools import TaskManager, get_request_body_params, getargspec +from ._version import APIVersion try: from typing import Any, Dict, List, Optional, Tuple, Union @@ -219,7 +219,8 @@ class UiApiController(Controller): 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=DEFAULT_API_VERSION): + json_response=True, proxy=False, xml=False, + version: Optional[APIVersion] = APIVersion.DEFAULT): if method is None: method = 'GET' elif not isinstance(method, str) or \ @@ -692,7 +693,7 @@ class BaseController(object): @staticmethod def _request_wrapper(func, method, json_response, xml, # pylint: disable=unused-argument - version): + version: Optional[APIVersion]): @wraps(func) def inner(*args, **kwargs): client_version = None @@ -705,23 +706,21 @@ class BaseController(object): kwargs.update(params) if version is not None: - server_version = APIVersion.to_tuple(version) - accept_header = cherrypy.request.headers.get('Accept') - req_match = APIVersion.from_string(accept_header) - if accept_header and req_match: - client_version = req_match - else: - raise cherrypy.HTTPError(415, "Unable to find version in request header") + 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 server_version.supports(client_version): + if version.supports(client_version): ret = func(*args, **kwargs) else: - raise cherrypy.HTTPError(415, - "Incorrect version: " - "{} requested but {} is expected" - "".format( - f'{client_version.major}.{client_version.minor}', - version)) + 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): @@ -729,14 +728,14 @@ class BaseController(object): if xml: if version: cherrypy.response.headers['Content-Type'] = \ - 'application/vnd.ceph.api.v{}+xml'.format(version) + version.to_mime_type(subtype='xml') 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) + version.to_mime_type() else: cherrypy.response.headers['Content-Type'] = 'application/json' ret = json.dumps(ret).encode('utf8') @@ -813,14 +812,14 @@ class RESTController(BaseController): } _method_mapping = collections.OrderedDict([ - ('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': DEFAULT_API_VERSION}), # noqa E501 #pylint: disable=line-too-long - ('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': DEFAULT_API_VERSION}), # noqa E501 #pylint: disable=line-too-long - ('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_API_VERSION}), # noqa E501 #pylint: disable=line-too-long - ('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': DEFAULT_API_VERSION}), # noqa E501 #pylint: disable=line-too-long - ('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': DEFAULT_API_VERSION}), - ('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': DEFAULT_API_VERSION}), # noqa E501 #pylint: disable=line-too-long - ('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': DEFAULT_API_VERSION}), - ('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_API_VERSION}) # noqa E501 #pylint: disable=line-too-long + ('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 @@ -850,7 +849,7 @@ class RESTController(BaseController): 'method': None, 'query_params': None, 'path': '', - 'version': DEFAULT_API_VERSION, + 'version': APIVersion.DEFAULT, 'sec_permissions': hasattr(func, '_security_permissions'), 'permission': None, } @@ -887,7 +886,7 @@ class RESTController(BaseController): 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) + 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)) @@ -954,7 +953,7 @@ class RESTController(BaseController): @staticmethod def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802 - version=DEFAULT_API_VERSION): + version: Optional[APIVersion] = APIVersion.DEFAULT): if not method: method = 'GET' @@ -973,7 +972,8 @@ class RESTController(BaseController): return _wrapper @staticmethod - def MethodMap(resource=False, status=None, version=DEFAULT_API_VERSION): # noqa: N802 + def MethodMap(resource=False, status=None, + version: Optional[APIVersion] = APIVersion.DEFAULT): # noqa: N802 if status is None: status = 200 @@ -989,7 +989,7 @@ class RESTController(BaseController): @staticmethod def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802 - version=DEFAULT_API_VERSION): + version: Optional[APIVersion] = APIVersion.DEFAULT): if not method: method = 'GET' diff --git a/src/pybind/mgr/dashboard/controllers/_version.py b/src/pybind/mgr/dashboard/controllers/_version.py new file mode 100644 index 0000000000000..3e7331c88ac78 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/_version.py @@ -0,0 +1,75 @@ +import re +from typing import NamedTuple + + +class APIVersion(NamedTuple): + """ + >>> APIVersion(1,0) + APIVersion(major=1, minor=0) + + >>> APIVersion._make([1,0]) + APIVersion(major=1, minor=0) + + >>> f'{APIVersion(1, 0)!r}' + 'APIVersion(major=1, minor=0)' + """ + major: int + minor: int + + DEFAULT = ... # type: ignore + EXPERIMENTAL = ... # type: ignore + NONE = ... # type: ignore + + __MIME_TYPE_REGEX = re.compile( # type: ignore + r'^application/vnd\.ceph\.api\.v(\d+\.\d+)\+json$') + + @classmethod + def from_string(cls, version_string: str) -> 'APIVersion': + """ + >>> APIVersion.from_string("1.0") + APIVersion(major=1, minor=0) + """ + return cls._make(int(s) for s in version_string.split('.')) + + @classmethod + def from_mime_type(cls, mime_type: str) -> 'APIVersion': + """ + >>> APIVersion.from_mime_type('application/vnd.ceph.api.v1.0+json') + APIVersion(major=1, minor=0) + + """ + return cls.from_string(cls.__MIME_TYPE_REGEX.match(mime_type).group(1)) + + def __str__(self): + """ + >>> f'{APIVersion(1, 0)}' + '1.0' + """ + return f'{self.major}.{self.minor}' + + def to_mime_type(self, subtype='json'): + """ + >>> APIVersion(1, 0).to_mime_type(subtype='xml') + 'application/vnd.ceph.api.v1.0+xml' + """ + return f'application/vnd.ceph.api.v{self!s}+{subtype}' + + def supports(self, client_version: "APIVersion") -> bool: + """ + >>> APIVersion(1, 1).supports(APIVersion(1, 0)) + True + + >>> APIVersion(1, 0).supports(APIVersion(1, 1)) + False + + >>> APIVersion(2, 0).supports(APIVersion(1, 1)) + False + """ + return (self.major == client_version.major + and client_version.minor <= self.minor) + + +# Sentinel Values +APIVersion.DEFAULT = APIVersion(1, 0) # type: ignore +APIVersion.EXPERIMENTAL = APIVersion(0, 1) # type: ignore +APIVersion.NONE = APIVersion(0, 0) # type: ignore diff --git a/src/pybind/mgr/dashboard/controllers/crush_rule.py b/src/pybind/mgr/dashboard/controllers/crush_rule.py index 57f05fd39e0a0..f57f5d4e3ee1a 100644 --- a/src/pybind/mgr/dashboard/controllers/crush_rule.py +++ b/src/pybind/mgr/dashboard/controllers/crush_rule.py @@ -5,7 +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, \ +from . import ApiController, APIVersion, ControllerDoc, Endpoint, EndpointDoc, \ ReadPermission, RESTController, UiApiController LIST_SCHEMA = { @@ -21,11 +21,11 @@ LIST_SCHEMA = { class CrushRule(RESTController): @EndpointDoc("List Crush Rule Configuration", responses={200: LIST_SCHEMA}) - @RESTController.MethodMap(version='2.0') + @RESTController.MethodMap(version=APIVersion(2, 0)) def list(self): return mgr.get('osd_map_crush')['rules'] - @RESTController.MethodMap(version='2.0') + @RESTController.MethodMap(version=APIVersion(2, 0)) def get(self, name): rules = mgr.get('osd_map_crush')['rules'] for r in rules: diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py index 58e522f8705cd..87326aaa0a258 100644 --- a/src/pybind/mgr/dashboard/controllers/docs.py +++ b/src/pybind/mgr/dashboard/controllers/docs.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- import logging -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional, Union import cherrypy -from .. import DEFAULT_API_VERSION, mgr +from .. import mgr from ..api.doc import Schema, SchemaInput, SchemaType -from . import ENDPOINT_MAP, BaseController, Controller, Endpoint +from . import ENDPOINT_MAP, APIVersion, BaseController, Controller, Endpoint NO_DESCRIPTION_AVAILABLE = "*No description available*" @@ -183,7 +183,8 @@ class Docs(BaseController): return schema.as_dict() @classmethod - def _gen_responses(cls, method, resp_object=None, version=None): + def _gen_responses(cls, method, resp_object=None, + version: Optional[APIVersion] = None): resp: Dict[str, Dict[str, Union[str, Any]]] = { '400': { "description": "Operation exception. Please check the " @@ -203,37 +204,38 @@ class Docs(BaseController): } if not version: - version = DEFAULT_API_VERSION + version = APIVersion.DEFAULT if method.lower() == 'get': resp['200'] = {'description': "OK", - 'content': {'application/vnd.ceph.api.v{}+json'.format(version): + 'content': {version.to_mime_type(): {'type': 'object'}}} if method.lower() == 'post': resp['201'] = {'description': "Resource created.", - 'content': {'application/vnd.ceph.api.v{}+json'.format(version): + 'content': {version.to_mime_type(): {'type': 'object'}}} if method.lower() == 'put': resp['200'] = {'description': "Resource updated.", - 'content': {'application/vnd.ceph.api.v{}+json'.format(version): + 'content': {version.to_mime_type(): {'type': 'object'}}} if method.lower() == 'delete': resp['204'] = {'description': "Resource deleted.", - 'content': {'application/vnd.ceph.api.v{}+json'.format(version): + 'content': {version.to_mime_type(): {'type': 'object'}}} if method.lower() in ['post', 'put', 'delete']: resp['202'] = {'description': "Operation is still executing." " Please check the task queue.", - 'content': {'application/vnd.ceph.api.v{}+json'.format(version): + 'content': {version.to_mime_type(): {'type': 'object'}}} if resp_object: for status_code, response_body in resp_object.items(): if status_code in resp: - resp[status_code].update({ - 'content': { - 'application/vnd.ceph.api.v{}+json'.format(version): { - 'schema': cls._gen_schema_for_content(response_body)}}}) + resp[status_code].update( + {'content': + {version.to_mime_type(): + {'schema': cls._gen_schema_for_content(response_body)} + }}) return resp @@ -285,7 +287,7 @@ class Docs(BaseController): func = endpoint.func summary = '' - version = '' + version = None resp = {} p_info = [] diff --git a/src/pybind/mgr/dashboard/controllers/feedback.py b/src/pybind/mgr/dashboard/controllers/feedback.py index 99c14086476b1..1ae8c15c6b06b 100644 --- a/src/pybind/mgr/dashboard/controllers/feedback.py +++ b/src/pybind/mgr/dashboard/controllers/feedback.py @@ -5,10 +5,10 @@ from ..model.feedback import Feedback from ..rest_client import RequestException from ..security import Scope from ..services import feedback -from . import ApiController, ControllerDoc, RESTController +from . import ControllerDoc, RESTController, UiApiController -@ApiController('/feedback', Scope.CONFIG_OPT) +@UiApiController('/feedback', Scope.CONFIG_OPT) @ControllerDoc("Feedback API", "Report") class FeedbackController(RESTController): issueAPIkey = None @@ -17,7 +17,6 @@ class FeedbackController(RESTController): super(FeedbackController, self).__init__() self.tracker_client = feedback.CephTrackerClient() - @RESTController.MethodMap(version='0.1') def create(self, project, tracker, subject, description): """ Create an issue. @@ -44,7 +43,6 @@ class FeedbackController(RESTController): http_status_code=401, component='feedback') - @RESTController.MethodMap(version='0.1') def get(self, issue_number): """ Fetch issue details. diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index fddeb2e5d7f02..dc1aef3aebc5e 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -16,9 +16,9 @@ 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, BaseController, ControllerDoc, Endpoint, \ - EndpointDoc, ReadPermission, RESTController, Task, UiApiController, \ - UpdatePermission, allow_empty_body +from . import ApiController, APIVersion, BaseController, ControllerDoc, \ + Endpoint, EndpointDoc, ReadPermission, RESTController, Task, \ + UiApiController, UpdatePermission, allow_empty_body from .orchestrator import raise_if_no_orchestrator LIST_HOST_SCHEMA = { @@ -293,7 +293,7 @@ class Host(RESTController): 'status': (str, 'Host Status') }, responses={200: None, 204: None}) - @RESTController.MethodMap(version='0.1') + @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL) def create(self, hostname: str, addr: Optional[str] = None, labels: Optional[List[str]] = None, @@ -406,7 +406,7 @@ class Host(RESTController): 'force': (bool, 'Force Enter Maintenance') }, responses={200: None, 204: None}) - @RESTController.MethodMap(version='0.1') + @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL) def set(self, hostname: str, update_labels: bool = False, labels: List[str] = None, maintenance: bool = False, force: bool = False): diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index e30cb5dfac08d..ff2dea0e99a38 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2644,85 +2644,6 @@ 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.v0.1+json: - type: object - description: Resource created. - '202': - content: - application/vnd.ceph.api.v0.1+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.v0.1+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: [] @@ -10440,8 +10361,6 @@ 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 diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py index 3ad0ef71adb05..198e5502f488a 100644 --- a/src/pybind/mgr/dashboard/tests/__init__.py +++ b/src/pybind/mgr/dashboard/tests/__init__.py @@ -13,8 +13,8 @@ from cherrypy.test import helper from mgr_module import HandleCommandResult from pyfakefs import fake_filesystem -from .. import DEFAULT_API_VERSION, mgr -from ..controllers import generate_controller_routes, json_error_page +from .. import mgr +from ..controllers import APIVersion, generate_controller_routes, json_error_page from ..module import Module from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa from ..services.auth import AuthManagerTool @@ -152,18 +152,18 @@ class ControllerTestCase(helper.CPWebCase): if cls._request_logging: cherrypy.config.update({'tools.request_logging.on': False}) - def _request(self, url, method, data=None, headers=None, version=DEFAULT_API_VERSION): + def _request(self, url, method, data=None, headers=None, version=APIVersion.DEFAULT): if not data: b = None if version: - h = [('Accept', 'application/vnd.ceph.api.v{}+json'.format(version)), + h = [('Accept', version.to_mime_type()), ('Content-Length', '0')] else: h = None else: b = json.dumps(data) if version is not None: - h = [('Accept', 'application/vnd.ceph.api.v{}+json'.format(version)), + h = [('Accept', version.to_mime_type()), ('Content-Type', 'application/json'), ('Content-Length', str(len(b)))] @@ -175,19 +175,19 @@ class ControllerTestCase(helper.CPWebCase): h = headers self.getPage(url, method=method, body=b, headers=h) - def _get(self, url, headers=None, version=DEFAULT_API_VERSION): + def _get(self, url, headers=None, version=APIVersion.DEFAULT): self._request(url, 'GET', headers=headers, version=version) - def _post(self, url, data=None, version=DEFAULT_API_VERSION): + def _post(self, url, data=None, version=APIVersion.DEFAULT): self._request(url, 'POST', data, version=version) - def _delete(self, url, data=None, version=DEFAULT_API_VERSION): + def _delete(self, url, data=None, version=APIVersion.DEFAULT): self._request(url, 'DELETE', data, version=version) - def _put(self, url, data=None, version=DEFAULT_API_VERSION): + def _put(self, url, data=None, version=APIVersion.DEFAULT): self._request(url, 'PUT', data, version=version) - def _task_request(self, method, url, data, timeout, version=DEFAULT_API_VERSION): + def _task_request(self, method, url, data, timeout, version=APIVersion.DEFAULT): self._request(url, method, data, version=version) if self.status != '202 Accepted': logger.info("task finished immediately") @@ -229,13 +229,13 @@ class ControllerTestCase(helper.CPWebCase): elif method == 'DELETE': self.status = '204 No Content' - def _task_post(self, url, data=None, timeout=60, version=DEFAULT_API_VERSION): + def _task_post(self, url, data=None, timeout=60, version=APIVersion.DEFAULT): self._task_request('POST', url, data, timeout, version=version) - def _task_delete(self, url, timeout=60, version=DEFAULT_API_VERSION): + def _task_delete(self, url, timeout=60, version=APIVersion.DEFAULT): self._task_request('DELETE', url, None, timeout, version=version) - def _task_put(self, url, data=None, timeout=60, version=DEFAULT_API_VERSION): + def _task_put(self, url, data=None, timeout=60, version=APIVersion.DEFAULT): self._task_request('PUT', url, data, timeout, version=version) def json_body(self): diff --git a/src/pybind/mgr/dashboard/tests/test_docs.py b/src/pybind/mgr/dashboard/tests/test_docs.py index a07a7ef001323..a49305fd29eda 100644 --- a/src/pybind/mgr/dashboard/tests/test_docs.py +++ b/src/pybind/mgr/dashboard/tests/test_docs.py @@ -3,7 +3,8 @@ import unittest from ..api.doc import SchemaType -from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController +from ..controllers import ApiController, APIVersion, ControllerDoc, Endpoint, \ + EndpointDoc, RESTController from ..controllers.docs import Docs from . import ControllerTestCase # pylint: disable=no-name-in-module @@ -30,11 +31,11 @@ class DecoratedController(RESTController): }, ) @Endpoint(json_response=False) - @RESTController.Resource('PUT', version='0.1') + @RESTController.Resource('PUT', version=APIVersion(0, 1)) def decorated_func(self, parameter): pass - @RESTController.MethodMap(version='0.1') + @RESTController.MethodMap(version=APIVersion(0, 1)) def list(self): pass @@ -87,7 +88,7 @@ class DocsTest(ControllerTestCase): expected_response_content = { '200': { - 'application/vnd.ceph.api.v0.1+json': { + APIVersion(0, 1).to_mime_type(): { 'schema': {'type': 'array', 'items': {'type': 'object', 'properties': { 'my_prop': { @@ -95,7 +96,7 @@ class DocsTest(ControllerTestCase): 'description': '200 property desc.'}}}, 'required': ['my_prop']}}}, '202': { - 'application/vnd.ceph.api.v0.1+json': { + APIVersion(0, 1).to_mime_type(): { 'schema': {'type': 'object', 'properties': {'my_prop': { 'type': 'string', @@ -111,7 +112,7 @@ class DocsTest(ControllerTestCase): def test_gen_method_paths(self): outcome = Docs().gen_paths(False)['/api/doctest/']['get'] - self.assertEqual({'application/vnd.ceph.api.v0.1+json': {'type': 'object'}}, + self.assertEqual({APIVersion(0, 1).to_mime_type(): {'type': 'object'}}, outcome['responses']['200']['content']) def test_gen_paths_all(self): diff --git a/src/pybind/mgr/dashboard/tests/test_host.py b/src/pybind/mgr/dashboard/tests/test_host.py index 6d719a0fc9221..56a117f048d03 100644 --- a/src/pybind/mgr/dashboard/tests/test_host.py +++ b/src/pybind/mgr/dashboard/tests/test_host.py @@ -6,6 +6,7 @@ from unittest import mock from orchestrator import HostSpec, InventoryHost from .. import mgr +from ..controllers 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 @@ -135,7 +136,7 @@ class HostControllerTest(ControllerTestCase): 'labels': 'mon', 'status': 'maintenance' } - self._post(self.URL_HOST, payload, version='0.1') + self._post(self.URL_HOST, payload, version=APIVersion(0, 1)) self.assertStatus(201) mock_add_host.assert_called() @@ -149,7 +150,7 @@ class HostControllerTest(ControllerTestCase): fake_client.hosts.add_label = mock.Mock() payload = {'update_labels': True, 'labels': ['bbb', 'ccc']} - self._put('{}/node0'.format(self.URL_HOST), payload, version='0.1') + self._put('{}/node0'.format(self.URL_HOST), payload, version=APIVersion(0, 1)) self.assertStatus(200) self.assertHeader('Content-Type', 'application/vnd.ceph.api.v0.1+json') @@ -157,8 +158,9 @@ class HostControllerTest(ControllerTestCase): fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc') # return 400 if type other than List[str] - self._put('{}/node0'.format(self.URL_HOST), {'update_labels': True, - 'labels': 'ddd'}, version='0.1') + self._put('{}/node0'.format(self.URL_HOST), + {'update_labels': True, 'labels': 'ddd'}, + version=APIVersion(0, 1)) self.assertStatus(400) def test_host_maintenance(self): @@ -169,25 +171,29 @@ class HostControllerTest(ControllerTestCase): ] with patch_orch(True, hosts=orch_hosts): # enter maintenance mode - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, + version=APIVersion(0, 1)) self.assertStatus(200) self.assertHeader('Content-Type', 'application/vnd.ceph.api.v0.1+json') # force enter maintenance mode self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True}, - version='0.1') + version=APIVersion(0, 1)) self.assertStatus(200) # exit maintenance mode - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, + version=APIVersion(0, 1)) self.assertStatus(200) - self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}, version='0.1') + self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}, + version=APIVersion(0, 1)) self.assertStatus(200) # maintenance without orchestrator service with patch_orch(False): - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, + version=APIVersion(0, 1)) self.assertStatus(503) @mock.patch('dashboard.controllers.host.time') diff --git a/src/pybind/mgr/dashboard/tests/test_tools.py b/src/pybind/mgr/dashboard/tests/test_tools.py index 9a989d1f1db2b..fec42d6678335 100644 --- a/src/pybind/mgr/dashboard/tests/test_tools.py +++ b/src/pybind/mgr/dashboard/tests/test_tools.py @@ -10,8 +10,8 @@ try: except ImportError: from unittest.mock import patch -from .. import DEFAULT_API_VERSION -from ..controllers import ApiController, BaseController, Controller, Proxy, RESTController +from ..controllers import ApiController, APIVersion, BaseController, \ + Controller, Proxy, RESTController 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 @@ -86,8 +86,7 @@ class RESTControllerTest(ControllerTestCase): self.assertStatus(204) self._get("/foo") self.assertStatus('200 OK') - self.assertHeader('Content-Type', - 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_API_VERSION)) + self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type()) self.assertBody('[]') def test_fill(self): @@ -98,19 +97,16 @@ class RESTControllerTest(ControllerTestCase): self._post("/foo", data) self.assertJsonBody(data) self.assertStatus(201) - self.assertHeader('Content-Type', - 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_API_VERSION)) + self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type()) self._get("/foo") self.assertStatus('200 OK') - self.assertHeader('Content-Type', - 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_API_VERSION)) + self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type()) self.assertJsonBody([data] * 5) self._put('/foo/0', {'newdata': 'newdata'}) self.assertStatus('200 OK') - self.assertHeader('Content-Type', - 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_API_VERSION)) + self.assertHeader('Content-Type', APIVersion.DEFAULT.to_mime_type()) self.assertJsonBody({'newdata': 'newdata', 'key': '0'}) def test_not_implemented(self): diff --git a/src/pybind/mgr/dashboard/tests/test_versioning.py b/src/pybind/mgr/dashboard/tests/test_versioning.py index 6518df41b0c1d..b6d6d0c2ef502 100644 --- a/src/pybind/mgr/dashboard/tests/test_versioning.py +++ b/src/pybind/mgr/dashboard/tests/test_versioning.py @@ -2,7 +2,7 @@ import unittest -from ..controllers import ApiController, RESTController +from ..controllers import ApiController, APIVersion, RESTController from . import ControllerTestCase # pylint: disable=no-name-in-module @@ -10,22 +10,22 @@ from . import ControllerTestCase # pylint: disable=no-name-in-module class VTest(RESTController): RESOURCE_ID = "vid" - @RESTController.MethodMap(version="0.1") + @RESTController.MethodMap(version=APIVersion(0, 1)) def list(self): return {'version': ""} def get(self): return {'version': ""} - @RESTController.Collection('GET', version="1.0") + @RESTController.Collection('GET', version=APIVersion(1, 0)) def vmethod(self): return {'version': '1.0'} - @RESTController.Collection('GET', version="1.1") + @RESTController.Collection('GET', version=APIVersion(1, 1)) def vmethodv1_1(self): return {'version': '1.1'} - @RESTController.Collection('GET', version="2.0") + @RESTController.Collection('GET', version=APIVersion(2, 0)) def vmethodv2(self): return {'version': '2.0'} @@ -37,37 +37,40 @@ class RESTVersioningTest(ControllerTestCase, unittest.TestCase): def test_list(self): for (version, expected_status) in [ - ("0.1", 200), - ("2.0", 415) + ((0, 1), 200), + ((2, 0), 415) ]: with self.subTest(version=version): - self._get('/test/api/vtest', version=version) + self._get('/test/api/vtest', version=APIVersion._make(version)) self.assertStatus(expected_status) def test_v1(self): for (version, expected_status) in [ - ("1.0", 200), - ("2.0", 415) + ((1, 0), 200), + ((2, 0), 415) ]: with self.subTest(version=version): - self._get('/test/api/vtest/vmethod', version=version) + self._get('/test/api/vtest/vmethod', + version=APIVersion._make(version)) self.assertStatus(expected_status) def test_v2(self): for (version, expected_status) in [ - ("2.0", 200), - ("1.0", 415) + ((2, 0), 200), + ((1, 0), 415) ]: with self.subTest(version=version): - self._get('/test/api/vtest/vmethodv2', version=version) + self._get('/test/api/vtest/vmethodv2', + version=APIVersion._make(version)) self.assertStatus(expected_status) def test_backward_compatibility(self): for (version, expected_status) in [ - ("1.1", 200), - ("1.0", 200), - ("2.0", 415) + ((1, 1), 200), + ((1, 0), 200), + ((2, 0), 415) ]: with self.subTest(version=version): - self._get('/test/api/vtest/vmethodv1_1', version=version) + self._get('/test/api/vtest/vmethodv1_1', + version=APIVersion._make(version)) self.assertStatus(expected_status) -- 2.39.5