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:
parent
39a5a736d0
commit
d8a17851b4
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue