# Copyright 2015 Chuck Fouts # 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 functools import logging import re import warnings import manilaclient from manilaclient.common._i18n import _ from manilaclient.common import cliutils from manilaclient.common import constants from manilaclient import exceptions from manilaclient import utils LOG = logging.getLogger(__name__) MAX_VERSION = '2.28' MIN_VERSION = '2.0' DEPRECATED_VERSION = '1.0' _VERSIONED_METHOD_MAP = {} class APIVersion(object): """Top level object to support Manila API Versioning. This class represents an API Version with convenience methods for manipulation and comparison of version numbers that we need to do to implement microversions. """ TYPE_ERROR_MSG = _("'%(other)s' should be an instance of '%(cls)s'") 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)$", version_str) if match: self.ver_major = int(match.group(1)) 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.""" return ("API Version Major: %s, Minor: %s" % (self.ver_major, self.ver_minor)) def __repr__(self): if self.is_null(): return "" else: return "" % self.get_string() def __lt__(self, other): if not isinstance(other, APIVersion): raise TypeError(self.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(self.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(self.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 is_null(self): return self.ver_major == 0 and self.ver_minor == 0 def is_latest(self): return self == manilaclient.API_MAX_VERSION def matches(self, min_version, max_version): """Determines if version is within a 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. :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 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): """String representation of an APIVersion object.""" if self.is_null(): raise ValueError( _("Null APIVersion cannot be converted to string.")) return "%s.%s" % (self.ver_major, self.ver_minor) def get_major_version(self): return "%s" % self.ver_major 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 "" % self.name def check_version_supported(api_version): """Returns True if the API version is supported. :warn Sends warning if version is not supported. """ if (check_version_matches_min_max(api_version) or check_version_deprecated(api_version)): return True return False def check_version_matches_min_max(api_version): """Returns True if the API version is within the supported range.""" if (not api_version.matches( manilaclient.API_MIN_VERSION, manilaclient.API_MAX_VERSION)): msg = _("Invalid client version '%(version)s'. " "Current version range is '%(min)s' through " " '%(max)s'") % { "version": api_version.get_string(), "min": manilaclient.API_MIN_VERSION.get_string(), "max": manilaclient.API_MAX_VERSION.get_string()} warnings.warn(msg) return False return True def check_version_deprecated(api_version): """Returns True if API version is deprecated.""" if api_version == manilaclient.API_DEPRECATED_VERSION: msg = _("Client version '%(version)s' is deprecated.") % { "version": api_version.get_string()} warnings.warn(msg) return True return False def get_api_version(version_string): """Returns checked APIVersion object.""" version_string = str(version_string) api_version = APIVersion(version_string) check_version_supported(api_version) return api_version def _get_server_version_range(client): """Obtain version range from server.""" response = client.services.server_api_version('') server_version = None for resource in response: if hasattr(resource, 'version'): if resource.status == "CURRENT": server_version = resource break if not hasattr(server_version, 'version') or not server_version.version: return APIVersion(), APIVersion() min_version = APIVersion(server_version.min_version) max_version = APIVersion(server_version.version) return min_version, max_version def discover_version(client, requested_version): """Discovers the most recent version for client and API. Checks 'requested_version' and returns the most recent version supported by both the API and the client. If there is not a supported version then an UnsupportedVersion exception is thrown. :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) valid_version = requested_version if server_start_version.is_null() and server_end_version.is_null(): msg = ("Server does not support microversions. Changing server " "version to %(min_version)s.") LOG.debug(msg, {"min_version": DEPRECATED_VERSION}) valid_version = APIVersion(DEPRECATED_VERSION) else: valid_version = _validate_requested_version( requested_version, server_start_version, server_end_version) _validate_server_version(server_start_version, server_end_version) return valid_version def _validate_requested_version(requested_version, server_start_version, server_end_version): """Validates the requested version. Checks 'requested_version' is within the min/max range supported by the server. If 'requested_version' is not within range then attempts to downgrade to 'server_end_version'. Otherwise an UnsupportedVersion exception is thrown. :param requested_version: requestedversion represented by APIVersion obj :param server_start_version: APIVersion object representing server min :param server_end_version: APIVersion object representing server max """ valid_version = requested_version if not requested_version.matches(server_start_version, server_end_version): if server_end_version <= requested_version: if (manilaclient.API_MIN_VERSION <= server_end_version and server_end_version <= manilaclient.API_MAX_VERSION): msg = _("Requested version %(requested_version)s is " "not supported. Downgrading requested version " "to %(server_end_version)s.") LOG.debug(msg, { "requested_version": requested_version, "server_end_version": server_end_version}) valid_version = server_end_version else: 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 valid_version def _validate_server_version(server_start_version, server_end_version): """Validates the server version. Checks that the 'server_end_version' is greater than the minimum version supported by the client. Then checks that the 'server_start_version' is less than the maximum version supported by the client. :param server_start_version: :param server_end_version: :return: """ if manilaclient.API_MIN_VERSION > server_end_version: raise exceptions.UnsupportedVersion( _("Server's version is too old. The client's 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': manilaclient.API_MIN_VERSION.get_string(), 'client_max': manilaclient.API_MAX_VERSION.get_string(), 'server_min': server_start_version.get_string(), 'server_max': server_end_version.get_string()}) elif manilaclient.API_MAX_VERSION < server_start_version: raise exceptions.UnsupportedVersion( _("Server's version is too new. The client's 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': manilaclient.API_MIN_VERSION.get_string(), 'client_max': manilaclient.API_MAX_VERSION.get_string(), 'server_min': server_start_version.get_string(), 'server_max': server_end_version.get_string()}) def add_versioned_method(versioned_method): _VERSIONED_METHOD_MAP.setdefault(versioned_method.name, []) _VERSIONED_METHOD_MAP[versioned_method.name].append(versioned_method) def get_versioned_methods(func_name, api_version=None): versioned_methods = _VERSIONED_METHOD_MAP.get(func_name, []) if api_version and not api_version.is_null(): return [m for m in versioned_methods if api_version.matches(m.start_version, m.end_version)] return versioned_methods def experimental_api(f): """Adds to HTTP Header to indicate this is an experimental API call.""" @functools.wraps(f) def _wrapper(*args, **kwargs): client = args[0] if (isinstance(client, manilaclient.v2.client.Client) or hasattr(client, 'client')): dh = client.client.default_headers dh[constants.EXPERIMENTAL_HTTP_HEADER] = 'true' return f(*args, **kwargs) return _wrapper def wraps(start_version, end_version=MAX_VERSION): """Annotation used to return the correct method based on requested version. Creates a VersionedMethod based on data from the method using the 'wraps' annotation. The VersionedMethod is stored in the _VERSIONED_METHOD_MAP. Also, adds a 'substitution' method that is used to look up the latest method matching the start_version. :param start_version: String obj representing first supported version. :param end_version: String obj representing last supported version. """ start_version = APIVersion(start_version) end_version = APIVersion(end_version) def decor(func): func.versioned = True name = utils.get_function_name(func) versioned_method = VersionedMethod(name, start_version, end_version, func) add_versioned_method(versioned_method) @functools.wraps(func) def substitution(obj, *args, **kwargs): methods = get_versioned_methods(name, obj.api_version) if not methods: raise exceptions.UnsupportedVersion( _("API version '%(version)s' is not supported on " "'%(method)s' method.") % { "version": obj.api_version.get_string(), "method": 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: cliutils.add_arg(substitution, *cli_args, **cli_kwargs) return substitution return decor