Support api-microversions
Changes to cinderclient to use microversions. Implements: blueprint api-microversion-support-for-cinderclient api-microversion-support-for-cinderclient Change-Id: I840a1162b88e8ff36fa3fc4e1d6b9317104df3e0
This commit is contained in:
		
							
								
								
									
										366
									
								
								cinderclient/api_versions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								cinderclient/api_versions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -26,6 +26,7 @@ import os | ||||
| import six | ||||
| from six.moves.urllib import parse | ||||
|  | ||||
| from cinderclient import api_versions | ||||
| from cinderclient import exceptions | ||||
| from cinderclient.openstack.common.apiclient import base as common_base | ||||
| from cinderclient import utils | ||||
| @@ -62,8 +63,13 @@ class Manager(common_base.HookableMixin): | ||||
|     resource_class = None | ||||
|  | ||||
|     def __init__(self, api): | ||||
|         self._api_version = api_versions.APIVersion() | ||||
|         self.api = api | ||||
|  | ||||
|     @property | ||||
|     def api_version(self): | ||||
|         return self._api_version | ||||
|  | ||||
|     def _list(self, url, response_key, obj_class=None, body=None, | ||||
|               limit=None, items=None): | ||||
|         resp = None | ||||
|   | ||||
| @@ -36,6 +36,7 @@ from keystoneclient.auth.identity import base | ||||
| from keystoneclient import discover | ||||
| import requests | ||||
|  | ||||
| from cinderclient import api_versions | ||||
| from cinderclient import exceptions | ||||
| import cinderclient.extension | ||||
| from cinderclient._i18n import _ | ||||
| @@ -67,6 +68,12 @@ if not hasattr(urlparse, 'parse_qsl'): | ||||
|     urlparse.parse_qsl = cgi.parse_qsl | ||||
|  | ||||
| _VALID_VERSIONS = ['v1', 'v2', 'v3'] | ||||
| V3_SERVICE_TYPE = 'volumev3' | ||||
| V2_SERVICE_TYPE = 'volumev2' | ||||
| V1_SERVICE_TYPE = 'volume' | ||||
| SERVICE_TYPES = {'1': V1_SERVICE_TYPE, | ||||
|                  '2': V2_SERVICE_TYPE, | ||||
|                  '3': V3_SERVICE_TYPE} | ||||
|  | ||||
| # tell keystoneclient that we can ignore the /v1|v2/{project_id} component of | ||||
| # the service catalog when doing discovery lookups | ||||
| @@ -89,7 +96,14 @@ def get_volume_api_from_url(url): | ||||
|  | ||||
| class SessionClient(adapter.LegacyJsonAdapter): | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.api_version = kwargs.pop('api_version', None) | ||||
|         self.api_version = self.api_version or api_versions.APIVersion() | ||||
|         super(SessionClient, self).__init__(*args, **kwargs) | ||||
|  | ||||
|     def request(self, *args, **kwargs): | ||||
|         kwargs.setdefault('headers', kwargs.get('headers', {})) | ||||
|         api_versions.update_headers(kwargs["headers"], self.api_version) | ||||
|         kwargs.setdefault('authenticated', False) | ||||
|         # Note(tpatil): The standard call raises errors from | ||||
|         # keystoneclient, here we need to raise the cinderclient errors. | ||||
| @@ -157,11 +171,12 @@ class HTTPClient(object): | ||||
|                  service_name=None, volume_service_name=None, | ||||
|                  bypass_url=None, retries=None, | ||||
|                  http_log_debug=False, cacert=None, | ||||
|                  auth_system='keystone', auth_plugin=None): | ||||
|                  auth_system='keystone', auth_plugin=None, api_version=None): | ||||
|         self.user = user | ||||
|         self.password = password | ||||
|         self.projectid = projectid | ||||
|         self.tenant_id = tenant_id | ||||
|         self.api_version = api_version or api_versions.APIVersion() | ||||
|  | ||||
|         if auth_system and auth_system != 'keystone' and not auth_plugin: | ||||
|             raise exceptions.AuthSystemNotFound(auth_system) | ||||
| @@ -256,6 +271,7 @@ class HTTPClient(object): | ||||
|             kwargs['headers']['Content-Type'] = 'application/json' | ||||
|             kwargs['data'] = json.dumps(kwargs['body']) | ||||
|             del kwargs['body'] | ||||
|         api_versions.update_headers(kwargs["headers"], self.api_version) | ||||
|  | ||||
|         if self.timeout: | ||||
|             kwargs.setdefault('timeout', self.timeout) | ||||
| @@ -537,7 +553,7 @@ def _construct_http_client(username=None, password=None, project_id=None, | ||||
|                            auth_system='keystone', auth_plugin=None, | ||||
|                            cacert=None, tenant_id=None, | ||||
|                            session=None, | ||||
|                            auth=None, | ||||
|                            auth=None, api_version=None, | ||||
|                            **kwargs): | ||||
|  | ||||
|     # Don't use sessions if third party plugin is used | ||||
| @@ -549,6 +565,7 @@ def _construct_http_client(username=None, password=None, project_id=None, | ||||
|                              service_type=service_type, | ||||
|                              service_name=service_name, | ||||
|                              region_name=region_name, | ||||
|                              api_version=api_version, | ||||
|                              **kwargs) | ||||
|     else: | ||||
|         # FIXME(jamielennox): username and password are now optional. Need | ||||
| @@ -576,6 +593,18 @@ def _construct_http_client(username=None, password=None, project_id=None, | ||||
|                           ) | ||||
|  | ||||
|  | ||||
| def _get_client_class_and_version(version): | ||||
|     if not isinstance(version, api_versions.APIVersion): | ||||
|         version = api_versions.get_api_version(version) | ||||
|     else: | ||||
|         api_versions.check_major_version(version) | ||||
|     if version.is_latest(): | ||||
|         raise exceptions.UnsupportedVersion( | ||||
|             _("The version should be explicit, not latest.")) | ||||
|     return version, importutils.import_class( | ||||
|         "cinderclient.v%s.client.Client" % version.ver_major) | ||||
|  | ||||
|  | ||||
| def get_client_class(version): | ||||
|     version_map = { | ||||
|         '1': 'cinderclient.v1.client.Client', | ||||
| @@ -632,5 +661,28 @@ def _discover_via_contrib_path(version): | ||||
|  | ||||
|  | ||||
| def Client(version, *args, **kwargs): | ||||
|     client_class = get_client_class(version) | ||||
|     return client_class(*args, **kwargs) | ||||
|     """Initialize client object based on given version. | ||||
|  | ||||
|     HOW-TO: | ||||
|     The simplest way to create a client instance is initialization with your | ||||
|     credentials:: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         >>> from cinderclient import client | ||||
|         >>> cinder = client.Client(VERSION, USERNAME, PASSWORD, | ||||
|         ...                      PROJECT_ID, AUTH_URL) | ||||
|  | ||||
|     Here ``VERSION`` can be a string or | ||||
|     ``cinderclient.api_versions.APIVersion`` obj. If you prefer string value, | ||||
|     you can use ``1`` (deprecated now), ``2``, or ``3.X`` | ||||
|     (where X is a microversion). | ||||
|  | ||||
|  | ||||
|     Alternatively, you can create a client instance using the keystoneclient | ||||
|     session API. See "The cinderclient Python API" page at | ||||
|     python-cinderclient's doc. | ||||
|     """ | ||||
|     api_version, client_class = _get_client_class_and_version(version) | ||||
|     return client_class(api_version=api_version, | ||||
|                         *args, **kwargs) | ||||
|   | ||||
| @@ -132,6 +132,14 @@ class NotFound(ClientException): | ||||
|     message = "Not found" | ||||
|  | ||||
|  | ||||
| class NotAcceptable(ClientException): | ||||
|     """ | ||||
|     HTTP 406 - Not Acceptable | ||||
|     """ | ||||
|     http_status = 406 | ||||
|     message = "Not Acceptable" | ||||
|  | ||||
|  | ||||
| class OverLimit(ClientException): | ||||
|     """ | ||||
|     HTTP 413 - Over limit: you're over the API limits for this time period. | ||||
| @@ -157,6 +165,7 @@ class HTTPNotImplemented(ClientException): | ||||
| # Instead, we have to hardcode it: | ||||
| _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, | ||||
|                                               Forbidden, NotFound, | ||||
|                                               NotAcceptable, | ||||
|                                               OverLimit, HTTPNotImplemented]) | ||||
|  | ||||
|  | ||||
| @@ -188,3 +197,14 @@ def from_response(response, body): | ||||
|     else: | ||||
|         return cls(code=response.status_code, request_id=request_id, | ||||
|                    message=response.reason) | ||||
|  | ||||
|  | ||||
| class VersionNotFoundForAPIMethod(Exception): | ||||
|     msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method." | ||||
|  | ||||
|     def __init__(self, version, method): | ||||
|         self.version = version | ||||
|         self.method = method | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.msg_fmt % {"vers": self.version, "method": self.method} | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import sys | ||||
|  | ||||
| import requests | ||||
|  | ||||
| from cinderclient import api_versions | ||||
| from cinderclient import client | ||||
| from cinderclient import exceptions as exc | ||||
| from cinderclient import utils | ||||
| @@ -49,9 +50,8 @@ from cinderclient import _i18n | ||||
| # Enable i18n lazy translation | ||||
| _i18n.enable_lazy() | ||||
|  | ||||
| DEFAULT_OS_VOLUME_API_VERSION = "2" | ||||
| DEFAULT_MAJOR_OS_VOLUME_API_VERSION = "2" | ||||
| DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL' | ||||
| DEFAULT_CINDER_SERVICE_TYPE = 'volumev2' | ||||
| V1_SHELL = 'cinderclient.v1.shell' | ||||
| V2_SHELL = 'cinderclient.v2.shell' | ||||
| V3_SHELL = 'cinderclient.v3.shell' | ||||
| @@ -186,7 +186,8 @@ class OpenStackCinderShell(object): | ||||
|                             default=utils.env('OS_VOLUME_API_VERSION', | ||||
|                                               default=None), | ||||
|                             help='Block Storage API version. ' | ||||
|                             'Valid values are 1 or 2. ' | ||||
|                             'Accepts X, X.Y (where X is major and Y is minor ' | ||||
|                             'part).' | ||||
|                             'Default=env[OS_VOLUME_API_VERSION].') | ||||
|         parser.add_argument('--os_volume_api_version', | ||||
|                             help=argparse.SUPPRESS) | ||||
| @@ -495,19 +496,18 @@ class OpenStackCinderShell(object): | ||||
|         self.options = options | ||||
|  | ||||
|         if not options.os_volume_api_version: | ||||
|             # Environment variable OS_VOLUME_API_VERSION was | ||||
|             # not set and '--os-volume-api-version' option doesn't | ||||
|             # specify a value.  Fall back to default. | ||||
|             options.os_volume_api_version = DEFAULT_OS_VOLUME_API_VERSION | ||||
|             api_version_input = False | ||||
|             api_version = api_versions.get_api_version( | ||||
|                 DEFAULT_MAJOR_OS_VOLUME_API_VERSION) | ||||
|         else: | ||||
|             api_version = api_versions.get_api_version( | ||||
|                 options.os_volume_api_version) | ||||
|  | ||||
|         # build available subcommands based on version | ||||
|         self.extensions = client.discover_extensions( | ||||
|             options.os_volume_api_version) | ||||
|         major_version_string = "%s" % api_version.ver_major | ||||
|         self.extensions = client.discover_extensions(major_version_string) | ||||
|         self._run_extension_hooks('__pre_parse_args__') | ||||
|  | ||||
|         subcommand_parser = self.get_subcommand_parser( | ||||
|             options.os_volume_api_version) | ||||
|         subcommand_parser = self.get_subcommand_parser(major_version_string) | ||||
|         self.parser = subcommand_parser | ||||
|  | ||||
|         if options.help or not argv: | ||||
| @@ -544,8 +544,7 @@ class OpenStackCinderShell(object): | ||||
|             auth_plugin = None | ||||
|  | ||||
|         if not service_type: | ||||
|             service_type = DEFAULT_CINDER_SERVICE_TYPE | ||||
|             service_type = utils.get_service_type(args.func) or service_type | ||||
|             service_type = client.SERVICE_TYPES[major_version_string] | ||||
|  | ||||
|         # FIXME(usrleon): Here should be restrict for project id same as | ||||
|         # for os_username or os_password but for compatibility it is not. | ||||
| @@ -634,7 +633,7 @@ class OpenStackCinderShell(object): | ||||
|  | ||||
|         insecure = self.options.insecure | ||||
|  | ||||
|         self.cs = client.Client(options.os_volume_api_version, os_username, | ||||
|         self.cs = client.Client(api_version, os_username, | ||||
|                                 os_password, os_tenant_name, os_auth_url, | ||||
|                                 region_name=os_region_name, | ||||
|                                 tenant_id=os_tenant_id, | ||||
| @@ -667,13 +666,6 @@ class OpenStackCinderShell(object): | ||||
|         try: | ||||
|             endpoint_api_version = \ | ||||
|                 self.cs.get_volume_api_version_from_endpoint() | ||||
|             if endpoint_api_version != options.os_volume_api_version: | ||||
|                 msg = (("OpenStack Block Storage API version is set to %s " | ||||
|                         "but you are accessing a %s endpoint. " | ||||
|                         "Change its value through --os-volume-api-version " | ||||
|                         "or env[OS_VOLUME_API_VERSION].") | ||||
|                        % (options.os_volume_api_version, endpoint_api_version)) | ||||
|                 raise exc.InvalidAPIVersion(msg) | ||||
|         except exc.UnsupportedVersion: | ||||
|             endpoint_api_version = options.os_volume_api_version | ||||
|             if api_version_input: | ||||
|   | ||||
							
								
								
									
										141
									
								
								cinderclient/tests/unit/test_api_versions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								cinderclient/tests/unit/test_api_versions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| # Copyright 2016 Mirantis | ||||
| # 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 ddt | ||||
| import mock | ||||
|  | ||||
| from cinderclient import api_versions | ||||
| from cinderclient import exceptions | ||||
| from cinderclient.tests.unit import utils | ||||
|  | ||||
|  | ||||
| @ddt.ddt | ||||
| class APIVersionTestCase(utils.TestCase): | ||||
|     def test_valid_version_strings(self): | ||||
|         def _test_string(version, exp_major, exp_minor): | ||||
|             v = api_versions.APIVersion(version) | ||||
|             self.assertEqual(v.ver_major, exp_major) | ||||
|             self.assertEqual(v.ver_minor, exp_minor) | ||||
|  | ||||
|         _test_string("1.1", 1, 1) | ||||
|         _test_string("2.10", 2, 10) | ||||
|         _test_string("5.234", 5, 234) | ||||
|         _test_string("12.5", 12, 5) | ||||
|         _test_string("2.0", 2, 0) | ||||
|         _test_string("2.200", 2, 200) | ||||
|  | ||||
|     def test_null_version(self): | ||||
|         v = api_versions.APIVersion() | ||||
|         self.assertTrue(v.is_null()) | ||||
|  | ||||
|     @ddt.data("2", "200", "2.1.4", "200.23.66.3", "5 .3", "5. 3", "5.03", | ||||
|               "02.1", "2.001", "", " 2.1", "2.1 ") | ||||
|     def test_invalid_version_strings(self, version_string): | ||||
|         self.assertRaises(exceptions.UnsupportedVersion, | ||||
|                           api_versions.APIVersion, version_string) | ||||
|  | ||||
|     def test_version_comparisons(self): | ||||
|         v1 = api_versions.APIVersion("2.0") | ||||
|         v2 = api_versions.APIVersion("2.5") | ||||
|         v3 = api_versions.APIVersion("5.23") | ||||
|         v4 = api_versions.APIVersion("2.0") | ||||
|         v_null = api_versions.APIVersion() | ||||
|  | ||||
|         self.assertTrue(v1 < v2) | ||||
|         self.assertTrue(v3 > v2) | ||||
|         self.assertTrue(v1 != v2) | ||||
|         self.assertTrue(v1 == v4) | ||||
|         self.assertTrue(v1 != v_null) | ||||
|         self.assertTrue(v_null == v_null) | ||||
|         self.assertRaises(TypeError, v1.__le__, "2.1") | ||||
|  | ||||
|     def test_version_matches(self): | ||||
|         v1 = api_versions.APIVersion("2.0") | ||||
|         v2 = api_versions.APIVersion("2.5") | ||||
|         v3 = api_versions.APIVersion("2.45") | ||||
|         v4 = api_versions.APIVersion("3.3") | ||||
|         v5 = api_versions.APIVersion("3.23") | ||||
|         v6 = api_versions.APIVersion("2.0") | ||||
|         v7 = api_versions.APIVersion("3.3") | ||||
|         v8 = api_versions.APIVersion("4.0") | ||||
|         v_null = api_versions.APIVersion() | ||||
|  | ||||
|         self.assertTrue(v2.matches(v1, v3)) | ||||
|         self.assertTrue(v2.matches(v1, v_null)) | ||||
|         self.assertTrue(v1.matches(v6, v2)) | ||||
|         self.assertTrue(v4.matches(v2, v7)) | ||||
|         self.assertTrue(v4.matches(v_null, v7)) | ||||
|         self.assertTrue(v4.matches(v_null, v8)) | ||||
|         self.assertFalse(v1.matches(v2, v3)) | ||||
|         self.assertFalse(v5.matches(v2, v4)) | ||||
|         self.assertFalse(v2.matches(v3, v1)) | ||||
|  | ||||
|         self.assertRaises(ValueError, v_null.matches, v1, v3) | ||||
|  | ||||
|     def test_get_string(self): | ||||
|         v1_string = "3.23" | ||||
|         v1 = api_versions.APIVersion(v1_string) | ||||
|         self.assertEqual(v1_string, v1.get_string()) | ||||
|  | ||||
|         self.assertRaises(ValueError, | ||||
|                           api_versions.APIVersion().get_string) | ||||
|  | ||||
|  | ||||
| class UpdateHeadersTestCase(utils.TestCase): | ||||
|     def test_api_version_is_null(self): | ||||
|         headers = {} | ||||
|         api_versions.update_headers(headers, api_versions.APIVersion()) | ||||
|         self.assertEqual({}, headers) | ||||
|  | ||||
|     def test_api_version_is_major(self): | ||||
|         headers = {} | ||||
|         api_versions.update_headers(headers, api_versions.APIVersion("7.0")) | ||||
|         self.assertEqual({}, headers) | ||||
|  | ||||
|     def test_api_version_is_not_null(self): | ||||
|         api_version = api_versions.APIVersion("2.3") | ||||
|         headers = {} | ||||
|         api_versions.update_headers(headers, api_version) | ||||
|         self.assertEqual( | ||||
|             {"OpenStack-API-Version": "volume " + api_version.get_string()}, | ||||
|             headers) | ||||
|  | ||||
|  | ||||
| class GetAPIVersionTestCase(utils.TestCase): | ||||
|     def test_get_available_client_versions(self): | ||||
|         output = api_versions.get_available_major_versions() | ||||
|         self.assertNotEqual([], output) | ||||
|  | ||||
|     def test_wrong_format(self): | ||||
|         self.assertRaises(exceptions.UnsupportedVersion, | ||||
|                           api_versions.get_api_version, "something_wrong") | ||||
|  | ||||
|     def test_wrong_major_version(self): | ||||
|         self.assertRaises(exceptions.UnsupportedVersion, | ||||
|                           api_versions.get_api_version, "4") | ||||
|  | ||||
|     @mock.patch("cinderclient.api_versions.APIVersion") | ||||
|     def test_only_major_part_is_presented(self, mock_apiversion): | ||||
|         version = 7 | ||||
|         self.assertEqual(mock_apiversion.return_value, | ||||
|                          api_versions.get_api_version(version)) | ||||
|         mock_apiversion.assert_called_once_with("%s.0" % str(version)) | ||||
|  | ||||
|     @mock.patch("cinderclient.api_versions.APIVersion") | ||||
|     def test_major_and_minor_parts_is_presented(self, mock_apiversion): | ||||
|         version = "2.7" | ||||
|         self.assertEqual(mock_apiversion.return_value, | ||||
|                          api_versions.get_api_version(version)) | ||||
|         mock_apiversion.assert_called_once_with(version) | ||||
| @@ -64,6 +64,17 @@ def mock_http_request(resp=None): | ||||
|                             }, | ||||
|                         ], | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "volumev3", | ||||
|                         "endpoints": [ | ||||
|                             { | ||||
|                                 "region": "RegionOne", | ||||
|                                 "adminURL": "http://localhost:8774/v3", | ||||
|                                 "internalURL": "http://localhost:8774/v3", | ||||
|                                 "publicURL": "http://localhost:8774/v3/", | ||||
|                             }, | ||||
|                         ], | ||||
|                     }, | ||||
|                 ], | ||||
|             }, | ||||
|         } | ||||
|   | ||||
| @@ -183,10 +183,11 @@ def _stub_extend(id, new_size): | ||||
|  | ||||
| class FakeClient(fakes.FakeClient, client.Client): | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|     def __init__(self, api_version=None, *args, **kwargs): | ||||
|         client.Client.__init__(self, 'username', 'password', | ||||
|                                'project_id', 'auth_url', | ||||
|                                extensions=kwargs.get('extensions')) | ||||
|         self.api_version = api_version | ||||
|         self.client = FakeHTTPClient(**kwargs) | ||||
|  | ||||
|     def get_volume_api_version_from_endpoint(self): | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
| #    under the License. | ||||
|  | ||||
| import fixtures | ||||
| import mock | ||||
| from requests_mock.contrib import fixture as requests_mock_fixture | ||||
|  | ||||
| from cinderclient import client | ||||
| @@ -27,6 +28,7 @@ from cinderclient.tests.unit import utils | ||||
| from cinderclient.tests.unit.fixture_data import keystone_client | ||||
|  | ||||
|  | ||||
| @mock.patch.object(client, 'Client', fakes.FakeClient) | ||||
| class ShellTest(utils.TestCase): | ||||
|  | ||||
|     FAKE_ENV = { | ||||
| @@ -48,26 +50,14 @@ class ShellTest(utils.TestCase): | ||||
|         self.shell = shell.OpenStackCinderShell() | ||||
|  | ||||
|         # HACK(bcwaldon): replace this when we start using stubs | ||||
|         self.old_get_client_class = client.get_client_class | ||||
|         client.get_client_class = lambda *_: fakes.FakeClient | ||||
|         self.old_client_class = client.Client | ||||
|         client.Client = fakes.FakeClient | ||||
|  | ||||
|         self.requests = self.useFixture(requests_mock_fixture.Fixture()) | ||||
|         self.requests.register_uri( | ||||
|             'GET', keystone_client.BASE_URL, | ||||
|             text=keystone_client.keystone_request_callback) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         # For some method like test_image_meta_bad_action we are | ||||
|         # testing a SystemExit to be thrown and object self.shell has | ||||
|         # no time to get instantatiated which is OK in this case, so | ||||
|         # we make sure the method is there before launching it. | ||||
|         if hasattr(self.shell, 'cs'): | ||||
|             self.shell.cs.clear_callstack() | ||||
|  | ||||
|         # HACK(bcwaldon): replace this when we start using stubs | ||||
|         client.get_client_class = self.old_get_client_class | ||||
|         super(ShellTest, self).tearDown() | ||||
|  | ||||
|     def run_command(self, cmd): | ||||
|         self.shell.main(cmd.split()) | ||||
|  | ||||
|   | ||||
| @@ -251,10 +251,11 @@ def _stub_extend(id, new_size): | ||||
|  | ||||
| class FakeClient(fakes.FakeClient, client.Client): | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|     def __init__(self, api_version=None, *args, **kwargs): | ||||
|         client.Client.__init__(self, 'username', 'password', | ||||
|                                'project_id', 'auth_url', | ||||
|                                extensions=kwargs.get('extensions')) | ||||
|         self.api_version = api_version | ||||
|         self.client = FakeHTTPClient(**kwargs) | ||||
|  | ||||
|     def get_volume_api_version_from_endpoint(self): | ||||
|   | ||||
| @@ -28,6 +28,7 @@ from cinderclient.tests.unit.v2 import fakes | ||||
| from cinderclient.tests.unit.fixture_data import keystone_client | ||||
|  | ||||
|  | ||||
| @mock.patch.object(client, 'Client', fakes.FakeClient) | ||||
| class ShellTest(utils.TestCase): | ||||
|  | ||||
|     FAKE_ENV = { | ||||
| @@ -48,10 +49,6 @@ class ShellTest(utils.TestCase): | ||||
|  | ||||
|         self.shell = shell.OpenStackCinderShell() | ||||
|  | ||||
|         # HACK(bcwaldon): replace this when we start using stubs | ||||
|         self.old_get_client_class = client.get_client_class | ||||
|         client.get_client_class = lambda *_: fakes.FakeClient | ||||
|  | ||||
|         self.requests = self.useFixture(requests_mock_fixture.Fixture()) | ||||
|         self.requests.register_uri( | ||||
|             'GET', keystone_client.BASE_URL, | ||||
| @@ -66,18 +63,6 @@ class ShellTest(utils.TestCase): | ||||
|  | ||||
|         return Args(args) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         # For some methods like test_image_meta_bad_action we are | ||||
|         # testing a SystemExit to be thrown and object self.shell has | ||||
|         # no time to get instantiated, which is OK in this case, so | ||||
|         # we make sure the method is there before launching it. | ||||
|         if hasattr(self.shell, 'cs'): | ||||
|             self.shell.cs.clear_callstack() | ||||
|  | ||||
|         # HACK(bcwaldon): replace this when we start using stubs | ||||
|         client.get_client_class = self.old_get_client_class | ||||
|         super(ShellTest, self).tearDown() | ||||
|  | ||||
|     def run_command(self, cmd): | ||||
|         self.shell.main(cmd.split()) | ||||
|  | ||||
|   | ||||
| @@ -24,6 +24,7 @@ from cinderclient.tests.unit.v3 import fakes | ||||
| from cinderclient.tests.unit.fixture_data import keystone_client | ||||
|  | ||||
|  | ||||
| @mock.patch.object(client, 'Client', fakes.FakeClient) | ||||
| class ShellTest(utils.TestCase): | ||||
|  | ||||
|     FAKE_ENV = { | ||||
| @@ -44,10 +45,6 @@ class ShellTest(utils.TestCase): | ||||
|  | ||||
|         self.shell = shell.OpenStackCinderShell() | ||||
|  | ||||
|         # HACK(bcwaldon): replace this when we start using stubs | ||||
|         self.old_get_client_class = client.get_client_class | ||||
|         client.get_client_class = lambda *_: fakes.FakeClient | ||||
|  | ||||
|         self.requests = self.useFixture(requests_mock_fixture.Fixture()) | ||||
|         self.requests.register_uri( | ||||
|             'GET', keystone_client.BASE_URL, | ||||
|   | ||||
| @@ -276,3 +276,13 @@ def retype_method(old_type, new_type, namespace): | ||||
|         if (isinstance(attr, types.FunctionType) and | ||||
|                 getattr(attr, 'service_type', None) == old_type): | ||||
|             setattr(attr, 'service_type', new_type) | ||||
|  | ||||
|  | ||||
| def get_function_name(func): | ||||
|     if six.PY2: | ||||
|         if hasattr(func, "im_class"): | ||||
|             return "%s.%s" % (func.im_class, func.__name__) | ||||
|         else: | ||||
|             return "%s.%s" % (func.__module__, func.__name__) | ||||
|     else: | ||||
|         return "%s.%s" % (func.__module__, func.__qualname__) | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| #    under the License. | ||||
|  | ||||
| from cinderclient import client | ||||
| from cinderclient import api_versions | ||||
| from cinderclient.v2 import availability_zones | ||||
| from cinderclient.v2 import cgsnapshots | ||||
| from cinderclient.v2 import consistencygroups | ||||
| @@ -47,8 +48,6 @@ class Client(object): | ||||
|         ... | ||||
|     """ | ||||
|  | ||||
|     version = '2' | ||||
|  | ||||
|     def __init__(self, username=None, api_key=None, project_id=None, | ||||
|                  auth_url='', insecure=False, timeout=None, tenant_id=None, | ||||
|                  proxy_tenant_id=None, proxy_token=None, region_name=None, | ||||
| @@ -56,10 +55,11 @@ class Client(object): | ||||
|                  service_type='volumev2', service_name=None, | ||||
|                  volume_service_name=None, bypass_url=None, retries=None, | ||||
|                  http_log_debug=False, cacert=None, auth_system='keystone', | ||||
|                  auth_plugin=None, session=None, **kwargs): | ||||
|                  auth_plugin=None, session=None, api_version=None, **kwargs): | ||||
|         # FIXME(comstud): Rename the api_key argument above when we | ||||
|         # know it's not being used as keyword argument | ||||
|         password = api_key | ||||
|         self.version = '2.0' | ||||
|         self.limits = limits.LimitsManager(self) | ||||
|  | ||||
|         # extensions | ||||
| @@ -84,6 +84,7 @@ class Client(object): | ||||
|             availability_zones.AvailabilityZoneManager(self) | ||||
|         self.pools = pools.PoolManager(self) | ||||
|         self.capabilities = capabilities.CapabilitiesManager(self) | ||||
|         self.api_version = api_version or api_versions.APIVersion(self.version) | ||||
|  | ||||
|         # Add in any extensions... | ||||
|         if extensions: | ||||
| @@ -114,6 +115,7 @@ class Client(object): | ||||
|             auth_system=auth_system, | ||||
|             auth_plugin=auth_plugin, | ||||
|             session=session, | ||||
|             api_version=api_version, | ||||
|             **kwargs) | ||||
|  | ||||
|     def authenticate(self): | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| #    under the License. | ||||
|  | ||||
| from cinderclient import client | ||||
| from cinderclient import api_versions | ||||
| from cinderclient.v3 import availability_zones | ||||
| from cinderclient.v3 import cgsnapshots | ||||
| from cinderclient.v3 import consistencygroups | ||||
| @@ -47,8 +48,6 @@ class Client(object): | ||||
|         ... | ||||
|     """ | ||||
|  | ||||
|     version = '3' | ||||
|  | ||||
|     def __init__(self, username=None, api_key=None, project_id=None, | ||||
|                  auth_url='', insecure=False, timeout=None, tenant_id=None, | ||||
|                  proxy_tenant_id=None, proxy_token=None, region_name=None, | ||||
| @@ -56,10 +55,11 @@ class Client(object): | ||||
|                  service_type='volumev3', service_name=None, | ||||
|                  volume_service_name=None, bypass_url=None, retries=None, | ||||
|                  http_log_debug=False, cacert=None, auth_system='keystone', | ||||
|                  auth_plugin=None, session=None, **kwargs): | ||||
|                  auth_plugin=None, session=None, api_version=None, **kwargs): | ||||
|         # FIXME(comstud): Rename the api_key argument above when we | ||||
|         # know it's not being used as keyword argument | ||||
|         password = api_key | ||||
|         self.version = '3.0' | ||||
|         self.limits = limits.LimitsManager(self) | ||||
|  | ||||
|         # extensions | ||||
| @@ -84,6 +84,7 @@ class Client(object): | ||||
|             availability_zones.AvailabilityZoneManager(self) | ||||
|         self.pools = pools.PoolManager(self) | ||||
|         self.capabilities = capabilities.CapabilitiesManager(self) | ||||
|         self.api_version = api_version or api_versions.APIVersion(self.version) | ||||
|  | ||||
|         # Add in any extensions... | ||||
|         if extensions: | ||||
| @@ -114,6 +115,7 @@ class Client(object): | ||||
|             auth_system=auth_system, | ||||
|             auth_plugin=auth_plugin, | ||||
|             session=session, | ||||
|             api_version=api_version, | ||||
|             **kwargs) | ||||
|  | ||||
|     def authenticate(self): | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| # Hacking already pins down pep8, pyflakes and flake8 | ||||
| hacking<0.11,>=0.10.0 | ||||
| coverage>=3.6 # Apache-2.0 | ||||
| ddt>=1.0.1 # MIT | ||||
| discover # BSD | ||||
| fixtures>=1.3.1 # Apache-2.0/BSD | ||||
| mock>=1.2 # BSD | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 scottda
					scottda