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
This commit is contained in:
scottda 2016-03-01 14:42:05 -07:00
parent 6fa468270c
commit ef7ed8dcb2
4 changed files with 95 additions and 56 deletions

View File

@ -16,7 +16,7 @@ user documentation.
A user can specify a header in the API request::
OpenStack-Volume-microversion: <version>
OpenStack-API-Version: volume <version>
where ``<version>`` is any valid api version for this API.

View File

@ -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

View File

@ -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'))

View File

@ -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()