]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
support new HTTP microversion header
authorscottda <scott.dangelo@hp.com>
Tue, 1 Mar 2016 21:42:05 +0000 (14:42 -0700)
committerscottda <scott.dangelo@hp.com>
Fri, 4 Mar 2016 16:02:34 +0000 (09:02 -0700)
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

cinder/api/openstack/rest_api_version_history.rst
cinder/api/openstack/wsgi.py
cinder/tests/unit/api/test_versions.py
doc/source/devref/api_microversion_dev.rst

index 2cc45d2cbee5e87ca6e451a0718f9bc08fffdcbb..2ec94e4a373d65936f32ce54de4e491c69c44412 100644 (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.
 
index 821f112a89b56d690eb975c827aa312b5b525f50..8335b14b8e70b2208a7a596c76ccb8975d4347c2 100644 (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
 
index df008fb07ab0cb791f7c8f3796f97b7e200af8a2..1c8fd8969d272e8953737e3eeca9fc95287fde59 100644 (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'))
index 31ba63f25dada46354cab05e2e0dd5b03dc03896..765e91dd6ba60112bbd01378e8ee8d0be4059fbb 100644 (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()