]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: replace string version with class 43285/head
authorErnesto Puerta <epuertat@redhat.com>
Fri, 24 Sep 2021 15:46:42 +0000 (17:46 +0200)
committerErnesto Puerta <epuertat@redhat.com>
Sat, 25 Sep 2021 11:47:06 +0000 (13:47 +0200)
* 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 <epuertat@redhat.com>
13 files changed:
src/pybind/mgr/dashboard/__init__.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/_version.py [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/crush_rule.py
src/pybind/mgr/dashboard/controllers/docs.py
src/pybind/mgr/dashboard/controllers/feedback.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tests/__init__.py
src/pybind/mgr/dashboard/tests/test_docs.py
src/pybind/mgr/dashboard/tests/test_host.py
src/pybind/mgr/dashboard/tests/test_tools.py
src/pybind/mgr/dashboard/tests/test_versioning.py

index 46273971ea0aabfc8af9f8a68750529e6d51595b..653474c6304dba59240d46618b6baffa677d03c4 100644 (file)
@@ -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__)),
index d83a034482ee6743292747b14669a6ba76f32661..25fd383fc1574472d18236c3a84c6ca354ab9286 100755 (executable)
@@ -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 (file)
index 0000000..3e7331c
--- /dev/null
@@ -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
index 57f05fd39e0a0b7bcdd3363382d65388fcfa49f9..f57f5d4e3ee1a61189561772d5fb31b632f16fd8 100644 (file)
@@ -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:
index 58e522f8705cdca5ee471acb5e34293e0cf58bf9..87326aaa0a2581d10ea65936cec72cbcbe25dfd2 100644 (file)
@@ -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 = []
 
index 99c14086476b13e1d2ab1a28b8a7762dac253c8e..1ae8c15c6b06b1aaed48e845b5493e59fa4978b3 100644 (file)
@@ -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.
index fddeb2e5d7f0245c274ba852de2065de973cd12b..dc1aef3aebc5e987b5c6d4d8569518bce64e09c3 100644 (file)
@@ -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):
index e30cb5dfac08d6f09d2490c718ae06b0a9042b3c..ff2dea0e99a38b5ee86156305898a27c3f35fc5f 100644 (file)
@@ -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
index 3ad0ef71adb05f9f20ce47125ee1e127b12370ca..198e5502f488a83b5b8c352de3a3e86b2dfd831f 100644 (file)
@@ -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):
index a07a7ef0013232ecb85d42b86104c3fc5ad90760..a49305fd29edaedeba58af48797df324a6bdcdf6 100644 (file)
@@ -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):
index 6d719a0fc92218052c64f36e9a1927ef5da95932..56a117f048d0306daa40e776014f15210e974ad8 100644 (file)
@@ -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')
index 9a989d1f1db2b1f06134b1d389b79a270bf6f751..fec42d66783355d15266ebb3c6ac7576c8ba4455 100644 (file)
@@ -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):
index 6518df41b0c1df6f7621ce3956dd3b2c050fa409..b6d6d0c2ef502e1ae0cc029af790825f20e8f84c 100644 (file)
@@ -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)