diff options
| author | scottda <scott.dangelo@hpe.com> | 2016-04-05 15:45:28 -0600 |
|---|---|---|
| committer | scottda <scott.dangelo@hpe.com> | 2016-04-19 11:19:45 -0600 |
| commit | 3f75b48f067abae846655e48ce8f96fda79a3ae8 (patch) | |
| tree | 8dbf05c1076e34b21b666899e070234fb19a456f /cinderclient/api_versions.py | |
| parent | 7d570540a4ade624a2305a010e485aec82e3edcf (diff) | |
| download | python-cinderclient-3f75b48f067abae846655e48ce8f96fda79a3ae8.tar.gz | |
Support api-microversions
Changes to cinderclient to use microversions.
Implements: blueprint api-microversion-support-for-cinderclient
api-microversion-support-for-cinderclient
Change-Id: I840a1162b88e8ff36fa3fc4e1d6b9317104df3e0
Diffstat (limited to 'cinderclient/api_versions.py')
| -rw-r--r-- | cinderclient/api_versions.py | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py new file mode 100644 index 0000000..7235d00 --- /dev/null +++ b/cinderclient/api_versions.py @@ -0,0 +1,366 @@ +# +# 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 functools +import logging +import os +import pkgutil +import re + +from oslo_utils import strutils + +import cinderclient +from cinderclient import exceptions +from cinderclient import utils +from cinderclient._i18n import _ + +logging.basicConfig() +LOG = logging.getLogger(__name__) +if not LOG.handlers: + LOG.addHandler(logging.StreamHandler()) + + +# key is a deprecated version and value is an alternative version. +DEPRECATED_VERSIONS = {"1": "2"} +MAX_VERSION = "3.0" + +_SUBSTITUTIONS = {} + +_type_error_msg = "'%(other)s' should be an instance of '%(cls)s'" + + +class APIVersion(object): + """This class represents an API Version with convenience + methods for manipulation and comparison of version + numbers that we need to do to implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object.""" + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # NOTE(andreykurilin): Infinity allows to easily determine + # latest version and doesn't require any additional checks + # in comparison methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = (_("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str) + raise exceptions.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "<APIVersion: null>" + else: + return "<APIVersion: %s>" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version=None): + """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. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + 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 + """ + + if self.is_null(): + raise ValueError("Null APIVersion doesn't support 'matches'.") + + if isinstance(min_version, str): + min_version = APIVersion(version_str=min_version) + if isinstance(max_version, str): + max_version = APIVersion(version_str=max_version) + + if not min_version and not max_version: + return True + elif ((min_version and max_version) and + max_version.is_null() and min_version.is_null()): + return True + + elif not max_version or max_version.is_null(): + return min_version <= self + elif not min_version or min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError("Null APIVersion cannot be converted to string.") + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) + + +class VersionedMethod(object): + + def __init__(self, name, start_version, end_version, func): + """Versioning information for a single method + + :param name: Name of the method + :param start_version: Minimum acceptable version + :param end_version: Maximum acceptable_version + :param 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)) + + def __repr__(self): + return "<VersionedMethod %s>" % self.name + + +def get_available_major_versions(): + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9]*$") + submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) + available_versions = [name[1:] for loader, name, ispkg in submodules + if matcher.search(name)] + + return available_versions + + +def check_major_version(api_version): + """Checks major part of ``APIVersion`` obj is supported. + + :raises cinderclient.exceptions.UnsupportedVersion: if major part is not + supported + """ + available_versions = get_available_major_versions() + if (not api_version.is_null() and + str(api_version.ver_major) not in available_versions): + if len(available_versions) == 1: + msg = ("Invalid client version '%(version)s'. " + "Major part should be '%(major)s'") % { + "version": api_version.get_string(), + "major": available_versions[0]} + else: + msg = ("Invalid client version '%(version)s'. " + "Major part must be one of: '%(major)s'") % { + "version": api_version.get_string(), + "major": ", ".join(available_versions)} + raise exceptions.UnsupportedVersion(msg) + + +def get_api_version(version_string): + """Returns checked APIVersion object""" + version_string = str(version_string) + if version_string in DEPRECATED_VERSIONS: + LOG.warning("Version %(deprecated_version)s is deprecated, use " + "alternative version %(alternative)s instead." % + {"deprecated_version": version_string, + "alternative": DEPRECATED_VERSIONS[version_string]}) + if strutils.is_int_like(version_string): + version_string = "%s.0" % version_string + + api_version = APIVersion(version_string) + check_major_version(api_version) + return api_version + + +def _get_server_version_range(client): + version = client.versions.get_current() + + if not hasattr(version, 'version') or not version.version: + return APIVersion(), APIVersion() + + return APIVersion(version.min_version), APIVersion(version.version) + + +def discover_version(client, requested_version): + """Checks ``requested_version`` and returns the most recent version + supported by both the API and the client. + + :param client: client object + :param requested_version: requested version represented by APIVersion obj + :returns: APIVersion + """ + + server_start_version, server_end_version = _get_server_version_range( + client) + + if (not requested_version.is_latest() and + requested_version != APIVersion('2.0')): + if server_start_version.is_null() and server_end_version.is_null(): + raise exceptions.UnsupportedVersion( + _("Server doesn't support microversions")) + if not requested_version.matches(server_start_version, + server_end_version): + raise exceptions.UnsupportedVersion( + _("The specified version isn't supported by server. The valid " + "version range is '%(min)s' to '%(max)s'") % { + "min": server_start_version.get_string(), + "max": server_end_version.get_string()}) + return requested_version + + if requested_version == APIVersion('2.0'): + if (server_start_version == APIVersion('2.1') or + (server_start_version.is_null() and + server_end_version.is_null())): + return APIVersion('2.0') + else: + raise exceptions.UnsupportedVersion( + _("The server isn't backward compatible with Cinder V2 REST " + "API")) + + if server_start_version.is_null() and server_end_version.is_null(): + return APIVersion('2.0') + elif cinderclient.API_MIN_VERSION > server_end_version: + raise exceptions.UnsupportedVersion( + _("Server version is too old. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': cinderclient.API_MIN_VERSION.get_string(), + 'client_max': cinderclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif cinderclient.API_MAX_VERSION < server_start_version: + raise exceptions.UnsupportedVersion( + _("Server version is too new. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': cinderclient.API_MIN_VERSION.get_string(), + 'client_max': cinderclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif cinderclient.API_MAX_VERSION <= server_end_version: + return cinderclient.API_MAX_VERSION + elif server_end_version < cinderclient.API_MAX_VERSION: + return server_end_version + + +def update_headers(headers, api_version): + """Set 'OpenStack-API-Version' header if api_version is not + null + """ + + if not api_version.is_null() and api_version.ver_minor != 0: + headers["OpenStack-API-Version"] = "volume " + api_version.get_string() + + +def add_substitution(versioned_method): + _SUBSTITUTIONS.setdefault(versioned_method.name, []) + _SUBSTITUTIONS[versioned_method.name].append(versioned_method) + + +def get_substitutions(func_name, api_version=None): + substitutions = _SUBSTITUTIONS.get(func_name, []) + if api_version and not api_version.is_null(): + return [m for m in substitutions + if api_version.matches(m.start_version, m.end_version)] + return substitutions + + +def wraps(start_version, end_version=None): + start_version = APIVersion(start_version) + if end_version: + end_version = APIVersion(end_version) + else: + end_version = APIVersion("%s.latest" % start_version.ver_major) + + def decor(func): + func.versioned = True + name = utils.get_function_name(func) + versioned_method = VersionedMethod(name, start_version, + end_version, func) + add_substitution(versioned_method) + + @functools.wraps(func) + def substitution(obj, *args, **kwargs): + methods = get_substitutions(name, obj.api_version) + + if not methods: + raise exceptions.VersionNotFoundForAPIMethod( + obj.api_version.get_string(), name) + + method = max(methods, key=lambda f: f.start_version) + + return method.func(obj, *args, **kwargs) + + if hasattr(func, 'arguments'): + for cli_args, cli_kwargs in func.arguments: + utils.add_arg(substitution, *cli_args, **cli_kwargs) + return substitution + + return decor |
