From ea0b3bd60853dccd189b86c9198274a9cd00faf7 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin <akurilin@mirantis.com> Date: Thu, 2 Apr 2015 16:37:59 +0300 Subject: [PATCH] Implements 'microversions' api type - Part 1 Compute API version will be transmitted to API side via X-OpenStack-Nova-API-Version header, if minor part of version is presented. New module "novaclient.api_versions" was added as storage for all api versions related functions, classes, variables and etc. `novaclient.api_versions.APIVersion` class is similar to `nova.api.openstack.api_version_request.APIVersionRequest`. The main difference relates to compare methods(method `cmp` is missed from Py3) and processing "latest" version. Related to bp api-microversion-support Change-Id: I0e6574ddaec11fdd053a49adb6b9de9056d0fbac --- novaclient/api_versions.py | 208 +++++++++++++++++++++ novaclient/client.py | 76 ++++---- novaclient/exceptions.py | 12 +- novaclient/shell.py | 52 +++--- novaclient/tests/unit/test_api_versions.py | 170 +++++++++++++++++ novaclient/tests/unit/test_client.py | 10 +- novaclient/tests/unit/test_shell.py | 8 +- novaclient/tests/unit/v2/fakes.py | 3 +- novaclient/tests/unit/v2/test_shell.py | 8 +- novaclient/v2/client.py | 6 +- 10 files changed, 466 insertions(+), 87 deletions(-) create mode 100644 novaclient/api_versions.py create mode 100644 novaclient/tests/unit/test_api_versions.py diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py new file mode 100644 index 000000000..6d1c0672b --- /dev/null +++ b/novaclient/api_versions.py @@ -0,0 +1,208 @@ +# +# 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 logging +import os +import pkgutil +import re + +from oslo_utils import strutils + +from novaclient import exceptions +from novaclient.i18n import _, _LW + +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.1": "2"} + + +_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): + """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): + """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) + + +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 novaclient.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( + _LW("Version %(deprecated_version)s is deprecated, using " + "alternative version %(alternative)s instead.") % + {"deprecated_version": version_string, + "alternative": DEPRECATED_VERSIONS[version_string]}) + version_string = 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 update_headers(headers, api_version): + """Set 'X-OpenStack-Nova-API-Version' header if api_version is not null""" + + if not api_version.is_null() and api_version.ver_minor != 0: + headers["X-OpenStack-Nova-API-Version"] = api_version.get_string() diff --git a/novaclient/client.py b/novaclient/client.py index f8858b003..5ee6867dc 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,7 +31,6 @@ import os import pkgutil import re import socket -import warnings from keystoneclient import adapter from oslo_utils import importutils @@ -47,17 +46,14 @@ except ImportError: from six.moves.urllib import parse +from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext -from novaclient.i18n import _, _LW +from novaclient.i18n import _ from novaclient import service_catalog from novaclient import utils -# key is a deprecated version and value is an alternative version. -DEPRECATED_VERSIONS = {"1.1": "2"} - - class TCPKeepAliveAdapter(adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections.""" def init_poolmanager(self, *args, **kwargs): @@ -88,9 +84,13 @@ class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): self.times = [] self.timings = kwargs.pop('timings', False) + 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, url, method, **kwargs): + kwargs.setdefault('headers', kwargs.get('headers', {})) + api_versions.update_headers(kwargs["headers"], self.api_version) # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) @@ -144,12 +144,13 @@ class HTTPClient(object): http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, api_version=None): self.user = user self.user_id = user_id self.password = password self.projectid = projectid self.tenant_id = tenant_id + self.api_version = api_version or api_versions.APIVersion() self._connection_pool = (_ClientConnectionPool() if connection_pool else None) @@ -357,6 +358,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 is not None: kwargs.setdefault('timeout', self.timeout) kwargs['verify'] = self.verify_cert @@ -681,7 +683,7 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, user_agent='python-novaclient', - interface=None, **kwargs): + interface=None, api_version=None, **kwargs): if session: return SessionClient(session=session, auth=auth, @@ -691,6 +693,7 @@ def _construct_http_client(username=None, password=None, project_id=None, service_name=service_name, user_agent=user_agent, timings=timings, + api_version=api_version, **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need @@ -718,10 +721,13 @@ def _construct_http_client(username=None, password=None, project_id=None, os_cache=os_cache, http_log_debug=http_log_debug, cacert=cacert, - connection_pool=connection_pool) + connection_pool=connection_pool, + api_version=api_version) def discover_extensions(version): + if not isinstance(version, api_versions.APIVersion): + version = api_versions.get_api_version(version) extensions = [] for name, module in itertools.chain( _discover_via_python_path(), @@ -750,12 +756,7 @@ def _discover_via_python_path(): def _discover_via_contrib_path(version): module_path = os.path.dirname(os.path.abspath(__file__)) - version_str = "v%s" % version.replace('.', '_') - # NOTE(andreykurilin): v1.1 uses implementation of v2, so we should - # discover contrib modules in novaclient.v2 dir. - if version_str == "v1_1": - version_str = "v2" - ext_path = os.path.join(module_path, version_str, 'contrib') + ext_path = os.path.join(module_path, "v%s" % version.ver_major, 'contrib') ext_glob = os.path.join(ext_path, "*.py") for ext_path in glob.iglob(ext_glob): @@ -776,38 +777,25 @@ def _discover_via_entry_points(): yield name, module -def _get_available_client_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:].replace("_", ".") for loader, name, ispkg in submodules - if matcher.search(name)] - - return available_versions +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( + "novaclient.v%s.client.Client" % version.ver_major) def get_client_class(version): - version = str(version) - if version in DEPRECATED_VERSIONS: - warnings.warn(_LW( - "Version %(deprecated_version)s is deprecated, using " - "alternative version %(alternative)s instead.") % - {"deprecated_version": version, - "alternative": DEPRECATED_VERSIONS[version]}) - version = DEPRECATED_VERSIONS[version] - try: - return importutils.import_class( - "novaclient.v%s.client.Client" % version) - except ImportError: - available_versions = _get_available_client_versions() - msg = _("Invalid client version '%(version)s'. must be one of: " - "%(keys)s") % {'version': version, - 'keys': ', '.join(available_versions)} - raise exceptions.UnsupportedVersion(msg) + """Returns Client class based on given version.""" + _api_version, client_class = _get_client_class_and_version(version) + return client_class def Client(version, *args, **kwargs): - client_class = get_client_class(version) - return client_class(*args, **kwargs) + """Initialize client object based on given version.""" + api_version, client_class = _get_client_class_and_version(version) + return client_class(api_version=api_version, *args, **kwargs) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index d550b754b..c95925416 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -159,6 +159,14 @@ class MethodNotAllowed(ClientException): message = "Method Not Allowed" +class NotAcceptable(ClientException): + """ + HTTP 406 - Not Acceptable + """ + http_status = 406 + message = "Not Acceptable" + + class Conflict(ClientException): """ HTTP 409 - Conflict @@ -199,8 +207,8 @@ class HTTPNotImplemented(ClientException): # # Instead, we have to hardcode it: _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, - MethodNotAllowed, Conflict, OverLimit, RateLimit, - HTTPNotImplemented] + MethodNotAllowed, NotAcceptable, Conflict, OverLimit, + RateLimit, HTTPNotImplemented] _code_map = dict((c.http_status, c) for c in _error_classes) diff --git a/novaclient/shell.py b/novaclient/shell.py index ec415bb6e..c7713d3d5 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,6 +29,7 @@ from keystoneclient.auth.identity.generic import token from keystoneclient.auth.identity import v3 as identity from keystoneclient import session as ksession from oslo_utils import encodeutils +from oslo_utils import importutils from oslo_utils import strutils import six @@ -41,6 +42,7 @@ except ImportError: pass import novaclient +from novaclient import api_versions import novaclient.auth_plugin from novaclient import client from novaclient import exceptions as exc @@ -48,7 +50,6 @@ import novaclient.extension from novaclient.i18n import _ from novaclient.openstack.common import cliutils from novaclient import utils -from novaclient.v2 import shell as shell_v2 DEFAULT_OS_COMPUTE_API_VERSION = "2" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' @@ -402,7 +403,7 @@ class OpenStackComputeShell(object): metavar='<compute-api-ver>', default=cliutils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help=_('Accepts number of API version, ' + help=_('Accepts X, X.Y (where X is major and Y is minor part), ' 'defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( '--os_compute_api_version', @@ -431,15 +432,10 @@ class OpenStackComputeShell(object): self.subcommands = {} subparsers = parser.add_subparsers(metavar='<subcommand>') - try: - actions_module = { - '1.1': shell_v2, - '2': shell_v2, - '3': shell_v2, - }[version] - except KeyError: - actions_module = shell_v2 + actions_module = importutils.import_module( + "novaclient.v%s.shell" % version.ver_major) + # TODO(andreykurilin): discover actions based on microversions self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) @@ -522,9 +518,11 @@ class OpenStackComputeShell(object): # Discover available auth plugins novaclient.auth_plugin.discover_auth_systems() - # build available subcommands based on version - self.extensions = self._discover_extensions( + api_version = api_versions.get_api_version( options.os_compute_api_version) + + # build available subcommands based on version + self.extensions = self._discover_extensions(api_version) self._run_extension_hooks('__pre_parse_args__') # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse @@ -535,8 +533,7 @@ class OpenStackComputeShell(object): spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' - subcommand_parser = self.get_subcommand_parser( - options.os_compute_api_version) + subcommand_parser = self.get_subcommand_parser(api_version) self.parser = subcommand_parser if options.help or not argv: @@ -669,23 +666,22 @@ class OpenStackComputeShell(object): project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) - if options.os_compute_api_version: - if not any([args.os_tenant_id, args.os_tenant_name, - args.os_project_id, args.os_project_name]): - raise exc.CommandError(_("You must provide a project name or" - " project id via --os-project-name," - " --os-project-id, env[OS_PROJECT_ID]" - " or env[OS_PROJECT_NAME]. You may" - " use os-project and os-tenant" - " interchangeably.")) + if not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) - if not os_auth_url: - raise exc.CommandError( - _("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]")) + if not os_auth_url: + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) self.cs = client.Client( - options.os_compute_api_version, + api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py new file mode 100644 index 000000000..13f557cfd --- /dev/null +++ b/novaclient/tests/unit/test_api_versions.py @@ -0,0 +1,170 @@ +# Copyright 2015 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 mock + +from novaclient import api_versions +from novaclient import exceptions +from novaclient.tests.unit import utils + + +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()) + + def test_invalid_version_strings(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1.4") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200.23.66.3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5 .3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5. 3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5.03") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "02.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.001") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, " 2.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1 ") + + 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( + {"X-OpenStack-Nova-API-Version": 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, "1") + + @mock.patch("novaclient.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("novaclient.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) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index ace5751a1..1cade390d 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -161,10 +161,6 @@ class ClientTest(utils.TestCase): self._check_version_url('http://foo.com/nova/v2/%s', 'http://foo.com/nova/') - def test_get_available_client_versions(self): - output = novaclient.client._get_available_client_versions() - self.assertNotEqual([], output) - def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v2.client.Client) @@ -181,6 +177,12 @@ class ClientTest(utils.TestCase): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '0') + def test_get_client_class_latest(self): + self.assertRaises(novaclient.exceptions.UnsupportedVersion, + novaclient.client.get_client_class, 'latest') + self.assertRaises(novaclient.exceptions.UnsupportedVersion, + novaclient.client.get_client_class, '2.latest') + def test_client_with_os_cache_enabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", os_cache=True) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 4958023d7..3d6fc63c0 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -97,8 +97,8 @@ class ShellTest(utils.TestCase): def setUp(self): super(ShellTest, self).setUp() self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - mock.MagicMock)) + 'novaclient.client.Client', + mock.MagicMock())) self.nc_util = mock.patch( 'novaclient.openstack.common.cliutils.isunauthenticated').start() self.nc_util.return_value = False @@ -344,7 +344,9 @@ class ShellTest(utils.TestCase): @mock.patch('novaclient.client.Client') def test_v_unknown_service_type(self, mock_client): - self._test_service_type('unknown', 'compute', mock_client) + self.assertRaises(exceptions.UnsupportedVersion, + self._test_service_type, + 'unknown', 'compute', mock_client) @mock.patch('sys.argv', ['nova']) @mock.patch('sys.stdout', six.StringIO()) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f3052252e..5b5971e4d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -30,10 +30,11 @@ from novaclient.v2 import client 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) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e5d8914a5..1a540ee22 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -70,8 +70,8 @@ class ShellTest(utils.TestCase): self.shell = self.useFixture(ShellFixture()).shell self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - lambda *_: fakes.FakeClient)) + 'novaclient.client.Client', + lambda *args, **kwargs: fakes.FakeClient(*args, **kwargs))) @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @@ -2433,8 +2433,8 @@ class ShellWithSessionClientTest(ShellTest): """Run before each test.""" super(ShellWithSessionClientTest, self).setUp() self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - lambda *_: fakes.FakeSessionClient)) + 'novaclient.client.Client', + lambda *args, **kwargs: fakes.FakeSessionClient(*args, **kwargs))) class GetSecgroupTest(utils.TestCase): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index c759dcec6..1c195d5a1 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -103,7 +103,7 @@ class Client(object): auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - **kwargs): + api_version=None, **kwargs): """ :param str username: Username :param str api_key: API Key @@ -133,6 +133,8 @@ class Client(object): :param bool connection_pool: Use a connection pool :param str session: Session :param str auth: Auth + :param api_version: Compute API version + :type api_version: novaclient.api_versions.APIVersion """ # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -153,6 +155,7 @@ class Client(object): self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) + self.api_version = api_version # extensions self.agents = agents.AgentsManager(self) @@ -224,6 +227,7 @@ class Client(object): connection_pool=connection_pool, session=session, auth=auth, + api_version=api_version, **kwargs) @client._original_only