Adds global API version check for microversions

Adds a check for a request that the version requested is within the
global API version range supported by the REST API. Both the minimum
and maximum are currently set to "2.1". The maximum will be increased
everytime an API change is made.

Also sets up some template/doc files for clearly documenting
the REST API changes with each microversion increment.

Partially implements blueprint api-microversions

Change-Id: Ie7fdb2928d957c03ed788c2ddd29fe798c645fce
This commit is contained in:
Chris Yeoh 2014-11-25 13:32:06 +10:30 committed by Eli Qiao
parent 39a5a736d0
commit d8a17851b4
7 changed files with 154 additions and 10 deletions

View File

@ -16,6 +16,50 @@ import re
from nova import exception
# Define the minimum and maximum version of the API across all of the
# REST API. The format of the version is:
# X.Y where:
#
# - X will only be changed if a significant backwards incompatible API
# change is made which affects the API as whole. That is, something
# that is only very very rarely incremented.
#
# - Y when you make any change to the API. Note that this includes
# semantic changes which may not affect the input or output formats or
# even originate in the API code layer. We are not distinguishing
# between backwards compatible and backwards incompatible changes in
# the versioning system. It must be made clear in the documentation as
# to what is a backwards compatible change and what is a backwards
# incompatible one.
#
# You must update the API version history string below with a one or
# two line description as well as update rest_api_version_history.rst
REST_API_VERSION_HISTORY = """REST API Version History:
* 2.1 - Initial version. Equivalent to v2.0 code
"""
# The minimum and maximum versions of the API supported
# The default api version request is definied to be the
# the minimum version of the API supported.
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.1"
DEFAULT_API_VERSION = _MIN_API_VERSION
# NOTE(cyeoh): min and max versions declared as functions so we can
# mock them for unittests. Do not use the constants directly anywhere
# else.
def min_api_version():
return APIVersionRequest(_MIN_API_VERSION)
def max_api_version():
return APIVersionRequest(_MAX_API_VERSION)
class APIVersionRequest(object):
"""This class represents an API Version Request with convenience
@ -38,6 +82,7 @@ class APIVersionRequest(object):
raise exception.InvalidAPIVersionString(version=version_string)
def __str__(self):
"""Debug/Logging representation of object."""
return ("API Version Request Major: %s, Minor: %s"
% (self.ver_major, self.ver_minor))
@ -74,3 +119,11 @@ class APIVersionRequest(object):
return self <= max_version
else:
return min_version <= self <= max_version
def get_string(self):
"""Converts object to string representation which if used to create
an APIVersionRequest object results in the same version request.
"""
if self.is_null():
raise ValueError
return "%s.%s" % (self.ver_major, self.ver_minor)

View File

@ -0,0 +1,22 @@
REST API VERSION HISTORY
========================
This documents the changes made to the REST API with every
microversion change. The description for each version should be a
verbose one which has enough information to be suitable for use in
user documentation.
- **2.1**
This is the initial version of the v2.1 API which supports
microversions. The V2.1 API is from the REST API users's point of
view exactly the same as v2.0 except with strong input validation.
A user can specify a header in the API request:
X-OpenStack-Compute-API-Version: <version>
where <version> is any valid api version for this API.
If no version is specified then the API will behave as if a version
request of v2.1 was requested.

View File

@ -238,9 +238,20 @@ class Request(webob.Request):
if 'X-OpenStack-Compute-API-Version' in self.headers:
self.api_version_request = api_version.APIVersionRequest(
self.headers['X-OpenStack-Compute-API-Version'])
# Check that the version requested is within the global
# minimum/maximum of supported API versions
if not self.api_version_request.matches(
api_version.min_api_version(),
api_version.max_api_version()):
raise exception.InvalidGlobalAPIVersion(
req_ver=self.api_version_request.get_string(),
min_ver=api_version.min_api_version().get_string(),
max_ver=api_version.max_api_version().get_string())
else:
self.api_version_request = api_version.APIVersionRequest(
DEFAULT_API_VERSION)
api_version.DEFAULT_API_VERSION)
class ActionDispatcher(object):
@ -919,6 +930,9 @@ class Resource(wsgi.Application):
except exception.InvalidAPIVersionString as e:
return Fault(webob.exc.HTTPBadRequest(
explanation=e.format_message()))
except exception.InvalidGlobalAPIVersion as e:
return Fault(webob.exc.HTTPNotAcceptable(
explanation=e.format_message()))
# Identify the action, its arguments, and the requested
# content type

View File

@ -330,6 +330,11 @@ class VersionNotFoundForAPIMethod(Invalid):
msg_fmt = _("API version %(version)s is not supported on this method.")
class InvalidGlobalAPIVersion(Invalid):
msg_fmt = _("Version %(req_ver)s is not supported by the API. Minimum "
"is %(min_ver)s and maximum is %(max_ver)s.")
# Cannot be templated as the error syntax varies.
# msg needs to be constructed when raised.
class InvalidParameterValue(Invalid):

View File

@ -16,6 +16,7 @@ import mock
from oslo.config import cfg
from oslo.serialization import jsonutils
from nova.api.openstack import api_version_request as api_version
from nova import test
from nova.tests.unit.api.openstack import fakes
@ -34,9 +35,12 @@ class MicroversionsTest(test.NoDBTestCase):
resp_json = jsonutils.loads(res.body)
self.assertEqual('val', resp_json['param'])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions_with_header(self, mock_namespace):
def test_microversions_with_header(self, mock_namespace, mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.3")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions')
req.headers = {'X-OpenStack-Compute-API-Version': '2.3'}
@ -45,9 +49,13 @@ class MicroversionsTest(test.NoDBTestCase):
resp_json = jsonutils.loads(res.body)
self.assertEqual('val2', resp_json['param'])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions_with_header_exact_match(self, mock_namespace):
def test_microversions_with_header_exact_match(self, mock_namespace,
mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.3")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions')
req.headers = {'X-OpenStack-Compute-API-Version': '2.2'}
@ -56,9 +64,12 @@ class MicroversionsTest(test.NoDBTestCase):
resp_json = jsonutils.loads(res.body)
self.assertEqual('val2', resp_json['param'])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions2_no_2_1_version(self, mock_namespace):
def test_microversions2_no_2_1_version(self, mock_namespace, mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.3")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions2')
req.headers = {'X-OpenStack-Compute-API-Version': '2.3'}
@ -67,9 +78,12 @@ class MicroversionsTest(test.NoDBTestCase):
resp_json = jsonutils.loads(res.body)
self.assertEqual('controller2_val1', resp_json['param'])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions2_later_version(self, mock_namespace):
def test_microversions2_later_version(self, mock_namespace, mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("3.1")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions2')
req.headers = {'X-OpenStack-Compute-API-Version': '3.0'}
@ -78,9 +92,13 @@ class MicroversionsTest(test.NoDBTestCase):
resp_json = jsonutils.loads(res.body)
self.assertEqual('controller2_val2', resp_json['param'])
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions2_version_too_high(self, mock_namespace):
def test_microversions2_version_too_high(self, mock_namespace,
mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("3.5")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions2')
req.headers = {'X-OpenStack-Compute-API-Version': '3.2'}
@ -95,3 +113,20 @@ class MicroversionsTest(test.NoDBTestCase):
req.headers = {'X-OpenStack-Compute-API-Version': '2.1'}
res = req.get_response(app)
self.assertEqual(404, res.status_int)
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
@mock.patch("nova.api.openstack.APIRouterV21.api_extension_namespace",
return_value='nova.api.v3.test_extensions')
def test_microversions_global_version_too_high(self, mock_namespace,
mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("3.5")
app = fakes.wsgi_app_v21(init_only='test-microversions')
req = fakes.HTTPRequest.blank('/v2/fake/microversions2')
req.headers = {'X-OpenStack-Compute-API-Version': '3.7'}
res = req.get_response(app)
self.assertEqual(406, res.status_int)
res_json = jsonutils.loads(res.body)
self.assertEqual("Version 3.7 is not supported by the API. "
"Minimum is 2.1 and maximum is 3.5.",
res_json['computeFault']['message'])

View File

@ -109,3 +109,11 @@ class APIVersionRequestTests(test.NoDBTestCase):
self.assertFalse(v2.matches(v3, v1))
self.assertRaises(ValueError, v_null.matches, v1, v3)
def test_get_string(self):
v1_string = "3.23"
v1 = api_version_request.APIVersionRequest(v1_string)
self.assertEqual(v1_string, v1.get_string())
self.assertRaises(ValueError,
api_version_request.APIVersionRequest().get_string)

View File

@ -12,6 +12,7 @@
import inspect
import mock
import webob
from nova.api.openstack import api_version_request as api_version
@ -196,9 +197,12 @@ class RequestTest(test.NoDBTestCase):
request = wsgi.Request.blank('/')
request.set_api_version_request()
self.assertEqual(api_version.APIVersionRequest(
wsgi.DEFAULT_API_VERSION), request.api_version_request)
api_version.DEFAULT_API_VERSION), request.api_version_request)
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
def test_api_version_request_header(self, mock_maxver):
mock_maxver.return_value = api_version.APIVersionRequest("2.14")
def test_api_version_request_header(self):
request = wsgi.Request.blank('/')
request.headers = {'X-OpenStack-Compute-API-Version': '2.14'}
request.set_api_version_request()
@ -381,7 +385,8 @@ class ResourceTest(test.NoDBTestCase):
class Controller(object):
def index(self, req):
if req.api_version_request != \
api_version.APIVersionRequest(wsgi.DEFAULT_API_VERSION):
api_version.APIVersionRequest(
api_version.DEFAULT_API_VERSION):
raise webob.exc.HTTPInternalServerError()
return 'success'
@ -391,8 +396,10 @@ class ResourceTest(test.NoDBTestCase):
self.assertEqual(response.body, 'success')
self.assertEqual(response.status_int, 200)
def test_resource_receives_api_version_request(self):
@mock.patch("nova.api.openstack.api_version_request.max_api_version")
def test_resource_receives_api_version_request(self, mock_maxver):
version = "2.5"
mock_maxver.return_value = api_version.APIVersionRequest(version)
class Controller(object):
def index(self, req):