From 5f231b51636294ccb68c6998b4f66a1e394c8c8f Mon Sep 17 00:00:00 2001 From: Julian Edwards Date: Wed, 23 Mar 2016 12:57:14 +1000 Subject: [PATCH] Add ability to select keystone API version You can now pass the version to the keystone client. e.g.: self.admin_clients("keystone", version=3) Change-Id: I4d8e1cb43b6fd7dcbdcaf063cc01a142e2cf1156 --- rally/osclients.py | 47 ++++++++++++++++++++++++++------ tests/unit/test_osclients.py | 52 +++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/rally/osclients.py b/rally/osclients.py index 893b02cc8c..9ce97cd991 100644 --- a/rally/osclients.py +++ b/rally/osclients.py @@ -16,6 +16,7 @@ import abc from oslo_config import cfg +from six.moves.urllib import parse from rally.cli import envutils from rally.common.i18n import _ @@ -94,9 +95,12 @@ class OSClient(plugin.Plugin): # version is a string object. # For those clients which doesn't accept string value(for example # zaqarclient), this method should be overridden. - return str(version + version = (version or self.api_info.get(self.get_name(), {}).get("version") or self._meta_get("default_version")) + if version is not None: + version = str(version) + return version @classmethod def get_supported_versions(cls): @@ -208,13 +212,14 @@ class OSClient(plugin.Plugin): return super(OSClient, cls).get(name, namespace) -@configure("keystone") +@configure("keystone", supported_versions=("2", "3")) class Keystone(OSClient): + def keystone(self, *args, **kwargs): raise exceptions.RallyException(_("Method 'keystone' is restricted " "for keystoneclient. :)")) - def _create_keystone_client(self, args): + def _create_keystone_client(self, args, version=None): from keystoneclient.auth import identity from keystoneclient import client auth_arg_list = [ @@ -223,7 +228,7 @@ class Keystone(OSClient): ] # NOTE(bigjools): If forcing a v2.0 URL then you cannot specify # domain-related info, or the service discovery will fail. - if "v2.0" not in args["auth_url"]: + if "v2.0" not in args["auth_url"] and version != "2": auth_arg_list.extend( ["user_domain_name", "domain_name", "project_domain_name"]) auth_args = {key: args.get(key) for key in auth_arg_list} @@ -240,12 +245,36 @@ class Keystone(OSClient): # list which is why we need to ensure service_catalog is still # present. auth_ref = auth.get_access(session) - ks = client.Client(**args) + ks = client.Client(version=version, **args) ks.auth_ref = auth_ref return ks - def create_client(self): - """Return keystone client.""" + def _remove_url_version(self): + """Remove any version from the auth_url. + + The keystone Client code requires that auth_url be the root url + if a version override is used. + """ + url = parse.urlparse(self.credential.auth_url) + # NOTE(bigjools): This assumes that non-versioned URLs have no + # path component at all. + parts = (url.scheme, url.netloc, "/", url.params, url.query, + url.fragment) + return parse.urlunparse(parts) + + def create_client(self, version=None): + """Return a keystone client. + + :param version: Keystone API version, can be one of: + ("2", "3") + + If this object was constructed with a version in the api_info + then that will be used unless the version parameter is passed. + """ + # Use the version in the api_info if provided, otherwise fall + # back to the passed version (which may be None, in which case + # keystoneclient chooses). + version = self.choose_version(version) new_kw = { "timeout": CONF.openstack_client_http_timeout, "insecure": self.credential.insecure, @@ -253,7 +282,9 @@ class Keystone(OSClient): } kw = self.credential.to_dict() kw.update(new_kw) - return self._create_keystone_client(kw) + if version is not None: + kw["auth_url"] = self._remove_url_version() + return self._create_keystone_client(kw, version=version) @configure("nova", default_version="2", default_service_type="compute") diff --git a/tests/unit/test_osclients.py b/tests/unit/test_osclients.py index 1ad51162d8..f5b9f03b12 100644 --- a/tests/unit/test_osclients.py +++ b/tests/unit/test_osclients.py @@ -18,6 +18,7 @@ from keystoneclient.auth import token_endpoint from keystoneclient import exceptions as keystone_exceptions import mock from oslo_config import cfg +from testtools import matchers from rally.common import objects from rally import consts @@ -125,7 +126,8 @@ class TestCreateKeystoneClient(test.TestCase): self.ksc_session.Session.assert_called_once_with( auth=self.ksc_identity.Password(), timeout=mock.ANY, verify=mock.ANY) - self.ksc_client.Client.assert_called_once_with(**all_kwargs) + self.ksc_client.Client.assert_called_once_with( + version=None, **all_kwargs) self.assertIs(client, self.ksc_client.Client()) def test_client_is_pre_authed(self): @@ -139,6 +141,27 @@ class TestCreateKeystoneClient(test.TestCase): client = keystone._create_keystone_client(all_kwargs) auth_ref = getattr(client, "auth_ref", None) self.assertIsNot(auth_ref, None) + self.ksc_client.Client.assert_called_once_with( + version=None, **all_kwargs) + self.assertIs(client, self.ksc_client.Client()) + + def test_create_client_removes_url_path_if_version_specified(self): + # If specifying a version on the client creation call, ensure + # the auth_url is versionless and the version required is passed + # into the Client() call. + self.set_up_keystone_mocks() + auth_kwargs, all_kwargs = self.make_auth_args() + credential = objects.Credential( + "http://auth_url/v2.0", "user", "pass", "tenant") + keystone = osclients.Keystone( + credential, {}, mock.MagicMock()) + client = keystone.create_client(version="3") + + self.assertIs(client, self.ksc_client.Client()) + called_with = self.ksc_client.Client.call_args_list[0][1] + self.expectThat( + called_with["auth_url"], matchers.Equals("http://auth_url/")) + self.expectThat(called_with["version"], matchers.Equals("3")) def test_create_keystone_client_with_v2_url_omits_domain(self): # NOTE(bigjools): Test that domain-related info is not present @@ -160,7 +183,29 @@ class TestCreateKeystoneClient(test.TestCase): self.ksc_session.Session.assert_called_once_with( auth=self.ksc_identity.Password(), timeout=mock.ANY, verify=mock.ANY) - self.ksc_client.Client.assert_called_once_with(**all_kwargs) + self.ksc_client.Client.assert_called_once_with( + version=None, **all_kwargs) + self.assertIs(client, self.ksc_client.Client()) + + def test_create_keystone_client_with_v2_version_omits_domain(self): + self.set_up_keystone_mocks() + auth_kwargs, all_kwargs = self.make_auth_args() + + all_kwargs["auth_url"] = "http://auth_url/" + auth_kwargs["auth_url"] = all_kwargs["auth_url"] + keystone = osclients.Keystone( + mock.MagicMock(), mock.sentinel, mock.sentinel) + client = keystone._create_keystone_client(all_kwargs, version="2") + + auth_kwargs.pop("user_domain_name") + auth_kwargs.pop("project_domain_name") + auth_kwargs.pop("domain_name") + self.ksc_password.assert_called_once_with(**auth_kwargs) + self.ksc_session.Session.assert_called_once_with( + auth=self.ksc_identity.Password(), timeout=mock.ANY, + verify=mock.ANY) + self.ksc_client.Client.assert_called_once_with( + version="2", **all_kwargs) self.assertIs(client, self.ksc_client.Client()) @@ -260,7 +305,8 @@ class OSClientsTestCase(test.TestCase): "insecure": False, "cacert": None} kwargs = self.credential.to_dict() kwargs.update(credential.items()) - self.mock_create_keystone_client.assert_called_once_with(kwargs) + self.mock_create_keystone_client.assert_called_once_with( + kwargs, version=None) self.assertEqual(self.fake_keystone, self.clients.cache["keystone"]) @mock.patch("rally.osclients.Keystone.create_client")