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:
parent
d19fa2a839
commit
520005b13d
176
blazar/api/v1/api_version_request.py
Normal file
176
blazar/api/v1/api_version_request.py
Normal 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})
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
23
blazar/api/v1/rest_api_version_history.rst
Normal file
23
blazar/api/v1/rest_api_version_history.rst
Normal 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.
|
@ -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."""
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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.")
|
||||
|
@ -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"}]}]}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
121
blazar/tests/api/v1/test_api_version_request.py
Normal file
121
blazar/tests/api/v1/test_api_version_request.py
Normal 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)
|
@ -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"}]
|
||||
},
|
||||
|
@ -61,6 +61,11 @@ For Contributors
|
||||
|
||||
contributor/index
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
reference/index
|
||||
|
||||
Specs
|
||||
-----
|
||||
|
||||
|
1
doc/source/reference/api-microversion-history.rst
Normal file
1
doc/source/reference/api-microversion-history.rst
Normal file
@ -0,0 +1 @@
|
||||
.. include:: ../../../blazar/api/v1/rest_api_version_history.rst
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user