From ef7ed8dcb28e743edbd839261a948fa84089f8fa Mon Sep 17 00:00:00 2001 From: scottda Date: Tue, 1 Mar 2016 14:42:05 -0700 Subject: [PATCH] support new HTTP microversion header According to API working group guidelines: https://review.openstack.org/#/c/243414 microversion headers should be of the form: OpenStack-API-Version: [SERVICE_TYPE] 2.114 i.e OpenStack-API-Version: volume 3.22 Two extra headers are always returned in the response: OpenStack-API-Version: [SERVICE_TYPE] version_number Vary: OpenStack-API-Version note: Servers must be prepared to deal with multiple OpenStack-API-Version headers. This could happen when a client designed to address multiple services always sends the headers it thinks it needs. Most Python frameworks will handle this by setting the value of the header to the values of all matching headers, joined by a ',' (comma). For example ``compute 2.11,identity 2.114``. Closes-Bug: #1551941 Change-Id: I658e54966c390b41e3b551dd9827606c2e013511 --- .../openstack/rest_api_version_history.rst | 2 +- cinder/api/openstack/wsgi.py | 18 ++- cinder/tests/unit/api/test_versions.py | 117 +++++++++++------- doc/source/devref/api_microversion_dev.rst | 14 ++- 4 files changed, 95 insertions(+), 56 deletions(-) diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 2cc45d2cbee..2ec94e4a373 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 821f112a89b..8335b14b8e7 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 df008fb07ab..1c8fd8969d2 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 31ba63f25da..765e91dd6ba 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()