Merge pull request #35769 from Codom/master

mgr/dashboard: Added Versioning to the REST API

Reviewed-by: Alfonso Martínez <almartin@redhat.com>
Reviewed-by: Avan Thakkar <athakkar@redhat.com>
Reviewed-by: Ernesto Puerta <epuertat@redhat.com>
Reviewed-by: Laura Paduano <lpaduano@suse.com>
Reviewed-by: Tatjana Dehler <tdehler@suse.com>
Reviewed-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
Lenz Grimmer 2020-10-30 09:50:05 +01:00 committed by GitHub
commit 587a84049b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1027 additions and 122 deletions

View File

@ -0,0 +1 @@
DEFAULT_VERSION = '1.0'

View File

@ -4,6 +4,7 @@ from __future__ import absolute_import
import json import json
import logging import logging
import re
import time import time
from collections import namedtuple from collections import namedtuple
@ -12,6 +13,8 @@ from tasks.mgr.mgr_test_case import MgrTestCase
from teuthology.exceptions import \ from teuthology.exceptions import \
CommandFailedError # pylint: disable=import-error CommandFailedError # pylint: disable=import-error
from . import DEFAULT_VERSION
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -268,16 +271,19 @@ class DashboardTestCase(MgrTestCase):
def tearDownClass(cls): def tearDownClass(cls):
super(DashboardTestCase, cls).tearDownClass() super(DashboardTestCase, cls).tearDownClass()
# pylint: disable=inconsistent-return-statements # pylint: disable=inconsistent-return-statements, too-many-arguments
@classmethod @classmethod
def _request(cls, url, method, data=None, params=None): def _request(cls, url, method, data=None, params=None, version=DEFAULT_VERSION):
cls.update_base_uri()
url = "{}{}".format(cls._base_uri, url) url = "{}{}".format(cls._base_uri, url)
log.info("Request %s to %s", method, url) log.info("Request %s to %s", method, url)
headers = {} headers = {}
if cls._token: if cls._token:
headers['Authorization'] = "Bearer {}".format(cls._token) headers['Authorization'] = "Bearer {}".format(cls._token)
if version is None:
headers['Accept'] = 'application/json'
else:
headers['Accept'] = 'application/vnd.ceph.api.v{}+json'.format(version)
if method == 'GET': if method == 'GET':
cls._resp = cls._session.get(url, params=params, verify=False, cls._resp = cls._session.get(url, params=params, verify=False,
headers=headers) headers=headers)
@ -297,7 +303,8 @@ class DashboardTestCase(MgrTestCase):
# Output response for easier debugging. # Output response for easier debugging.
log.error("Request response: %s", cls._resp.text) log.error("Request response: %s", cls._resp.text)
content_type = cls._resp.headers['content-type'] content_type = cls._resp.headers['content-type']
if content_type == 'application/json' and cls._resp.text and cls._resp.text != "": if re.match(r'^application/.*json',
content_type) and cls._resp.text and cls._resp.text != "":
return cls._resp.json() return cls._resp.json()
return cls._resp.text return cls._resp.text
except ValueError as ex: except ValueError as ex:
@ -305,15 +312,15 @@ class DashboardTestCase(MgrTestCase):
raise ex raise ex
@classmethod @classmethod
def _get(cls, url, params=None): def _get(cls, url, params=None, version=DEFAULT_VERSION):
return cls._request(url, 'GET', params=params) return cls._request(url, 'GET', params=params, version=version)
@classmethod @classmethod
def _view_cache_get(cls, url, retries=5): def _view_cache_get(cls, url, retries=5):
retry = True retry = True
while retry and retries > 0: while retry and retries > 0:
retry = False retry = False
res = cls._get(url) res = cls._get(url, version=DEFAULT_VERSION)
if isinstance(res, dict): if isinstance(res, dict):
res = [res] res = [res]
for view in res: for view in res:
@ -327,16 +334,16 @@ class DashboardTestCase(MgrTestCase):
return res return res
@classmethod @classmethod
def _post(cls, url, data=None, params=None): def _post(cls, url, data=None, params=None, version=DEFAULT_VERSION):
cls._request(url, 'POST', data, params) cls._request(url, 'POST', data, params, version=version)
@classmethod @classmethod
def _delete(cls, url, data=None, params=None): def _delete(cls, url, data=None, params=None, version=DEFAULT_VERSION):
cls._request(url, 'DELETE', data, params) cls._request(url, 'DELETE', data, params, version=version)
@classmethod @classmethod
def _put(cls, url, data=None, params=None): def _put(cls, url, data=None, params=None, version=DEFAULT_VERSION):
cls._request(url, 'PUT', data, params) cls._request(url, 'PUT', data, params, version=version)
@classmethod @classmethod
def _assertEq(cls, v1, v2): def _assertEq(cls, v1, v2):
@ -355,8 +362,8 @@ class DashboardTestCase(MgrTestCase):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
@classmethod @classmethod
def _task_request(cls, method, url, data, timeout): def _task_request(cls, method, url, data, timeout, version=DEFAULT_VERSION):
res = cls._request(url, method, data) res = cls._request(url, method, data, version=version)
cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404]) cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404])
if cls._resp.status_code == 403: if cls._resp.status_code == 403:
@ -378,7 +385,7 @@ class DashboardTestCase(MgrTestCase):
log.info("task (%s, %s) is still executing", task_name, log.info("task (%s, %s) is still executing", task_name,
task_metadata) task_metadata)
time.sleep(1) time.sleep(1)
_res = cls._get('/api/task?name={}'.format(task_name)) _res = cls._get('/api/task?name={}'.format(task_name), version=version)
cls._assertEq(cls._resp.status_code, 200) cls._assertEq(cls._resp.status_code, 200)
executing_tasks = [task for task in _res['executing_tasks'] if executing_tasks = [task for task in _res['executing_tasks'] if
task['metadata'] == task_metadata] task['metadata'] == task_metadata]
@ -408,16 +415,16 @@ class DashboardTestCase(MgrTestCase):
return res_task['exception'] return res_task['exception']
@classmethod @classmethod
def _task_post(cls, url, data=None, timeout=60): def _task_post(cls, url, data=None, timeout=60, version=DEFAULT_VERSION):
return cls._task_request('POST', url, data, timeout) return cls._task_request('POST', url, data, timeout, version=version)
@classmethod @classmethod
def _task_delete(cls, url, timeout=60): def _task_delete(cls, url, timeout=60, version=DEFAULT_VERSION):
return cls._task_request('DELETE', url, None, timeout) return cls._task_request('DELETE', url, None, timeout, version=version)
@classmethod @classmethod
def _task_put(cls, url, data=None, timeout=60): def _task_put(cls, url, data=None, timeout=60, version=DEFAULT_VERSION):
return cls._task_request('PUT', url, data, timeout) return cls._task_request('PUT', url, data, timeout, version=version)
@classmethod @classmethod
def cookies(cls): def cookies(cls):

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import unittest
from . import DEFAULT_VERSION
from .helper import DashboardTestCase
class VersionReqTest(DashboardTestCase, unittest.TestCase):
def test_version(self):
for (version, expected_status) in [
(DEFAULT_VERSION, 200),
(None, 415),
("99.99", 415)
]:
with self.subTest(version=version):
self._get('/api/summary', version=version)
self.assertStatus(expected_status)

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from . import DEFAULT_VERSION
from .helper import DashboardTestCase from .helper import DashboardTestCase
@ -10,7 +11,7 @@ class RequestsTest(DashboardTestCase):
self._get('/api/summary') self._get('/api/summary')
self.assertHeaders({ self.assertHeaders({
'Content-Encoding': 'gzip', 'Content-Encoding': 'gzip',
'Content-Type': 'application/json', 'Content-Type': 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION)
}) })
def test_force_no_gzip(self): def test_force_no_gzip(self):
@ -19,11 +20,12 @@ class RequestsTest(DashboardTestCase):
)) ))
self.assertNotIn('Content-Encoding', self._resp.headers) self.assertNotIn('Content-Encoding', self._resp.headers)
self.assertHeaders({ self.assertHeaders({
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}) })
def test_server(self): def test_server(self):
self._get('/api/summary') self._get('/api/summary')
self.assertHeaders({ self.assertHeaders({
'server': 'Ceph-Dashboard' 'server': 'Ceph-Dashboard',
'Content-Type': 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION)
}) })

View File

@ -9,6 +9,8 @@ import os
import cherrypy import cherrypy
DEFAULT_VERSION = '1.0'
if 'COVERAGE_ENABLED' in os.environ: if 'COVERAGE_ENABLED' in os.environ:
import coverage # pylint: disable=import-error import coverage # pylint: disable=import-error
__cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)), __cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)),

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=protected-access,too-many-branches # pylint: disable=protected-access,too-many-branches,too-many-lines
from __future__ import absolute_import from __future__ import absolute_import
import collections import collections
@ -17,6 +17,7 @@ from urllib.parse import unquote
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
import cherrypy import cherrypy
from .. import DEFAULT_VERSION
from ..api.doc import SchemaInput, SchemaType from ..api.doc import SchemaInput, SchemaType
from ..exceptions import PermissionNotValid, ScopeNotValid from ..exceptions import PermissionNotValid, ScopeNotValid
from ..plugins import PLUGIN_MANAGER from ..plugins import PLUGIN_MANAGER
@ -200,7 +201,7 @@ class UiApiController(Controller):
def Endpoint(method=None, path=None, path_params=None, query_params=None, # noqa: N802 def Endpoint(method=None, path=None, path_params=None, query_params=None, # noqa: N802
json_response=True, proxy=False, xml=False): json_response=True, proxy=False, xml=False, version=DEFAULT_VERSION):
if method is None: if method is None:
method = 'GET' method = 'GET'
@ -251,7 +252,8 @@ def Endpoint(method=None, path=None, path_params=None, query_params=None, # noq
'query_params': query_params, 'query_params': query_params,
'json_response': json_response, 'json_response': json_response,
'proxy': proxy, 'proxy': proxy,
'xml': xml 'xml': xml,
'version': version
} }
return func return func
return _wrapper return _wrapper
@ -514,7 +516,8 @@ class BaseController(object):
def function(self): def function(self):
return self.ctrl._request_wrapper(self.func, self.method, return self.ctrl._request_wrapper(self.func, self.method,
self.config['json_response'], self.config['json_response'],
self.config['xml']) self.config['xml'],
self.config['version'])
@property @property
def method(self): def method(self):
@ -663,9 +666,11 @@ class BaseController(object):
return result return result
@staticmethod @staticmethod
def _request_wrapper(func, method, json_response, xml): # pylint: disable=unused-argument def _request_wrapper(func, method, json_response, xml, # pylint: disable=unused-argument
version):
@wraps(func) @wraps(func)
def inner(*args, **kwargs): def inner(*args, **kwargs):
req_version = None
for key, value in kwargs.items(): for key, value in kwargs.items():
if isinstance(value, str): if isinstance(value, str):
kwargs[key] = unquote(value) kwargs[key] = unquote(value)
@ -674,14 +679,35 @@ class BaseController(object):
params = get_request_body_params(cherrypy.request) params = get_request_body_params(cherrypy.request)
kwargs.update(params) kwargs.update(params)
ret = func(*args, **kwargs) if version is not None:
accept_header = cherrypy.request.headers.get('Accept')
if accept_header and accept_header.startswith('application/vnd.ceph.api.v'):
req_match = re.search(r"\d\.\d", accept_header)
if req_match:
req_version = req_match[0]
else:
raise cherrypy.HTTPError(415, "Unable to find version in request header")
if req_version and req_version == version:
ret = func(*args, **kwargs)
else:
raise cherrypy.HTTPError(415,
"Incorrect version: "
"{} requested but {} is expected"
"".format(req_version, version))
else:
ret = func(*args, **kwargs)
if isinstance(ret, bytes): if isinstance(ret, bytes):
ret = ret.decode('utf-8') ret = ret.decode('utf-8')
if xml: if xml:
cherrypy.response.headers['Content-Type'] = 'application/xml' cherrypy.response.headers['Content-Type'] = 'application/xml'
return ret.encode('utf8') return ret.encode('utf8')
if json_response: if json_response:
cherrypy.response.headers['Content-Type'] = 'application/json' 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') ret = json.dumps(ret).encode('utf8')
return ret return ret
return inner return inner
@ -792,6 +818,7 @@ class RESTController(BaseController):
method = None method = None
query_params = None query_params = None
path = "" path = ""
version = DEFAULT_VERSION
sec_permissions = hasattr(func, '_security_permissions') sec_permissions = hasattr(func, '_security_permissions')
permission = None permission = None
@ -818,6 +845,7 @@ class RESTController(BaseController):
status = func._collection_method_['status'] status = func._collection_method_['status']
method = func._collection_method_['method'] method = func._collection_method_['method']
query_params = func._collection_method_['query_params'] query_params = func._collection_method_['query_params']
version = func._collection_method_['version']
if not sec_permissions: if not sec_permissions:
permission = cls._permission_map[method] permission = cls._permission_map[method]
@ -833,6 +861,7 @@ class RESTController(BaseController):
path += "/{}".format(func.__name__) path += "/{}".format(func.__name__)
status = func._resource_method_['status'] status = func._resource_method_['status']
method = func._resource_method_['method'] method = func._resource_method_['method']
version = func._resource_method_['version']
query_params = func._resource_method_['query_params'] query_params = func._resource_method_['query_params']
if not sec_permissions: if not sec_permissions:
permission = cls._permission_map[method] permission = cls._permission_map[method]
@ -857,7 +886,7 @@ class RESTController(BaseController):
func = cls._status_code_wrapper(func, status) func = cls._status_code_wrapper(func, status)
endp_func = Endpoint(method, path=path, endp_func = Endpoint(method, path=path,
query_params=query_params)(func) query_params=query_params, version=version)(func)
if permission: if permission:
_set_func_permissions(endp_func, [permission]) _set_func_permissions(endp_func, [permission])
result.append(cls.Endpoint(cls, endp_func)) result.append(cls.Endpoint(cls, endp_func))
@ -874,7 +903,8 @@ class RESTController(BaseController):
return wrapper return wrapper
@staticmethod @staticmethod
def Resource(method=None, path=None, status=None, query_params=None): # noqa: N802 def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802
version=DEFAULT_VERSION):
if not method: if not method:
method = 'GET' method = 'GET'
@ -886,13 +916,15 @@ class RESTController(BaseController):
'method': method, 'method': method,
'path': path, 'path': path,
'status': status, 'status': status,
'query_params': query_params 'query_params': query_params,
'version': version
} }
return func return func
return _wrapper return _wrapper
@staticmethod @staticmethod
def Collection(method=None, path=None, status=None, query_params=None): # noqa: N802 def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
version=DEFAULT_VERSION):
if not method: if not method:
method = 'GET' method = 'GET'
@ -904,7 +936,8 @@ class RESTController(BaseController):
'method': method, 'method': method,
'path': path, 'path': path,
'status': status, 'status': status,
'query_params': query_params 'query_params': query_params,
'version': version
} }
return func return func
return _wrapper return _wrapper

