From ef7ed8dcb28e743edbd839261a948fa84089f8fa Mon Sep 17 00:00:00 2001
From: scottda <scott.dangelo@hp.com>
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 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: <version>
+    OpenStack-API-Version: volume <version>
 
   where ``<version>`` 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()
-- 
2.45.2