1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
|
# 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.88'
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 "<APIVersion: null>"
else:
return "<APIVersion: %s>" % 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 "<VersionedMethod %s>" % 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
|