diff options
40 files changed, 430 insertions, 119 deletions
diff --git a/doc/source/conf.py b/doc/source/conf.py index ce6d5b7..010a259 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -151,8 +151,7 @@ modindex_common_prefix = ['keystoneclient.'] git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", "-n1"] try: - html_last_updated_fmt = subprocess.Popen(git_cmd, - stdout=subprocess.PIPE).communicate()[0] + html_last_updated_fmt = subprocess.check_output(git_cmd).decode('utf-8') except Exception: warnings.warn('Cannot get last updated time from git repository. ' 'Not setting "html_last_updated_fmt".') diff --git a/keystoneclient/_discover.py b/keystoneclient/_discover.py index a6d5727..69eed9a 100644 --- a/keystoneclient/_discover.py +++ b/keystoneclient/_discover.py @@ -27,7 +27,7 @@ import re from positional import positional from keystoneclient import exceptions -from keystoneclient.i18n import _, _LI, _LW +from keystoneclient.i18n import _ _LOGGER = logging.getLogger(__name__) @@ -167,8 +167,8 @@ class Discover(object): try: status = v['status'] except KeyError: - _LOGGER.warning(_LW('Skipping over invalid version data. ' - 'No stability status in version.')) + _LOGGER.warning('Skipping over invalid version data. ' + 'No stability status in version.') continue status = status.lower() @@ -210,14 +210,13 @@ class Discover(object): try: version_str = v['id'] except KeyError: - _LOGGER.info(_LI('Skipping invalid version data. Missing ID.')) + _LOGGER.info('Skipping invalid version data. Missing ID.') continue try: links = v['links'] except KeyError: - _LOGGER.info( - _LI('Skipping invalid version data. Missing links')) + _LOGGER.info('Skipping invalid version data. Missing links') continue version_number = normalize_version_number(version_str) @@ -227,15 +226,15 @@ class Discover(object): rel = link['rel'] url = link['href'] except (KeyError, TypeError): - _LOGGER.info(_LI('Skipping invalid version link. ' - 'Missing link URL or relationship.')) + _LOGGER.info('Skipping invalid version link. ' + 'Missing link URL or relationship.') continue if rel.lower() == 'self': break else: - _LOGGER.info(_LI('Skipping invalid version data. ' - 'Missing link to endpoint.')) + _LOGGER.info('Skipping invalid version data. ' + 'Missing link to endpoint.') continue versions.append({'version': version_number, diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py index 29ab121..b20d3e2 100644 --- a/keystoneclient/auth/identity/base.py +++ b/keystoneclient/auth/identity/base.py @@ -22,7 +22,6 @@ import six from keystoneclient import _discover from keystoneclient.auth import base from keystoneclient import exceptions -from keystoneclient.i18n import _LW LOG = logging.getLogger(__name__) @@ -317,10 +316,10 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): else: if not service_type: - LOG.warning(_LW( + LOG.warning( 'Plugin cannot return an endpoint without knowing the ' 'service type that is required. Add service_type to ' - 'endpoint filtering data.')) + 'endpoint filtering data.') return None if not interface: @@ -353,10 +352,9 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): # NOTE(jamielennox): Again if we can't contact the server we fall # back to just returning the URL from the catalog. This may not be # the best default but we need it for now. - LOG.warning(_LW( + LOG.warning( 'Failed to contact the endpoint at %s for discovery. Fallback ' - 'to using that endpoint as the base url.'), - url) + 'to using that endpoint as the base url.', url) else: url = disc.url_for(version) diff --git a/keystoneclient/auth/identity/generic/base.py b/keystoneclient/auth/identity/generic/base.py index 85207ac..98680ef 100644 --- a/keystoneclient/auth/identity/generic/base.py +++ b/keystoneclient/auth/identity/generic/base.py @@ -20,7 +20,7 @@ import six.moves.urllib.parse as urlparse from keystoneclient import _discover from keystoneclient.auth.identity import base from keystoneclient import exceptions -from keystoneclient.i18n import _, _LW +from keystoneclient.i18n import _ LOG = logging.getLogger(__name__) @@ -140,9 +140,9 @@ class BaseGenericPlugin(base.BaseIdentityPlugin): except (exceptions.DiscoveryFailure, exceptions.HTTPError, exceptions.ConnectionError): - LOG.warning(_LW('Discovering versions from the identity service ' - 'failed when creating the password plugin. ' - 'Attempting to determine version from URL.')) + LOG.warning('Discovering versions from the identity service ' + 'failed when creating the password plugin. ' + 'Attempting to determine version from URL.') url_parts = urlparse.urlparse(self.auth_url) path = url_parts.path.lower() diff --git a/keystoneclient/base.py b/keystoneclient/base.py index dc449f7..80e09a0 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -52,14 +52,14 @@ def getid(obj): def filter_none(**kwargs): """Remove any entries from a dictionary where the value is None.""" - return dict((k, v) for k, v in six.iteritems(kwargs) if v is not None) + return dict((k, v) for k, v in kwargs.items() if v is not None) def filter_kwargs(f): @functools.wraps(f) def func(*args, **kwargs): new_kwargs = {} - for key, ref in six.iteritems(kwargs): + for key, ref in kwargs.items(): if ref is None: # drop null values continue @@ -481,7 +481,7 @@ class Resource(object): return None def _add_details(self, info): - for (k, v) in six.iteritems(info): + for (k, v) in info.items(): try: try: setattr(self, k, v) diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py index fb30602..9c3e0bd 100644 --- a/keystoneclient/common/cms.py +++ b/keystoneclient/common/cms.py @@ -29,7 +29,7 @@ from debtcollector import removals import six from keystoneclient import exceptions -from keystoneclient.i18n import _, _LE +from keystoneclient.i18n import _ subprocess = None @@ -376,11 +376,11 @@ def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name, if retcode != OpensslCmsExitStatus.SUCCESS or ('Error' in err): if retcode == OpensslCmsExitStatus.CREATE_CMS_READ_MIME_ERROR: - LOG.error(_LE('Signing error: Unable to load certificate - ' - 'ensure you have configured PKI with ' - '"keystone-manage pki_setup"')) + LOG.error('Signing error: Unable to load certificate - ' + 'ensure you have configured PKI with ' + '"keystone-manage pki_setup"') else: - LOG.error(_LE('Signing error: %s'), err) + LOG.error('Signing error: %s', err) raise subprocess.CalledProcessError(retcode, 'openssl') if outform == PKI_ASN1_FORM: return output.decode('utf-8') diff --git a/keystoneclient/contrib/ec2/utils.py b/keystoneclient/contrib/ec2/utils.py index dcd3ff5..1ef5df4 100644 --- a/keystoneclient/contrib/ec2/utils.py +++ b/keystoneclient/contrib/ec2/utils.py @@ -218,7 +218,7 @@ class Ec2Signer(object): # - the Authorization header (SignedHeaders key) # - the X-Amz-SignedHeaders query parameter headers_lower = dict((k.lower().strip(), v.strip()) - for (k, v) in six.iteritems(headers)) + for (k, v) in headers.items()) # Boto versions < 2.9.3 strip the port component of the host:port # header, so detect the user-agent via the header and strip the diff --git a/keystoneclient/discover.py b/keystoneclient/discover.py index a70b409..823c94c 100644 --- a/keystoneclient/discover.py +++ b/keystoneclient/discover.py @@ -15,7 +15,6 @@ import warnings from debtcollector import removals from keystoneauth1 import plugin from positional import positional -import six from keystoneclient import _discover from keystoneclient import exceptions @@ -296,7 +295,7 @@ class Discover(_discover.Discover): raise exceptions.DiscoveryFailure(msg) # kwargs should take priority over stored kwargs. - for k, v in six.iteritems(self._client_kwargs): + for k, v in self._client_kwargs.items(): kwargs.setdefault(k, v) # restore the url to either auth_url or endpoint depending on what diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index 2e3270a..5b06be9 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -415,7 +415,7 @@ An alias of :py:exc:`keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin` class UnsupportedParameters(ClientException): """A parameter that was provided or returned is not supported. - :param list(str) names: Names of the unsupported parameters. + :param List(str) names: Names of the unsupported parameters. .. py:attribute:: names diff --git a/keystoneclient/generic/client.py b/keystoneclient/generic/client.py index 2f1ffca..e6b5833 100644 --- a/keystoneclient/generic/client.py +++ b/keystoneclient/generic/client.py @@ -20,7 +20,7 @@ from six.moves.urllib import parse as urlparse from keystoneclient import exceptions from keystoneclient import httpclient -from keystoneclient.i18n import _, _LE +from keystoneclient.i18n import _ _logger = logging.getLogger(__name__) @@ -131,7 +131,7 @@ class Client(httpclient.HTTPClient): else: raise exceptions.from_response(resp, "GET", url) except Exception: - _logger.exception(_LE('Failed to detect available versions.')) + _logger.exception('Failed to detect available versions.') def discover_extensions(self, url=None): """Discover Keystone extensions supported. @@ -175,7 +175,7 @@ class Client(httpclient.HTTPClient): raise exceptions.from_response( resp, "GET", "%sextensions" % url) except Exception: - _logger.exception(_LE('Failed to check keystone extensions.')) + _logger.exception('Failed to check keystone extensions.') @staticmethod def _get_version_info(version, root_url): diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py index 4d893c3..e6813f3 100644 --- a/keystoneclient/httpclient.py +++ b/keystoneclient/httpclient.py @@ -54,7 +54,7 @@ from keystoneclient import access from keystoneclient.auth import base from keystoneclient import baseclient from keystoneclient import exceptions -from keystoneclient.i18n import _, _LW +from keystoneclient.i18n import _ from keystoneclient import session as client_session @@ -393,7 +393,7 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): # keyring setup if use_keyring and keyring is None: - _logger.warning(_LW('Failed to load keyring modules.')) + _logger.warning('Failed to load keyring modules.') self.use_keyring = use_keyring and keyring is not None self.force_new_token = force_new_token self.stale_duration = stale_duration or access.STALE_TOKEN_DURATION @@ -633,8 +633,7 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): auth_ref = None except Exception as e: auth_ref = None - _logger.warning( - _LW('Unable to retrieve token from keyring %s'), e) + _logger.warning('Unable to retrieve token from keyring %s', e) return (keyring_key, auth_ref) def store_auth_ref_into_keyring(self, keyring_key): @@ -646,8 +645,7 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): pickle.dumps(self.auth_ref)) # nosec # (cjschaef): see bug 1534288 except Exception as e: - _logger.warning( - _LW("Failed to store token into keyring %s"), e) + _logger.warning("Failed to store token into keyring %s", e) def _process_management_url(self, region_name): try: diff --git a/keystoneclient/i18n.py b/keystoneclient/i18n.py index a196472..fdbf183 100644 --- a/keystoneclient/i18n.py +++ b/keystoneclient/i18n.py @@ -25,13 +25,3 @@ _translators = oslo_i18n.TranslatorFactory(domain='keystoneclient') # The primary translation function using the well-known name "_" _ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical diff --git a/keystoneclient/session.py b/keystoneclient/session.py index 17b1d7f..9faedac 100644 --- a/keystoneclient/session.py +++ b/keystoneclient/session.py @@ -31,7 +31,7 @@ import six from six.moves import urllib from keystoneclient import exceptions -from keystoneclient.i18n import _, _LI, _LW +from keystoneclient.i18n import _ osprofiler_web = importutils.try_import("osprofiler.web") @@ -201,7 +201,7 @@ class Session(object): string_parts.append(url) if headers: - for header in six.iteritems(headers): + for header in headers.items(): string_parts.append('-H "%s: %s"' % self._process_header(header)) @@ -249,7 +249,7 @@ class Session(object): 'RESP:', '[%s]' % response.status_code ] - for header in six.iteritems(response.headers): + for header in response.headers.items(): string_parts.append('%s: %s' % self._process_header(header)) string_parts.append('\nRESP BODY: %s\n' % strutils.mask_password(text)) @@ -476,7 +476,7 @@ class Session(object): if connect_retries <= 0: raise - logger.info(_LI('Failure: %(e)s. Retrying in %(delay).1fs.'), + logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', {'e': e, 'delay': connect_retry_delay}) time.sleep(connect_retry_delay) @@ -503,8 +503,8 @@ class Session(object): try: location = resp.headers['location'] except KeyError: - logger.warning(_LW("Failed to redirect request to %s as new " - "location was not provided."), resp.url) + logger.warning("Failed to redirect request to %s as new " + "location was not provided.", resp.url) else: # NOTE(jamielennox): We don't pass through connect_retry_delay. # This request actually worked so we can reset the delay count. diff --git a/keystoneclient/tests/functional/v3/test_endpoint_filters.py b/keystoneclient/tests/functional/v3/test_endpoint_filters.py new file mode 100644 index 0000000..d8956be --- /dev/null +++ b/keystoneclient/tests/functional/v3/test_endpoint_filters.py @@ -0,0 +1,86 @@ +# 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. + +from keystoneauth1.exceptions import http + +from keystoneclient.tests.functional import base +from keystoneclient.tests.functional.v3 import client_fixtures as fixtures +from keystoneclient.tests.functional.v3 import test_endpoint_groups +from keystoneclient.tests.functional.v3 import test_projects + + +class EndpointFiltersTestCase(base.V3ClientTestCase, + test_endpoint_groups.EndpointGroupsTestMixin, + test_projects.ProjectsTestMixin): + + def setUp(self): + super(EndpointFiltersTestCase, self).setUp() + + self.project = fixtures.Project(self.client) + self.endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(self.project) + self.useFixture(self.endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, self.project) + + def test_add_endpoint_group_to_project(self): + project = fixtures.Project(self.client) + endpoint_group = fixtures.EndpointGroup(self.client) + self.useFixture(project) + self.useFixture(endpoint_group) + + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group, project) + self.client.endpoint_filter.check_endpoint_group_in_project( + endpoint_group, project) + + def test_delete_endpoint_group_from_project(self): + self.client.endpoint_filter.delete_endpoint_group_from_project( + self.endpoint_group, self.project) + self.assertRaises( + http.NotFound, + self.client.endpoint_filter.check_endpoint_group_in_project, + self.endpoint_group, self.project) + + def test_list_endpoint_groups_for_project(self): + endpoint_group_two = fixtures.EndpointGroup(self.client) + self.useFixture(endpoint_group_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group_two, self.project) + + endpoint_groups = ( + self.client.endpoint_filter.list_endpoint_groups_for_project( + self.project + ) + ) + + for endpoint_group in endpoint_groups: + self.check_endpoint_group(endpoint_group) + + self.assertIn(self.endpoint_group.entity, endpoint_groups) + self.assertIn(endpoint_group_two.entity, endpoint_groups) + + def test_list_projects_for_endpoint_group(self): + project_two = fixtures.Project(self.client) + self.useFixture(project_two) + self.client.endpoint_filter.add_endpoint_group_to_project( + self.endpoint_group, project_two) + + f = self.client.endpoint_filter.list_projects_for_endpoint_group + projects = f(self.endpoint_group) + + for project in projects: + self.check_project(project) + + self.assertIn(self.project.entity, projects) + self.assertIn(project_two.entity, projects) diff --git a/keystoneclient/tests/functional/v3/test_endpoint_groups.py b/keystoneclient/tests/functional/v3/test_endpoint_groups.py index 10eccfb..52fcf72 100644 --- a/keystoneclient/tests/functional/v3/test_endpoint_groups.py +++ b/keystoneclient/tests/functional/v3/test_endpoint_groups.py @@ -18,7 +18,7 @@ from keystoneclient.tests.functional import base from keystoneclient.tests.functional.v3 import client_fixtures as fixtures -class EndpointGroupsTestCase(base.V3ClientTestCase): +class EndpointGroupsTestMixin(object): def check_endpoint_group(self, endpoint_group, endpoint_group_ref=None): self.assertIsNotNone(endpoint_group.id) @@ -40,6 +40,9 @@ class EndpointGroupsTestCase(base.V3ClientTestCase): self.assertIsNotNone(endpoint_group.name) self.assertIsNotNone(endpoint_group.filters) + +class EndpointGroupsTestCase(base.V3ClientTestCase, EndpointGroupsTestMixin): + def test_create_endpoint_group(self): endpoint_group_ref = { 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, diff --git a/keystoneclient/tests/functional/v3/test_projects.py b/keystoneclient/tests/functional/v3/test_projects.py index 0fa631d..4b8d749 100644 --- a/keystoneclient/tests/functional/v3/test_projects.py +++ b/keystoneclient/tests/functional/v3/test_projects.py @@ -18,15 +18,7 @@ from keystoneclient.tests.functional import base from keystoneclient.tests.functional.v3 import client_fixtures as fixtures -class ProjectsTestCase(base.V3ClientTestCase): - - def setUp(self): - super(ProjectsTestCase, self).setUp() - self.test_domain = fixtures.Domain(self.client) - self.useFixture(self.test_domain) - - self.test_project = fixtures.Project(self.client, self.test_domain.id) - self.useFixture(self.test_project) +class ProjectsTestMixin(object): def check_project(self, project, project_ref=None): self.assertIsNotNone(project.id) @@ -51,6 +43,17 @@ class ProjectsTestCase(base.V3ClientTestCase): self.assertIsNotNone(project.domain_id) self.assertIsNotNone(project.enabled) + +class ProjectsTestCase(base.V3ClientTestCase, ProjectsTestMixin): + + def setUp(self): + super(ProjectsTestCase, self).setUp() + self.test_domain = fixtures.Domain(self.client) + self.useFixture(self.test_domain) + + self.test_project = fixtures.Project(self.client, self.test_domain.id) + self.useFixture(self.test_project) + def test_create_subproject(self): project_ref = { 'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex, diff --git a/keystoneclient/tests/unit/apiclient/test_exceptions.py b/keystoneclient/tests/unit/apiclient/test_exceptions.py index ddf8d98..65cf080 100644 --- a/keystoneclient/tests/unit/apiclient/test_exceptions.py +++ b/keystoneclient/tests/unit/apiclient/test_exceptions.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from keystoneclient import exceptions from keystoneclient.tests.unit import utils @@ -23,7 +21,7 @@ class FakeResponse(object): json_data = {} def __init__(self, **kwargs): - for key, value in six.iteritems(kwargs): + for key, value in kwargs.items(): setattr(self, key, value) def json(self): diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py index 5793672..be8a062 100644 --- a/keystoneclient/tests/unit/auth/test_identity_common.py +++ b/keystoneclient/tests/unit/auth/test_identity_common.py @@ -461,7 +461,7 @@ class GenericAuthPluginTests(utils.TestCase): self.assertEqual(text, resp.text) - for k, v in six.iteritems(self.auth.headers): + for k, v in self.auth.headers.items(): self.assertRequestHeaderEqual(k, v) with self.deprecations.expect_deprecations_here(): diff --git a/keystoneclient/tests/unit/auth/test_loading.py b/keystoneclient/tests/unit/auth/test_loading.py index f8ef3b7..3c2689d 100644 --- a/keystoneclient/tests/unit/auth/test_loading.py +++ b/keystoneclient/tests/unit/auth/test_loading.py @@ -12,7 +12,6 @@ import uuid -import six from keystoneclient.tests.unit.auth import utils @@ -39,7 +38,7 @@ class TestOtherLoading(utils.TestCase): self.assertEqual(set(vals), set(called_opts)) - for k, v in six.iteritems(vals): + for k, v in vals.items(): # replace - to _ because it's the dest used to create kwargs self.assertEqual(v, p[k.replace('-', '_')]) diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py index 6c8be8c..b693172 100644 --- a/keystoneclient/tests/unit/auth/utils.py +++ b/keystoneclient/tests/unit/auth/utils.py @@ -16,7 +16,6 @@ import uuid from keystoneauth1 import fixture import mock from oslo_config import cfg -import six from keystoneclient import access from keystoneclient.auth import base @@ -88,7 +87,7 @@ class TestCase(utils.TestCase): 'a_bool': a_bool} def assertTestVals(self, plugin, vals=TEST_VALS): - for k, v in six.iteritems(vals): + for k, v in vals.items(): self.assertEqual(v, plugin[k]) diff --git a/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py index 0e00545..6da259c 100644 --- a/keystoneclient/tests/unit/client_fixtures.py +++ b/keystoneclient/tests/unit/client_fixtures.py @@ -703,7 +703,7 @@ class Examples(fixtures.Fixture): self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY]) self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in - six.iteritems(self.TOKEN_RESPONSES)]) + self.TOKEN_RESPONSES.items()]) EXAMPLES_RESOURCE = testresources.FixtureResource(Examples()) diff --git a/keystoneclient/tests/unit/test_base.py b/keystoneclient/tests/unit/test_base.py index f6ca651..0a0fde1 100644 --- a/keystoneclient/tests/unit/test_base.py +++ b/keystoneclient/tests/unit/test_base.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures from keystoneauth1.identity import v2 from keystoneauth1 import session -from oslotest import mockpatch from keystoneclient import base from keystoneclient.tests.unit import utils @@ -44,7 +44,7 @@ class BaseTest(utils.TestCase): session_ = session.Session(auth=auth) self.client = client.Client(session=session_) - self.useFixture(mockpatch.PatchObject( + self.useFixture(fixtures.MockPatchObject( self.client._adapter, 'get', side_effect=AttributeError, autospec=True)) @@ -127,7 +127,7 @@ class ManagerTest(utils.TestCase): self.assertEqual(self.mgr.api, self.client) def test_get(self): - get_mock = self.useFixture(mockpatch.PatchObject( + get_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'get', autospec=True, return_value=(None, self.body)) ).mock rsrc = self.mgr._get(self.url, "hello") @@ -135,7 +135,7 @@ class ManagerTest(utils.TestCase): self.assertEqual(rsrc.hi, 1) def test_post(self): - post_mock = self.useFixture(mockpatch.PatchObject( + post_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'post', autospec=True, return_value=(None, self.body)) ).mock @@ -150,7 +150,7 @@ class ManagerTest(utils.TestCase): self.assertEqual(rsrc["hi"], 1) def test_put(self): - put_mock = self.useFixture(mockpatch.PatchObject( + put_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'put', autospec=True, return_value=(None, self.body)) ).mock @@ -165,7 +165,7 @@ class ManagerTest(utils.TestCase): self.assertEqual(rsrc.hello["hi"], 1) def test_patch(self): - patch_mock = self.useFixture(mockpatch.PatchObject( + patch_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'patch', autospec=True, return_value=(None, self.body)) ).mock @@ -181,12 +181,12 @@ class ManagerTest(utils.TestCase): self.assertEqual(rsrc.hello["hi"], 1) def test_update(self): - patch_mock = self.useFixture(mockpatch.PatchObject( + patch_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'patch', autospec=True, return_value=(None, self.body)) ).mock - put_mock = self.useFixture(mockpatch.PatchObject( + put_mock = self.useFixture(fixtures.MockPatchObject( self.client, 'put', autospec=True, return_value=(None, self.body)) ).mock diff --git a/keystoneclient/tests/unit/test_discovery.py b/keystoneclient/tests/unit/test_discovery.py index cc7fb0f..f9d5dbf 100644 --- a/keystoneclient/tests/unit/test_discovery.py +++ b/keystoneclient/tests/unit/test_discovery.py @@ -243,7 +243,7 @@ class AvailableVersionsTests(utils.TestCase): 'cinder': jsonutils.dumps(CINDER_EXAMPLES), 'glance': jsonutils.dumps(GLANCE_EXAMPLES)} - for path, text in six.iteritems(examples): + for path, text in examples.items(): url = "%s%s" % (BASE_URL, path) self.requests_mock.get(url, status_code=300, text=text) diff --git a/keystoneclient/tests/unit/test_fixtures.py b/keystoneclient/tests/unit/test_fixtures.py index 345ae45..d7fd26e 100644 --- a/keystoneclient/tests/unit/test_fixtures.py +++ b/keystoneclient/tests/unit/test_fixtures.py @@ -12,7 +12,6 @@ import uuid -import six from keystoneclient import fixture from keystoneclient.tests.unit import utils @@ -246,7 +245,7 @@ class V3TokenTests(utils.TestCase): # the endpoint content below easier. self.assertTrue(endpoint.pop('id')) - for interface, url in six.iteritems(endpoints): + for interface, url in endpoints.items(): endpoint = {'interface': interface, 'url': url, 'region': region, 'region_id': region} self.assertIn(endpoint, service['endpoints']) diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py index 0282f1a..af9058f 100644 --- a/keystoneclient/tests/unit/test_http.py +++ b/keystoneclient/tests/unit/test_http.py @@ -202,10 +202,10 @@ class BasicRequestTests(utils.TestCase): self.request(headers=headers) - for k, v in six.iteritems(headers): + for k, v in headers.items(): self.assertRequestHeaderEqual(k, v) - for header in six.iteritems(headers): + for header in headers.items(): self.assertThat(self.logger_message.getvalue(), matchers.Contains('-H "%s: %s"' % header)) diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py index 7a3c57d..27d224d 100644 --- a/keystoneclient/tests/unit/test_session.py +++ b/keystoneclient/tests/unit/test_session.py @@ -171,13 +171,13 @@ class SessionTests(utils.TestCase): self.assertIn(body, self.logger.output) self.assertIn("'%s'" % data, self.logger.output) - for k, v in six.iteritems(headers): + for k, v in headers.items(): self.assertIn(k, self.logger.output) self.assertIn(v, self.logger.output) # Assert that response headers contains actual values and # only debug logs has been masked - for k, v in six.iteritems(security_headers): + for k, v in security_headers.items(): self.assertIn('%s: {SHA1}' % k, self.logger.output) self.assertEqual(v, resp.headers[k]) self.assertNotIn(v, self.logger.output) diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py index 378f912..6921b4b 100644 --- a/keystoneclient/tests/unit/utils.py +++ b/keystoneclient/tests/unit/utils.py @@ -19,7 +19,6 @@ from oslo_serialization import jsonutils import requests import requests_mock from requests_mock.contrib import fixture -import six from six.moves.urllib import parse as urlparse import testscenarios import testtools @@ -97,7 +96,7 @@ class TestCase(testtools.TestCase): parts = urlparse.urlparse(self.requests_mock.last_request.url) qs = urlparse.parse_qs(parts.query, keep_blank_values=True) - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): self.assertIn(k, qs) self.assertIn(v, qs[k]) diff --git a/keystoneclient/tests/unit/v2_0/test_client.py b/keystoneclient/tests/unit/v2_0/test_client.py index fc9bf14..cddac4d 100644 --- a/keystoneclient/tests/unit/v2_0/test_client.py +++ b/keystoneclient/tests/unit/v2_0/test_client.py @@ -14,7 +14,6 @@ import json import uuid from keystoneauth1 import fixture -import six from keystoneauth1 import session as auth_session from keystoneclient.auth import token_endpoint @@ -207,7 +206,7 @@ class KeystoneClientTest(utils.TestCase): cl = client.Client(session=sess, **opts) - for k, v in six.iteritems(opts): + for k, v in opts.items(): self.assertEqual(v, getattr(cl._adapter, k)) self.assertEqual('identity', cl._adapter.service_type) diff --git a/keystoneclient/tests/unit/v3/test_client.py b/keystoneclient/tests/unit/v3/test_client.py index 29a2818..feb921a 100644 --- a/keystoneclient/tests/unit/v3/test_client.py +++ b/keystoneclient/tests/unit/v3/test_client.py @@ -14,7 +14,6 @@ import copy import json import uuid -import six from keystoneauth1 import session as auth_session from keystoneclient.auth import token_endpoint @@ -257,7 +256,7 @@ class KeystoneClientTest(utils.TestCase): cl = client.Client(session=sess, **opts) - for k, v in six.iteritems(opts): + for k, v in opts.items(): self.assertEqual(v, getattr(cl._adapter, k)) self.assertEqual('identity', cl._adapter.service_type) diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py index 2eed705..62e89cb 100644 --- a/keystoneclient/tests/unit/v3/test_endpoint_filter.py +++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py @@ -36,6 +36,13 @@ class EndpointTestUtils(object): kwargs.setdefault('url', uuid.uuid4().hex) return kwargs + def new_endpoint_group_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('filters') + return kwargs + class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): """Test project-endpoint associations (a.k.a. EndpointFilter Extension). @@ -147,3 +154,140 @@ class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): project['id'] for project in projects['projects']] actual_project_ids = [project.id for project in projects_resp] self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group(self): + endpoint_group_id = uuid.uuid4().hex + projects = {'projects': [self.new_project_ref(), + self.new_project_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects'], + json=projects, + status_code=200) + + projects_resp = self.manager.list_projects_for_endpoint_group( + endpoint_group=endpoint_group_id) + + expected_project_ids = [ + project['id'] for project in projects['projects']] + actual_project_ids = [project.id for project in projects_resp] + self.assertEqual(expected_project_ids, actual_project_ids) + + def test_list_projects_for_endpoint_group_value_error(self): + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group='') + self.assertRaises(ValueError, + self.manager.list_projects_for_endpoint_group, + endpoint_group=None) + + def test_list_endpoint_groups_for_project(self): + project_id = uuid.uuid4().hex + endpoint_groups = { + 'endpoint_groups': [self.new_endpoint_group_ref(), + self.new_endpoint_group_ref()]} + self.stub_url('GET', + [self.manager.OS_EP_FILTER_EXT, 'projects', + project_id, 'endpoint_groups'], + json=endpoint_groups, + status_code=200) + + endpoint_groups_resp = self.manager.list_endpoint_groups_for_project( + project=project_id) + + expected_endpoint_group_ids = [ + endpoint_group['id'] for endpoint_group + in endpoint_groups['endpoint_groups'] + ] + actual_endpoint_group_ids = [ + endpoint_group.id for endpoint_group in endpoint_groups_resp + ] + self.assertEqual(expected_endpoint_group_ids, + actual_endpoint_group_ids) + + def test_list_endpoint_groups_for_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.list_endpoint_groups_for_project, + project=value) + + def test_add_endpoint_group_to_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('PUT', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.add_endpoint_group_to_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_add_endpoint_group_to_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.add_endpoint_group_to_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_check_endpoint_group_in_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('HEAD', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.check_endpoint_group_in_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_check_endpoint_group_in_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.check_endpoint_group_in_project, + project=value, + endpoint_group=uuid.uuid4().hex) + + def test_delete_endpoint_group_from_project(self): + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.stub_url('DELETE', + [self.manager.OS_EP_FILTER_EXT, 'endpoint_groups', + endpoint_group_id, 'projects', project_id], + status_code=201) + + self.manager.delete_endpoint_group_from_project( + project=project_id, endpoint_group=endpoint_group_id) + + def test_delete_endpoint_group_from_project_value_error(self): + for value in ('', None): + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=uuid.uuid4().hex, + endpoint_group=value) + self.assertRaises(ValueError, + self.manager.delete_endpoint_group_from_project, + project=value, + endpoint_group=uuid.uuid4().hex) diff --git a/keystoneclient/tests/unit/v3/test_trusts.py b/keystoneclient/tests/unit/v3/test_trusts.py index 72fb5b7..1c74ac9 100644 --- a/keystoneclient/tests/unit/v3/test_trusts.py +++ b/keystoneclient/tests/unit/v3/test_trusts.py @@ -64,6 +64,22 @@ class TrustTests(utils.ClientTestCase, utils.CrudTests): req_ref['roles'] = [{'name': 'atestrole'}] super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + def test_create_role_id_and_names(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + req_ref = ref.copy() + req_ref.pop('id') + + # Note the TrustManager takes a list of role_names, and converts + # internally to the slightly odd list-of-dict API format, so we + # have to pass the expected request data to allow correct stubbing + ref['role_names'] = ['atestrole'] + ref['role_ids'] = [uuid.uuid4().hex] + req_ref['roles'] = [{'name': 'atestrole'}, {'id': ref['role_ids'][0]}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + def test_create_expires(self): ref = self.new_ref() ref['trustor_user_id'] = uuid.uuid4().hex diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py index 2c9c86d..d9cb5a4 100644 --- a/keystoneclient/tests/unit/v3/utils.py +++ b/keystoneclient/tests/unit/v3/utils.py @@ -12,7 +12,6 @@ import uuid -import six from six.moves.urllib import parse as urlparse from keystoneclient.tests.unit import client_fixtures @@ -301,7 +300,7 @@ class CrudTests(object): qs_args = self.requests_mock.last_request.qs qs_args_expected = expected_query or filter_kwargs - for key, value in six.iteritems(qs_args_expected): + for key, value in qs_args_expected.items(): self.assertIn(key, qs_args) # The querystring value is a list. Note we convert the value to a # string and lower, as the query string is always a string and the diff --git a/keystoneclient/v2_0/tenants.py b/keystoneclient/v2_0/tenants.py index d375da6..1b43990 100644 --- a/keystoneclient/v2_0/tenants.py +++ b/keystoneclient/v2_0/tenants.py @@ -15,7 +15,6 @@ # under the License. from keystoneauth1 import plugin -import six from six.moves import urllib from keystoneclient import base @@ -92,7 +91,7 @@ class TenantManager(base.ManagerWithFind): "enabled": enabled}} # Allow Extras Passthru and ensure we don't clobber primary arguments. - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if k not in params['tenant']: params['tenant'][k] = v @@ -142,7 +141,7 @@ class TenantManager(base.ManagerWithFind): body['tenant']['description'] = description # Allow Extras Passthru and ensure we don't clobber primary arguments. - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if k not in body['tenant']: body['tenant'][k] = v diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py index 586a74a..26d5a87 100644 --- a/keystoneclient/v3/contrib/endpoint_filter.py +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -15,12 +15,18 @@ from keystoneclient import base from keystoneclient import exceptions from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoint_groups from keystoneclient.v3 import endpoints from keystoneclient.v3 import projects class EndpointFilterManager(base.Manager): - """Manager class for manipulating project-endpoint associations.""" + """Manager class for manipulating project-endpoint associations. + + Project-endpoint associations can be with endpoints directly or via + endpoint groups. + + """ OS_EP_FILTER_EXT = '/OS-EP-FILTER' @@ -40,6 +46,23 @@ class EndpointFilterManager(base.Manager): return self.OS_EP_FILTER_EXT + api_path + def _build_group_base_url(self, project=None, endpoint_group=None): + project_id = base.getid(project) + endpoint_group_id = base.getid(endpoint_group) + + if project_id and endpoint_group_id: + api_path = '/endpoint_groups/%s/projects/%s' % ( + endpoint_group_id, project_id) + elif project_id: + api_path = '/projects/%s/endpoint_groups' % (project_id) + elif endpoint_group_id: + api_path = '/endpoint_groups/%s/projects' % (endpoint_group_id) + else: + msg = _('Must specify a project, an endpoint group, or both') + raise exceptions.ValidationError(msg) + + return self.OS_EP_FILTER_EXT + api_path + def add_endpoint_to_project(self, project, endpoint): """Create a project-endpoint association.""" if not (project and endpoint): @@ -59,7 +82,7 @@ class EndpointFilterManager(base.Manager): return super(EndpointFilterManager, self)._delete(url=base_url) def check_endpoint_in_project(self, project, endpoint): - """Check if project-endpoint association exist.""" + """Check if project-endpoint association exists.""" if not (project and endpoint): raise ValueError(_('project and endpoint are required')) @@ -88,3 +111,53 @@ class EndpointFilterManager(base.Manager): base_url, projects.ProjectManager.collection_key, obj_class=projects.ProjectManager.resource_class) + + def add_endpoint_group_to_project(self, endpoint_group, project): + """Create a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._put(url=base_url) + + def delete_endpoint_group_from_project(self, endpoint_group, project): + """Remove a project-endpoint group association.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._delete(url=base_url) + + def check_endpoint_group_in_project(self, endpoint_group, project): + """Check if project-endpoint group association exists.""" + if not (project and endpoint_group): + raise ValueError(_('project and endpoint_group are required')) + + base_url = self._build_group_base_url(project=project, + endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._head(url=base_url) + + def list_endpoint_groups_for_project(self, project): + """List all endpoint groups for a given project.""" + if not project: + raise ValueError(_('project is required')) + + base_url = self._build_group_base_url(project=project) + + return super(EndpointFilterManager, self)._list( + base_url, + 'endpoint_groups', + obj_class=endpoint_groups.EndpointGroupManager.resource_class) + + def list_projects_for_endpoint_group(self, endpoint_group): + """List all projects associated with a given endpoint group.""" + if not endpoint_group: + raise ValueError(_('endpoint_group is required')) + + base_url = self._build_group_base_url(endpoint_group=endpoint_group) + return super(EndpointFilterManager, self)._list( + base_url, + projects.ProjectManager.collection_key, + obj_class=projects.ProjectManager.resource_class) diff --git a/keystoneclient/v3/contrib/trusts.py b/keystoneclient/v3/contrib/trusts.py index e236188..a8ef579 100644 --- a/keystoneclient/v3/contrib/trusts.py +++ b/keystoneclient/v3/contrib/trusts.py @@ -39,13 +39,14 @@ class TrustManager(base.CrudManager): base_url = '/OS-TRUST' def create(self, trustee_user, trustor_user, role_names=None, - project=None, impersonation=False, expires_at=None, - remaining_uses=None, **kwargs): + role_ids=None, project=None, impersonation=False, + expires_at=None, remaining_uses=None, **kwargs): """Create a Trust. :param string trustee_user: user who is capable of consuming the trust :param string trustor_user: user who's authorization is being delegated :param string role_names: subset of trustor's roles to be granted + :param string role_ids: subset of trustor's roles to be granted :param string project: project which the trustor is delegating :param boolean impersonation: enable explicit impersonation :param datetime.datetime expires_at: expiry time @@ -55,9 +56,13 @@ class TrustManager(base.CrudManager): """ # Convert role_names list into list-of-dict API format + roles = [] if role_names: - roles = [{'name': n} for n in role_names] - else: + roles.extend([{'name': n} for n in role_names]) + if role_ids: + roles.extend([{'id': i} for i in role_ids]) + + if not roles: roles = None # Convert datetime.datetime expires_at to iso format string diff --git a/releasenotes/notes/bug-1641674-4862454115265e76.yaml b/releasenotes/notes/bug-1641674-4862454115265e76.yaml new file mode 100644 index 0000000..19c8ecc --- /dev/null +++ b/releasenotes/notes/bug-1641674-4862454115265e76.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + Keystone Client now supports endpoint group filtering. +features: + - | + Support for handling the relationship between endpoint groups and projects + has been added. It is now possible to list, associate, check and + disassociate endpoint groups that have access to a project. diff --git a/keystoneclient/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml b/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml index 61b9d17..61b9d17 100644 --- a/keystoneclient/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml +++ b/releasenotes/notes/removed-generic-client-ff505b2b01bc9302.yaml diff --git a/requirements.txt b/requirements.txt index 537de0f..8f6e64e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,15 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -pbr>=2.0.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 debtcollector>=1.2.0 # Apache-2.0 -keystoneauth1>=2.18.0 # Apache-2.0 -oslo.config>=3.22.0 # Apache-2.0 -oslo.i18n>=2.1.0 # Apache-2.0 +keystoneauth1>=2.21.0 # Apache-2.0 +oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0 +oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 positional>=1.1.1 # Apache-2.0 -requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 @@ -40,9 +40,9 @@ keystoneclient.auth.plugin = source-dir = doc/source build-dir = doc/build all_files = 1 +warning-is-error = 1 [pbr] -warnerrors = True autodoc_tree_index_modules = True autodoc_tree_excludes = setup.py diff --git a/test-requirements.txt b/test-requirements.txt index e5dcda8..a358611 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ hacking<0.11,>=0.10.0 flake8-docstrings==0.2.1.post1 # MIT -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF lxml!=3.7.0,>=2.3 # BSD @@ -13,9 +13,9 @@ mock>=2.0 # BSD oauthlib>=0.6 # BSD oslosphinx>=4.7.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -reno>=1.8.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx>=1.5.1 # BSD +sphinx>=1.6.2 # BSD tempest>=14.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testresources>=0.2.4 # Apache-2.0/BSD |