Add microversion support

This patch adds microversion support so that it is possible to make
minor changes to the APIs as required to fix Launchpad bug #1740091.

Change-Id: I7ea48be72897a77fc8424a57f4ce2d4798daf4eb
Related-Bug: #1740091
This commit is contained in:
asmita singh 2019-03-05 10:08:11 +00:00 committed by Pierre Riteau
parent d19fa2a839
commit 520005b13d
19 changed files with 558 additions and 25 deletions

View File

@ -0,0 +1,176 @@
# Copyright 2014 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
from blazar import exceptions
from blazar.i18n import _
# 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:
* 1.0 - Includes all V1 APIs and extensions. V2 API is deprecated.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
MIN_API_VERSION = "1.0"
MAX_API_VERSION = "1.0"
DEFAULT_API_VERSION = MIN_API_VERSION
# Name of header used by clients to request a specific version
# of the REST API
API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version'
VARY_HEADER = "Vary"
LATEST = "latest"
RESERVATION_SERVICE_TYPE = 'reservation'
BAD_REQUEST_STATUS_CODE = 400
BAD_REQUEST_STATUS_NAME = "BAD_REQUEST"
NOT_ACCEPTABLE_STATUS_CODE = 406
NOT_ACCEPTABLE_STATUS_NAME = "NOT_ACCEPTABLE"
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.
This class includes convenience methods for manipulation
and comparison of version numbers as needed to implement
API microversions.
"""
def __init__(self, api_version_request=None):
"""Create an API version request object."""
self._ver_major = 0
self._ver_minor = 0
if api_version_request is not None:
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
api_version_request)
if match:
self._ver_major = int(match.group(1))
self._ver_minor = int(match.group(2))
else:
raise exceptions.InvalidAPIVersionString(
version=api_version_request)
def __str__(self):
"""Debug/Logging representation of object."""
return ("API Version Request Major: %(major)s, Minor: %(minor)s"
% {'major': self._ver_major, 'minor': self._ver_minor})
def _format_type_error(self, other):
return TypeError(_("'%(other)s' should be an instance of '%(cls)s'") %
{"other": other, "cls": self.__class__})
def __lt__(self, other):
if not isinstance(other, APIVersionRequest):
raise self._format_type_error(other)
return ((self._ver_major, self._ver_minor) <
(other._ver_major, other._ver_minor))
def __eq__(self, other):
if not isinstance(other, APIVersionRequest):
raise self._format_type_error(other)
return ((self._ver_major, self._ver_minor) ==
(other._ver_major, other._ver_minor))
def __gt__(self, other):
if not isinstance(other, APIVersionRequest):
raise self._format_type_error(other)
return ((self._ver_major, self._ver_minor) >
(other._ver_major, other._ver_minor))
def __le__(self, other):
return self < other or self == other
def __ne__(self, other):
return not self.__eq__(other)
def __ge__(self, other):
return self > other or self == other
def matches(self, min_version, max_version=None):
"""Compares this version to the specified min/max range.
Returns whether the version object represents a version
greater than or equal to the minimum version and less than
or equal to the maximum version.
If min_version is null then there is no minimum limit.
If max_version is null then there is no maximum limit.
If self is null then raise ValueError.
:param min_version: Minimum acceptable version.
:param max_version: Maximum acceptable version.
:param experimental: Whether to match experimental APIs.
:returns: boolean
"""
if self.is_null():
raise ValueError
if max_version.is_null() and min_version.is_null():
return True
elif max_version.is_null():
return min_version <= self
elif min_version.is_null():
return self <= max_version
else:
return min_version <= self <= max_version
def is_null(self):
return self._ver_major == 0 and self._ver_minor == 0
def get_string(self):
"""Returns a string representation of this object.
If this method is used to create an APIVersionRequest,
the resulting object will be an equivalent request.
"""
if self.is_null():
raise ValueError
return ("%(major)s.%(minor)s" %
{'major': self._ver_major, 'minor': self._ver_minor})

View File

@ -25,6 +25,7 @@ from oslo_middleware import debug
from stevedore import enabled
from werkzeug import exceptions as werkzeug_exceptions
from blazar.api.v1 import api_version_request
from blazar.api.v1 import request_id
from blazar.api.v1 import request_log
from blazar.api.v1 import utils as api_utils
@ -55,7 +56,9 @@ def version_list():
{"id": "v1.0",
"status": "CURRENT",
"links": [{"href": "{0}v1".format(flask.request.host_url),
"rel": "self"}]
"rel": "self"}],
"min_version": api_version_request.MIN_API_VERSION,
"max_version": api_version_request.MAX_API_VERSION,
},
],
}, status="300 Multiple Choices")

View File

@ -35,34 +35,34 @@ _api = utils.LazyProxy(service.API)
# Leases operations
@rest.get('/leases', query=True)
def leases_list(query):
def leases_list(req, query):
"""List all existing leases."""
return api_utils.render(leases=_api.get_leases(query))
@rest.post('/leases')
def leases_create(data):
def leases_create(req, data):
"""Create new lease."""
return api_utils.render(lease=_api.create_lease(data))
@rest.get('/leases/<lease_id>')
@validation.check_exists(_api.get_lease, lease_id='lease_id')
def leases_get(lease_id):
def leases_get(req, lease_id):
"""Get lease by its ID."""
return api_utils.render(lease=_api.get_lease(lease_id))
@rest.put('/leases/<lease_id>')
@validation.check_exists(_api.get_lease, lease_id='lease_id')
def leases_update(lease_id, data):
def leases_update(req, lease_id, data):
"""Update lease."""
return api_utils.render(lease=_api.update_lease(lease_id, data))
@rest.delete('/leases/<lease_id>')
@validation.check_exists(_api.get_lease, lease_id='lease_id')
def leases_delete(lease_id):
def leases_delete(req, lease_id):
"""Delete specified lease."""
_api.delete_lease(lease_id)
return api_utils.render()

View File

@ -31,27 +31,27 @@ _api = utils.LazyProxy(service.API)
# Computehosts operations
@rest.get('', query=True)
def computehosts_list(query=None):
def computehosts_list(req, query=None):
"""List all existing computehosts."""
return api_utils.render(hosts=_api.get_computehosts(query))
@rest.post('')
def computehosts_create(data):
def computehosts_create(req, data):
"""Create new computehost."""
return api_utils.render(host=_api.create_computehost(data))
@rest.get('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_get(host_id):
def computehosts_get(req, host_id):
"""Get computehost by its ID."""
return api_utils.render(host=_api.get_computehost(host_id))
@rest.put('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_update(host_id, data):
def computehosts_update(req, host_id, data):
"""Update computehost. Only name changing may be proceeded."""
if len(data) == 0:
return api_utils.internal_error(status_code=400,
@ -62,20 +62,20 @@ def computehosts_update(host_id, data):
@rest.delete('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_delete(host_id):
def computehosts_delete(req, host_id):
"""Delete specified computehost."""
_api.delete_computehost(host_id)
return api_utils.render()
@rest.get('/allocations', query=True)
def allocations_list(query):
def allocations_list(req, query):
"""List all allocations on all computehosts."""
return api_utils.render(allocations=_api.list_allocations(query))
@rest.get('/<host_id>/allocation', query=True)
@validation.check_exists(_api.get_computehost, host_id='host_id')
def allocations_get(host_id, query):
def allocations_get(req, host_id, query):
"""List all allocations on a specific host."""
return api_utils.render(allocation=_api.get_allocations(host_id, query))

View File

@ -0,0 +1,23 @@
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.
1.0
---
This is the initial version of the v1.0 API which supports
microversions. The v1.0 API is from the REST API users's point of
view exactly the same as v1 except with strong input validation.
A user can specify a header in the API request::
OpenStack-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 v1.0 was requested.

View File

@ -16,12 +16,14 @@
import traceback
import flask
import microversion_parse
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_serialization import jsonutils
from werkzeug import datastructures
from blazar.api import context
from blazar.api.v1 import api_version_request as api_version
from blazar.db import exceptions as db_exceptions
from blazar import exceptions as ex
from blazar.i18n import _
@ -36,6 +38,8 @@ class Rest(flask.Blueprint):
def __init__(self, *args, **kwargs):
super(Rest, self).__init__(*args, **kwargs)
self.before_request(set_api_version_request)
self.after_request(add_vary_header)
self.url_prefix = kwargs.get('url_prefix', None)
self.routes_with_query_support = []
@ -83,7 +87,7 @@ class Rest(flask.Blueprint):
with context.ctx_from_headers(flask.request.headers):
try:
return func(**kwargs)
return func(flask.request, **kwargs)
except ex.BlazarException as e:
return bad_request(e)
except messaging.RemoteError as e:
@ -129,6 +133,74 @@ class Rest(flask.Blueprint):
RT_JSON = datastructures.MIMEAccept([("application/json", 1)])
def set_api_version_request():
requested_version = get_requested_microversion()
try:
api_version_request = api_version.APIVersionRequest(requested_version)
except ex.InvalidAPIVersionString:
flask.request.api_version_request = None
bad_request_microversion(requested_version)
if not api_version_request.matches(
api_version.min_api_version(),
api_version.max_api_version()):
flask.request.api_version_request = None
not_acceptable_microversion(requested_version)
flask.request.api_version_request = api_version_request
def get_requested_microversion():
requested_version = microversion_parse.get_version(
flask.request.headers,
api_version.RESERVATION_SERVICE_TYPE
)
if requested_version is None:
requested_version = api_version.MIN_API_VERSION
elif requested_version == api_version.LATEST:
requested_version = api_version.MAX_API_VERSION
return requested_version
def add_vary_header(response):
if flask.request.api_version_request:
response.headers[
api_version.VARY_HEADER] = api_version.API_VERSION_REQUEST_HEADER
response.headers[
api_version.API_VERSION_REQUEST_HEADER] = "{} {}".format(
api_version.RESERVATION_SERVICE_TYPE,
get_requested_microversion())
return response
def not_acceptable_microversion(requested_version):
message = ("Version {} is not supported by the API. "
"Minimum is {} and maximum is {}.".format(
requested_version,
api_version.MIN_API_VERSION,
api_version.MAX_API_VERSION))
resp = render_error_message(
api_version.NOT_ACCEPTABLE_STATUS_CODE,
message,
api_version.NOT_ACCEPTABLE_STATUS_NAME,
)
abort_and_log(resp.status_code, message)
def bad_request_microversion(requested_version):
message = ("API Version String {} is of invalid format. Must be of format"
" MajorNum.MinorNum.").format(requested_version)
resp = render_error_message(
api_version.BAD_REQUEST_STATUS_CODE,
message,
api_version.BAD_REQUEST_STATUS_NAME
)
abort_and_log(resp.status_code, message)
def _init_resp_type(file_upload):
"""Extracts response content type."""

View File

@ -40,7 +40,7 @@ CONF.register_opts(api_opts, 'api')
class V2Controller(rest.RestController):
"""Version 2 API controller root."""
versions = [{"id": "v2.0", "status": "CURRENT"}]
versions = [{"id": "v2.0", "status": "DEPRECATED"}]
_routes = {}
def _log_missing_plugins(self, names):

View File

@ -104,3 +104,8 @@ class UnsupportedAPIVersion(BlazarException):
class InvalidStatus(BlazarException):
msg_fmt = _("Invalid lease status.")
class InvalidAPIVersionString(BlazarException):
message = _("API Version String %(version)s is of invalid format. Must "
"be of format MajorNum.MinorNum.")

View File

@ -21,7 +21,7 @@ class TestRoot(api.APITest):
super(TestRoot, self).setUp()
self.versions = {
"versions":
[{"status": "CURRENT",
[{"status": "DEPRECATED",
"id": "v2.0",
"links": [{"href": "http://localhost/v2", "rel": "self"}]}]}

View File

@ -21,6 +21,7 @@ from testtools import matchers
from oslo_middleware import request_id as id
from blazar.api import context as api_context
from blazar.api.v1 import api_version_request
from blazar.api.v1.leases import service as service_api
from blazar.api.v1.leases import v1_0 as leases_api_v1_0
from blazar.api.v1 import request_id
@ -79,7 +80,8 @@ class LeaseAPITestCase(tests.TestCase):
def setUp(self):
super(LeaseAPITestCase, self).setUp()
self.app = make_app()
self.headers = {'Accept': 'application/json'}
self.headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.0'}
self.lease_uuid = six.text_type(uuidutils.generate_uuid())
self.mock_ctx = self.patch(api_context, 'ctx_from_headers')
self.mock_ctx.return_value = context.BlazarContext(
@ -91,12 +93,22 @@ class LeaseAPITestCase(tests.TestCase):
self.delete_lease = self.patch(service_api.API, 'delete_lease')
def _assert_response(self, actual_resp, expected_status_code,
expected_resp_body, key='lease'):
expected_resp_body, key='lease',
expected_api_version='reservation 1.0'):
res_id = actual_resp.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID, actual_resp.headers)
api_version = actual_resp.headers.get(
api_version_request.API_VERSION_REQUEST_HEADER)
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID,
actual_resp.headers)
self.assertIn(api_version_request.API_VERSION_REQUEST_HEADER,
actual_resp.headers)
self.assertIn(api_version_request.VARY_HEADER, actual_resp.headers)
self.assertThat(res_id, matchers.StartsWith('req-'))
self.assertEqual(expected_status_code, actual_resp.status_code)
self.assertEqual(expected_resp_body, actual_resp.get_json()[key])
self.assertEqual(expected_api_version, api_version)
self.assertEqual('OpenStack-API-Version', actual_resp.headers.get(
api_version_request.VARY_HEADER))
def test_list(self):
with self.app.test_client() as c:
@ -104,6 +116,13 @@ class LeaseAPITestCase(tests.TestCase):
res = c.get('/v1/leases', headers=self.headers)
self._assert_response(res, 200, [], key='leases')
def test_list_with_non_acceptable_api_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.2'}
with self.app.test_client() as c:
res = c.get('/v1/leases', headers=headers)
self.assertEqual(406, res.status_code)
def test_create(self):
with self.app.test_client() as c:
self.create_lease.return_value = fake_lease(id=self.lease_uuid)
@ -111,6 +130,14 @@ class LeaseAPITestCase(tests.TestCase):
id=self.lease_uuid), headers=self.headers)
self._assert_response(res, 201, fake_lease(id=self.lease_uuid))
def test_create_with_bad_api_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.a'}
with self.app.test_client() as c:
res = c.post('/v1/leases', json=fake_lease_request_body(
id=self.lease_uuid), headers=headers)
self.assertEqual(400, res.status_code)
def test_get(self):
with self.app.test_client() as c:
self.get_lease.return_value = fake_lease(id=self.lease_uuid)
@ -118,7 +145,18 @@ class LeaseAPITestCase(tests.TestCase):
headers=self.headers)
self._assert_response(res, 200, fake_lease(id=self.lease_uuid))
def test_get_with_latest_api_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation latest'}
with self.app.test_client() as c:
self.get_lease.return_value = fake_lease(id=self.lease_uuid)
res = c.get('/v1/leases/{0}'.format(self.lease_uuid),
headers=headers)
self._assert_response(res, 200, fake_lease(id=self.lease_uuid),
expected_api_version='reservation 1.0')
def test_update(self):
headers = {'Accept': 'application/json'}
with self.app.test_client() as c:
self.fake_lease = fake_lease(id=self.lease_uuid, name='updated')
self.fake_lease_body = fake_lease_request_body(
@ -129,7 +167,23 @@ class LeaseAPITestCase(tests.TestCase):
self.update_lease.return_value = self.fake_lease
res = c.put('/v1/leases/{0}'.format(self.lease_uuid),
json=self.fake_lease_body, headers=self.headers)
json=self.fake_lease_body, headers=headers)
self._assert_response(res, 200, self.fake_lease)
def test_update_with_no_service_type_in_header(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': '1.0'}
with self.app.test_client() as c:
self.fake_lease = fake_lease(id=self.lease_uuid, name='updated')
self.fake_lease_body = fake_lease_request_body(
exclude=set(['reservations', 'events']),
id=self.lease_uuid,
name='updated'
)
self.update_lease.return_value = self.fake_lease
res = c.put('/v1/leases/{0}'.format(self.lease_uuid),
json=self.fake_lease_body, headers=headers)
self._assert_response(res, 200, self.fake_lease)
def test_delete(self):

View File

@ -22,6 +22,7 @@ from testtools import matchers
from oslo_middleware import request_id as id
from blazar.api import context as api_context
from blazar.api.v1 import api_version_request
from blazar.api.v1.oshosts import service as service_api
from blazar.api.v1.oshosts import v1_0 as hosts_api_v1_0
from blazar.api.v1 import request_id
@ -61,7 +62,7 @@ def fake_computehost(**kw):
" \"topology\": {\"cores\": 1}}",
),
u'extra_capas': kw.get('extra_capas',
{u'vgpus': 2, u'fruits': u'bananas'}),
{u'vgpus': 2, u'fruits': u'bananas'})
}
@ -82,7 +83,8 @@ class OsHostAPITestCase(tests.TestCase):
def setUp(self):
super(OsHostAPITestCase, self).setUp()
self.app = make_app()
self.headers = {'Accept': 'application/json'}
self.headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.0'}
self.host_id = six.text_type('1')
self.mock_ctx = self.patch(api_context, 'ctx_from_headers')
self.mock_ctx.return_value = context.BlazarContext(
@ -101,13 +103,22 @@ class OsHostAPITestCase(tests.TestCase):
self.get_allocations = self.patch(service_api.API, 'get_allocations')
def _assert_response(self, actual_resp, expected_status_code,
expected_resp_body, key='host'):
expected_resp_body, key='host',
expected_api_version='reservation 1.0'):
res_id = actual_resp.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
api_version = actual_resp.headers.get(
api_version_request.API_VERSION_REQUEST_HEADER)
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID,
actual_resp.headers)
self.assertIn(api_version_request.API_VERSION_REQUEST_HEADER,
actual_resp.headers)
self.assertIn(api_version_request.VARY_HEADER, actual_resp.headers)
self.assertThat(res_id, matchers.StartsWith('req-'))
self.assertEqual(expected_status_code, actual_resp.status_code)
self.assertEqual(expected_resp_body, actual_resp.get_json()[key])
self.assertEqual(expected_api_version, api_version)
self.assertEqual('OpenStack-API-Version', actual_resp.headers.get(
api_version_request.VARY_HEADER))
def test_list(self):
with self.app.test_client() as c:
@ -115,6 +126,13 @@ class OsHostAPITestCase(tests.TestCase):
res = c.get('/v1', headers=self.headers)
self._assert_response(res, 200, [], key='hosts')
def test_list_with_non_acceptable_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.2'}
with self.app.test_client() as c:
res = c.get('/v1', headers=headers)
self.assertEqual(406, res.status_code)
def test_create(self):
with self.app.test_client() as c:
self.create_computehost.return_value = fake_computehost(
@ -124,6 +142,14 @@ class OsHostAPITestCase(tests.TestCase):
self._assert_response(res, 201, fake_computehost(
id=self.host_id))
def test_create_with_bad_api_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation 1.a'}
with self.app.test_client() as c:
res = c.post('/v1', json=fake_computehost_request_body(
id=self.host_id), headers=headers)
self.assertEqual(400, res.status_code)
def test_get(self):
with self.app.test_client() as c:
self.get_computehost.return_value = fake_computehost(
@ -131,7 +157,18 @@ class OsHostAPITestCase(tests.TestCase):
res = c.get('/v1/{0}'.format(self.host_id), headers=self.headers)
self._assert_response(res, 200, fake_computehost(id=self.host_id))
def test_get_with_latest_api_version(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': 'reservation latest'}
with self.app.test_client() as c:
self.get_computehost.return_value = fake_computehost(
id=self.host_id)
res = c.get('/v1/{0}'.format(self.host_id), headers=headers)
self._assert_response(res, 200, fake_computehost(id=self.host_id),
expected_api_version='reservation 1.0')
def test_update(self):
headers = {'Accept': 'application/json'}
with self.app.test_client() as c:
self.fake_computehost = fake_computehost(id=self.host_id,
name='updated')
@ -142,7 +179,23 @@ class OsHostAPITestCase(tests.TestCase):
self.update_computehost.return_value = self.fake_computehost
res = c.put('/v1/{0}'.format(self.host_id),
json=self.fake_computehost_body, headers=self.headers)
json=self.fake_computehost_body, headers=headers)
self._assert_response(res, 200, self.fake_computehost, 'host')
def test_update_with_no_service_type_in_header(self):
headers = {'Accept': 'application/json',
'OpenStack-API-Version': '1.0'}
with self.app.test_client() as c:
self.fake_computehost = fake_computehost(id=self.host_id,
name='updated')
self.fake_computehost_body = fake_computehost_request_body(
id=self.host_id,
name='updated'
)
self.update_computehost.return_value = self.fake_computehost
res = c.put('/v1/{0}'.format(self.host_id),
json=self.fake_computehost_body, headers=headers)
self._assert_response(res, 200, self.fake_computehost, 'host')
def test_delete(self):

View File

@ -0,0 +1,121 @@
# Copyright (c) 2019 NTT DATA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ddt
import six
from blazar.api.v1 import api_version_request
from blazar import exceptions
from blazar import tests
@ddt.ddt
class APIVersionRequestTests(tests.TestCase):
def test_valid_version_strings(self):
def _test_string(version, exp_major, exp_minor):
v = api_version_request.APIVersionRequest(version)
self.assertEqual(v._ver_major, exp_major)
self.assertEqual(v._ver_minor, exp_minor)
_test_string("1.1", 1, 1)
_test_string("2.10", 2, 10)
_test_string("5.234", 5, 234)
_test_string("12.5", 12, 5)
_test_string("2.0", 2, 0)
_test_string("2.200", 2, 200)
def test_min_version(self):
self.assertEqual(
api_version_request.APIVersionRequest(
api_version_request.MIN_API_VERSION),
api_version_request.min_api_version())
def test_max_api_version(self):
self.assertEqual(
api_version_request.APIVersionRequest(
api_version_request.MAX_API_VERSION),
api_version_request.max_api_version())
def test_null_version(self):
v = api_version_request.APIVersionRequest()
self.assertTrue(v.is_null())
def test_not_null_version(self):
v = api_version_request.APIVersionRequest('1.1')
self.assertTrue(bool(v))
@ddt.data("1", "100", "1.1.4", "100.23.66.3", "1 .1", "1. 1",
"1.03", "01.1", "1.001", "", " 1.1", "1.1 ")
def test_invalid_version_strings(self, version_string):
self.assertRaises(exceptions.InvalidAPIVersionString,
api_version_request.APIVersionRequest,
version_string)
def test_version_comparisons(self):
vers1 = api_version_request.APIVersionRequest("1.0")
vers2 = api_version_request.APIVersionRequest("1.5")
vers3 = api_version_request.APIVersionRequest("2.23")
vers4 = api_version_request.APIVersionRequest("2.0")
v_null = api_version_request.APIVersionRequest()
self.assertLess(v_null, vers2)
self.assertLess(vers1, vers2)
self.assertLessEqual(vers1, vers2)
self.assertLessEqual(vers1, vers4)
self.assertGreater(vers2, v_null)
self.assertGreater(vers3, vers2)
self.assertGreaterEqual(vers4, vers1)
self.assertGreaterEqual(vers3, vers2)
self.assertNotEqual(vers1, vers2)
self.assertNotEqual(vers1, vers4)
self.assertNotEqual(vers1, v_null)
self.assertEqual(v_null, v_null)
self.assertRaises(TypeError, vers1.__lt__, "2.1")
self.assertRaises(TypeError, vers1.__gt__, "2.1")
self.assertRaises(TypeError, vers1.__eq__, "1.0")
def test_version_matches(self):
vers1 = api_version_request.APIVersionRequest("1.0")
vers2 = api_version_request.APIVersionRequest("1.1")
vers3 = api_version_request.APIVersionRequest("1.2")
vers4 = api_version_request.APIVersionRequest("2.0")
vers5 = api_version_request.APIVersionRequest("1.1")
v_null = api_version_request.APIVersionRequest()
self.assertTrue(vers2.matches(vers1, vers3))
self.assertTrue(vers2.matches(v_null, vers5))
self.assertTrue(vers2.matches(vers1, v_null))
self.assertTrue(vers1.matches(v_null, v_null))
self.assertFalse(vers2.matches(vers3, vers4))
self.assertRaises(ValueError, v_null.matches, vers1, vers3)
def test_get_string(self):
vers1_string = "1.13"
vers1 = api_version_request.APIVersionRequest(vers1_string)
self.assertEqual(vers1_string, vers1.get_string())
self.assertRaises(ValueError,
api_version_request.APIVersionRequest().get_string)
@ddt.data(('1', '0'), ('1', '1'))
@ddt.unpack
def test_str(self, major, minor):
request_input = '%s.%s' % (major, minor)
request = api_version_request.APIVersionRequest(request_input)
request_string = six.text_type(request)
self.assertEqual('API Version Request '
'Major: %s, Minor: %s' % (major, minor),
request_string)

View File

@ -62,6 +62,7 @@ class AppTestCase(tests.TestCase):
"versions": [
{"id": "v1.0",
"status": "CURRENT",
'min_version': '1.0', 'max_version': '1.0',
"links": [{"href": "{0}v1".format(flask.request.host_url),
"rel": "self"}]
},

View File

@ -61,6 +61,11 @@ For Contributors
contributor/index
.. toctree::
:maxdepth: 2
reference/index
Specs
-----

View File

@ -0,0 +1 @@
.. include:: ../../../blazar/api/v1/rest_api_version_history.rst

View File

@ -2,4 +2,13 @@
References
==========
None
The blazar project has lots of complicated parts in it where
it helps to have an overview to understand how the internals of a particular
part work.
Internals
=========
The following is a dive into some of the internals in blazar.
* :doc:`/reference/api-microversion-history`: How blazar uses API microversion.

View File

@ -41,6 +41,7 @@ logutils==0.3.5
Mako==1.0.7
MarkupSafe==1.0
mccabe==0.2.1
microversion-parse===0.2.1
mock==2.0.0
monotonic==1.4
mox3==0.25.0

View File

@ -0,0 +1,8 @@
---
features:
- |
Blazar now supports API microversions. All API changes should be made while
keeping backward compatibility. The API version is specified in the
``OpenStack-API-Version`` HTTP header. To view the mininum and maximum
supported versions by API, access the ``/`` and ``/versions`` resources.
The Blazar API will include supported versions in the response data.

View File

@ -11,6 +11,7 @@ iso8601>=0.1.11 # MIT
keystoneauth1>=3.4.0 # Apache-2.0
keystonemiddleware>=4.17.0 # Apache-2.0
kombu!=4.0.2,>=4.0.0 # BSD
microversion-parse>=0.2.1 # Apache-2.0
oslo.concurrency>=3.26.0 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.db>=4.27.0 # Apache-2.0