View File

@ -6,7 +6,7 @@ from typing import Any, Dict, List, Union
import cherrypy import cherrypy
from .. import mgr from .. import DEFAULT_VERSION, mgr
from ..api.doc import Schema, SchemaInput, SchemaType from ..api.doc import Schema, SchemaInput, SchemaType
from . import ENDPOINT_MAP, BaseController, Controller, Endpoint, allow_empty_body from . import ENDPOINT_MAP, BaseController, Controller, Endpoint, allow_empty_body
@ -204,23 +204,33 @@ class Docs(BaseController):
} }
} }
if method.lower() == 'get': if method.lower() == 'get':
resp['200'] = {'description': "OK"} resp['200'] = {'description': "OK",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
{'type': 'object'}}}
if method.lower() == 'post': if method.lower() == 'post':
resp['201'] = {'description': "Resource created."} resp['201'] = {'description': "Resource created.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
{'type': 'object'}}}
if method.lower() == 'put': if method.lower() == 'put':
resp['200'] = {'description': "Resource updated."} resp['200'] = {'description': "Resource updated.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
{'type': 'object'}}}
if method.lower() == 'delete': if method.lower() == 'delete':
resp['204'] = {'description': "Resource deleted."} resp['204'] = {'description': "Resource deleted.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
{'type': 'object'}}}
if method.lower() in ['post', 'put', 'delete']: if method.lower() in ['post', 'put', 'delete']:
resp['202'] = {'description': "Operation is still executing." resp['202'] = {'description': "Operation is still executing."
" Please check the task queue."} " Please check the task queue.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
{'type': 'object'}}}
if resp_object: if resp_object:
for status_code, response_body in resp_object.items(): for status_code, response_body in resp_object.items():
if status_code in resp: if status_code in resp:
resp[status_code].update({ resp[status_code].update({
'content': { 'content': {
'application/json': { 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'schema': cls._gen_schema_for_content(response_body)}}}) 'schema': cls._gen_schema_for_content(response_body)}}})
return resp return resp
@ -365,11 +375,11 @@ class Docs(BaseController):
return spec return spec
@Endpoint(path="api.json") @Endpoint(path="api.json", version=None)
def api_json(self): def api_json(self):
return self._gen_spec(False, "/") return self._gen_spec(False, "/")
@Endpoint(path="api-all.json") @Endpoint(path="api-all.json", version=None)
def api_all_json(self): def api_all_json(self):
return self._gen_spec(True, "/") return self._gen_spec(True, "/")
@ -446,12 +456,12 @@ class Docs(BaseController):
return page return page
@Endpoint(json_response=False) @Endpoint(json_response=False, version=None)
def __call__(self, all_endpoints=False): def __call__(self, all_endpoints=False):
return self._swagger_ui_page(all_endpoints) return self._swagger_ui_page(all_endpoints)
@Endpoint('POST', path="/", json_response=False, @Endpoint('POST', path="/", json_response=False,
query_params="{all_endpoints}") query_params="{all_endpoints}", version=None)
@allow_empty_body @allow_empty_body
def _with_token(self, token, all_endpoints=False): def _with_token(self, token, all_endpoints=False):
return self._swagger_ui_page(all_endpoints, token) return self._swagger_ui_page(all_endpoints, token)

