Add microversioning support for methods
Adds the functionality to allow versioning of api methods based by adding a decorator api_version("min_version", "max_version"). This is similar to how nova implemented api versioning but updated to work with pecan. Change-Id: Ie18d92531487f7c107b5132b3d35f38bd0a37aa0 Implements: blueprint api-versioning
This commit is contained in:
parent
6c265ce10a
commit
e6a71b9e6d
@ -13,12 +13,21 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import operator
|
||||||
|
import six
|
||||||
|
|
||||||
|
from magnum.api.controllers import versions
|
||||||
|
from magnum.api import versioned_method
|
||||||
|
from magnum.common import exception
|
||||||
|
from magnum.i18n import _
|
||||||
|
from pecan import rest
|
||||||
from webob import exc
|
from webob import exc
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from magnum.i18n import _
|
|
||||||
|
# name of attribute to keep version method information
|
||||||
|
VER_METHOD_ATTR = 'versioned_methods'
|
||||||
|
|
||||||
|
|
||||||
class APIBase(wtypes.Base):
|
class APIBase(wtypes.Base):
|
||||||
@ -50,80 +59,171 @@ class APIBase(wtypes.Base):
|
|||||||
setattr(self, k, wsme.Unset)
|
setattr(self, k, wsme.Unset)
|
||||||
|
|
||||||
|
|
||||||
class Version(object):
|
class ControllerMetaclass(type):
|
||||||
"""API Version object."""
|
"""Controller metaclass.
|
||||||
|
|
||||||
string = 'OpenStack-API-Version'
|
This metaclass automates the task of assembling a dictionary
|
||||||
"""HTTP Header string carrying the requested version"""
|
mapping action keys to method names.
|
||||||
|
"""
|
||||||
|
|
||||||
min_string = 'OpenStack-API-Minimum-Version'
|
def __new__(mcs, name, bases, cls_dict):
|
||||||
"""HTTP response header"""
|
"""Adds version function dictionary to the class."""
|
||||||
|
|
||||||
max_string = 'OpenStack-API-Maximum-Version'
|
versioned_methods = None
|
||||||
"""HTTP response header"""
|
|
||||||
|
|
||||||
service_string = 'container-infra'
|
for base in bases:
|
||||||
|
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)
|
||||||
|
|
||||||
def __init__(self, headers, default_version, latest_version):
|
if versioned_methods:
|
||||||
"""Create an API Version object from the supplied headers.
|
cls_dict[VER_METHOD_ATTR] = versioned_methods
|
||||||
|
|
||||||
:param headers: webob headers
|
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
|
||||||
:param default_version: version to use if not specified in headers
|
cls_dict)
|
||||||
:param latest_version: version to use if latest is requested
|
|
||||||
:raises: webob.HTTPNotAcceptable
|
|
||||||
|
@six.add_metaclass(ControllerMetaclass)
|
||||||
|
class Controller(rest.RestController):
|
||||||
|
"""Base Rest Controller"""
|
||||||
|
|
||||||
|
def __getattribute__(self, key):
|
||||||
|
|
||||||
|
def version_select():
|
||||||
|
"""Select the correct method based on version
|
||||||
|
|
||||||
|
@return: Returns the correct versioned method
|
||||||
|
@raises: HTTPNotAcceptable if there is no method which
|
||||||
|
matches the name and version constraints
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pecan import request
|
||||||
|
ver = request.version
|
||||||
|
|
||||||
|
func_list = self.versioned_methods[key]
|
||||||
|
for func in func_list:
|
||||||
|
if ver.matches(func.start_version, func.end_version):
|
||||||
|
return func.func
|
||||||
|
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Version %(ver)s was requested but the requested API %(api)s "
|
||||||
|
"is not supported for this version.") % {'ver': ver,
|
||||||
|
'api': key})
|
||||||
|
|
||||||
|
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 version_meth_dict:
|
||||||
|
return version_select().__get__(self, self.__class__)
|
||||||
|
|
||||||
|
return object.__getattribute__(self, key)
|
||||||
|
|
||||||
|
# NOTE: 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):
|
||||||
|
"""Decorator for versioning api methods.
|
||||||
|
|
||||||
|
Add the decorator to any pecan method that has been exposed.
|
||||||
|
This decorator will store the method, min version, and max
|
||||||
|
version in a list for each api. It will check that there is no
|
||||||
|
overlap between versions and methods. When the api is called the
|
||||||
|
controller will use the list for each api to determine which
|
||||||
|
method to call.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@base.Controller.api_version("1.1", "1.2")
|
||||||
|
@expose.expose(Bay, types.uuid_or_name)
|
||||||
|
def get_one(self, bay_ident):
|
||||||
|
{...code for versions 1.1 to 1.2...}
|
||||||
|
|
||||||
|
@base.Controller.api_version("1.3")
|
||||||
|
@expose.expose(Bay, types.uuid_or_name)
|
||||||
|
def get_one(self, bay_ident):
|
||||||
|
{...code for versions 1.3 to latest}
|
||||||
|
|
||||||
|
@min_ver: string representing minimum version
|
||||||
|
@max_ver: optional string representing maximum version
|
||||||
|
@raises: ApiVersionsIntersect if an version overlap is found between
|
||||||
|
method versions.
|
||||||
"""
|
"""
|
||||||
(self.major, self.minor) = Version.parse_headers(headers,
|
|
||||||
default_version,
|
|
||||||
latest_version)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def decorator(f):
|
||||||
return '%s.%s' % (self.major, self.minor)
|
obj_min_ver = versions.Version('', '', '', min_ver)
|
||||||
|
if max_ver:
|
||||||
|
obj_max_ver = versions.Version('', '', '', max_ver)
|
||||||
|
else:
|
||||||
|
obj_max_ver = versions.Version('', '', '',
|
||||||
|
versions.CURRENT_MAX_VER)
|
||||||
|
|
||||||
|
# Add to list of versioned methods registered
|
||||||
|
func_name = f.__name__
|
||||||
|
new_func = versioned_method.VersionedMethod(
|
||||||
|
func_name, obj_min_ver, obj_max_ver, 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)
|
||||||
|
|
||||||
|
is_intersect = Controller.check_for_versions_intersection(
|
||||||
|
func_list)
|
||||||
|
|
||||||
|
if is_intersect:
|
||||||
|
raise exception.ApiVersionsIntersect(
|
||||||
|
name=new_func.name,
|
||||||
|
min_ver=new_func.start_version,
|
||||||
|
max_ver=new_func.end_version
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
func_list.sort(key=lambda f: f.start_version, reverse=True)
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_headers(headers, default_version, latest_version):
|
def check_for_versions_intersection(func_list):
|
||||||
"""Determine the API version requested based on the headers supplied.
|
"""Determines whether function list intersections
|
||||||
|
|
||||||
:param headers: webob headers
|
General algorithm:
|
||||||
:param default_version: version to use if not specified in headers
|
https://en.wikipedia.org/wiki/Intersection_algorithm
|
||||||
:param latest_version: version to use if latest is requested
|
|
||||||
:returns: a tuple of (major, minor) version numbers
|
:param func_list: list of VersionedMethod objects
|
||||||
:raises: webob.HTTPNotAcceptable
|
:return: boolean
|
||||||
"""
|
"""
|
||||||
|
|
||||||
version_hdr = headers.get(Version.string, default_version)
|
pairs = []
|
||||||
|
counter = 0
|
||||||
|
|
||||||
try:
|
for f in func_list:
|
||||||
version_service, version_str = version_hdr.split()
|
pairs.append((f.start_version, 1))
|
||||||
except ValueError:
|
pairs.append((f.end_version, -1))
|
||||||
raise exc.HTTPNotAcceptable(_(
|
|
||||||
"Invalid service type for %s header") % Version.string)
|
|
||||||
|
|
||||||
if version_str.lower() == 'latest':
|
pairs.sort(key=operator.itemgetter(1), reverse=True)
|
||||||
version_service, version_str = latest_version.split()
|
pairs.sort(key=operator.itemgetter(0))
|
||||||
|
|
||||||
if version_service != Version.service_string:
|
for p in pairs:
|
||||||
raise exc.HTTPNotAcceptable(_(
|
counter += p[1]
|
||||||
"Invalid service type for %s header") % Version.string)
|
|
||||||
try:
|
|
||||||
version = tuple(int(i) for i in version_str.split('.'))
|
|
||||||
except ValueError:
|
|
||||||
version = ()
|
|
||||||
|
|
||||||
if len(version) != 2:
|
if counter > 1:
|
||||||
raise exc.HTTPNotAcceptable(_(
|
return True
|
||||||
"Invalid value for %s header") % Version.string)
|
|
||||||
return version
|
|
||||||
|
|
||||||
def __lt__(a, b):
|
|
||||||
if (a.major < b.major):
|
|
||||||
return True
|
|
||||||
if (a.major == b.major and a.minor < b.minor):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __gt__(a, b):
|
|
||||||
if (a.major > b.major):
|
|
||||||
return True
|
|
||||||
if (a.major == b.major and a.minor > b.minor):
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
@ -19,6 +19,7 @@ from wsme import types as wtypes
|
|||||||
from magnum.api.controllers import base
|
from magnum.api.controllers import base
|
||||||
from magnum.api.controllers import link
|
from magnum.api.controllers import link
|
||||||
from magnum.api.controllers import v1
|
from magnum.api.controllers import v1
|
||||||
|
from magnum.api.controllers import versions
|
||||||
from magnum.api import expose
|
from magnum.api import expose
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +70,9 @@ class Root(base.APIBase):
|
|||||||
root.name = "OpenStack Magnum API"
|
root.name = "OpenStack Magnum API"
|
||||||
root.description = ("Magnum is an OpenStack project which aims to "
|
root.description = ("Magnum is an OpenStack project which aims to "
|
||||||
"provide container management.")
|
"provide container management.")
|
||||||
root.versions = [Version.convert('v1', "CURRENT", "1.1", "1.1")]
|
root.versions = [Version.convert('v1', "CURRENT",
|
||||||
|
versions.CURRENT_MAX_VER,
|
||||||
|
versions.BASE_VER)]
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
|
|||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from magnum.api.controllers import base as controllers_base
|
from magnum.api.controllers import base as controllers_base
|
||||||
@ -29,6 +28,7 @@ from magnum.api.controllers.v1 import bay
|
|||||||
from magnum.api.controllers.v1 import baymodel
|
from magnum.api.controllers.v1 import baymodel
|
||||||
from magnum.api.controllers.v1 import certificate
|
from magnum.api.controllers.v1 import certificate
|
||||||
from magnum.api.controllers.v1 import magnum_services
|
from magnum.api.controllers.v1 import magnum_services
|
||||||
|
from magnum.api.controllers import versions as ver
|
||||||
from magnum.api import expose
|
from magnum.api import expose
|
||||||
from magnum.api import http_error
|
from magnum.api import http_error
|
||||||
from magnum.i18n import _
|
from magnum.i18n import _
|
||||||
@ -38,28 +38,14 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
BASE_VERSION = 1
|
BASE_VERSION = 1
|
||||||
|
|
||||||
# NOTE(yuntong): v1.0 is reserved to indicate Kilo's API, but is not presently
|
MIN_VER_STR = '%s %s' % (ver.Version.service_string, ver.BASE_VER)
|
||||||
# supported by the API service. All changes between Kilo and the
|
|
||||||
# point where we added microversioning are considered backwards-
|
|
||||||
# compatible, but are not specifically discoverable at this time.
|
|
||||||
#
|
|
||||||
# The v1.1 version indicates this "initial" version as being
|
|
||||||
# different from Kilo (v1.0), and includes the following changes:
|
|
||||||
#
|
|
||||||
|
|
||||||
# v1.1: API at the point in time when microversioning support was added
|
MAX_VER_STR = '%s %s' % (ver.Version.service_string, ver.CURRENT_MAX_VER)
|
||||||
MIN_VER_STR = 'container-infra 1.1'
|
|
||||||
|
|
||||||
# v1.1: Add API changelog here
|
MIN_VER = ver.Version({ver.Version.string: MIN_VER_STR},
|
||||||
MAX_VER_STR = 'container-infra 1.1'
|
MIN_VER_STR, MAX_VER_STR)
|
||||||
|
MAX_VER = ver.Version({ver.Version.string: MAX_VER_STR},
|
||||||
|
MIN_VER_STR, MAX_VER_STR)
|
||||||
MIN_VER = controllers_base.Version(
|
|
||||||
{controllers_base.Version.string: MIN_VER_STR},
|
|
||||||
MIN_VER_STR, MAX_VER_STR)
|
|
||||||
MAX_VER = controllers_base.Version(
|
|
||||||
{controllers_base.Version.string: MAX_VER_STR},
|
|
||||||
MIN_VER_STR, MAX_VER_STR)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaType(controllers_base.APIBase):
|
class MediaType(controllers_base.APIBase):
|
||||||
@ -137,7 +123,7 @@ class V1(controllers_base.APIBase):
|
|||||||
return v1
|
return v1
|
||||||
|
|
||||||
|
|
||||||
class Controller(rest.RestController):
|
class Controller(controllers_base.Controller):
|
||||||
"""Version 1 API controller root."""
|
"""Version 1 API controller root."""
|
||||||
|
|
||||||
bays = bay.BaysController()
|
bays = bay.BaysController()
|
||||||
@ -180,22 +166,18 @@ class Controller(rest.RestController):
|
|||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
def _route(self, args):
|
def _route(self, args):
|
||||||
version = controllers_base.Version(
|
version = ver.Version(
|
||||||
pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
|
pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
|
||||||
|
|
||||||
# Always set the basic version headers
|
# Always set the basic version headers
|
||||||
pecan.response.headers[
|
pecan.response.headers[ver.Version.min_string] = MIN_VER_STR
|
||||||
controllers_base.Version.min_string] = MIN_VER_STR
|
pecan.response.headers[ver.Version.max_string] = MAX_VER_STR
|
||||||
pecan.response.headers[
|
pecan.response.headers[ver.Version.string] = " ".join(
|
||||||
controllers_base.Version.max_string] = MAX_VER_STR
|
[ver.Version.service_string, str(version)])
|
||||||
pecan.response.headers[
|
pecan.response.headers["vary"] = ver.Version.string
|
||||||
controllers_base.Version.string] = " ".join(
|
|
||||||
[controllers_base.Version.service_string, str(version)])
|
|
||||||
pecan.response.headers["vary"] = controllers_base.Version.string
|
|
||||||
|
|
||||||
# assert that requested version is supported
|
# assert that requested version is supported
|
||||||
self._check_version(version, pecan.response.headers)
|
self._check_version(version, pecan.response.headers)
|
||||||
pecan.response.headers[controllers_base.Version.string] = str(version)
|
|
||||||
pecan.request.version = version
|
pecan.request.version = version
|
||||||
if pecan.request.body:
|
if pecan.request.body:
|
||||||
msg = ("Processing request: url: %(url)s, %(method)s, "
|
msg = ("Processing request: url: %(url)s, %(method)s, "
|
||||||
@ -207,4 +189,5 @@ class Controller(rest.RestController):
|
|||||||
|
|
||||||
return super(Controller, self)._route(args)
|
return super(Controller, self)._route(args)
|
||||||
|
|
||||||
|
|
||||||
__all__ = (Controller)
|
__all__ = (Controller)
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
@ -199,7 +198,7 @@ class BayCollection(collection.Collection):
|
|||||||
return sample
|
return sample
|
||||||
|
|
||||||
|
|
||||||
class BaysController(rest.RestController):
|
class BaysController(base.Controller):
|
||||||
"""REST controller for Bays."""
|
"""REST controller for Bays."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(BaysController, self).__init__()
|
super(BaysController, self).__init__()
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
@ -226,7 +225,7 @@ class BayModelCollection(collection.Collection):
|
|||||||
return sample
|
return sample
|
||||||
|
|
||||||
|
|
||||||
class BayModelsController(rest.RestController):
|
class BayModelsController(base.Controller):
|
||||||
"""REST controller for BayModels."""
|
"""REST controller for BayModels."""
|
||||||
|
|
||||||
_custom_actions = {
|
_custom_actions = {
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
@ -114,7 +113,7 @@ class Certificate(base.APIBase):
|
|||||||
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
|
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
|
||||||
|
|
||||||
|
|
||||||
class CertificateController(rest.RestController):
|
class CertificateController(base.Controller):
|
||||||
"""REST controller for Certificate."""
|
"""REST controller for Certificate."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ class MagnumServiceCollection(collection.Collection):
|
|||||||
return collection
|
return collection
|
||||||
|
|
||||||
|
|
||||||
class MagnumServiceController(rest.RestController):
|
class MagnumServiceController(base.Controller):
|
||||||
"""REST controller for magnum-services."""
|
"""REST controller for magnum-services."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
138
magnum/api/controllers/versions.py
Normal file
138
magnum/api/controllers/versions.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# 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 webob import exc
|
||||||
|
|
||||||
|
from magnum.i18n import _
|
||||||
|
|
||||||
|
# NOTE(yuntong): v1.0 is reserved to indicate Kilo's API, but is not presently
|
||||||
|
# supported by the API service. All changes between Kilo and the
|
||||||
|
# point where we added microversioning are considered backwards-
|
||||||
|
# compatible, but are not specifically discoverable at this time.
|
||||||
|
#
|
||||||
|
# The v1.1 version indicates this "initial" version as being
|
||||||
|
# different from Kilo (v1.0), and includes the following changes:
|
||||||
|
#
|
||||||
|
# Add details of new api versions here:
|
||||||
|
|
||||||
|
BASE_VER = '1.1'
|
||||||
|
CURRENT_MAX_VER = '1.1'
|
||||||
|
|
||||||
|
|
||||||
|
class Version(object):
|
||||||
|
"""API Version object."""
|
||||||
|
|
||||||
|
string = 'OpenStack-API-Version'
|
||||||
|
"""HTTP Header string carrying the requested version"""
|
||||||
|
|
||||||
|
min_string = 'OpenStack-API-Minimum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
max_string = 'OpenStack-API-Maximum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
service_string = 'container-infra'
|
||||||
|
|
||||||
|
def __init__(self, headers, default_version, latest_version,
|
||||||
|
from_string=None):
|
||||||
|
"""Create an API Version object from the supplied headers.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:param from_string: create the version from string not headers
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
if from_string:
|
||||||
|
(self.major, self.minor) = tuple(int(i)
|
||||||
|
for i in from_string.split('.'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
(self.major, self.minor) = Version.parse_headers(headers,
|
||||||
|
default_version,
|
||||||
|
latest_version)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s.%s' % (self.major, self.minor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_headers(headers, default_version, latest_version):
|
||||||
|
"""Determine the API version requested based on the headers supplied.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:returns: a tuple of (major, minor) version numbers
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
|
||||||
|
version_hdr = headers.get(Version.string, default_version)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_service, version_str = version_hdr.split()
|
||||||
|
except ValueError:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid service type for %s header") % Version.string)
|
||||||
|
|
||||||
|
if version_str.lower() == 'latest':
|
||||||
|
version_service, version_str = latest_version.split()
|
||||||
|
|
||||||
|
if version_service != Version.service_string:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid service type for %s header") % Version.string)
|
||||||
|
try:
|
||||||
|
version = tuple(int(i) for i in version_str.split('.'))
|
||||||
|
except ValueError:
|
||||||
|
version = ()
|
||||||
|
|
||||||
|
if len(version) != 2:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid value for %s header") % Version.string)
|
||||||
|
return version
|
||||||
|
|
||||||
|
def is_null(self):
|
||||||
|
return self.major == 0 and self.minor == 0
|
||||||
|
|
||||||
|
def matches(self, start_version, end_version):
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
return start_version <= self <= end_version
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if self.major < other.major:
|
||||||
|
return True
|
||||||
|
if self.major == other.major and self.minor < other.minor:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if self.major > other.major:
|
||||||
|
return True
|
||||||
|
if self.major == other.major and self.minor > other.minor:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.major == other.major and self.minor == other.minor
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self < other or self == other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self > other or self == other
|
@ -29,7 +29,7 @@ class HTTPNotAcceptableAPIVersion(exc.HTTPNotAcceptable):
|
|||||||
#
|
#
|
||||||
# differences from webob.exc.HTTPNotAcceptable:
|
# differences from webob.exc.HTTPNotAcceptable:
|
||||||
#
|
#
|
||||||
# - additional max and min version paramters
|
# - additional max and min version parameters
|
||||||
# - additional error info for code, title, and links
|
# - additional error info for code, title, and links
|
||||||
code = 406
|
code = 406
|
||||||
title = 'Not Acceptable'
|
title = 'Not Acceptable'
|
||||||
|
35
magnum/api/versioned_method.py
Normal file
35
magnum/api/versioned_method.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Copyright 2014 IBM Corp.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedMethod(object):
|
||||||
|
|
||||||
|
def __init__(self, name, start_version, end_version, func):
|
||||||
|
"""Versioning information for a single method
|
||||||
|
|
||||||
|
@name: Name of the method
|
||||||
|
@start_version: Minimum acceptable version
|
||||||
|
@end_version: Maximum acceptable_version
|
||||||
|
@func: Method to call
|
||||||
|
|
||||||
|
Minimum and maximums are inclusive
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.start_version = start_version
|
||||||
|
self.end_version = end_version
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("Version Method %s: min: %s, max: %s"
|
||||||
|
% (self.name, self.start_version, self.end_version))
|
@ -184,6 +184,11 @@ class Conflict(MagnumException):
|
|||||||
code = 409
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class ApiVersionsIntersect(MagnumException):
|
||||||
|
message = _("Version of %(name)s %(min_ver)s %(max_ver)s intersects "
|
||||||
|
"with another versions.")
|
||||||
|
|
||||||
|
|
||||||
# Cannot be templated as the error syntax varies.
|
# Cannot be templated as the error syntax varies.
|
||||||
# msg needs to be constructed when raised.
|
# msg needs to be constructed when raised.
|
||||||
class InvalidParameterValue(Invalid):
|
class InvalidParameterValue(Invalid):
|
||||||
|
@ -14,19 +14,45 @@ import mock
|
|||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from magnum.api.controllers import base
|
from magnum.api.controllers import base
|
||||||
|
from magnum.api.controllers import versions
|
||||||
|
from magnum.api import versioned_method
|
||||||
from magnum.tests import base as test_base
|
from magnum.tests import base as test_base
|
||||||
|
|
||||||
|
|
||||||
class TestVersion(test_base.TestCase):
|
class TestVersion(test_base.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestVersion, self).setUp()
|
super(TestVersion, self).setUp()
|
||||||
self.a = base.Version(
|
self.a = versions.Version(
|
||||||
{base.Version.string: "container-infra 2.0"},
|
{versions.Version.string: "container-infra 2.0"},
|
||||||
"container-infra 2.0", "container-infra 2.1")
|
"container-infra 2.0", "container-infra 2.1")
|
||||||
self.b = base.Version(
|
self.b = versions.Version(
|
||||||
{base.Version.string: "container-infra 2.0"},
|
{versions.Version.string: "container-infra 2.0"},
|
||||||
"container-infra 2.0", "container-infra 2.1")
|
"container-infra 2.0", "container-infra 2.1")
|
||||||
|
self.c = versions.Version(
|
||||||
|
{versions.Version.string: "container-infra 2.2"},
|
||||||
|
"container-infra 2.0", "container-infra 2.2")
|
||||||
|
|
||||||
|
def test_is_null_true(self):
|
||||||
|
self.a.major = 0
|
||||||
|
self.a.minor = 0
|
||||||
|
self.assertEqual(0 == 0, self.a.is_null())
|
||||||
|
|
||||||
|
def test_is_null_false(self):
|
||||||
|
self.assertEqual(2 == 0, self.a.is_null())
|
||||||
|
|
||||||
|
def test__eq__with_equal(self):
|
||||||
|
self.assertEqual(2 == 2, self.a == self.b)
|
||||||
|
|
||||||
|
def test__eq__with_unequal(self):
|
||||||
|
self.a.major = 1
|
||||||
|
self.assertEqual(1 == 2, self.a == self.b)
|
||||||
|
|
||||||
|
def test__ne__with_equal(self):
|
||||||
|
self.assertEqual(2 != 2, self.a != self.b)
|
||||||
|
|
||||||
|
def test__ne__with_unequal(self):
|
||||||
|
self.a.major = 1
|
||||||
|
self.assertEqual(1 != 2, self.a != self.b)
|
||||||
|
|
||||||
def test__lt__with_higher_major_version(self):
|
def test__lt__with_higher_major_version(self):
|
||||||
self.a.major = 2
|
self.a.major = 2
|
||||||
@ -80,73 +106,304 @@ class TestVersion(test_base.TestCase):
|
|||||||
self.assertEqual(self.a.major, self.b.major)
|
self.assertEqual(self.a.major, self.b.major)
|
||||||
self.assertEqual(1 > 2, self.a > self.b)
|
self.assertEqual(1 > 2, self.a > self.b)
|
||||||
|
|
||||||
@mock.patch('magnum.api.controllers.base.Version.parse_headers')
|
def test__le__with_equal(self):
|
||||||
|
self.assertEqual(2 == 2, self.a <= self.b)
|
||||||
|
|
||||||
|
def test__le__with_higher_version(self):
|
||||||
|
self.a.major = 3
|
||||||
|
self.assertEqual(3 <= 2, self.a <= self.b)
|
||||||
|
|
||||||
|
def test__le__with_lower_version(self):
|
||||||
|
self.a.major = 1
|
||||||
|
self.assertEqual(1 <= 2, self.a <= self.b)
|
||||||
|
|
||||||
|
def test__ge__with_equal(self):
|
||||||
|
self.assertEqual(2 >= 2, self.a >= self.b)
|
||||||
|
|
||||||
|
def test__ge__with_higher_version(self):
|
||||||
|
self.a.major = 3
|
||||||
|
self.assertEqual(3 >= 2, self.a >= self.b)
|
||||||
|
|
||||||
|
def test__ge__with_lower_version(self):
|
||||||
|
self.a.major = 1
|
||||||
|
self.assertEqual(1 >= 2, self.a >= self.b)
|
||||||
|
|
||||||
|
def test_matches_start_version(self):
|
||||||
|
self.assertEqual(0 >= 0, self.a.matches(self.b, self.c))
|
||||||
|
|
||||||
|
def test_matches_end_version(self):
|
||||||
|
self.a.minor = 2
|
||||||
|
self.assertEqual(2 <= 2, self.a.matches(self.b, self.c))
|
||||||
|
|
||||||
|
def test_matches_valid_version(self):
|
||||||
|
self.a.minor = 1
|
||||||
|
self.assertEqual(0 <= 1 <= 2, self.a.matches(self.b, self.c))
|
||||||
|
|
||||||
|
def test_matches_version_too_high(self):
|
||||||
|
self.a.minor = 3
|
||||||
|
self.assertEqual(0 <= 3 <= 2, self.a.matches(self.b, self.c))
|
||||||
|
|
||||||
|
def test_matches_version_too_low(self):
|
||||||
|
self.a.major = 1
|
||||||
|
self.assertEqual(2 <= 1 <= 2, self.a.matches(self.b, self.c))
|
||||||
|
|
||||||
|
def test_matches_null_version(self):
|
||||||
|
self.a.major = 0
|
||||||
|
self.a.minor = 0
|
||||||
|
self.assertRaises(ValueError, self.a.matches, self.b, self.c)
|
||||||
|
|
||||||
|
@mock.patch('magnum.api.controllers.versions.Version.parse_headers')
|
||||||
def test_init(self, mock_parse):
|
def test_init(self, mock_parse):
|
||||||
a = mock.Mock()
|
a = mock.Mock()
|
||||||
b = mock.Mock()
|
b = mock.Mock()
|
||||||
mock_parse.return_value = (a, b)
|
mock_parse.return_value = (a, b)
|
||||||
v = base.Version('test', 'foo', 'bar')
|
v = versions.Version('test', 'foo', 'bar')
|
||||||
|
|
||||||
mock_parse.assert_called_with('test', 'foo', 'bar')
|
mock_parse.assert_called_with('test', 'foo', 'bar')
|
||||||
self.assertEqual(a, v.major)
|
self.assertEqual(a, v.major)
|
||||||
self.assertEqual(b, v.minor)
|
self.assertEqual(b, v.minor)
|
||||||
|
|
||||||
@mock.patch('magnum.api.controllers.base.Version.parse_headers')
|
@mock.patch('magnum.api.controllers.versions.Version.parse_headers')
|
||||||
def test_repr(self, mock_parse):
|
def test_repr(self, mock_parse):
|
||||||
mock_parse.return_value = (123, 456)
|
mock_parse.return_value = (123, 456)
|
||||||
v = base.Version('test', mock.ANY, mock.ANY)
|
v = versions.Version('test', mock.ANY, mock.ANY)
|
||||||
result = "%s" % v
|
result = "%s" % v
|
||||||
self.assertEqual('123.456', result)
|
self.assertEqual('123.456', result)
|
||||||
|
|
||||||
@mock.patch('magnum.api.controllers.base.Version.parse_headers')
|
@mock.patch('magnum.api.controllers.versions.Version.parse_headers')
|
||||||
def test_repr_with_strings(self, mock_parse):
|
def test_repr_with_strings(self, mock_parse):
|
||||||
mock_parse.return_value = ('abc', 'def')
|
mock_parse.return_value = ('abc', 'def')
|
||||||
v = base.Version('test', mock.ANY, mock.ANY)
|
v = versions.Version('test', mock.ANY, mock.ANY)
|
||||||
result = "%s" % v
|
result = "%s" % v
|
||||||
self.assertEqual('abc.def', result)
|
self.assertEqual('abc.def', result)
|
||||||
|
|
||||||
def test_parse_headers_ok(self):
|
def test_parse_headers_ok(self):
|
||||||
version = base.Version.parse_headers(
|
version = versions.Version.parse_headers(
|
||||||
{base.Version.string: 'container-infra 123.456'},
|
{versions.Version.string: 'container-infra 123.456'},
|
||||||
mock.ANY, mock.ANY)
|
mock.ANY, mock.ANY)
|
||||||
self.assertEqual((123, 456), version)
|
self.assertEqual((123, 456), version)
|
||||||
|
|
||||||
def test_parse_headers_latest(self):
|
def test_parse_headers_latest(self):
|
||||||
for s in ['magnum latest', 'magnum LATEST']:
|
for s in ['magnum latest', 'magnum LATEST']:
|
||||||
version = base.Version.parse_headers(
|
version = versions.Version.parse_headers(
|
||||||
{base.Version.string: s}, mock.ANY, 'container-infra 1.9')
|
{versions.Version.string: s}, mock.ANY, 'container-infra 1.9')
|
||||||
self.assertEqual((1, 9), version)
|
self.assertEqual((1, 9), version)
|
||||||
|
|
||||||
def test_parse_headers_bad_length(self):
|
def test_parse_headers_bad_length(self):
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPNotAcceptable,
|
exc.HTTPNotAcceptable,
|
||||||
base.Version.parse_headers,
|
versions.Version.parse_headers,
|
||||||
{base.Version.string: 'container-infra 1'},
|
{versions.Version.string: 'container-infra 1'},
|
||||||
mock.ANY,
|
mock.ANY,
|
||||||
mock.ANY)
|
mock.ANY)
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPNotAcceptable,
|
exc.HTTPNotAcceptable,
|
||||||
base.Version.parse_headers,
|
versions.Version.parse_headers,
|
||||||
{base.Version.string: 'container-infra 1.2.3'},
|
{versions.Version.string: 'container-infra 1.2.3'},
|
||||||
mock.ANY,
|
mock.ANY,
|
||||||
mock.ANY)
|
mock.ANY)
|
||||||
|
|
||||||
def test_parse_no_header(self):
|
def test_parse_no_header(self):
|
||||||
# this asserts that the minimum version string is applied
|
# this asserts that the minimum version string is applied
|
||||||
version = base.Version.parse_headers({}, 'container-infra 1.1',
|
version = versions.Version.parse_headers({}, 'container-infra 1.1',
|
||||||
'container-infra 1.5')
|
'container-infra 1.5')
|
||||||
self.assertEqual((1, 1), version)
|
self.assertEqual((1, 1), version)
|
||||||
|
|
||||||
def test_parse_incorrect_service_type(self):
|
def test_parse_incorrect_service_type(self):
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPNotAcceptable,
|
exc.HTTPNotAcceptable,
|
||||||
base.Version.parse_headers,
|
versions.Version.parse_headers,
|
||||||
{base.Version.string: '1.1'},
|
{versions.Version.string: '1.1'},
|
||||||
'container-infra 1.1',
|
'container-infra 1.1',
|
||||||
'container-infra 1.1')
|
'container-infra 1.1')
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPNotAcceptable,
|
exc.HTTPNotAcceptable,
|
||||||
base.Version.parse_headers,
|
versions.Version.parse_headers,
|
||||||
{base.Version.string: 'nova 1.1'},
|
{versions.Version.string: 'nova 1.1'},
|
||||||
'container-infra 1.1',
|
'container-infra 1.1',
|
||||||
'container-infra 1.1')
|
'container-infra 1.1')
|
||||||
|
|
||||||
|
|
||||||
|
class TestController(test_base.TestCase):
|
||||||
|
def test_check_for_versions_intersection_negative(self):
|
||||||
|
func_list = \
|
||||||
|
[versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.1'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.4'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.11'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'3.1'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.8'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.9'),
|
||||||
|
None),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = base.Controller.check_for_versions_intersection(
|
||||||
|
func_list=func_list)
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
func_list = \
|
||||||
|
[versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.12'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.14'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'3.0'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'3.4'),
|
||||||
|
None)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = base.Controller.check_for_versions_intersection(
|
||||||
|
func_list=func_list)
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
def test_check_for_versions_intersection_positive(self):
|
||||||
|
func_list = \
|
||||||
|
[versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.1'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.4'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.3'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'3.1'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'2.9'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'3.4'),
|
||||||
|
None)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = base.Controller.check_for_versions_intersection(
|
||||||
|
func_list=func_list)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
def test_check_for_versions_intersection_shared_start_end(self):
|
||||||
|
func_list = \
|
||||||
|
[versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'1.1'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'1.1'),
|
||||||
|
None),
|
||||||
|
versioned_method.VersionedMethod('foo',
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'1.1'),
|
||||||
|
versions.Version('', '', '',
|
||||||
|
'1.2'),
|
||||||
|
None)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = base.Controller.check_for_versions_intersection(
|
||||||
|
func_list=func_list)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
def test_api_version_decorator(self):
|
||||||
|
|
||||||
|
class MyController(base.Controller):
|
||||||
|
@base.Controller.api_version('1.0', '1.1')
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.0_1.1'
|
||||||
|
|
||||||
|
@base.Controller.api_version('1.2', '1.3') # noqa
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.2_1.3'
|
||||||
|
|
||||||
|
@base.Controller.api_version('2.1', '2.2')
|
||||||
|
def testapi2(self):
|
||||||
|
return 'API2_2.1_2.2'
|
||||||
|
|
||||||
|
@base.Controller.api_version('1.0', '2.0') # noqa
|
||||||
|
def testapi2(self):
|
||||||
|
return 'API2_1.0_2.0'
|
||||||
|
|
||||||
|
controller = MyController()
|
||||||
|
# verify list was added to controller
|
||||||
|
self.assertIsNotNone(controller.versioned_methods)
|
||||||
|
|
||||||
|
api1_list = controller.versioned_methods['testapi1']
|
||||||
|
api2_list = controller.versioned_methods['testapi2']
|
||||||
|
|
||||||
|
# verify versioned_methods reordered correctly
|
||||||
|
self.assertEqual('1.2', str(api1_list[0].start_version))
|
||||||
|
self.assertEqual('1.3', str(api1_list[0].end_version))
|
||||||
|
self.assertEqual('1.0', str(api1_list[1].start_version))
|
||||||
|
self.assertEqual('1.1', str(api1_list[1].end_version))
|
||||||
|
|
||||||
|
# verify stored methods can be called
|
||||||
|
result = api1_list[0].func(controller)
|
||||||
|
self.assertEqual('API1_1.2_1.3', result)
|
||||||
|
result = api1_list[1].func(controller)
|
||||||
|
self.assertEqual('API1_1.0_1.1', result)
|
||||||
|
|
||||||
|
# verify versioned_methods reordered correctly
|
||||||
|
self.assertEqual('2.1', str(api2_list[0].start_version))
|
||||||
|
self.assertEqual('2.2', str(api2_list[0].end_version))
|
||||||
|
self.assertEqual('1.0', str(api2_list[1].start_version))
|
||||||
|
self.assertEqual('2.0', str(api2_list[1].end_version))
|
||||||
|
|
||||||
|
# Verify stored methods can be called
|
||||||
|
result = api2_list[0].func(controller)
|
||||||
|
self.assertEqual('API2_2.1_2.2', result)
|
||||||
|
result = api2_list[1].func(controller)
|
||||||
|
self.assertEqual('API2_1.0_2.0', result)
|
||||||
|
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
def test_controller_get_attribute(self, mock_pecan_request):
|
||||||
|
|
||||||
|
class MyController(base.Controller):
|
||||||
|
@base.Controller.api_version('1.0', '1.1')
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.0_1.1'
|
||||||
|
|
||||||
|
@base.Controller.api_version('1.2', '1.3') # noqa
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.2_1.3'
|
||||||
|
|
||||||
|
controller = MyController()
|
||||||
|
mock_pecan_request.version = versions.Version("", "",
|
||||||
|
"", "1.2")
|
||||||
|
controller.request = mock_pecan_request
|
||||||
|
|
||||||
|
method = controller.__getattribute__('testapi1')
|
||||||
|
result = method()
|
||||||
|
self.assertEqual('API1_1.2_1.3', result)
|
||||||
|
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
def test_controller_get_attr_version_not_found(self,
|
||||||
|
mock_pecan_request):
|
||||||
|
|
||||||
|
class MyController(base.Controller):
|
||||||
|
@base.Controller.api_version('1.0', '1.1')
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.0_1.1'
|
||||||
|
|
||||||
|
@base.Controller.api_version('1.3', '1.4') # noqa
|
||||||
|
def testapi1(self):
|
||||||
|
return 'API1_1.3_1.4'
|
||||||
|
|
||||||
|
controller = MyController()
|
||||||
|
mock_pecan_request.version = versions.Version("", "",
|
||||||
|
"", "1.2")
|
||||||
|
controller.request = mock_pecan_request
|
||||||
|
|
||||||
|
self.assertRaises(exc.HTTPNotAcceptable,
|
||||||
|
controller.__getattribute__, 'testapi1')
|
||||||
|
Loading…
Reference in New Issue
Block a user