Merge "cinder-api-microversions code"
This commit is contained in:
commit
26ac08ca79
@ -28,11 +28,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def root_app_factory(loader, global_conf, **local_conf):
|
def root_app_factory(loader, global_conf, **local_conf):
|
||||||
if CONF.enable_v1_api:
|
if CONF.enable_v1_api:
|
||||||
LOG.warning(_LW('The v1 api is deprecated and will be removed in the '
|
LOG.warning(_LW('The v1 api is deprecated and is not under active '
|
||||||
'Liberty release. You should set enable_v1_api=false '
|
'development. You should set enable_v1_api=false '
|
||||||
'and enable_v2_api=true in your cinder.conf file.'))
|
'and enable_v3_api=true in your cinder.conf file.'))
|
||||||
else:
|
|
||||||
del local_conf['/v1']
|
|
||||||
if not CONF.enable_v2_api:
|
|
||||||
del local_conf['/v2']
|
|
||||||
return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
|
return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
|
||||||
|
@ -38,6 +38,16 @@ api_common_opts = [
|
|||||||
help='Base URL that will be presented to users in links '
|
help='Base URL that will be presented to users in links '
|
||||||
'to the OpenStack Volume API',
|
'to the OpenStack Volume API',
|
||||||
deprecated_name='osapi_compute_link_prefix'),
|
deprecated_name='osapi_compute_link_prefix'),
|
||||||
|
cfg.ListOpt('query_volume_filters',
|
||||||
|
default=['name', 'status', 'metadata',
|
||||||
|
'availability_zone',
|
||||||
|
'bootable'],
|
||||||
|
help="Volume filter options which "
|
||||||
|
"non-admin user could use to "
|
||||||
|
"query volumes. Default values "
|
||||||
|
"are: ['name', 'status', "
|
||||||
|
"'metadata', 'availability_zone' ,"
|
||||||
|
"'bootable']")
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
164
cinder/api/openstack/api_version_request.py
Normal file
164
cinder/api/openstack/api_version_request.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Copyright 2014 IBM Corp.
|
||||||
|
# Copyright 2015 Clinton Knight
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 cinder.api.openstack import versioned_method
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder import utils
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
* 3.0 - Includes all V2 APIs and extensions. V1 API is still supported.
|
||||||
|
* 3.0 - Versions API updated to reflect beginning of microversions epoch.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The minimum and maximum versions of the API supported
|
||||||
|
# The default api version request is defined to be the
|
||||||
|
# the minimum version of the API supported.
|
||||||
|
# Explicitly using /v1 or /v2 enpoints will still work
|
||||||
|
_MIN_API_VERSION = "3.0"
|
||||||
|
_MAX_API_VERSION = "3.0"
|
||||||
|
_LEGACY_API_VERSION1 = "1.0"
|
||||||
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_api_version1():
|
||||||
|
return APIVersionRequest(_LEGACY_API_VERSION1)
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_api_version2():
|
||||||
|
return APIVersionRequest(_LEGACY_API_VERSION2)
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersionRequest(utils.ComparableMixin):
|
||||||
|
"""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, version_string=None, experimental=False):
|
||||||
|
"""Create an API version request object."""
|
||||||
|
self._ver_major = None
|
||||||
|
self._ver_minor = None
|
||||||
|
|
||||||
|
if version_string is not None:
|
||||||
|
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
|
||||||
|
version_string)
|
||||||
|
if match:
|
||||||
|
self._ver_major = int(match.group(1))
|
||||||
|
self._ver_minor = int(match.group(2))
|
||||||
|
else:
|
||||||
|
raise exception.InvalidAPIVersionString(version=version_string)
|
||||||
|
|
||||||
|
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 is_null(self):
|
||||||
|
return self._ver_major is None and self._ver_minor is None
|
||||||
|
|
||||||
|
def _cmpkey(self):
|
||||||
|
"""Return the value used by ComparableMixin for rich comparisons."""
|
||||||
|
return self._ver_major, self._ver_minor
|
||||||
|
|
||||||
|
def matches_versioned_method(self, method):
|
||||||
|
"""Compares this version to that of a versioned method."""
|
||||||
|
|
||||||
|
if type(method) != versioned_method.VersionedMethod:
|
||||||
|
msg = _('An API version request must be compared '
|
||||||
|
'to a VersionedMethod object.')
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
return self.matches(method.start_version,
|
||||||
|
method.end_version,
|
||||||
|
method.experimental)
|
||||||
|
|
||||||
|
def matches(self, min_version, max_version, experimental=False):
|
||||||
|
"""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 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})
|
30
cinder/api/openstack/rest_api_version_history.rst
Normal file
30
cinder/api/openstack/rest_api_version_history.rst
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
3.0
|
||||||
|
---
|
||||||
|
The 3.0 Cinder API includes all v2 core APIs existing prior to
|
||||||
|
the introduction of microversions. The /v3 URL is used to call
|
||||||
|
3.0 APIs.
|
||||||
|
This it the initial version of the Cinder API which supports
|
||||||
|
microversions.
|
||||||
|
|
||||||
|
A user can specify a header in the API request::
|
||||||
|
|
||||||
|
OpenStack-Volume-microversion: <version>
|
||||||
|
|
||||||
|
where ``<version>`` is any valid api version for this API.
|
||||||
|
|
||||||
|
If no version is specified then the API will behave as if version 3.0
|
||||||
|
was requested.
|
||||||
|
|
||||||
|
The only API change in version 3.0 is versions, i.e.
|
||||||
|
GET http://localhost:8786/, which now returns information about
|
||||||
|
3.0 and later versions and their respective /v3 endpoints.
|
||||||
|
|
||||||
|
All other 3.0 APIs are functionally identical to version 2.0.
|
48
cinder/api/openstack/versioned_method.py
Normal file
48
cinder/api/openstack/versioned_method.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Copyright 2014 IBM Corp.
|
||||||
|
# Copyright 2015 Clinton Knight
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from cinder import utils
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedMethod(utils.ComparableMixin):
|
||||||
|
|
||||||
|
def __init__(self, name, start_version, end_version, experimental, func):
|
||||||
|
"""Versioning information for a single method.
|
||||||
|
|
||||||
|
Minimum and maximums are inclusive.
|
||||||
|
|
||||||
|
:param name: Name of the method
|
||||||
|
:param start_version: Minimum acceptable version
|
||||||
|
:param end_version: Maximum acceptable_version
|
||||||
|
:param func: Method to call
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.start_version = start_version
|
||||||
|
self.end_version = end_version
|
||||||
|
self.experimental = experimental
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
args = {
|
||||||
|
'name': self.name,
|
||||||
|
'start': self.start_version,
|
||||||
|
'end': self.end_version
|
||||||
|
}
|
||||||
|
return ("Version Method %(name)s: min: %(start)s, max: %(end)s" % args)
|
||||||
|
|
||||||
|
def _cmpkey(self):
|
||||||
|
"""Return the value used by ComparableMixin for rich comparisons."""
|
||||||
|
return self.start_version
|
@ -14,6 +14,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
@ -25,9 +26,13 @@ from oslo_log import log as logging
|
|||||||
from oslo_log import versionutils
|
from oslo_log import versionutils
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import strutils
|
||||||
import six
|
import six
|
||||||
import webob
|
import webob
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
|
from cinder.api.openstack import api_version_request as api_version
|
||||||
|
from cinder.api.openstack import versioned_method
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder import i18n
|
from cinder import i18n
|
||||||
from cinder.i18n import _, _LE, _LI
|
from cinder.i18n import _, _LE, _LI
|
||||||
@ -58,12 +63,22 @@ _MEDIA_TYPE_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# name of attribute to keep version method information
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
class Request(webob.Request):
|
class Request(webob.Request):
|
||||||
"""Add some OpenStack API-specific logic to the base webob.Request."""
|
"""Add some OpenStack API-specific logic to the base webob.Request."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Request, self).__init__(*args, **kwargs)
|
super(Request, self).__init__(*args, **kwargs)
|
||||||
self._resource_cache = {}
|
self._resource_cache = {}
|
||||||
|
if not hasattr(self, 'api_version_request'):
|
||||||
|
self.api_version_request = api_version.APIVersionRequest()
|
||||||
|
|
||||||
def cache_resource(self, resource_to_cache, id_attribute='id', name=None):
|
def cache_resource(self, resource_to_cache, id_attribute='id', name=None):
|
||||||
"""Cache the given resource.
|
"""Cache the given resource.
|
||||||
@ -269,6 +284,45 @@ class Request(webob.Request):
|
|||||||
all_languages = i18n.get_available_languages()
|
all_languages = i18n.get_available_languages()
|
||||||
return self.accept_language.best_match(all_languages)
|
return self.accept_language.best_match(all_languages)
|
||||||
|
|
||||||
|
def set_api_version_request(self, url):
|
||||||
|
"""Set API version request based on the request header information.
|
||||||
|
|
||||||
|
Microversions starts with /v3, so if a client sends a request for
|
||||||
|
version 1.0 or 2.0 with the /v3 endpoint, throw an exception.
|
||||||
|
Sending a header with any microversion to a /v1 or /v2 endpoint will
|
||||||
|
be ignored.
|
||||||
|
Note that a microversion must be set for the legacy endpoints. This
|
||||||
|
will appear as 1.0 and 2.0 for /v1 and /v2.
|
||||||
|
"""
|
||||||
|
if API_VERSION_REQUEST_HEADER in self.headers and 'v3' in url:
|
||||||
|
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':
|
||||||
|
self.api_version_request = api_version.max_api_version()
|
||||||
|
else:
|
||||||
|
self.api_version_request = api_version.APIVersionRequest(
|
||||||
|
hdr_string)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
if 'v1' in url:
|
||||||
|
self.api_version_request = api_version.legacy_api_version1()
|
||||||
|
elif 'v2' in url:
|
||||||
|
self.api_version_request = api_version.legacy_api_version2()
|
||||||
|
else:
|
||||||
|
self.api_version_request = api_version.APIVersionRequest(
|
||||||
|
api_version._MIN_API_VERSION)
|
||||||
|
|
||||||
|
|
||||||
class ActionDispatcher(object):
|
class ActionDispatcher(object):
|
||||||
"""Maps method name to local methods through action name."""
|
"""Maps method name to local methods through action name."""
|
||||||
@ -276,7 +330,7 @@ class ActionDispatcher(object):
|
|||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
"""Find and call local method."""
|
"""Find and call local method."""
|
||||||
action = kwargs.pop('action', 'default')
|
action = kwargs.pop('action', 'default')
|
||||||
action_method = getattr(self, str(action), self.default)
|
action_method = getattr(self, six.text_type(action), self.default)
|
||||||
return action_method(*args, **kwargs)
|
return action_method(*args, **kwargs)
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
@ -571,7 +625,7 @@ class ResponseObject(object):
|
|||||||
optional.
|
optional.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, obj, code=None, **serializers):
|
def __init__(self, obj, code=None, headers=None, **serializers):
|
||||||
"""Binds serializers with an object.
|
"""Binds serializers with an object.
|
||||||
|
|
||||||
Takes keyword arguments akin to the @serializer() decorator
|
Takes keyword arguments akin to the @serializer() decorator
|
||||||
@ -584,7 +638,7 @@ class ResponseObject(object):
|
|||||||
self.serializers = serializers
|
self.serializers = serializers
|
||||||
self._default_code = 200
|
self._default_code = 200
|
||||||
self._code = code
|
self._code = code
|
||||||
self._headers = {}
|
self._headers = headers or {}
|
||||||
self.serializer = None
|
self.serializer = None
|
||||||
self.media_type = None
|
self.media_type = None
|
||||||
|
|
||||||
@ -677,8 +731,8 @@ class ResponseObject(object):
|
|||||||
response = webob.Response()
|
response = webob.Response()
|
||||||
response.status_int = self.code
|
response.status_int = self.code
|
||||||
for hdr, value in self._headers.items():
|
for hdr, value in self._headers.items():
|
||||||
response.headers[hdr] = value
|
response.headers[hdr] = six.text_type(value)
|
||||||
response.headers['Content-Type'] = content_type
|
response.headers['Content-Type'] = six.text_type(content_type)
|
||||||
if self.obj is not None:
|
if self.obj is not None:
|
||||||
body = serializer.serialize(self.obj)
|
body = serializer.serialize(self.obj)
|
||||||
if isinstance(body, six.text_type):
|
if isinstance(body, six.text_type):
|
||||||
@ -743,10 +797,13 @@ class ResourceExceptionHandler(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if isinstance(ex_value, exception.NotAuthorized):
|
if isinstance(ex_value, exception.NotAuthorized):
|
||||||
raise Fault(webob.exc.HTTPForbidden(explanation=ex_value.msg))
|
msg = six.text_type(ex_value)
|
||||||
|
raise Fault(webob.exc.HTTPForbidden(explanation=msg))
|
||||||
|
elif isinstance(ex_value, exception.VersionNotFoundForAPIMethod):
|
||||||
|
raise
|
||||||
elif isinstance(ex_value, exception.Invalid):
|
elif isinstance(ex_value, exception.Invalid):
|
||||||
raise Fault(exception.ConvertedException(
|
raise Fault(exception.ConvertedException(
|
||||||
code=ex_value.code, explanation=ex_value.msg))
|
code=ex_value.code, explanation=six.text_type(ex_value)))
|
||||||
elif isinstance(ex_value, TypeError):
|
elif isinstance(ex_value, TypeError):
|
||||||
exc_info = (ex_type, ex_value, ex_traceback)
|
exc_info = (ex_type, ex_value, ex_traceback)
|
||||||
LOG.error(_LE(
|
LOG.error(_LE(
|
||||||
@ -754,10 +811,10 @@ class ResourceExceptionHandler(object):
|
|||||||
ex_value, exc_info=exc_info)
|
ex_value, exc_info=exc_info)
|
||||||
raise Fault(webob.exc.HTTPBadRequest())
|
raise Fault(webob.exc.HTTPBadRequest())
|
||||||
elif isinstance(ex_value, Fault):
|
elif isinstance(ex_value, Fault):
|
||||||
LOG.info(_LI("Fault thrown: %s"), ex_value)
|
LOG.info(_LI("Fault thrown: %s"), six.text_type(ex_value))
|
||||||
raise ex_value
|
raise ex_value
|
||||||
elif isinstance(ex_value, webob.exc.HTTPException):
|
elif isinstance(ex_value, webob.exc.HTTPException):
|
||||||
LOG.info(_LI("HTTP exception thrown: %s"), ex_value)
|
LOG.info(_LI("HTTP exception thrown: %s"), six.text_type(ex_value))
|
||||||
raise Fault(ex_value)
|
raise Fault(ex_value)
|
||||||
|
|
||||||
# We didn't handle the exception
|
# We didn't handle the exception
|
||||||
@ -778,6 +835,7 @@ class Resource(wsgi.Application):
|
|||||||
Exceptions derived from webob.exc.HTTPException will be automatically
|
Exceptions derived from webob.exc.HTTPException will be automatically
|
||||||
wrapped in Fault() to provide API friendly error responses.
|
wrapped in Fault() to provide API friendly error responses.
|
||||||
"""
|
"""
|
||||||
|
support_api_request_version = True
|
||||||
|
|
||||||
def __init__(self, controller, action_peek=None, **deserializers):
|
def __init__(self, controller, action_peek=None, **deserializers):
|
||||||
"""Initialize Resource.
|
"""Initialize Resource.
|
||||||
@ -943,6 +1001,11 @@ class Resource(wsgi.Application):
|
|||||||
with ResourceExceptionHandler():
|
with ResourceExceptionHandler():
|
||||||
response = ext(req=request, resp_obj=resp_obj,
|
response = ext(req=request, resp_obj=resp_obj,
|
||||||
**action_args)
|
**action_args)
|
||||||
|
except exception.VersionNotFoundForAPIMethod:
|
||||||
|
# If an attached extension (@wsgi.extends) for the
|
||||||
|
# method has no version match its not an error. We
|
||||||
|
# just don't run the extends code
|
||||||
|
continue
|
||||||
except Fault as ex:
|
except Fault as ex:
|
||||||
response = ex
|
response = ex
|
||||||
|
|
||||||
@ -960,6 +1023,17 @@ class Resource(wsgi.Application):
|
|||||||
{"method": request.method,
|
{"method": request.method,
|
||||||
"url": request.url})
|
"url": request.url})
|
||||||
|
|
||||||
|
if self.support_api_request_version:
|
||||||
|
# Set the version of the API requested based on the header
|
||||||
|
try:
|
||||||
|
request.set_api_version_request(request.url)
|
||||||
|
except exception.InvalidAPIVersionString as e:
|
||||||
|
return Fault(webob.exc.HTTPBadRequest(
|
||||||
|
explanation=six.text_type(e)))
|
||||||
|
except exception.InvalidGlobalAPIVersion as e:
|
||||||
|
return Fault(webob.exc.HTTPNotAcceptable(
|
||||||
|
explanation=six.text_type(e)))
|
||||||
|
|
||||||
# Identify the action, its arguments, and the requested
|
# Identify the action, its arguments, and the requested
|
||||||
# content type
|
# content type
|
||||||
action_args = self.get_action_args(request.environ)
|
action_args = self.get_action_args(request.environ)
|
||||||
@ -992,6 +1066,16 @@ class Resource(wsgi.Application):
|
|||||||
msg = _("Malformed request body")
|
msg = _("Malformed request body")
|
||||||
return Fault(webob.exc.HTTPBadRequest(explanation=msg))
|
return Fault(webob.exc.HTTPBadRequest(explanation=msg))
|
||||||
|
|
||||||
|
if body:
|
||||||
|
msg = ("Action: '%(action)s', calling method: %(meth)s, body: "
|
||||||
|
"%(body)s") % {'action': action,
|
||||||
|
'body': six.text_type(body),
|
||||||
|
'meth': six.text_type(meth)}
|
||||||
|
LOG.debug(strutils.mask_password(msg))
|
||||||
|
else:
|
||||||
|
LOG.debug("Calling method '%(meth)s'",
|
||||||
|
{'meth': six.text_type(meth)})
|
||||||
|
|
||||||
# Now, deserialize the request body...
|
# Now, deserialize the request body...
|
||||||
try:
|
try:
|
||||||
if content_type:
|
if content_type:
|
||||||
@ -1029,7 +1113,7 @@ class Resource(wsgi.Application):
|
|||||||
# No exceptions; convert action_result into a
|
# No exceptions; convert action_result into a
|
||||||
# ResponseObject
|
# ResponseObject
|
||||||
resp_obj = None
|
resp_obj = None
|
||||||
if type(action_result) is dict or action_result is None:
|
if isinstance(action_result, dict) or action_result is None:
|
||||||
resp_obj = ResponseObject(action_result)
|
resp_obj = ResponseObject(action_result)
|
||||||
elif isinstance(action_result, ResponseObject):
|
elif isinstance(action_result, ResponseObject):
|
||||||
resp_obj = action_result
|
resp_obj = action_result
|
||||||
@ -1063,6 +1147,21 @@ class Resource(wsgi.Application):
|
|||||||
|
|
||||||
LOG.info(msg, msg_dict)
|
LOG.info(msg, msg_dict)
|
||||||
|
|
||||||
|
if hasattr(response, 'headers'):
|
||||||
|
for hdr, val in response.headers.items():
|
||||||
|
# Headers must be utf-8 strings
|
||||||
|
try:
|
||||||
|
# python 2.x
|
||||||
|
response.headers[hdr] = val.encode('utf-8')
|
||||||
|
except Exception:
|
||||||
|
# python 3.x
|
||||||
|
response.headers[hdr] = six.text_type(val)
|
||||||
|
|
||||||
|
if not request.api_version_request.is_null():
|
||||||
|
response.headers[API_VERSION_REQUEST_HEADER] = (
|
||||||
|
request.api_version_request.get_string())
|
||||||
|
response.headers['Vary'] = API_VERSION_REQUEST_HEADER
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_method(self, request, action, content_type, body):
|
def get_method(self, request, action, content_type, body):
|
||||||
@ -1101,7 +1200,13 @@ class Resource(wsgi.Application):
|
|||||||
def dispatch(self, method, request, action_args):
|
def dispatch(self, method, request, action_args):
|
||||||
"""Dispatch a call to the action-specific method."""
|
"""Dispatch a call to the action-specific method."""
|
||||||
|
|
||||||
return method(req=request, **action_args)
|
try:
|
||||||
|
return method(req=request, **action_args)
|
||||||
|
except exception.VersionNotFoundForAPIMethod:
|
||||||
|
# We deliberately don't return any message information
|
||||||
|
# about the exception to the user so it looks as if
|
||||||
|
# the method is simply not implemented.
|
||||||
|
return Fault(webob.exc.HTTPNotFound())
|
||||||
|
|
||||||
|
|
||||||
def action(name):
|
def action(name):
|
||||||
@ -1161,9 +1266,22 @@ class ControllerMetaclass(type):
|
|||||||
# Find all actions
|
# Find all actions
|
||||||
actions = {}
|
actions = {}
|
||||||
extensions = []
|
extensions = []
|
||||||
|
versioned_methods = None
|
||||||
# start with wsgi actions from base classes
|
# start with wsgi actions from base classes
|
||||||
for base in bases:
|
for base in bases:
|
||||||
actions.update(getattr(base, 'wsgi_actions', {}))
|
actions.update(getattr(base, 'wsgi_actions', {}))
|
||||||
|
|
||||||
|
if base.__name__ == "Controller":
|
||||||
|
# NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
|
||||||
|
# between API controller class creations. This allows us
|
||||||
|
# to use a class decorator on the API methods that doesn't
|
||||||
|
# require naming explicitly what method is being versioned as
|
||||||
|
# it can be implicit based on the method decorated. It is a bit
|
||||||
|
# ugly.
|
||||||
|
if VER_METHOD_ATTR in base.__dict__:
|
||||||
|
versioned_methods = getattr(base, VER_METHOD_ATTR)
|
||||||
|
delattr(base, VER_METHOD_ATTR)
|
||||||
|
|
||||||
for key, value in cls_dict.items():
|
for key, value in cls_dict.items():
|
||||||
if not callable(value):
|
if not callable(value):
|
||||||
continue
|
continue
|
||||||
@ -1175,6 +1293,8 @@ class ControllerMetaclass(type):
|
|||||||
# Add the actions and extensions to the class dict
|
# Add the actions and extensions to the class dict
|
||||||
cls_dict['wsgi_actions'] = actions
|
cls_dict['wsgi_actions'] = actions
|
||||||
cls_dict['wsgi_extensions'] = extensions
|
cls_dict['wsgi_extensions'] = extensions
|
||||||
|
if versioned_methods:
|
||||||
|
cls_dict[VER_METHOD_ATTR] = versioned_methods
|
||||||
|
|
||||||
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
|
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
|
||||||
cls_dict)
|
cls_dict)
|
||||||
@ -1195,6 +1315,99 @@ class Controller(object):
|
|||||||
else:
|
else:
|
||||||
self._view_builder = None
|
self._view_builder = None
|
||||||
|
|
||||||
|
def __getattribute__(self, key):
|
||||||
|
|
||||||
|
def version_select(*args, **kwargs):
|
||||||
|
"""Select and call the matching version of the specified method.
|
||||||
|
|
||||||
|
Look for the method which matches the name supplied and version
|
||||||
|
constraints and calls it with the supplied arguments.
|
||||||
|
|
||||||
|
:returns: Returns the result of the method called
|
||||||
|
:raises: VersionNotFoundForAPIMethod if there is no method which
|
||||||
|
matches the name and version constraints
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The first arg to all versioned methods is always the request
|
||||||
|
# object. The version for the request is attached to the
|
||||||
|
# request object
|
||||||
|
if len(args) == 0:
|
||||||
|
version_request = kwargs['req'].api_version_request
|
||||||
|
else:
|
||||||
|
version_request = args[0].api_version_request
|
||||||
|
|
||||||
|
func_list = self.versioned_methods[key]
|
||||||
|
for func in func_list:
|
||||||
|
if version_request.matches_versioned_method(func):
|
||||||
|
# Update the version_select wrapper function so
|
||||||
|
# other decorator attributes like wsgi.response
|
||||||
|
# are still respected.
|
||||||
|
functools.update_wrapper(version_select, func.func)
|
||||||
|
return func.func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
# No version match
|
||||||
|
raise exception.VersionNotFoundForAPIMethod(
|
||||||
|
version=version_request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
|
||||||
|
except AttributeError:
|
||||||
|
# No versioning on this class
|
||||||
|
return object.__getattribute__(self, key)
|
||||||
|
|
||||||
|
if (version_meth_dict and key in
|
||||||
|
object.__getattribute__(self, VER_METHOD_ATTR)):
|
||||||
|
|
||||||
|
return version_select
|
||||||
|
|
||||||
|
return object.__getattribute__(self, key)
|
||||||
|
|
||||||
|
# NOTE(cyeoh): This decorator MUST appear first (the outermost
|
||||||
|
# decorator) on an API method for it to work correctly
|
||||||
|
@classmethod
|
||||||
|
def api_version(cls, min_ver, max_ver=None, experimental=False):
|
||||||
|
"""Decorator for versioning API methods.
|
||||||
|
|
||||||
|
Add the decorator to any method which takes a request object
|
||||||
|
as the first parameter and belongs to a class which inherits from
|
||||||
|
wsgi.Controller.
|
||||||
|
|
||||||
|
:param min_ver: string representing minimum version
|
||||||
|
:param max_ver: optional string representing maximum version
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
obj_min_ver = api_version.APIVersionRequest(min_ver)
|
||||||
|
if max_ver:
|
||||||
|
obj_max_ver = api_version.APIVersionRequest(max_ver)
|
||||||
|
else:
|
||||||
|
obj_max_ver = api_version.APIVersionRequest()
|
||||||
|
|
||||||
|
# Add to list of versioned methods registered
|
||||||
|
func_name = f.__name__
|
||||||
|
new_func = versioned_method.VersionedMethod(
|
||||||
|
func_name, obj_min_ver, obj_max_ver, experimental, f)
|
||||||
|
|
||||||
|
func_dict = getattr(cls, VER_METHOD_ATTR, {})
|
||||||
|
if not func_dict:
|
||||||
|
setattr(cls, VER_METHOD_ATTR, func_dict)
|
||||||
|
|
||||||
|
func_list = func_dict.get(func_name, [])
|
||||||
|
if not func_list:
|
||||||
|
func_dict[func_name] = func_list
|
||||||
|
func_list.append(new_func)
|
||||||
|
# Ensure the list is sorted by minimum version (reversed)
|
||||||
|
# so later when we work through the list in order we find
|
||||||
|
# the method which has the latest version which supports
|
||||||
|
# the version requested.
|
||||||
|
# TODO(cyeoh): Add check to ensure that there are no overlapping
|
||||||
|
# ranges of valid versions as that is ambiguous
|
||||||
|
func_list.sort(reverse=True)
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_valid_body(body, entity_name):
|
def is_valid_body(body, entity_name):
|
||||||
if not (body and entity_name in body):
|
if not (body and entity_name in body):
|
||||||
@ -1330,6 +1543,11 @@ class Fault(webob.exc.HTTPException):
|
|||||||
if retry:
|
if retry:
|
||||||
fault_data[fault_name]['retryAfter'] = retry
|
fault_data[fault_name]['retryAfter'] = retry
|
||||||
|
|
||||||
|
if not req.api_version_request.is_null():
|
||||||
|
self.wrapped_exc.headers[API_VERSION_REQUEST_HEADER] = (
|
||||||
|
req.api_version_request.get_string())
|
||||||
|
self.wrapped_exc.headers['Vary'] = API_VERSION_REQUEST_HEADER
|
||||||
|
|
||||||
# 'code' is an attribute on the fault tag itself
|
# 'code' is an attribute on the fault tag itself
|
||||||
metadata = {'attributes': {fault_name: 'code'}}
|
metadata = {'attributes': {fault_name: 'code'}}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||||||
self.resources['versions'] = versions.create_resource()
|
self.resources['versions'] = versions.create_resource()
|
||||||
mapper.connect("versions", "/",
|
mapper.connect("versions", "/",
|
||||||
controller=self.resources['versions'],
|
controller=self.resources['versions'],
|
||||||
action='show')
|
action='index')
|
||||||
|
|
||||||
mapper.redirect("", "/")
|
mapper.redirect("", "/")
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||||||
self.resources['versions'] = versions.create_resource()
|
self.resources['versions'] = versions.create_resource()
|
||||||
mapper.connect("versions", "/",
|
mapper.connect("versions", "/",
|
||||||
controller=self.resources['versions'],
|
controller=self.resources['versions'],
|
||||||
action='show')
|
action='index')
|
||||||
|
|
||||||
mapper.redirect("", "/")
|
mapper.redirect("", "/")
|
||||||
|
|
||||||
|
@ -35,20 +35,7 @@ from cinder import volume as cinder_volume
|
|||||||
from cinder.volume import utils as volume_utils
|
from cinder.volume import utils as volume_utils
|
||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
|
|
||||||
query_volume_filters_opt = cfg.ListOpt('query_volume_filters',
|
|
||||||
default=['name', 'status', 'metadata',
|
|
||||||
'availability_zone',
|
|
||||||
'bootable'],
|
|
||||||
help="Volume filter options which "
|
|
||||||
"non-admin user could use to "
|
|
||||||
"query volumes. Default values "
|
|
||||||
"are: ['name', 'status', "
|
|
||||||
"'metadata', 'availability_zone',"
|
|
||||||
"'bootable']")
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opt(query_volume_filters_opt)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
SCHEDULER_HINTS_NAMESPACE =\
|
SCHEDULER_HINTS_NAMESPACE =\
|
||||||
|
0
cinder/api/v3/__init__.py
Normal file
0
cinder/api/v3/__init__.py
Normal file
99
cinder/api/v3/router.py
Normal file
99
cinder/api/v3/router.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Copyright 2011 OpenStack Foundation
|
||||||
|
# Copyright 2011 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
WSGI middleware for OpenStack Volume API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from cinder.api import extensions
|
||||||
|
import cinder.api.openstack
|
||||||
|
from cinder.api.v2 import limits
|
||||||
|
from cinder.api.v2 import snapshot_metadata
|
||||||
|
from cinder.api.v2 import snapshots
|
||||||
|
from cinder.api.v2 import types
|
||||||
|
from cinder.api.v2 import volume_metadata
|
||||||
|
from cinder.api.v2 import volumes
|
||||||
|
from cinder.api import versions
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIRouter(cinder.api.openstack.APIRouter):
|
||||||
|
"""Routes requests on the API to the appropriate controller and method."""
|
||||||
|
ExtensionManager = extensions.ExtensionManager
|
||||||
|
|
||||||
|
def _setup_routes(self, mapper, ext_mgr):
|
||||||
|
self.resources['versions'] = versions.create_resource()
|
||||||
|
mapper.connect("versions", "/",
|
||||||
|
controller=self.resources['versions'],
|
||||||
|
action='index')
|
||||||
|
|
||||||
|
mapper.redirect("", "/")
|
||||||
|
|
||||||
|
self.resources['volumes'] = volumes.create_resource(ext_mgr)
|
||||||
|
mapper.resource("volume", "volumes",
|
||||||
|
controller=self.resources['volumes'],
|
||||||
|
collection={'detail': 'GET'},
|
||||||
|
member={'action': 'POST'})
|
||||||
|
|
||||||
|
self.resources['types'] = types.create_resource()
|
||||||
|
mapper.resource("type", "types",
|
||||||
|
controller=self.resources['types'],
|
||||||
|
member={'action': 'POST'})
|
||||||
|
|
||||||
|
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
|
||||||
|
mapper.resource("snapshot", "snapshots",
|
||||||
|
controller=self.resources['snapshots'],
|
||||||
|
collection={'detail': 'GET'},
|
||||||
|
member={'action': 'POST'})
|
||||||
|
|
||||||
|
self.resources['limits'] = limits.create_resource()
|
||||||
|
mapper.resource("limit", "limits",
|
||||||
|
controller=self.resources['limits'])
|
||||||
|
|
||||||
|
self.resources['snapshot_metadata'] = \
|
||||||
|
snapshot_metadata.create_resource()
|
||||||
|
snapshot_metadata_controller = self.resources['snapshot_metadata']
|
||||||
|
|
||||||
|
mapper.resource("snapshot_metadata", "metadata",
|
||||||
|
controller=snapshot_metadata_controller,
|
||||||
|
parent_resource=dict(member_name='snapshot',
|
||||||
|
collection_name='snapshots'))
|
||||||
|
|
||||||
|
mapper.connect("metadata",
|
||||||
|
"/{project_id}/snapshots/{snapshot_id}/metadata",
|
||||||
|
controller=snapshot_metadata_controller,
|
||||||
|
action='update_all',
|
||||||
|
conditions={"method": ['PUT']})
|
||||||
|
|
||||||
|
self.resources['volume_metadata'] = \
|
||||||
|
volume_metadata.create_resource()
|
||||||
|
volume_metadata_controller = self.resources['volume_metadata']
|
||||||
|
|
||||||
|
mapper.resource("volume_metadata", "metadata",
|
||||||
|
controller=volume_metadata_controller,
|
||||||
|
parent_resource=dict(member_name='volume',
|
||||||
|
collection_name='volumes'))
|
||||||
|
|
||||||
|
mapper.connect("metadata",
|
||||||
|
"/{project_id}/volumes/{volume_id}/metadata",
|
||||||
|
controller=volume_metadata_controller,
|
||||||
|
action='update_all',
|
||||||
|
conditions={"method": ['PUT']})
|
@ -1,4 +1,5 @@
|
|||||||
# Copyright 2010 OpenStack Foundation
|
# Copyright 2010 OpenStack Foundation
|
||||||
|
# Copyright 2015 Clinton Knight
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -14,11 +15,15 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from cinder.api import extensions
|
||||||
|
from cinder.api import openstack
|
||||||
|
from cinder.api.openstack import api_version_request
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
from cinder.api.views import versions as views_versions
|
from cinder.api.views import versions as views_versions
|
||||||
from cinder.api import xmlutil
|
from cinder.api import xmlutil
|
||||||
@ -26,67 +31,115 @@ from cinder.api import xmlutil
|
|||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
_LINKS = [{
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "http://docs.openstack.org/",
|
||||||
|
}]
|
||||||
|
|
||||||
|
_MEDIA_TYPES = [{
|
||||||
|
"base":
|
||||||
|
"application/json",
|
||||||
|
"type":
|
||||||
|
"application/vnd.openstack.volume+json;version=1",
|
||||||
|
},
|
||||||
|
{"base":
|
||||||
|
"application/xml",
|
||||||
|
"type":
|
||||||
|
"application/vnd.openstack.volume+xml;version=1",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
_KNOWN_VERSIONS = {
|
_KNOWN_VERSIONS = {
|
||||||
"v2.0": {
|
|
||||||
"id": "v2.0",
|
|
||||||
"status": "CURRENT",
|
|
||||||
"updated": "2012-11-21T11:33:21Z",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "describedby",
|
|
||||||
"type": "text/html",
|
|
||||||
"href": "http://docs.openstack.org/",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"media-types": [
|
|
||||||
{
|
|
||||||
"base": "application/xml",
|
|
||||||
"type": "application/vnd.openstack.volume+xml;version=1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"base": "application/json",
|
|
||||||
"type": "application/vnd.openstack.volume+json;version=1",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"v1.0": {
|
"v1.0": {
|
||||||
"id": "v1.0",
|
"id": "v1.0",
|
||||||
"status": "SUPPORTED",
|
"status": "SUPPORTED",
|
||||||
|
"version": "",
|
||||||
|
"min_version": "",
|
||||||
"updated": "2014-06-28T12:20:21Z",
|
"updated": "2014-06-28T12:20:21Z",
|
||||||
"links": [
|
"links": _LINKS,
|
||||||
{
|
"media-types": _MEDIA_TYPES,
|
||||||
"rel": "describedby",
|
},
|
||||||
"type": "text/html",
|
"v2.0": {
|
||||||
"href": "http://docs.openstack.org/",
|
"id": "v2.0",
|
||||||
},
|
"status": "SUPPORTED",
|
||||||
],
|
"version": "",
|
||||||
"media-types": [
|
"min_version": "",
|
||||||
{
|
"updated": "2014-06-28T12:20:21Z",
|
||||||
"base": "application/xml",
|
"links": _LINKS,
|
||||||
"type": "application/vnd.openstack.volume+xml;version=1",
|
"media-types": _MEDIA_TYPES,
|
||||||
},
|
},
|
||||||
{
|
"v3.0": {
|
||||||
"base": "application/json",
|
"id": "v3.0",
|
||||||
"type": "application/vnd.openstack.volume+json;version=1",
|
"status": "CURRENT",
|
||||||
}
|
"version": api_version_request._MAX_API_VERSION,
|
||||||
],
|
"min_version": api_version_request._MIN_API_VERSION,
|
||||||
}
|
"updated": "2016-02-08T12:20:21Z",
|
||||||
|
"links": _LINKS,
|
||||||
|
"media-types": _MEDIA_TYPES,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_supported_versions():
|
class Versions(openstack.APIRouter):
|
||||||
versions = {}
|
"""Route versions requests."""
|
||||||
|
|
||||||
if CONF.enable_v1_api:
|
ExtensionManager = extensions.ExtensionManager
|
||||||
versions['v1.0'] = _KNOWN_VERSIONS['v1.0']
|
|
||||||
if CONF.enable_v2_api:
|
|
||||||
versions['v2.0'] = _KNOWN_VERSIONS['v2.0']
|
|
||||||
|
|
||||||
return versions
|
def _setup_routes(self, mapper, ext_mgr):
|
||||||
|
self.resources['versions'] = create_resource()
|
||||||
|
mapper.connect('versions', '/',
|
||||||
|
controller=self.resources['versions'],
|
||||||
|
action='all')
|
||||||
|
mapper.redirect('', '/')
|
||||||
|
|
||||||
|
|
||||||
|
class VersionsController(wsgi.Controller):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(VersionsController, self).__init__(None)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('1.0')
|
||||||
|
def index(self, req): # pylint: disable=E0102
|
||||||
|
"""Return versions supported prior to the microversions epoch."""
|
||||||
|
builder = views_versions.get_view_builder(req)
|
||||||
|
known_versions = copy.deepcopy(_KNOWN_VERSIONS)
|
||||||
|
known_versions.pop('v2.0')
|
||||||
|
known_versions.pop('v3.0')
|
||||||
|
return builder.build_versions(known_versions)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('2.0') # noqa
|
||||||
|
def index(self, req): # pylint: disable=E0102
|
||||||
|
"""Return versions supported prior to the microversions epoch."""
|
||||||
|
builder = views_versions.get_view_builder(req)
|
||||||
|
known_versions = copy.deepcopy(_KNOWN_VERSIONS)
|
||||||
|
known_versions.pop('v1.0')
|
||||||
|
known_versions.pop('v3.0')
|
||||||
|
return builder.build_versions(known_versions)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('3.0') # noqa
|
||||||
|
def index(self, req): # pylint: disable=E0102
|
||||||
|
"""Return versions supported after the start of microversions."""
|
||||||
|
builder = views_versions.get_view_builder(req)
|
||||||
|
known_versions = copy.deepcopy(_KNOWN_VERSIONS)
|
||||||
|
known_versions.pop('v1.0')
|
||||||
|
known_versions.pop('v2.0')
|
||||||
|
return builder.build_versions(known_versions)
|
||||||
|
|
||||||
|
# NOTE (cknight): Calling the versions API without
|
||||||
|
# /v1, /v2, or /v3 in the URL will lead to this unversioned
|
||||||
|
# method, which should always return info about all
|
||||||
|
# available versions.
|
||||||
|
@wsgi.response(300)
|
||||||
|
def all(self, req):
|
||||||
|
"""Return all known versions."""
|
||||||
|
builder = views_versions.get_view_builder(req)
|
||||||
|
known_versions = copy.deepcopy(_KNOWN_VERSIONS)
|
||||||
|
return builder.build_versions(known_versions)
|
||||||
|
|
||||||
|
|
||||||
class MediaTypesTemplateElement(xmlutil.TemplateElement):
|
class MediaTypesTemplateElement(xmlutil.TemplateElement):
|
||||||
|
|
||||||
def will_render(self, datum):
|
def will_render(self, datum):
|
||||||
return 'media-types' in datum
|
return 'media-types' in datum
|
||||||
|
|
||||||
@ -110,6 +163,7 @@ version_nsmap = {None: xmlutil.XMLNS_COMMON_V10, 'atom': xmlutil.XMLNS_ATOM}
|
|||||||
|
|
||||||
|
|
||||||
class VersionTemplate(xmlutil.TemplateBuilder):
|
class VersionTemplate(xmlutil.TemplateBuilder):
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
root = xmlutil.TemplateElement('version', selector='version')
|
root = xmlutil.TemplateElement('version', selector='version')
|
||||||
make_version(root)
|
make_version(root)
|
||||||
@ -117,6 +171,7 @@ class VersionTemplate(xmlutil.TemplateBuilder):
|
|||||||
|
|
||||||
|
|
||||||
class VersionsTemplate(xmlutil.TemplateBuilder):
|
class VersionsTemplate(xmlutil.TemplateBuilder):
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
root = xmlutil.TemplateElement('versions')
|
root = xmlutil.TemplateElement('versions')
|
||||||
elem = xmlutil.SubTemplateElement(root, 'version', selector='versions')
|
elem = xmlutil.SubTemplateElement(root, 'version', selector='versions')
|
||||||
@ -125,6 +180,7 @@ class VersionsTemplate(xmlutil.TemplateBuilder):
|
|||||||
|
|
||||||
|
|
||||||
class ChoicesTemplate(xmlutil.TemplateBuilder):
|
class ChoicesTemplate(xmlutil.TemplateBuilder):
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
root = xmlutil.TemplateElement('choices')
|
root = xmlutil.TemplateElement('choices')
|
||||||
elem = xmlutil.SubTemplateElement(root, 'version', selector='choices')
|
elem = xmlutil.SubTemplateElement(root, 'version', selector='choices')
|
||||||
@ -209,6 +265,7 @@ class AtomSerializer(wsgi.XMLDictSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class VersionsAtomSerializer(AtomSerializer):
|
class VersionsAtomSerializer(AtomSerializer):
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
versions = data['versions']
|
versions = data['versions']
|
||||||
feed_id = self._get_base_url(versions[0]['links'][0]['href'])
|
feed_id = self._get_base_url(versions[0]['links'][0]['href'])
|
||||||
@ -217,6 +274,7 @@ class VersionsAtomSerializer(AtomSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class VersionAtomSerializer(AtomSerializer):
|
class VersionAtomSerializer(AtomSerializer):
|
||||||
|
|
||||||
def default(self, data):
|
def default(self, data):
|
||||||
version = data['version']
|
version = data['version']
|
||||||
feed_id = version['links'][0]['href']
|
feed_id = version['links'][0]['href']
|
||||||
@ -224,46 +282,5 @@ class VersionAtomSerializer(AtomSerializer):
|
|||||||
return self._to_xml(feed)
|
return self._to_xml(feed)
|
||||||
|
|
||||||
|
|
||||||
class Versions(wsgi.Resource):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Versions, self).__init__(None)
|
|
||||||
|
|
||||||
@wsgi.serializers(xml=VersionsTemplate,
|
|
||||||
atom=VersionsAtomSerializer)
|
|
||||||
def index(self, req):
|
|
||||||
"""Return all versions."""
|
|
||||||
builder = views_versions.get_view_builder(req)
|
|
||||||
return builder.build_versions(get_supported_versions())
|
|
||||||
|
|
||||||
@wsgi.serializers(xml=ChoicesTemplate)
|
|
||||||
@wsgi.response(300)
|
|
||||||
def multi(self, req):
|
|
||||||
"""Return multiple choices."""
|
|
||||||
builder = views_versions.get_view_builder(req)
|
|
||||||
return builder.build_choices(get_supported_versions(), req)
|
|
||||||
|
|
||||||
def get_action_args(self, request_environment):
|
|
||||||
"""Parse dictionary created by routes library."""
|
|
||||||
args = {}
|
|
||||||
if request_environment['PATH_INFO'] == '/':
|
|
||||||
args['action'] = 'index'
|
|
||||||
else:
|
|
||||||
args['action'] = 'multi'
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeVersion(object):
|
|
||||||
@wsgi.serializers(xml=VersionTemplate,
|
|
||||||
atom=VersionAtomSerializer)
|
|
||||||
def show(self, req):
|
|
||||||
builder = views_versions.get_view_builder(req)
|
|
||||||
if 'v1' in builder.base_url:
|
|
||||||
return builder.build_version(_KNOWN_VERSIONS['v1.0'])
|
|
||||||
else:
|
|
||||||
return builder.build_version(_KNOWN_VERSIONS['v2.0'])
|
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
return wsgi.Resource(VolumeVersion())
|
return wsgi.Resource(VersionsController())
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# Copyright 2010-2011 OpenStack Foundation
|
# Copyright 2010-2011 OpenStack Foundation
|
||||||
|
# Copyright 2015 Clinton Knight
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -14,9 +15,10 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import os
|
import re
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
|
||||||
versions_opts = [
|
versions_opts = [
|
||||||
@ -45,57 +47,32 @@ class ViewBuilder(object):
|
|||||||
"""
|
"""
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
|
||||||
def build_choices(self, VERSIONS, req):
|
|
||||||
version_objs = []
|
|
||||||
for version in VERSIONS:
|
|
||||||
version = VERSIONS[version]
|
|
||||||
version_objs.append({
|
|
||||||
"id": version['id'],
|
|
||||||
"status": version['status'],
|
|
||||||
"links": [{"rel": "self",
|
|
||||||
"href": self.generate_href(version['id'],
|
|
||||||
req.path), }, ],
|
|
||||||
"media-types": version['media-types'], })
|
|
||||||
|
|
||||||
return dict(choices=version_objs)
|
|
||||||
|
|
||||||
def build_versions(self, versions):
|
def build_versions(self, versions):
|
||||||
version_objs = []
|
views = [self._build_version(versions[key])
|
||||||
for version in sorted(versions.keys()):
|
for key in sorted(list(versions.keys()))]
|
||||||
version = versions[version]
|
return dict(versions=views)
|
||||||
version_objs.append({
|
|
||||||
"id": version['id'],
|
|
||||||
"status": version['status'],
|
|
||||||
"updated": version['updated'],
|
|
||||||
"links": self._build_links(version), })
|
|
||||||
|
|
||||||
return dict(versions=version_objs)
|
def _build_version(self, version):
|
||||||
|
view = copy.deepcopy(version)
|
||||||
def build_version(self, version):
|
view['links'] = self._build_links(version)
|
||||||
reval = copy.deepcopy(version)
|
return view
|
||||||
reval['links'].insert(0, {
|
|
||||||
"rel": "self",
|
|
||||||
"href": self.base_url.rstrip('/') + '/', })
|
|
||||||
return dict(version=reval)
|
|
||||||
|
|
||||||
def _build_links(self, version_data):
|
def _build_links(self, version_data):
|
||||||
"""Generate a container of links that refer to the provided version."""
|
"""Generate a container of links that refer to the provided version."""
|
||||||
href = self.generate_href(version_data['id'])
|
links = copy.deepcopy(version_data.get('links', {}))
|
||||||
|
version_num = version_data["id"].split('.')[0]
|
||||||
links = [{'rel': 'self',
|
links.append({'rel': 'self',
|
||||||
'href': href, }, ]
|
'href': self._generate_href(version=version_num)})
|
||||||
|
|
||||||
return links
|
return links
|
||||||
|
|
||||||
def generate_href(self, version, path=None):
|
def _generate_href(self, version='v3', path=None):
|
||||||
"""Create an url that refers to a specific version_number."""
|
"""Create a URL that refers to a specific version_number."""
|
||||||
if version.find('v1.') == 0:
|
base_url = self._get_base_url_without_version()
|
||||||
version_number = 'v1'
|
href = urllib.parse.urljoin(base_url, version).rstrip('/') + '/'
|
||||||
else:
|
|
||||||
version_number = 'v2'
|
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
path = path.strip('/')
|
href += path.lstrip('/')
|
||||||
return os.path.join(self.base_url, version_number, path)
|
return href
|
||||||
else:
|
|
||||||
return os.path.join(self.base_url, version_number) + '/'
|
def _get_base_url_without_version(self):
|
||||||
|
"""Get the base URL with out the /v1 suffix."""
|
||||||
|
return re.sub('v[1-9]+/?$', '', self.base_url)
|
||||||
|
@ -103,7 +103,10 @@ global_opts = [
|
|||||||
help=_("DEPRECATED: Deploy v1 of the Cinder API.")),
|
help=_("DEPRECATED: Deploy v1 of the Cinder API.")),
|
||||||
cfg.BoolOpt('enable_v2_api',
|
cfg.BoolOpt('enable_v2_api',
|
||||||
default=True,
|
default=True,
|
||||||
help=_("Deploy v2 of the Cinder API.")),
|
help=_("DEPRECATED: Deploy v2 of the Cinder API.")),
|
||||||
|
cfg.BoolOpt('enable_v3_api',
|
||||||
|
default=True,
|
||||||
|
help=_("Deploy v3 of the Cinder API.")),
|
||||||
cfg.BoolOpt('api_rate_limit',
|
cfg.BoolOpt('api_rate_limit',
|
||||||
default=True,
|
default=True,
|
||||||
help='Enables or disables rate limit of the API.'),
|
help='Enables or disables rate limit of the API.'),
|
||||||
|
@ -243,6 +243,20 @@ class InvalidUUID(Invalid):
|
|||||||
message = _("Expected a uuid but received %(uuid)s.")
|
message = _("Expected a uuid but received %(uuid)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAPIVersionString(Invalid):
|
||||||
|
message = _("API Version String %(version)s is of invalid format. Must "
|
||||||
|
"be of format MajorNum.MinorNum.")
|
||||||
|
|
||||||
|
|
||||||
|
class VersionNotFoundForAPIMethod(Invalid):
|
||||||
|
message = _("API version %(version)s is not supported on this method.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidGlobalAPIVersion(Invalid):
|
||||||
|
message = _("Version %(req_ver)s is not supported by the API. Minimum "
|
||||||
|
"is %(min_ver)s and maximum is %(max_ver)s.")
|
||||||
|
|
||||||
|
|
||||||
class APIException(CinderException):
|
class APIException(CinderException):
|
||||||
message = _("Error while requesting %(service)s API.")
|
message = _("Error while requesting %(service)s API.")
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import itertools
|
|||||||
from cinder.api import common as cinder_api_common
|
from cinder.api import common as cinder_api_common
|
||||||
from cinder.api.middleware import auth as cinder_api_middleware_auth
|
from cinder.api.middleware import auth as cinder_api_middleware_auth
|
||||||
from cinder.api.middleware import sizelimit as cinder_api_middleware_sizelimit
|
from cinder.api.middleware import sizelimit as cinder_api_middleware_sizelimit
|
||||||
from cinder.api.v2 import volumes as cinder_api_v2_volumes
|
|
||||||
from cinder.api.views import versions as cinder_api_views_versions
|
from cinder.api.views import versions as cinder_api_views_versions
|
||||||
from cinder.backup import api as cinder_backup_api
|
from cinder.backup import api as cinder_backup_api
|
||||||
from cinder.backup import chunkeddriver as cinder_backup_chunkeddriver
|
from cinder.backup import chunkeddriver as cinder_backup_chunkeddriver
|
||||||
@ -330,7 +329,6 @@ def list_opts():
|
|||||||
cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts,
|
cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts,
|
||||||
cinder_volume_drivers_datera.d_opts,
|
cinder_volume_drivers_datera.d_opts,
|
||||||
cinder_volume_drivers_blockdevice.volume_opts,
|
cinder_volume_drivers_blockdevice.volume_opts,
|
||||||
[cinder_api_v2_volumes.query_volume_filters_opt],
|
|
||||||
cinder_volume_drivers_quobyte.volume_opts,
|
cinder_volume_drivers_quobyte.volume_opts,
|
||||||
cinder_volume_drivers_vzstorage.vzstorage_opts,
|
cinder_volume_drivers_vzstorage.vzstorage_opts,
|
||||||
cinder_volume_drivers_nfs.nfs_opts,
|
cinder_volume_drivers_nfs.nfs_opts,
|
||||||
|
@ -24,6 +24,7 @@ import webob.request
|
|||||||
|
|
||||||
from cinder.api.middleware import auth
|
from cinder.api.middleware import auth
|
||||||
from cinder.api.middleware import fault
|
from cinder.api.middleware import fault
|
||||||
|
from cinder.api.openstack import api_version_request as api_version
|
||||||
from cinder.api.openstack import wsgi as os_wsgi
|
from cinder.api.openstack import wsgi as os_wsgi
|
||||||
from cinder.api import urlmap
|
from cinder.api import urlmap
|
||||||
from cinder.api.v2 import limits
|
from cinder.api.v2 import limits
|
||||||
@ -78,7 +79,7 @@ def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None,
|
|||||||
|
|
||||||
mapper = urlmap.URLMap()
|
mapper = urlmap.URLMap()
|
||||||
mapper['/v2'] = api_v2
|
mapper['/v2'] = api_v2
|
||||||
mapper['/'] = fault.FaultWrapper(versions.Versions())
|
mapper['/'] = fault.FaultWrapper(versions.VersionsController())
|
||||||
return mapper
|
return mapper
|
||||||
|
|
||||||
|
|
||||||
@ -106,17 +107,21 @@ class HTTPRequest(webob.Request):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def blank(cls, *args, **kwargs):
|
def blank(cls, *args, **kwargs):
|
||||||
if args is not None:
|
if args is not None:
|
||||||
if args[0].find('v1') == 0:
|
if 'v1' in args[0]:
|
||||||
kwargs['base_url'] = 'http://localhost/v1'
|
kwargs['base_url'] = 'http://localhost/v1'
|
||||||
else:
|
if 'v2' in args[0]:
|
||||||
kwargs['base_url'] = 'http://localhost/v2'
|
kwargs['base_url'] = 'http://localhost/v2'
|
||||||
|
if 'v3' in args[0]:
|
||||||
|
kwargs['base_url'] = 'http://localhost/v3'
|
||||||
|
|
||||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||||
|
version = kwargs.pop('version', api_version._MIN_API_VERSION)
|
||||||
out = os_wsgi.Request.blank(*args, **kwargs)
|
out = os_wsgi.Request.blank(*args, **kwargs)
|
||||||
out.environ['cinder.context'] = FakeRequestContext(
|
out.environ['cinder.context'] = FakeRequestContext(
|
||||||
'fake_user',
|
'fake_user',
|
||||||
'fakeproject',
|
'fakeproject',
|
||||||
is_admin=use_admin_context)
|
is_admin=use_admin_context)
|
||||||
|
out.api_version_request = api_version.APIVersionRequest(version)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
149
cinder/tests/unit/api/openstack/test_api_version_request.py
Normal file
149
cinder/tests/unit/api/openstack/test_api_version_request.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# Copyright 2014 IBM Corp.
|
||||||
|
# Copyright 2015 Clinton Knight
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 cinder.api.openstack import api_version_request
|
||||||
|
from cinder import exception
|
||||||
|
from cinder import test
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class APIVersionRequestTests(test.TestCase):
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
|
||||||
|
result = api_version_request.APIVersionRequest()
|
||||||
|
|
||||||
|
self.assertIsNone(result._ver_major)
|
||||||
|
self.assertIsNone(result._ver_minor)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('1.1', 1, 1),
|
||||||
|
('2.10', 2, 10),
|
||||||
|
('5.234', 5, 234),
|
||||||
|
('12.5', 12, 5),
|
||||||
|
('2.0', 2, 0),
|
||||||
|
('2.200', 2, 200)
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_valid_version_strings(self, version_string, major, minor):
|
||||||
|
|
||||||
|
request = api_version_request.APIVersionRequest(version_string)
|
||||||
|
|
||||||
|
self.assertEqual(major, request._ver_major)
|
||||||
|
self.assertEqual(minor, request._ver_minor)
|
||||||
|
|
||||||
|
def test_null_version(self):
|
||||||
|
|
||||||
|
v = api_version_request.APIVersionRequest()
|
||||||
|
|
||||||
|
self.assertTrue(v.is_null())
|
||||||
|
|
||||||
|
@ddt.data('2', '200', '2.1.4', '200.23.66.3', '5 .3', '5. 3',
|
||||||
|
'5.03', '02.1', '2.001', '', ' 2.1', '2.1 ')
|
||||||
|
def test_invalid_version_strings(self, version_string):
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidAPIVersionString,
|
||||||
|
api_version_request.APIVersionRequest,
|
||||||
|
version_string)
|
||||||
|
|
||||||
|
def test_cmpkey(self):
|
||||||
|
request = api_version_request.APIVersionRequest('1.2')
|
||||||
|
self.assertEqual((1, 2), request._cmpkey())
|
||||||
|
|
||||||
|
def test_version_comparisons(self):
|
||||||
|
v1 = api_version_request.APIVersionRequest('2.0')
|
||||||
|
v2 = api_version_request.APIVersionRequest('2.5')
|
||||||
|
v3 = api_version_request.APIVersionRequest('5.23')
|
||||||
|
v4 = api_version_request.APIVersionRequest('2.0')
|
||||||
|
v_null = api_version_request.APIVersionRequest()
|
||||||
|
|
||||||
|
self.assertTrue(v1 < v2)
|
||||||
|
self.assertTrue(v1 <= v2)
|
||||||
|
self.assertTrue(v3 > v2)
|
||||||
|
self.assertTrue(v3 >= v2)
|
||||||
|
self.assertTrue(v1 != v2)
|
||||||
|
self.assertTrue(v1 == v4)
|
||||||
|
self.assertTrue(v1 != v_null)
|
||||||
|
self.assertTrue(v_null == v_null)
|
||||||
|
self.assertFalse(v1 == '2.0')
|
||||||
|
|
||||||
|
def test_version_matches(self):
|
||||||
|
v1 = api_version_request.APIVersionRequest('2.0')
|
||||||
|
v2 = api_version_request.APIVersionRequest('2.5')
|
||||||
|
v3 = api_version_request.APIVersionRequest('2.45')
|
||||||
|
v4 = api_version_request.APIVersionRequest('3.3')
|
||||||
|
v5 = api_version_request.APIVersionRequest('3.23')
|
||||||
|
v6 = api_version_request.APIVersionRequest('2.0')
|
||||||
|
v7 = api_version_request.APIVersionRequest('3.3')
|
||||||
|
v8 = api_version_request.APIVersionRequest('4.0')
|
||||||
|
v_null = api_version_request.APIVersionRequest()
|
||||||
|
|
||||||
|
self.assertTrue(v2.matches(v1, v3))
|
||||||
|
self.assertTrue(v2.matches(v1, v_null))
|
||||||
|
self.assertTrue(v1.matches(v6, v2))
|
||||||
|
self.assertTrue(v4.matches(v2, v7))
|
||||||
|
self.assertTrue(v4.matches(v_null, v7))
|
||||||
|
self.assertTrue(v4.matches(v_null, v8))
|
||||||
|
self.assertFalse(v1.matches(v2, v3))
|
||||||
|
self.assertFalse(v5.matches(v2, v4))
|
||||||
|
self.assertFalse(v2.matches(v3, v1))
|
||||||
|
self.assertTrue(v1.matches(v_null, v_null))
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, v_null.matches, v1, v3)
|
||||||
|
|
||||||
|
def test_matches_versioned_method(self):
|
||||||
|
|
||||||
|
request = api_version_request.APIVersionRequest('2.0')
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
request.matches_versioned_method,
|
||||||
|
'fake_method')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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)
|
36
cinder/tests/unit/api/openstack/test_versioned_method.py
Normal file
36
cinder/tests/unit/api/openstack/test_versioned_method.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright 2015 Clinton Knight
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 six
|
||||||
|
|
||||||
|
from cinder.api.openstack import versioned_method
|
||||||
|
from cinder import test
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedMethodTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
args = ('fake_name', 'fake_min', 'fake_max')
|
||||||
|
method = versioned_method.VersionedMethod(*(args + (False, None)))
|
||||||
|
method_string = six.text_type(method)
|
||||||
|
|
||||||
|
self.assertEqual('Version Method %s: min: %s, max: %s' % args,
|
||||||
|
method_string)
|
||||||
|
|
||||||
|
def test_cmpkey(self):
|
||||||
|
method = versioned_method.VersionedMethod(
|
||||||
|
'fake_name', 'fake_start_version', 'fake_end_version', False,
|
||||||
|
'fake_func')
|
||||||
|
self.assertEqual('fake_start_version', method._cmpkey())
|
@ -797,6 +797,28 @@ class ResourceTest(test.TestCase):
|
|||||||
self.assertEqual([2], called)
|
self.assertEqual([2], called)
|
||||||
self.assertEqual('foo', response)
|
self.assertEqual('foo', response)
|
||||||
|
|
||||||
|
def test_post_process_extensions_version_not_found(self):
|
||||||
|
class Controller(object):
|
||||||
|
def index(self, req, pants=None):
|
||||||
|
return pants
|
||||||
|
|
||||||
|
controller = Controller()
|
||||||
|
resource = wsgi.Resource(controller)
|
||||||
|
|
||||||
|
called = []
|
||||||
|
|
||||||
|
def extension1(req, resp_obj):
|
||||||
|
called.append(1)
|
||||||
|
return 'bar'
|
||||||
|
|
||||||
|
def extension2(req, resp_obj):
|
||||||
|
raise exception.VersionNotFoundForAPIMethod(version='fake_version')
|
||||||
|
|
||||||
|
response = resource.post_process_extensions([extension2, extension1],
|
||||||
|
None, None, {})
|
||||||
|
self.assertEqual([1], called)
|
||||||
|
self.assertEqual('bar', response)
|
||||||
|
|
||||||
def test_post_process_extensions_generator(self):
|
def test_post_process_extensions_generator(self):
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
def index(self, req, pants=None):
|
def index(self, req, pants=None):
|
||||||
|
@ -1,258 +0,0 @@
|
|||||||
# Copyright 2011 Denali Systems, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from cinder.api.openstack import wsgi
|
|
||||||
from cinder.api.v1 import router
|
|
||||||
from cinder.api.v1 import snapshots
|
|
||||||
from cinder.api.v1 import volumes
|
|
||||||
from cinder.api import versions
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.unit.api import fakes
|
|
||||||
|
|
||||||
|
|
||||||
class FakeController(object):
|
|
||||||
def __init__(self, ext_mgr=None):
|
|
||||||
self.ext_mgr = ext_mgr
|
|
||||||
|
|
||||||
def index(self, req):
|
|
||||||
obj_type = req.path.split("/")[3]
|
|
||||||
return {obj_type: []}
|
|
||||||
|
|
||||||
def detail(self, req):
|
|
||||||
obj_type = req.path.split("/")[3]
|
|
||||||
return {obj_type: []}
|
|
||||||
|
|
||||||
|
|
||||||
def create_resource(ext_mgr):
|
|
||||||
return wsgi.Resource(FakeController(ext_mgr))
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeRouterTestCase(test.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(VolumeRouterTestCase, self).setUp()
|
|
||||||
# NOTE(vish): versions is just returning text so, no need to stub.
|
|
||||||
self.stubs.Set(snapshots, 'create_resource', create_resource)
|
|
||||||
self.stubs.Set(volumes, 'create_resource', create_resource)
|
|
||||||
self.app = router.APIRouter()
|
|
||||||
|
|
||||||
def test_versions(self):
|
|
||||||
req = fakes.HTTPRequest.blank('')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(302, response.status_int)
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
|
||||||
def test_versions_action_args_index(self):
|
|
||||||
request_environment = {'PATH_INFO': '/'}
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.get_action_args(request_environment)
|
|
||||||
self.assertEqual('index', result['action'])
|
|
||||||
|
|
||||||
def test_versions_action_args_multi(self):
|
|
||||||
request_environment = {'PATH_INFO': '/fake/path'}
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.get_action_args(request_environment)
|
|
||||||
self.assertEqual('multi', result['action'])
|
|
||||||
|
|
||||||
def test_versions_get_most_recent_update(self):
|
|
||||||
res = versions.AtomSerializer()
|
|
||||||
fake_date_updated = [
|
|
||||||
{"updated": '2012-01-04T11:33:21Z'},
|
|
||||||
{"updated": '2012-11-21T11:33:21Z'}
|
|
||||||
]
|
|
||||||
result = res._get_most_recent_update(fake_date_updated)
|
|
||||||
self.assertEqual('2012-11-21T11:33:21Z', result)
|
|
||||||
|
|
||||||
def test_versions_create_version_entry(self):
|
|
||||||
res = versions.AtomSerializer()
|
|
||||||
vers = {
|
|
||||||
"id": "v2.0",
|
|
||||||
"status": "CURRENT",
|
|
||||||
"updated": "2015-05-07T11:33:21Z",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "describedby",
|
|
||||||
"type": "application/pdf",
|
|
||||||
"href": "http://developer.openstack.org/api-ref-guides/"
|
|
||||||
"bk-api-ref-blockstorage-v2.pdf",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
fake_result = {
|
|
||||||
'id': 'http://developer.openstack.org/api-ref-guides/'
|
|
||||||
'bk-api-ref-blockstorage-v2.pdf',
|
|
||||||
'title': 'Version v2.0',
|
|
||||||
'updated': '2015-05-07T11:33:21Z',
|
|
||||||
'link': {
|
|
||||||
'href': 'http://developer.openstack.org/api-ref-guides/'
|
|
||||||
'bk-api-ref-blockstorage-v2.pdf',
|
|
||||||
'type': 'application/pdf',
|
|
||||||
'rel': 'describedby'
|
|
||||||
},
|
|
||||||
'content': 'Version v2.0 CURRENT (2015-05-07T11:33:21Z)'
|
|
||||||
}
|
|
||||||
result_function = res._create_version_entry(vers)
|
|
||||||
result = {}
|
|
||||||
for subElement in result_function:
|
|
||||||
if subElement.text:
|
|
||||||
result[subElement.tag] = subElement.text
|
|
||||||
else:
|
|
||||||
result[subElement.tag] = subElement.attrib
|
|
||||||
self.assertEqual(fake_result, result)
|
|
||||||
|
|
||||||
def test_versions_create_feed(self):
|
|
||||||
res = versions.AtomSerializer()
|
|
||||||
vers = [
|
|
||||||
{
|
|
||||||
"id": "v2.0",
|
|
||||||
"status": "CURRENT",
|
|
||||||
"updated": "2015-05-07T11:33:21Z",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "describedby",
|
|
||||||
"type": "application/pdf",
|
|
||||||
"href": "http://developer.openstack.org/"
|
|
||||||
"api-ref-guides/"
|
|
||||||
"bk-api-ref-blockstorage-v2.pdf",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "v1.0",
|
|
||||||
"status": "CURRENT",
|
|
||||||
"updated": "2012-01-04T11:33:21Z",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "describedby",
|
|
||||||
"type": "application/vnd.sun.wadl+xml",
|
|
||||||
"href": "http://docs.rackspacecloud.com/"
|
|
||||||
"servers/api/v1.1/application.wadl",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
result = res._create_feed(vers, "fake_feed_title",
|
|
||||||
"http://developer.openstack.org/"
|
|
||||||
"api-ref-guides/"
|
|
||||||
"bk-api-ref-blockstorage-v1.pdf")
|
|
||||||
fake_data = {
|
|
||||||
'id': 'http://developer.openstack.org/api-ref-guides/'
|
|
||||||
'bk-api-ref-blockstorage-v1.pdf',
|
|
||||||
'title': 'fake_feed_title',
|
|
||||||
'updated': '2015-05-07T11:33:21Z',
|
|
||||||
}
|
|
||||||
data = {}
|
|
||||||
for subElement in result:
|
|
||||||
if subElement.text:
|
|
||||||
data[subElement.tag] = subElement.text
|
|
||||||
self.assertEqual(fake_data, data)
|
|
||||||
|
|
||||||
def test_versions_multi(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.multi, req, {})
|
|
||||||
ids = [v['id'] for v in result['choices']]
|
|
||||||
self.assertEqual(set(['v1.0', 'v2.0']), set(ids))
|
|
||||||
|
|
||||||
def test_versions_multi_disable_v1(self):
|
|
||||||
self.flags(enable_v1_api=False)
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.multi, req, {})
|
|
||||||
ids = [v['id'] for v in result['choices']]
|
|
||||||
self.assertEqual(set(['v2.0']), set(ids))
|
|
||||||
|
|
||||||
def test_versions_multi_disable_v2(self):
|
|
||||||
self.flags(enable_v2_api=False)
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.multi, req, {})
|
|
||||||
ids = [v['id'] for v in result['choices']]
|
|
||||||
self.assertEqual(set(['v1.0']), set(ids))
|
|
||||||
|
|
||||||
def test_versions_index(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.index, req, {})
|
|
||||||
ids = [v['id'] for v in result['versions']]
|
|
||||||
self.assertEqual(set(['v1.0', 'v2.0']), set(ids))
|
|
||||||
|
|
||||||
def test_versions_index_disable_v1(self):
|
|
||||||
self.flags(enable_v1_api=False)
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.index, req, {})
|
|
||||||
ids = [v['id'] for v in result['versions']]
|
|
||||||
self.assertEqual(set(['v2.0']), set(ids))
|
|
||||||
|
|
||||||
def test_versions_index_disable_v2(self):
|
|
||||||
self.flags(enable_v2_api=False)
|
|
||||||
req = fakes.HTTPRequest.blank('/')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
resource = versions.Versions()
|
|
||||||
result = resource.dispatch(resource.index, req, {})
|
|
||||||
ids = [v['id'] for v in result['versions']]
|
|
||||||
self.assertEqual(set(['v1.0']), set(ids))
|
|
||||||
|
|
||||||
def test_volumes(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/volumes')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
|
||||||
def test_volumes_detail(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/volumes/detail')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
|
||||||
def test_types(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/types')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
|
||||||
def test_snapshots(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/snapshots')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
|
||||||
def test_snapshots_detail(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/snapshots/detail')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2015 - 2016 Huawei Technologies Co., Ltd.
|
# Copyright 2015 Clinton Knight
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -13,131 +13,186 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import webob
|
import ddt
|
||||||
|
import mock
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
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.api import versions
|
||||||
from cinder import test
|
from cinder import test
|
||||||
|
from cinder.tests.unit.api import fakes
|
||||||
|
|
||||||
|
|
||||||
class VersionsTest(test.TestCase):
|
version_header_name = 'OpenStack-Volume-microversion'
|
||||||
|
|
||||||
"""Test the version information returned from the API service."""
|
|
||||||
|
|
||||||
def test_get_version_list_public_endpoint(self):
|
@ddt.ddt
|
||||||
req = webob.Request.blank('/', base_url='http://127.0.0.1:8776/')
|
class VersionsControllerTestCase(test.TestCase):
|
||||||
req.accept = 'application/json'
|
|
||||||
self.override_config('public_endpoint', 'https://example.com:8776')
|
|
||||||
res = versions.Versions().index(req)
|
|
||||||
results = res['versions']
|
|
||||||
expected = [
|
|
||||||
{
|
|
||||||
'id': 'v1.0',
|
|
||||||
'status': 'SUPPORTED',
|
|
||||||
'updated': '2014-06-28T12:20:21Z',
|
|
||||||
'links': [{'rel': 'self',
|
|
||||||
'href': 'https://example.com:8776/v1/'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'v2.0',
|
|
||||||
'status': 'CURRENT',
|
|
||||||
'updated': '2012-11-21T11:33:21Z',
|
|
||||||
'links': [{'rel': 'self',
|
|
||||||
'href': 'https://example.com:8776/v2/'}],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
self.assertEqual(expected, results)
|
|
||||||
|
|
||||||
def test_get_version_list(self):
|
def setUp(self):
|
||||||
req = webob.Request.blank('/', base_url='http://127.0.0.1:8776/')
|
super(VersionsControllerTestCase, self).setUp()
|
||||||
req.accept = 'application/json'
|
self.wsgi_apps = (versions.Versions(), router.APIRouter())
|
||||||
res = versions.Versions().index(req)
|
|
||||||
results = res['versions']
|
|
||||||
expected = [
|
|
||||||
{
|
|
||||||
'id': 'v1.0',
|
|
||||||
'status': 'SUPPORTED',
|
|
||||||
'updated': '2014-06-28T12:20:21Z',
|
|
||||||
'links': [{'rel': 'self',
|
|
||||||
'href': 'http://127.0.0.1:8776/v1/'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'v2.0',
|
|
||||||
'status': 'CURRENT',
|
|
||||||
'updated': '2012-11-21T11:33:21Z',
|
|
||||||
'links': [{'rel': 'self',
|
|
||||||
'href': 'http://127.0.0.1:8776/v2/'}],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
self.assertEqual(expected, results)
|
|
||||||
|
|
||||||
def test_get_version_detail_v1(self):
|
@ddt.data('1.0', '2.0', '3.0')
|
||||||
req = webob.Request.blank('/', base_url='http://127.0.0.1:8776/v1')
|
def test_versions_root(self, version):
|
||||||
req.accept = 'application/json'
|
req = fakes.HTTPRequest.blank('/', base_url='http://localhost')
|
||||||
res = versions.VolumeVersion().show(req)
|
req.method = 'GET'
|
||||||
expected = {
|
req.content_type = 'application/json'
|
||||||
"version": {
|
|
||||||
"status": "SUPPORTED",
|
|
||||||
"updated": "2014-06-28T12:20:21Z",
|
|
||||||
"media-types": [
|
|
||||||
{
|
|
||||||
"base": "application/xml",
|
|
||||||
"type":
|
|
||||||
"application/vnd.openstack.volume+xml;version=1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"base": "application/json",
|
|
||||||
"type":
|
|
||||||
"application/vnd.openstack.volume+json;version=1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "v1.0",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"href": "http://127.0.0.1:8776/v1/",
|
|
||||||
"rel": "self"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "http://docs.openstack.org/",
|
|
||||||
"type": "text/html",
|
|
||||||
"rel": "describedby"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(expected, res)
|
|
||||||
|
|
||||||
def test_get_version_detail_v2(self):
|
response = req.get_response(versions.Versions())
|
||||||
req = webob.Request.blank('/', base_url='http://127.0.0.1:8776/v2')
|
self.assertEqual(300, response.status_int)
|
||||||
req.accept = 'application/json'
|
body = jsonutils.loads(response.body)
|
||||||
res = versions.VolumeVersion().show(req)
|
version_list = body['versions']
|
||||||
expected = {
|
|
||||||
"version": {
|
ids = [v['id'] for v in version_list]
|
||||||
"status": "CURRENT",
|
self.assertEqual({'v1.0', 'v2.0', 'v3.0'}, set(ids))
|
||||||
"updated": "2012-11-21T11:33:21Z",
|
|
||||||
"media-types": [
|
v1 = [v for v in version_list if v['id'] == 'v1.0'][0]
|
||||||
{
|
self.assertEqual('', v1.get('min_version'))
|
||||||
"base": "application/xml",
|
self.assertEqual('', v1.get('version'))
|
||||||
"type":
|
|
||||||
"application/vnd.openstack.volume+xml;version=1"
|
v2 = [v for v in version_list if v['id'] == 'v2.0'][0]
|
||||||
},
|
self.assertEqual('', v2.get('min_version'))
|
||||||
{
|
self.assertEqual('', v2.get('version'))
|
||||||
"base": "application/json",
|
|
||||||
"type":
|
v3 = [v for v in version_list if v['id'] == 'v3.0'][0]
|
||||||
"application/vnd.openstack.volume+json;version=1"
|
self.assertEqual(api_version_request._MAX_API_VERSION,
|
||||||
}
|
v3.get('version'))
|
||||||
],
|
self.assertEqual(api_version_request._MIN_API_VERSION,
|
||||||
"id": "v2.0",
|
v3.get('min_version'))
|
||||||
"links": [
|
|
||||||
{
|
def test_versions_v1_no_header(self):
|
||||||
"href": "http://127.0.0.1:8776/v2/",
|
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1')
|
||||||
"rel": "self"
|
req.method = 'GET'
|
||||||
},
|
req.content_type = 'application/json'
|
||||||
{
|
|
||||||
"href": "http://docs.openstack.org/",
|
response = req.get_response(router.APIRouter())
|
||||||
"type": "text/html",
|
self.assertEqual(200, response.status_int)
|
||||||
"rel": "describedby"
|
|
||||||
}
|
def test_versions_v2_no_header(self):
|
||||||
]
|
req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2')
|
||||||
}
|
req.method = 'GET'
|
||||||
}
|
req.content_type = 'application/json'
|
||||||
self.assertEqual(expected, res)
|
|
||||||
|
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'
|
||||||
|
if version is not None:
|
||||||
|
req.headers = {version_header_name: version}
|
||||||
|
|
||||||
|
response = req.get_response(router.APIRouter())
|
||||||
|
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({'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.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}
|
||||||
|
|
||||||
|
response = req.get_response(router.APIRouter())
|
||||||
|
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({'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.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}
|
||||||
|
|
||||||
|
response = req.get_response(router.APIRouter())
|
||||||
|
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.assertEqual('3.0', response.headers[version_header_name])
|
||||||
|
self.assertEqual(version_header_name, response.headers['Vary'])
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
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'}
|
||||||
|
|
||||||
|
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'}
|
||||||
|
|
||||||
|
for app in self.wsgi_apps:
|
||||||
|
response = req.get_response(app)
|
||||||
|
|
||||||
|
self.assertEqual(400, response.status_int)
|
||||||
|
|
||||||
|
def test_versions_version_not_found(self):
|
||||||
|
api_version_request_4_0 = api_version_request.APIVersionRequest('4.0')
|
||||||
|
self.mock_object(api_version_request,
|
||||||
|
'max_api_version',
|
||||||
|
mock.Mock(return_value=api_version_request_4_0))
|
||||||
|
|
||||||
|
class Controller(wsgi.Controller):
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('3.0', '3.0')
|
||||||
|
def index(self, req):
|
||||||
|
return 'off'
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tests', base_url='http://localhost/v3')
|
||||||
|
req.headers = {version_header_name: '3.5'}
|
||||||
|
app = fakes.TestRouter(Controller())
|
||||||
|
|
||||||
|
response = req.get_response(app)
|
||||||
|
|
||||||
|
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'}
|
||||||
|
|
||||||
|
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'])
|
||||||
|
@ -406,8 +406,8 @@ class SnapshotApiTest(test.TestCase):
|
|||||||
"""Check a page of snapshots list."""
|
"""Check a page of snapshots list."""
|
||||||
# Since we are accessing v2 api directly we don't need to specify
|
# Since we are accessing v2 api directly we don't need to specify
|
||||||
# v2 in the request path, if we did, we'd get /v2/v2 links back
|
# v2 in the request path, if we did, we'd get /v2/v2 links back
|
||||||
request_path = '/%s/snapshots' % project
|
request_path = '/v2/%s/snapshots' % project
|
||||||
expected_path = '/v2' + request_path
|
expected_path = request_path
|
||||||
|
|
||||||
# Construct the query if there are kwargs
|
# Construct the query if there are kwargs
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
159
cinder/tests/unit/api/views/test_versions.py
Normal file
159
cinder/tests/unit/api/views/test_versions.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Copyright 2015 Clinton Knight
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 copy
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinder.api.views import versions
|
||||||
|
from cinder import test
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRequest(object):
|
||||||
|
def __init__(self, application_url):
|
||||||
|
self.application_url = application_url
|
||||||
|
|
||||||
|
|
||||||
|
URL_BASE = 'http://localhost/'
|
||||||
|
FAKE_HREF = URL_BASE + 'v1/'
|
||||||
|
|
||||||
|
FAKE_VERSIONS = {
|
||||||
|
"v1.0": {
|
||||||
|
"id": "v1.0",
|
||||||
|
"status": "CURRENT",
|
||||||
|
"version": "1.1",
|
||||||
|
"min_version": "1.0",
|
||||||
|
"updated": "2015-07-30T11:33:21Z",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": 'http://docs.openstack.org/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"media-types": [
|
||||||
|
{
|
||||||
|
"base": "application/json",
|
||||||
|
"type": "application/vnd.openstack.share+json;version=1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"base": "application/xml",
|
||||||
|
"type": "application/vnd.openstack.share+xml;version=1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
FAKE_LINKS = [
|
||||||
|
{
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": 'http://docs.openstack.org/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rel': 'self',
|
||||||
|
'href': FAKE_HREF
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class ViewBuilderTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def _get_builder(self):
|
||||||
|
request = FakeRequest('fake')
|
||||||
|
return versions.get_view_builder(request)
|
||||||
|
|
||||||
|
def test_build_versions(self):
|
||||||
|
|
||||||
|
self.mock_object(versions.ViewBuilder,
|
||||||
|
'_build_links',
|
||||||
|
mock.Mock(return_value=FAKE_LINKS))
|
||||||
|
|
||||||
|
result = self._get_builder().build_versions(FAKE_VERSIONS)
|
||||||
|
|
||||||
|
expected = {'versions': list(FAKE_VERSIONS.values())}
|
||||||
|
expected['versions'][0]['links'] = FAKE_LINKS
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_build_version(self):
|
||||||
|
|
||||||
|
self.mock_object(versions.ViewBuilder,
|
||||||
|
'_build_links',
|
||||||
|
mock.Mock(return_value=FAKE_LINKS))
|
||||||
|
|
||||||
|
result = self._get_builder()._build_version(FAKE_VERSIONS['v1.0'])
|
||||||
|
|
||||||
|
expected = copy.deepcopy(FAKE_VERSIONS['v1.0'])
|
||||||
|
expected['links'] = FAKE_LINKS
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_build_links(self):
|
||||||
|
|
||||||
|
self.mock_object(versions.ViewBuilder,
|
||||||
|
'_generate_href',
|
||||||
|
mock.Mock(return_value=FAKE_HREF))
|
||||||
|
|
||||||
|
result = self._get_builder()._build_links(FAKE_VERSIONS['v1.0'])
|
||||||
|
|
||||||
|
self.assertEqual(FAKE_LINKS, result)
|
||||||
|
|
||||||
|
def test_generate_href_defaults(self):
|
||||||
|
|
||||||
|
self.mock_object(versions.ViewBuilder,
|
||||||
|
'_get_base_url_without_version',
|
||||||
|
mock.Mock(return_value=URL_BASE))
|
||||||
|
|
||||||
|
result = self._get_builder()._generate_href()
|
||||||
|
|
||||||
|
self.assertEqual('http://localhost/v1/', result)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('v2', None, URL_BASE + 'v2/'),
|
||||||
|
('/v2/', None, URL_BASE + 'v2/'),
|
||||||
|
('/v2/', 'fake_path', URL_BASE + 'v2/fake_path'),
|
||||||
|
('/v2/', '/fake_path/', URL_BASE + 'v2/fake_path/'),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_generate_href_no_path(self, version, path, expected):
|
||||||
|
|
||||||
|
self.mock_object(versions.ViewBuilder,
|
||||||
|
'_get_base_url_without_version',
|
||||||
|
mock.Mock(return_value=URL_BASE))
|
||||||
|
|
||||||
|
result = self._get_builder()._generate_href(version=version,
|
||||||
|
path=path)
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('http://1.1.1.1/', 'http://1.1.1.1/'),
|
||||||
|
('http://localhost/', 'http://localhost/'),
|
||||||
|
('http://1.1.1.1/v1/', 'http://1.1.1.1/'),
|
||||||
|
('http://1.1.1.1/v1', 'http://1.1.1.1/'),
|
||||||
|
('http://1.1.1.1/v11', 'http://1.1.1.1/'),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_get_base_url_without_version(self, base_url, base_url_no_version):
|
||||||
|
|
||||||
|
request = FakeRequest(base_url)
|
||||||
|
builder = versions.get_view_builder(request)
|
||||||
|
|
||||||
|
result = builder._get_base_url_without_version()
|
||||||
|
|
||||||
|
self.assertEqual(base_url_no_version, result)
|
@ -1361,3 +1361,53 @@ class LogTracingTestCase(test.TestCase):
|
|||||||
host_stat['reserved_percentage'])
|
host_stat['reserved_percentage'])
|
||||||
|
|
||||||
self.assertEqual(37.02, free)
|
self.assertEqual(37.02, free)
|
||||||
|
|
||||||
|
|
||||||
|
class Comparable(utils.ComparableMixin):
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def _cmpkey(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class TestComparableMixin(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestComparableMixin, self).setUp()
|
||||||
|
self.one = Comparable(1)
|
||||||
|
self.two = Comparable(2)
|
||||||
|
|
||||||
|
def test_lt(self):
|
||||||
|
self.assertTrue(self.one < self.two)
|
||||||
|
self.assertFalse(self.two < self.one)
|
||||||
|
self.assertFalse(self.one < self.one)
|
||||||
|
|
||||||
|
def test_le(self):
|
||||||
|
self.assertTrue(self.one <= self.two)
|
||||||
|
self.assertFalse(self.two <= self.one)
|
||||||
|
self.assertTrue(self.one <= self.one)
|
||||||
|
|
||||||
|
def test_eq(self):
|
||||||
|
self.assertFalse(self.one == self.two)
|
||||||
|
self.assertFalse(self.two == self.one)
|
||||||
|
self.assertTrue(self.one == self.one)
|
||||||
|
|
||||||
|
def test_ge(self):
|
||||||
|
self.assertFalse(self.one >= self.two)
|
||||||
|
self.assertTrue(self.two >= self.one)
|
||||||
|
self.assertTrue(self.one >= self.one)
|
||||||
|
|
||||||
|
def test_gt(self):
|
||||||
|
self.assertFalse(self.one > self.two)
|
||||||
|
self.assertTrue(self.two > self.one)
|
||||||
|
self.assertFalse(self.one > self.one)
|
||||||
|
|
||||||
|
def test_ne(self):
|
||||||
|
self.assertTrue(self.one != self.two)
|
||||||
|
self.assertTrue(self.two != self.one)
|
||||||
|
self.assertFalse(self.one != self.one)
|
||||||
|
|
||||||
|
def test_compare(self):
|
||||||
|
self.assertEqual(NotImplemented,
|
||||||
|
self.one._compare(1, self.one._cmpkey))
|
||||||
|
@ -778,6 +778,34 @@ def is_blk_device(dev):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ComparableMixin(object):
|
||||||
|
def _compare(self, other, method):
|
||||||
|
try:
|
||||||
|
return method(self._cmpkey(), other._cmpkey())
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
# _cmpkey not implemented, or return different type,
|
||||||
|
# so I can't compare with "other".
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s < o)
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s <= o)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s == o)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s >= o)
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s > o)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self._compare(other, lambda s, o: s != o)
|
||||||
|
|
||||||
|
|
||||||
def retry(exceptions, interval=1, retries=3, backoff_rate=2,
|
def retry(exceptions, interval=1, retries=3, backoff_rate=2,
|
||||||
wait_random=False):
|
wait_random=False):
|
||||||
|
|
||||||
|
308
doc/source/devref/api_microversion_dev.rst
Normal file
308
doc/source/devref/api_microversion_dev.rst
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
API Microversions
|
||||||
|
=================
|
||||||
|
|
||||||
|
Background
|
||||||
|
----------
|
||||||
|
|
||||||
|
Cinder uses a framework we called 'API Microversions' for allowing changes
|
||||||
|
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 a monotonically increasing semantic version number starting from
|
||||||
|
``3.0``.
|
||||||
|
|
||||||
|
If a user makes a request without specifying a version, they will get
|
||||||
|
the ``DEFAULT_API_VERSION`` as defined in
|
||||||
|
``cinder/api/openstack/api_version_request.py``. This value is currently ``3.0``
|
||||||
|
and is expected to remain so for quite a long time.
|
||||||
|
|
||||||
|
The Nova project was the first to implement microversions. For full
|
||||||
|
details please read Nova's `Kilo spec for microversions
|
||||||
|
<http://git.openstack.org/cgit/openstack/nova-specs/tree/specs/kilo/implemented/api-microversions.rst>`_
|
||||||
|
|
||||||
|
When do I need a new Microversion?
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
A microversion is needed when the contract to the user is
|
||||||
|
changed. The user contract covers many kinds of information such as:
|
||||||
|
|
||||||
|
- the Request
|
||||||
|
|
||||||
|
- the list of resource urls which exist on the server
|
||||||
|
|
||||||
|
Example: adding a new shares/{ID}/foo which didn't exist in a
|
||||||
|
previous version of the code
|
||||||
|
|
||||||
|
- the list of query parameters that are valid on urls
|
||||||
|
|
||||||
|
Example: adding a new parameter ``is_yellow`` servers/{ID}?is_yellow=True
|
||||||
|
|
||||||
|
- the list of query parameter values for non free form fields
|
||||||
|
|
||||||
|
Example: parameter filter_by takes a small set of constants/enums "A",
|
||||||
|
"B", "C". Adding support for new enum "D".
|
||||||
|
|
||||||
|
- new headers accepted on a request
|
||||||
|
|
||||||
|
- the Response
|
||||||
|
|
||||||
|
- the list of attributes and data structures returned
|
||||||
|
|
||||||
|
Example: adding a new attribute 'locked': True/False to the output
|
||||||
|
of shares/{ID}
|
||||||
|
|
||||||
|
- the allowed values of non free form fields
|
||||||
|
|
||||||
|
Example: adding a new allowed ``status`` to shares/{ID}
|
||||||
|
|
||||||
|
- the list of status codes allowed for a particular request
|
||||||
|
|
||||||
|
Example: an API previously could return 200, 400, 403, 404 and the
|
||||||
|
change would make the API now also be allowed to return 409.
|
||||||
|
|
||||||
|
- changing a status code on a particular response
|
||||||
|
|
||||||
|
Example: changing the return code of an API from 501 to 400.
|
||||||
|
|
||||||
|
- new headers returned on a response
|
||||||
|
|
||||||
|
The following flow chart attempts to walk through the process of "do
|
||||||
|
we need a microversion".
|
||||||
|
|
||||||
|
|
||||||
|
.. graphviz::
|
||||||
|
|
||||||
|
digraph states {
|
||||||
|
|
||||||
|
label="Do I need a microversion?"
|
||||||
|
|
||||||
|
silent_fail[shape="diamond", style="", label="Did we silently
|
||||||
|
fail to do what is asked?"];
|
||||||
|
ret_500[shape="diamond", style="", label="Did we return a 500
|
||||||
|
before?"];
|
||||||
|
new_error[shape="diamond", style="", label="Are we changing what
|
||||||
|
status code is returned?"];
|
||||||
|
new_attr[shape="diamond", style="", label="Did we add or remove an
|
||||||
|
attribute to a payload?"];
|
||||||
|
new_param[shape="diamond", style="", label="Did we add or remove
|
||||||
|
an accepted query string parameter or value?"];
|
||||||
|
new_resource[shape="diamond", style="", label="Did we add or remove a
|
||||||
|
resource url?"];
|
||||||
|
|
||||||
|
|
||||||
|
no[shape="box", style=rounded, label="No microversion needed"];
|
||||||
|
yes[shape="box", style=rounded, label="Yes, you need a microversion"];
|
||||||
|
no2[shape="box", style=rounded, label="No microversion needed, it's
|
||||||
|
a bug"];
|
||||||
|
|
||||||
|
silent_fail -> ret_500[label="no"];
|
||||||
|
silent_fail -> no2[label="yes"];
|
||||||
|
|
||||||
|
ret_500 -> no2[label="yes [1]"];
|
||||||
|
ret_500 -> new_error[label="no"];
|
||||||
|
|
||||||
|
new_error -> new_attr[label="no"];
|
||||||
|
new_error -> yes[label="yes"];
|
||||||
|
|
||||||
|
new_attr -> new_param[label="no"];
|
||||||
|
new_attr -> yes[label="yes"];
|
||||||
|
|
||||||
|
new_param -> new_resource[label="no"];
|
||||||
|
new_param -> yes[label="yes"];
|
||||||
|
|
||||||
|
new_resource -> no[label="no"];
|
||||||
|
new_resource -> yes[label="yes"];
|
||||||
|
|
||||||
|
{rank=same; yes new_attr}
|
||||||
|
{rank=same; no2 ret_500}
|
||||||
|
{rank=min; silent_fail}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
**Footnotes**
|
||||||
|
|
||||||
|
[1] - When fixing 500 errors that previously caused stack traces, try
|
||||||
|
to map the new error into the existing set of errors that API call
|
||||||
|
could previously return (400 if nothing else is appropriate). Changing
|
||||||
|
the set of allowed status codes from a request is changing the
|
||||||
|
contract, and should be part of a microversion.
|
||||||
|
|
||||||
|
The reason why we are so strict on contract is that we'd like
|
||||||
|
application writers to be able to know, for sure, what the contract is
|
||||||
|
at every microversion in Cinder. If they do not, they will need to write
|
||||||
|
conditional code in their application to handle ambiguities.
|
||||||
|
|
||||||
|
When in doubt, consider application authors. If it would work with no
|
||||||
|
client side changes on both Cinder versions, you probably don't need a
|
||||||
|
microversion. If, on the other hand, there is any ambiguity, a
|
||||||
|
microversion is probably needed.
|
||||||
|
|
||||||
|
|
||||||
|
In Code
|
||||||
|
-------
|
||||||
|
|
||||||
|
In ``cinder/api/openstack/wsgi.py`` we define an ``@api_version`` decorator
|
||||||
|
which is intended to be used on top-level Controller methods. It is
|
||||||
|
not appropriate for lower-level methods. Some examples:
|
||||||
|
|
||||||
|
Adding a new API method
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In the controller class::
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("3.4")
|
||||||
|
def my_api_method(self, req, id):
|
||||||
|
....
|
||||||
|
|
||||||
|
This method would only be available if the caller had specified an
|
||||||
|
``OpenStack-Volume-microversion`` 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``.
|
||||||
|
|
||||||
|
Removing an API method
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In the controller class::
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("3.1", "3.4")
|
||||||
|
def my_api_method(self, req, id):
|
||||||
|
....
|
||||||
|
|
||||||
|
This method would only be available if the caller had specified an
|
||||||
|
``OpenStack-Volume-microversion`` of <= ``3.4``. If ``3.5`` or later
|
||||||
|
is specified the server will respond with ``HTTP/404``.
|
||||||
|
|
||||||
|
Changing a method's behaviour
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In the controller class::
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("3.1", "3.3")
|
||||||
|
def my_api_method(self, req, id):
|
||||||
|
.... method_1 ...
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("3.4") # noqa
|
||||||
|
def my_api_method(self, req, id):
|
||||||
|
.... method_2 ...
|
||||||
|
|
||||||
|
If a caller specified ``3.1``, ``3.2`` or ``3.3`` (or received the
|
||||||
|
default of ``3.1``) they would see the result from ``method_1``,
|
||||||
|
``3.4`` or later ``method_2``.
|
||||||
|
|
||||||
|
It is vital that the two methods have the same name, so the second of
|
||||||
|
them will need ``# noqa`` to avoid failing flake8's ``F811`` rule. The
|
||||||
|
two methods may be different in any kind of semantics (schema
|
||||||
|
validation, return values, response codes, etc)
|
||||||
|
|
||||||
|
A method with only small changes between versions
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A method may have only small changes between microversions, in which
|
||||||
|
case you can decorate a private method::
|
||||||
|
|
||||||
|
@api_version("3.1", "3.4")
|
||||||
|
def _version_specific_func(self, req, arg1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@api_version(min_version="3.5") # noqa
|
||||||
|
def _version_specific_func(self, req, arg1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def show(self, req, id):
|
||||||
|
.... common stuff ....
|
||||||
|
self._version_specific_func(req, "foo")
|
||||||
|
.... common stuff ....
|
||||||
|
|
||||||
|
When not using decorators
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When you don't want to use the ``@api_version`` decorator on a method
|
||||||
|
or you want to change behaviour within a method (say it leads to
|
||||||
|
simpler or simply a lot less code) you can directly test for the
|
||||||
|
requested version with a method as long as you have access to the api
|
||||||
|
request object (commonly called ``req``). Every API method has an
|
||||||
|
api_version_request object attached to the req object and that can be
|
||||||
|
used to modify behaviour based on its value::
|
||||||
|
|
||||||
|
def index(self, req):
|
||||||
|
<common code>
|
||||||
|
|
||||||
|
req_version = req.api_version_request
|
||||||
|
if req_version.matches("3.1", "3.5"):
|
||||||
|
....stuff....
|
||||||
|
elif req_version.matches("3.6", "3.10"):
|
||||||
|
....other stuff....
|
||||||
|
elif req_version > api_version_request.APIVersionRequest("3.10"):
|
||||||
|
....more stuff.....
|
||||||
|
|
||||||
|
<common code>
|
||||||
|
|
||||||
|
The first argument to the matches method is the minimum acceptable version
|
||||||
|
and the second is maximum acceptable version. A specified version can be null::
|
||||||
|
|
||||||
|
null_version = APIVersionRequest()
|
||||||
|
|
||||||
|
If the minimum version specified is null then there is no restriction on
|
||||||
|
the minimum version, and likewise if the maximum version is null there
|
||||||
|
is no restriction the maximum version. Alternatively a one sided comparison
|
||||||
|
can be used as in the example above.
|
||||||
|
|
||||||
|
Other necessary changes
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
If you are adding a patch which adds a new microversion, it is
|
||||||
|
necessary to add changes to other places which describe your change:
|
||||||
|
|
||||||
|
* Update ``REST_API_VERSION_HISTORY`` in
|
||||||
|
``cinder/api/openstack/api_version_request.py``
|
||||||
|
|
||||||
|
* Update ``_MAX_API_VERSION`` in
|
||||||
|
``cinder/api/openstack/api_version_request.py``
|
||||||
|
|
||||||
|
* Add a verbose description to
|
||||||
|
``cinder/api/openstack/rest_api_version_history.rst``. There should
|
||||||
|
be enough information that it could be used by the docs team for
|
||||||
|
release notes.
|
||||||
|
|
||||||
|
* Update the expected versions in affected tests.
|
||||||
|
|
||||||
|
Allocating a microversion
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
If you are adding a patch which adds a new microversion, it is
|
||||||
|
necessary to allocate the next microversion number. Except under
|
||||||
|
extremely unusual circumstances and this would have been mentioned in
|
||||||
|
the blueprint for the change, the minor number of ``_MAX_API_VERSION``
|
||||||
|
will be incremented. This will also be the new microversion number for
|
||||||
|
the API change.
|
||||||
|
|
||||||
|
It is possible that multiple microversion patches would be proposed in
|
||||||
|
parallel and the microversions would conflict between patches. This
|
||||||
|
will cause a merge conflict. We don't reserve a microversion for each
|
||||||
|
patch in advance as we don't know the final merge order. Developers
|
||||||
|
may need over time to rebase their patch calculating a new version
|
||||||
|
number as above based on the updated value of ``_MAX_API_VERSION``.
|
||||||
|
|
||||||
|
Testing Microversioned API Methods
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Unit tests for microversions should be put in cinder/tests/unit/api/v3/ .
|
||||||
|
Since all existing functionality is tested in cinder/tests/unit/api/v2,
|
||||||
|
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``
|
||||||
|
header, for example::
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/testable/url/endpoint')
|
||||||
|
req.headers = {'OpenStack-Volume-microversion': '3.2'}
|
||||||
|
req.api_version_request = api_version.APIVersionRequest('3.6')
|
||||||
|
|
||||||
|
controller = controller.TestableController()
|
||||||
|
|
||||||
|
res = controller.index(req)
|
||||||
|
... assertions about the response ...
|
||||||
|
|
1
doc/source/devref/api_microversion_history.rst
Normal file
1
doc/source/devref/api_microversion_history.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../../cinder/api/openstack/rest_api_version_history.rst
|
@ -27,6 +27,8 @@ Programming HowTos and Tutorials
|
|||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
|
|
||||||
development.environment
|
development.environment
|
||||||
|
api_microversion_dev
|
||||||
|
api_microversion_history
|
||||||
unit_tests
|
unit_tests
|
||||||
addmethod.openstackapi
|
addmethod.openstackapi
|
||||||
drivers
|
drivers
|
||||||
|
@ -7,6 +7,7 @@ use = call:cinder.api:root_app_factory
|
|||||||
/: apiversions
|
/: apiversions
|
||||||
/v1: openstack_volume_api_v1
|
/v1: openstack_volume_api_v1
|
||||||
/v2: openstack_volume_api_v2
|
/v2: openstack_volume_api_v2
|
||||||
|
/v3: openstack_volume_api_v3
|
||||||
|
|
||||||
[composite:openstack_volume_api_v1]
|
[composite:openstack_volume_api_v1]
|
||||||
use = call:cinder.api.middleware.auth:pipeline_factory
|
use = call:cinder.api.middleware.auth:pipeline_factory
|
||||||
@ -20,14 +21,20 @@ noauth = cors request_id faultwrap sizelimit osprofiler noauth apiv2
|
|||||||
keystone = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2
|
keystone = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2
|
||||||
keystone_nolimit = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2
|
keystone_nolimit = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2
|
||||||
|
|
||||||
|
[composite:openstack_volume_api_v3]
|
||||||
|
use = call:cinder.api.middleware.auth:pipeline_factory
|
||||||
|
noauth = cors request_id faultwrap sizelimit osprofiler noauth apiv3
|
||||||
|
keystone = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3
|
||||||
|
keystone_nolimit = cors request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3
|
||||||
|
|
||||||
[filter:request_id]
|
[filter:request_id]
|
||||||
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
||||||
|
|
||||||
[filter:cors]
|
[filter:cors]
|
||||||
paste.filter_factory = oslo_middleware.cors:filter_factory
|
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||||
oslo_config_project = cinder
|
oslo_config_project = cinder
|
||||||
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID, X-Trace-Info, X-Trace-HMAC
|
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID, X-Trace-Info, X-Trace-HMAC, OpenStack-Volume-microversion
|
||||||
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID
|
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID, OpenStack-Volume-microversion
|
||||||
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
|
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
|
||||||
|
|
||||||
[filter:faultwrap]
|
[filter:faultwrap]
|
||||||
@ -48,6 +55,9 @@ paste.app_factory = cinder.api.v1.router:APIRouter.factory
|
|||||||
[app:apiv2]
|
[app:apiv2]
|
||||||
paste.app_factory = cinder.api.v2.router:APIRouter.factory
|
paste.app_factory = cinder.api.v2.router:APIRouter.factory
|
||||||
|
|
||||||
|
[app:apiv3]
|
||||||
|
paste.app_factory = cinder.api.v3.router:APIRouter.factory
|
||||||
|
|
||||||
[pipeline:apiversions]
|
[pipeline:apiversions]
|
||||||
pipeline = cors faultwrap osvolumeversionapp
|
pipeline = cors faultwrap osvolumeversionapp
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add support for API microversions, as well as /v3 API endpoint
|
Loading…
Reference in New Issue
Block a user