Merge "Introduce API micro version"
This commit is contained in:
@@ -12,6 +12,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import operator
|
||||
import six
|
||||
|
||||
from pecan import rest
|
||||
from zun.api.controllers import versions
|
||||
from zun.api import versioned_method
|
||||
from zun.common import exception
|
||||
from zun.common.i18n import _
|
||||
|
||||
|
||||
# name of attribute to keep version method information
|
||||
VER_METHOD_ATTR = 'versioned_methods'
|
||||
|
||||
|
||||
class APIBase(object):
|
||||
|
||||
@@ -45,3 +58,171 @@ class APIBase(object):
|
||||
for k in self.as_dict():
|
||||
if k not in except_list:
|
||||
setattr(self, k, None)
|
||||
|
||||
|
||||
class ControllerMetaclass(type):
|
||||
"""Controller metaclass.
|
||||
|
||||
This metaclass automates the task of assembling a dictionary
|
||||
mapping action keys to method names.
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, cls_dict):
|
||||
"""Adds version function dictionary to the class."""
|
||||
|
||||
versioned_methods = None
|
||||
|
||||
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)
|
||||
|
||||
if versioned_methods:
|
||||
cls_dict[VER_METHOD_ATTR] = versioned_methods
|
||||
|
||||
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
|
||||
cls_dict)
|
||||
|
||||
|
||||
@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 exception.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")
|
||||
def get_one(self, container_id):
|
||||
{...code for versions 1.1 to 1.2...}
|
||||
|
||||
@base.Controller.api_version("1.3")
|
||||
def get_one(self, container_id):
|
||||
{...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.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
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
|
||||
def check_for_versions_intersection(func_list):
|
||||
"""Determines whether function list intersections
|
||||
|
||||
General algorithm:
|
||||
https://en.wikipedia.org/wiki/Intersection_algorithm
|
||||
|
||||
:param func_list: list of VersionedMethod objects
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
pairs = []
|
||||
counter = 0
|
||||
|
||||
for f in func_list:
|
||||
pairs.append((f.start_version, 1))
|
||||
pairs.append((f.end_version, -1))
|
||||
|
||||
pairs.sort(key=operator.itemgetter(1), reverse=True)
|
||||
pairs.sort(key=operator.itemgetter(0))
|
||||
|
||||
for p in pairs:
|
||||
counter += p[1]
|
||||
|
||||
if counter > 1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -16,6 +16,7 @@ from pecan import rest
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers import v1
|
||||
from zun.api.controllers import versions
|
||||
|
||||
|
||||
class Version(base.APIBase):
|
||||
@@ -24,14 +25,20 @@ class Version(base.APIBase):
|
||||
fields = (
|
||||
'id',
|
||||
'links',
|
||||
'status',
|
||||
'max_version',
|
||||
'min_version'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def convert(id):
|
||||
def convert(id, status, max, min):
|
||||
version = Version()
|
||||
version.id = id
|
||||
version.links = [link.make_link('self', pecan.request.host_url,
|
||||
id, '', bookmark=True)]
|
||||
version.status = status
|
||||
version.max_version = max
|
||||
version.min_version = min
|
||||
return version
|
||||
|
||||
|
||||
@@ -50,8 +57,13 @@ class Root(base.APIBase):
|
||||
root.name = "OpenStack Zun API"
|
||||
root.description = ("Zun is an OpenStack project which aims to "
|
||||
"provide container management.")
|
||||
root.versions = [Version.convert('v1')]
|
||||
root.default_version = Version.convert('v1')
|
||||
|
||||
root.versions = [Version.convert('v1', "CURRENT",
|
||||
versions.CURRENT_MAX_VER,
|
||||
versions.BASE_VER)]
|
||||
root.default_version = Version.convert('v1', "CURRENT",
|
||||
versions.CURRENT_MAX_VER,
|
||||
versions.BASE_VER)
|
||||
return root
|
||||
|
||||
|
||||
|
||||
@@ -20,17 +20,31 @@ NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
|
||||
|
||||
from oslo_log import log as logging
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from zun.api.controllers import base as controllers_base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers.v1 import containers as container_controller
|
||||
from zun.api.controllers.v1 import images as image_controller
|
||||
from zun.api.controllers.v1 import zun_services
|
||||
from zun.api.controllers import versions as ver
|
||||
from zun.api import http_error
|
||||
from zun.common.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE_VERSION = 1
|
||||
|
||||
MIN_VER_STR = '%s %s' % (ver.Version.service_string, ver.BASE_VER)
|
||||
|
||||
MAX_VER_STR = '%s %s' % (ver.Version.service_string, ver.CURRENT_MAX_VER)
|
||||
|
||||
MIN_VER = ver.Version({ver.Version.string: MIN_VER_STR},
|
||||
MIN_VER_STR, MAX_VER_STR)
|
||||
MAX_VER = ver.Version({ver.Version.string: MAX_VER_STR},
|
||||
MIN_VER_STR, MAX_VER_STR)
|
||||
|
||||
|
||||
class MediaType(controllers_base.APIBase):
|
||||
"""A media type representation."""
|
||||
|
||||
@@ -86,7 +100,7 @@ class V1(controllers_base.APIBase):
|
||||
return v1
|
||||
|
||||
|
||||
class Controller(rest.RestController):
|
||||
class Controller(controllers_base.Controller):
|
||||
"""Version 1 API controller root."""
|
||||
|
||||
services = zun_services.ZunServiceController()
|
||||
@@ -97,8 +111,47 @@ class Controller(rest.RestController):
|
||||
def get(self):
|
||||
return V1.convert()
|
||||
|
||||
def _check_version(self, version, headers=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
# ensure that major version in the URL matches the header
|
||||
if version.major != BASE_VERSION:
|
||||
raise http_error.HTTPNotAcceptableAPIVersion(_(
|
||||
"Mutually exclusive versions requested. Version %(ver)s "
|
||||
"requested but not supported by this service. "
|
||||
"The supported version range is: "
|
||||
"[%(min)s, %(max)s].") % {'ver': version,
|
||||
'min': MIN_VER_STR,
|
||||
'max': MAX_VER_STR},
|
||||
headers=headers,
|
||||
max_version=str(MAX_VER),
|
||||
min_version=str(MIN_VER))
|
||||
# ensure the minor version is within the supported range
|
||||
if version < MIN_VER or version > MAX_VER:
|
||||
raise http_error.HTTPNotAcceptableAPIVersion(_(
|
||||
"Version %(ver)s was requested but the minor version is not "
|
||||
"supported by this service. The supported version range is: "
|
||||
"[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR,
|
||||
'max': MAX_VER_STR},
|
||||
headers=headers,
|
||||
max_version=str(MAX_VER),
|
||||
min_version=str(MIN_VER))
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args):
|
||||
version = ver.Version(
|
||||
pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
|
||||
|
||||
# Always set the basic version headers
|
||||
pecan.response.headers[ver.Version.min_string] = MIN_VER_STR
|
||||
pecan.response.headers[ver.Version.max_string] = MAX_VER_STR
|
||||
pecan.response.headers[ver.Version.string] = " ".join(
|
||||
[ver.Version.service_string, str(version)])
|
||||
pecan.response.headers["vary"] = ver.Version.string
|
||||
|
||||
# assert that requested version is supported
|
||||
self._check_version(version, pecan.response.headers)
|
||||
pecan.request.version = version
|
||||
if pecan.request.body:
|
||||
msg = ("Processing request: url: %(url)s, %(method)s, "
|
||||
"body: %(body)s" %
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import strutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import six
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers.v1 import collection
|
||||
from zun.api.controllers.v1.schemas import containers as schema
|
||||
@@ -74,7 +74,7 @@ class ContainerCollection(collection.Collection):
|
||||
return collection
|
||||
|
||||
|
||||
class ContainersController(rest.RestController):
|
||||
class ContainersController(base.Controller):
|
||||
"""Controller for Containers."""
|
||||
|
||||
_custom_actions = {
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import strutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers import link
|
||||
from zun.api.controllers.v1 import collection
|
||||
from zun.api.controllers.v1.schemas import images as schema
|
||||
@@ -51,7 +51,7 @@ class ImageCollection(collection.Collection):
|
||||
return collection
|
||||
|
||||
|
||||
class ImagesController(rest.RestController):
|
||||
class ImagesController(base.Controller):
|
||||
'''Controller for Images'''
|
||||
|
||||
_custom_actions = {
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
# under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from zun.api.controllers import base
|
||||
from zun.api.controllers.v1 import collection
|
||||
from zun.api import servicegroup as svcgrp_api
|
||||
from zun.common import exception
|
||||
@@ -48,7 +48,7 @@ class ZunServiceCollection(collection.Collection):
|
||||
return collection
|
||||
|
||||
|
||||
class ZunServiceController(rest.RestController):
|
||||
class ZunServiceController(base.Controller):
|
||||
"""REST controller for zun-services."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
138
zun/api/controllers/versions.py
Normal file
138
zun/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 zun.common.i18n import _
|
||||
|
||||
# NOTE(yuntong): v1.0 is reserved to indicate Ocata's API, but is not presently
|
||||
# supported by the API service. All changes between Ocata 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 Ocata (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'
|
||||
|
||||
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
|
||||
70
zun/api/http_error.py
Normal file
70
zun/api/http_error.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# 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 json
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
|
||||
class HTTPNotAcceptableAPIVersion(exc.HTTPNotAcceptable):
|
||||
# subclass of :class:`~HTTPNotAcceptable`
|
||||
#
|
||||
# This indicates the resource identified by the request is only
|
||||
# capable of generating response entities which have content
|
||||
# characteristics not acceptable according to the accept headers
|
||||
# sent in the request.
|
||||
#
|
||||
# code: 406, title: Not Acceptable
|
||||
#
|
||||
# differences from webob.exc.HTTPNotAcceptable:
|
||||
#
|
||||
# - additional max and min version parameters
|
||||
# - additional error info for code, title, and links
|
||||
code = 406
|
||||
title = 'Not Acceptable'
|
||||
max_version = ''
|
||||
min_version = ''
|
||||
|
||||
def __init__(self, detail=None, headers=None, comment=None,
|
||||
body_template=None, max_version='', min_version='', **kw):
|
||||
|
||||
super(HTTPNotAcceptableAPIVersion, self).__init__(
|
||||
detail=detail, headers=headers, comment=comment,
|
||||
body_template=body_template, **kw)
|
||||
|
||||
self.max_version = max_version
|
||||
self.min_version = min_version
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
for err_str in self.app_iter:
|
||||
err = {}
|
||||
try:
|
||||
err = json.loads(err_str.decode('utf-8'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
links = {'rel': 'help', 'href': 'http://developer.openstack.org'
|
||||
'/api-guide/compute/microversions.html'}
|
||||
|
||||
err['max_version'] = self.max_version
|
||||
err['min_version'] = self.min_version
|
||||
err['code'] = "zun.microversion-unsupported"
|
||||
err['links'] = [links]
|
||||
err['title'] = "Requested microversion is unsupported"
|
||||
|
||||
self.app_iter = [six.b(json.dumps(err))]
|
||||
self.headers['Content-Length'] = str(len(self.app_iter[0]))
|
||||
|
||||
return super(HTTPNotAcceptableAPIVersion, self).__call__(
|
||||
environ, start_response)
|
||||
35
zun/api/versioned_method.py
Normal file
35
zun/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))
|
||||
@@ -470,3 +470,8 @@ class SchedulerHostFilterNotFound(NotFound):
|
||||
|
||||
class ClassNotFound(NotFound):
|
||||
msg_fmt = _("Class %(class_name)s could not be found: %(exception)s")
|
||||
|
||||
|
||||
class ApiVersionsIntersect(ZunException):
|
||||
message = _("Version of %(name)s %(min_ver)s %(max_ver)s intersects "
|
||||
"with another versions.")
|
||||
|
||||
@@ -23,14 +23,19 @@ class TestRootController(api_base.FunctionalTest):
|
||||
super(TestRootController, self).setUp()
|
||||
self.root_expected = {
|
||||
u'default_version':
|
||||
{u'id': u'v1', u'links':
|
||||
[{u'href': u'http://localhost/v1/', u'rel': u'self'}]},
|
||||
{u'id': u'v1',
|
||||
u'links': [{u'href': u'http://localhost/v1/', u'rel': u'self'}],
|
||||
u'max_version': u'1.1',
|
||||
u'min_version': u'1.1',
|
||||
u'status': u'CURRENT'},
|
||||
u'description': u'Zun is an OpenStack project which '
|
||||
'aims to provide container management.',
|
||||
u'versions': [{u'id': u'v1',
|
||||
u'links':
|
||||
[{u'href': u'http://localhost/v1/',
|
||||
u'rel': u'self'}]}]}
|
||||
u'links': [{u'href': u'http://localhost/v1/',
|
||||
u'rel': u'self'}],
|
||||
u'max_version': u'1.1',
|
||||
u'min_version': u'1.1',
|
||||
u'status': u'CURRENT'}]}
|
||||
|
||||
self.v1_expected = {
|
||||
u'media_types':
|
||||
|
||||
Reference in New Issue
Block a user