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
This commit is contained in:
Andrey Kurilin 2015-04-02 16:37:59 +03:00
parent d3afbd65f6
commit ea0b3bd608
10 changed files with 466 additions and 87 deletions

208
novaclient/api_versions.py Normal file
View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -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):

View File

@ -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