View File

@ -28,6 +28,7 @@ Cypress.Commands.add('login', () => {
cy.request({ cy.request({
method: 'POST', method: 'POST',
url: 'api/auth', url: 'api/auth',
headers: { Accept: 'application/vnd.ceph.api.v1.0+json' },
body: { username: username, password: password } body: { username: username, password: password }
}).then((resp) => { }).then((resp) => {
auth = resp.body; auth = resp.body;

View File

@ -34,7 +34,19 @@ export class ApiInterceptorService implements HttpInterceptor {
) {} ) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe( const defaultVersion = '1.0';
const acceptHeader = request.headers.get('Accept');
let reqWithVersion: HttpRequest<any>;
if (acceptHeader && acceptHeader.startsWith('application/vnd.ceph.api.v')) {
reqWithVersion = request.clone();
} else {
reqWithVersion = request.clone({
setHeaders: {
Accept: `application/vnd.ceph.api.v${defaultVersion}+json`
}
});
}
return next.handle(reqWithVersion).pipe(
catchError((resp: CdHttpErrorResponse) => { catchError((resp: CdHttpErrorResponse) => {
if (resp instanceof HttpErrorResponse) { if (resp instanceof HttpErrorResponse) {
let timeoutId: number; let timeoutId: number;

View File

@ -133,6 +133,7 @@ class CherryPyConfig(object):
'text/html', 'text/plain', 'text/html', 'text/plain',
# We also want JSON and JavaScript to be compressed # We also want JSON and JavaScript to be compressed
'application/json', 'application/json',
'application/*+json',
'application/javascript', 'application/javascript',
], ],
'tools.json_in.on': True, 'tools.json_in.on': True,

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ from cherrypy.test import helper
from mgr_module import CLICommand from mgr_module import CLICommand
from pyfakefs import fake_filesystem from pyfakefs import fake_filesystem
from .. import mgr from .. import DEFAULT_VERSION, mgr
from ..controllers import generate_controller_routes, json_error_page from ..controllers import generate_controller_routes, json_error_page
from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa
from ..services.auth import AuthManagerTool from ..services.auth import AuthManagerTool
@ -149,32 +149,43 @@ class ControllerTestCase(helper.CPWebCase):
if cls._request_logging: if cls._request_logging:
cherrypy.config.update({'tools.request_logging.on': False}) cherrypy.config.update({'tools.request_logging.on': False})
def _request(self, url, method, data=None, headers=None): def _request(self, url, method, data=None, headers=None, version=DEFAULT_VERSION):
if not data: if not data:
b = None b = None
h = None if version:
h = [('Accept', 'application/vnd.ceph.api.v{}+json'.format(version)),
('Content-Length', '0')]
else:
h = None
else: else:
b = json.dumps(data) b = json.dumps(data)
h = [('Content-Type', 'application/json'), if version is not None:
('Content-Length', str(len(b)))] h = [('Accept', 'application/vnd.ceph.api.v{}+json'.format(version)),
('Content-Type', 'application/json'),
('Content-Length', str(len(b)))]
else:
h = [('Content-Type', 'application/json'),
('Content-Length', str(len(b)))]
if headers: if headers:
h = headers h = headers
self.getPage(url, method=method, body=b, headers=h) self.getPage(url, method=method, body=b, headers=h)
def _get(self, url, headers=None): def _get(self, url, headers=None, version=DEFAULT_VERSION):
self._request(url, 'GET', headers=headers) self._request(url, 'GET', headers=headers, version=version)
def _post(self, url, data=None): def _post(self, url, data=None, version=DEFAULT_VERSION):
self._request(url, 'POST', data) self._request(url, 'POST', data, version=version)
def _delete(self, url, data=None): def _delete(self, url, data=None, version=DEFAULT_VERSION):
self._request(url, 'DELETE', data) self._request(url, 'DELETE', data, version=version)
def _put(self, url, data=None): def _put(self, url, data=None, version=DEFAULT_VERSION):
self._request(url, 'PUT', data) self._request(url, 'PUT', data, version=version)
def _task_request(self, method, url, data, timeout): def _task_request(self, method, url, data, timeout, version=DEFAULT_VERSION):
self._request(url, method, data) self._request(url, method, data, version=version)
if self.status != '202 Accepted': if self.status != '202 Accepted':
logger.info("task finished immediately") logger.info("task finished immediately")
return return
@ -204,7 +215,7 @@ class ControllerTestCase(helper.CPWebCase):
logger.info("task (%s, %s) is still executing", self.task_name, logger.info("task (%s, %s) is still executing", self.task_name,
self.task_metadata) self.task_metadata)
time.sleep(1) time.sleep(1)
self.tc._get('/api/task?name={}'.format(self.task_name)) self.tc._get('/api/task?name={}'.format(self.task_name), version=version)
res = self.tc.json_body() res = self.tc.json_body()
for task in res['finished_tasks']: for task in res['finished_tasks']:
if task['metadata'] == self.task_metadata: if task['metadata'] == self.task_metadata:
@ -239,14 +250,14 @@ class ControllerTestCase(helper.CPWebCase):
self.status = 500 self.status = 500
self.body = json.dumps(thread.res_task['exception']) self.body = json.dumps(thread.res_task['exception'])
def _task_post(self, url, data=None, timeout=60): def _task_post(self, url, data=None, timeout=60, version=DEFAULT_VERSION):
self._task_request('POST', url, data, timeout) self._task_request('POST', url, data, timeout, version=version)
def _task_delete(self, url, timeout=60): def _task_delete(self, url, timeout=60, version=DEFAULT_VERSION):
self._task_request('DELETE', url, None, timeout) self._task_request('DELETE', url, None, timeout, version=version)
def _task_put(self, url, data=None, timeout=60): def _task_put(self, url, data=None, timeout=60, version=DEFAULT_VERSION):
self._task_request('PUT', url, data, timeout) self._task_request('PUT', url, data, timeout, version=version)
def json_body(self): def json_body(self):
body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body

View File

@ -1,6 +1,7 @@
# # -*- coding: utf-8 -*- # # -*- coding: utf-8 -*-
from __future__ import absolute_import from __future__ import absolute_import
from .. import DEFAULT_VERSION
from ..api.doc import SchemaType from ..api.doc import SchemaType
from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController
from ..controllers.docs import Docs from ..controllers.docs import Docs
@ -82,7 +83,7 @@ class DocsTest(ControllerTestCase):
expected_response_content = { expected_response_content = {
'200': { '200': {
'application/json': { 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'schema': {'type': 'array', 'schema': {'type': 'array',
'items': {'type': 'object', 'properties': { 'items': {'type': 'object', 'properties': {
'my_prop': { 'my_prop': {
@ -90,7 +91,7 @@ class DocsTest(ControllerTestCase):
'description': '200 property desc.'}}}, 'description': '200 property desc.'}}},
'required': ['my_prop']}}}, 'required': ['my_prop']}}},
'202': { '202': {
'application/json': { 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'schema': {'type': 'object', 'schema': {'type': 'object',
'properties': {'my_prop': { 'properties': {'my_prop': {
'type': 'string', 'type': 'string',

View File

@ -1,4 +1,4 @@
# pylint: disable=too-many-public-methods,too-many-lines # pylint: disable=too-many-public-methods, too-many-lines
import copy import copy
import errno import errno
@ -599,10 +599,12 @@ class IscsiTestController(ControllerTestCase, KVStoreMockMixin):
update_response, response): update_response, response):
self._task_post('/api/iscsi/target', create_request) self._task_post('/api/iscsi/target', create_request)
self.assertStatus(201) self.assertStatus(201)
self._task_put('/api/iscsi/target/{}'.format(create_request['target_iqn']), update_request) self._task_put(
'/api/iscsi/target/{}'.format(create_request['target_iqn']), update_request)
self.assertStatus(update_response_code) self.assertStatus(update_response_code)
self.assertJsonBody(update_response) self.assertJsonBody(update_response)
self._get('/api/iscsi/target/{}'.format(update_request['new_target_iqn'])) self._get(
'/api/iscsi/target/{}'.format(update_request['new_target_iqn']))
self.assertStatus(200) self.assertStatus(200)
self.assertJsonBody(response) self.assertJsonBody(response)

View File

@ -149,7 +149,7 @@ class SettingsControllerTest(ControllerTestCase, KVStoreMockMixin):
}) })
def test_set(self): def test_set(self):
self._put('/api/settings/GRAFANA_API_USERNAME', {'value': 'foo'},) self._put('/api/settings/GRAFANA_API_USERNAME', {'value': 'foo'})
self.assertStatus(200) self.assertStatus(200)
self._get('/api/settings/GRAFANA_API_USERNAME') self._get('/api/settings/GRAFANA_API_USERNAME')

View File

@ -11,6 +11,7 @@ try:
except ImportError: except ImportError:
from unittest.mock import patch from unittest.mock import patch
from .. import DEFAULT_VERSION
from ..controllers import ApiController, BaseController, Controller, Proxy, RESTController from ..controllers import ApiController, BaseController, Controller, Proxy, RESTController
from ..services.exception import handle_rados_error from ..services.exception import handle_rados_error
from ..tools import dict_contains_path, dict_get, json_str_to_object, partial_dict from ..tools import dict_contains_path, dict_get, json_str_to_object, partial_dict
@ -86,7 +87,8 @@ class RESTControllerTest(ControllerTestCase):
self.assertStatus(204) self.assertStatus(204)
self._get("/foo") self._get("/foo")
self.assertStatus('200 OK') self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'application/json') self.assertHeader('Content-Type',
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION))
self.assertBody('[]') self.assertBody('[]')
def test_fill(self): def test_fill(self):
@ -97,16 +99,19 @@ class RESTControllerTest(ControllerTestCase):
self._post("/foo", data) self._post("/foo", data)
self.assertJsonBody(data) self.assertJsonBody(data)
self.assertStatus(201) self.assertStatus(201)
self.assertHeader('Content-Type', 'application/json') self.assertHeader('Content-Type',
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION))
self._get("/foo") self._get("/foo")
self.assertStatus('200 OK') self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'application/json') self.assertHeader('Content-Type',
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION))
self.assertJsonBody([data] * 5) self.assertJsonBody([data] * 5)
self._put('/foo/0', {'newdata': 'newdata'}) self._put('/foo/0', {'newdata': 'newdata'})
self.assertStatus('200 OK') self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'application/json') self.assertHeader('Content-Type',
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION))
self.assertJsonBody({'newdata': 'newdata', 'key': '0'}) self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
def test_not_implemented(self): def test_not_implemented(self):

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import unittest
from ..controllers import ApiController, RESTController
from . import ControllerTestCase # pylint: disable=no-name-in-module
@ApiController("/vtest", secure=False)
class VTest(RESTController):
RESOURCE_ID = "vid"
def list(self):
return {'version': ""}
def get(self):
return {'version': ""}
@RESTController.Collection('GET', version="1.0")
def vmethod(self):
return {'version': '1.0'}
@RESTController.Collection('GET', version="2.0")
def vmethodv2(self):
return {'version': '2.0'}
class RESTVersioningTest(ControllerTestCase, unittest.TestCase):
@classmethod
def setup_server(cls):
cls.setup_controllers([VTest], "/test")
def test_v1(self):
for (version, expected_status) in [
("1.0", 200),
("2.0", 415)
]:
with self.subTest(version=version):
self._get('/test/api/vtest/vmethod', version=version)
self.assertStatus(expected_status)
def test_v2(self):
for (version, expected_status) in [
("2.0", 200),
("1.0", 415)
]:
with self.subTest(version=version):
self._get('/test/api/vtest/vmethodv2', version=version)
self.assertStatus(expected_status)