diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 2cc45d2cb..2ec94e4a3 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -16,7 +16,7 @@ user documentation. A user can specify a header in the API request:: - OpenStack-Volume-microversion: + OpenStack-API-Version: volume where ```` is any valid api version for this API. diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index 821f112a8..8335b14b8 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -68,7 +68,9 @@ VER_METHOD_ATTR = 'versioned_methods' # Name of header used by clients to request a specific version # of the REST API -API_VERSION_REQUEST_HEADER = 'OpenStack-Volume-microversion' +API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version' + +VOLUME_SERVICE = 'volume' class Request(webob.Request): @@ -298,11 +300,20 @@ class Request(webob.Request): hdr_string = self.headers[API_VERSION_REQUEST_HEADER] # 'latest' is a special keyword which is equivalent to requesting # the maximum version of the API supported - if hdr_string == 'latest': + hdr_string_list = hdr_string.split(",") + volume_version = None + for hdr in hdr_string_list: + if VOLUME_SERVICE in hdr: + service, volume_version = hdr.split() + break + if not volume_version: + raise exception.VersionNotFoundForAPIMethod( + version=volume_version) + if volume_version == 'latest': self.api_version_request = api_version.max_api_version() else: self.api_version_request = api_version.APIVersionRequest( - hdr_string) + volume_version) # Check that the version requested is within the global # minimum/maximum of supported API versions @@ -1159,6 +1170,7 @@ class Resource(wsgi.Application): if not request.api_version_request.is_null(): response.headers[API_VERSION_REQUEST_HEADER] = ( + VOLUME_SERVICE + ' ' + request.api_version_request.get_string()) response.headers['Vary'] = API_VERSION_REQUEST_HEADER diff --git a/cinder/tests/unit/api/test_versions.py b/cinder/tests/unit/api/test_versions.py index df008fb07..1c8fd8969 100644 --- a/cinder/tests/unit/api/test_versions.py +++ b/cinder/tests/unit/api/test_versions.py @@ -21,11 +21,13 @@ from cinder.api.openstack import api_version_request from cinder.api.openstack import wsgi from cinder.api.v1 import router from cinder.api import versions +from cinder import exception from cinder import test from cinder.tests.unit.api import fakes -version_header_name = 'OpenStack-Volume-microversion' +VERSION_HEADER_NAME = 'OpenStack-API-Version' +VOLUME_SERVICE = 'volume ' @ddt.ddt @@ -35,11 +37,27 @@ class VersionsControllerTestCase(test.TestCase): super(VersionsControllerTestCase, self).setUp() self.wsgi_apps = (versions.Versions(), router.APIRouter()) - @ddt.data('1.0', '2.0', '3.0') - def test_versions_root(self, version): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost') + def build_request(self, base_url='http://localhost/v3', + header_version=None): + req = fakes.HTTPRequest.blank('/', base_url=base_url) req.method = 'GET' req.content_type = 'application/json' + if header_version: + req.headers = {VERSION_HEADER_NAME: VOLUME_SERVICE + + header_version} + + return req + + def check_response(self, response, version): + self.assertEqual(VOLUME_SERVICE + version, + response.headers[VERSION_HEADER_NAME]) + self.assertEqual(VOLUME_SERVICE + version, + response.headers[VERSION_HEADER_NAME]) + self.assertEqual(VERSION_HEADER_NAME, response.headers['Vary']) + + @ddt.data('1.0', '2.0', '3.0') + def test_versions_root(self, version): + req = self.build_request(base_url='http://localhost') response = req.get_response(versions.Versions()) self.assertEqual(300, response.status_int) @@ -64,28 +82,23 @@ class VersionsControllerTestCase(test.TestCase): v3.get('min_version')) def test_versions_v1_no_header(self): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1') - req.method = 'GET' - req.content_type = 'application/json' + req = self.build_request(base_url='http://localhost/v1') response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) def test_versions_v2_no_header(self): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2') - req.method = 'GET' - req.content_type = 'application/json' + req = self.build_request(base_url='http://localhost/v2') response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) @ddt.data('1.0') def test_versions_v1(self, version): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1') - req.method = 'GET' - req.content_type = 'application/json' + req = self.build_request(base_url='http://localhost/v1', + header_version=version) if version is not None: - req.headers = {version_header_name: version} + req.headers = {VERSION_HEADER_NAME: VOLUME_SERVICE + version} response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) @@ -94,19 +107,16 @@ class VersionsControllerTestCase(test.TestCase): ids = [v['id'] for v in version_list] self.assertEqual({'v1.0'}, set(ids)) - self.assertEqual('1.0', response.headers[version_header_name]) - self.assertEqual(version, response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) + + self.check_response(response, version) self.assertEqual('', version_list[0].get('min_version')) self.assertEqual('', version_list[0].get('version')) @ddt.data('2.0') def test_versions_v2(self, version): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2') - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: version} + req = self.build_request(base_url='http://localhost/v2', + header_version=version) response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) @@ -115,19 +125,15 @@ class VersionsControllerTestCase(test.TestCase): ids = [v['id'] for v in version_list] self.assertEqual({'v2.0'}, set(ids)) - self.assertEqual('2.0', response.headers[version_header_name]) - self.assertEqual(version, response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) + + self.check_response(response, version) self.assertEqual('', version_list[0].get('min_version')) self.assertEqual('', version_list[0].get('version')) @ddt.data('3.0', 'latest') def test_versions_v3_0_and_latest(self, version): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3') - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: version} + req = self.build_request(header_version=version) response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) @@ -136,8 +142,7 @@ class VersionsControllerTestCase(test.TestCase): ids = [v['id'] for v in version_list] self.assertEqual({'v3.0'}, set(ids)) - self.assertEqual('3.0', response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) + self.check_response(response, '3.0') self.assertEqual(api_version_request._MAX_API_VERSION, version_list[0].get('version')) @@ -145,20 +150,14 @@ class VersionsControllerTestCase(test.TestCase): version_list[0].get('min_version')) def test_versions_version_latest(self): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3') - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: 'latest'} + req = self.build_request(header_version='latest') response = req.get_response(router.APIRouter()) self.assertEqual(200, response.status_int) def test_versions_version_invalid(self): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3') - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: '2.0.1'} + req = self.build_request(header_version='2.0.1') for app in self.wsgi_apps: response = req.get_response(app) @@ -177,8 +176,7 @@ class VersionsControllerTestCase(test.TestCase): def index(self, req): return 'off' - req = fakes.HTTPRequest.blank('/tests', base_url='http://localhost/v3') - req.headers = {version_header_name: '3.5'} + req = self.build_request(header_version='3.5') app = fakes.TestRouter(Controller()) response = req.get_response(app) @@ -186,13 +184,40 @@ class VersionsControllerTestCase(test.TestCase): self.assertEqual(404, response.status_int) def test_versions_version_not_acceptable(self): - req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v3') - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: '4.0'} + req = self.build_request(header_version='4.0') response = req.get_response(router.APIRouter()) self.assertEqual(406, response.status_int) - self.assertEqual('4.0', response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) + self.assertEqual('4.0', response.headers[VERSION_HEADER_NAME]) + self.assertEqual(VERSION_HEADER_NAME, response.headers['Vary']) + + @ddt.data(['volume 3.0, compute 2.22', True], + ['volume 3.0, compute 2.22, identity 2.3', True], + ['compute 2.22, identity 2.3', False]) + @ddt.unpack + def test_versions_multiple_services_header( + self, service_list, should_pass): + req = self.build_request() + req.headers = {VERSION_HEADER_NAME: service_list} + + try: + response = req.get_response(router.APIRouter()) + except exception.VersionNotFoundForAPIMethod: + if should_pass: + raise + elif not should_pass: + return + + self.assertEqual(200, response.status_int) + body = jsonutils.loads(response.body) + version_list = body['versions'] + + ids = [v['id'] for v in version_list] + self.assertEqual({'v3.0'}, set(ids)) + self.check_response(response, '3.0') + + self.assertEqual(api_version_request._MAX_API_VERSION, + version_list[0].get('version')) + self.assertEqual(api_version_request._MIN_API_VERSION, + version_list[0].get('min_version')) diff --git a/doc/source/devref/api_microversion_dev.rst b/doc/source/devref/api_microversion_dev.rst index 31ba63f25..765e91dd6 100644 --- a/doc/source/devref/api_microversion_dev.rst +++ b/doc/source/devref/api_microversion_dev.rst @@ -9,9 +9,11 @@ to the API while preserving backward compatibility. The basic idea is that a user has to explicitly ask for their request to be treated with a particular version of the API. So breaking changes can be added to the API without breaking users who don't specifically ask for it. This -is done with an HTTP header ``OpenStack-Volume-microversion`` which +is done with an HTTP header ``OpenStack-API-Version`` which is a monotonically increasing semantic version number starting from -``3.0``. +``3.0``. Each service that uses microversions will share this header, so +the Volume service will need to specifiy ``volume``: + ``OpenStack-API-Version: volume 3.0`` If a user makes a request without specifying a version, they will get the ``DEFAULT_API_VERSION`` as defined in @@ -157,7 +159,7 @@ In the controller class:: .... This method would only be available if the caller had specified an -``OpenStack-Volume-microversion`` of >= ``3.4``. If they had specified a +``OpenStack-API-Version`` of >= ``3.4``. If they had specified a lower version (or not specified it and received the default of ``3.1``) the server would respond with ``HTTP/404``. @@ -171,7 +173,7 @@ In the controller class:: .... This method would only be available if the caller had specified an -``OpenStack-Volume-microversion`` of <= ``3.4``. If ``3.5`` or later +``OpenStack-API-Version`` of <= ``3.4``. If ``3.5`` or later is specified the server will respond with ``HTTP/404``. Changing a method's behaviour @@ -294,11 +296,11 @@ these unit tests are not replicated in .../v3, and only new functionality needs to be place in the .../v3/directory. Testing a microversioned API method is very similar to a normal controller -method test, you just need to add the ``OpenStack-Volume-microversion`` +method test, you just need to add the ``OpenStack-API-Version`` header, for example:: req = fakes.HTTPRequest.blank('/testable/url/endpoint') - req.headers = {'OpenStack-Volume-microversion': '3.2'} + req.headers = {'OpenStack-API-Version': 'volume 3.2'} req.api_version_request = api_version.APIVersionRequest('3.6') controller = controller.TestableController()