mirror of
https://github.com/ceph/ceph
synced 2025-01-03 09:32:43 +00:00
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:
commit
587a84049b
@ -0,0 +1 @@
|
|||||||
|
DEFAULT_VERSION = '1.0'
|
@ -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):
|
||||||
|
20
qa/tasks/mgr/dashboard/test_api.py
Normal file
20
qa/tasks/mgr/dashboard/test_api.py
Normal 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)
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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__)),
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
50
src/pybind/mgr/dashboard/tests/test_versioning.py
Normal file
50
src/pybind/mgr/dashboard/tests/test_versioning.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user