From c612a6407a7fbb059601b264b3b17708c6e7a962 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Nov 2017 09:37:54 -0600 Subject: [PATCH] Add ability to work in other auth contexts Add methods and context managers to facilitate working in different projects or different auth contexts, but sharing the Session and other config information from the existing cloud. Sharing the Session allows for version discovery cache to be shared, and keeps the user from needing to do a bunch of work to get a cloud that's like the current one but slightly different. Change-Id: I9190ee725cad00db4e0b6ed8321e3021a703a2e4 --- ...ternate-auth-context-3939f1492a0e1355.yaml | 5 + shade/openstackcloud.py | 109 ++++++++++++++++++ shade/tests/functional/test_project.py | 18 +++ shade/tests/functional/test_users.py | 14 ++- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml diff --git a/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml new file mode 100644 index 000000000..fe24fbc44 --- /dev/null +++ b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added methods and context managers for making new cloud connections + based on the current OpenStackCloud. This should enable working + more easily across projects or user accounts. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ef65bfa48..40984fc43 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -12,6 +12,7 @@ import base64 import collections +import copy import functools import hashlib import ipaddress @@ -33,6 +34,7 @@ import requestsexceptions from six.moves import urllib import keystoneauth1.exceptions +import keystoneauth1.session import shade from shade.exc import * # noqa @@ -304,6 +306,113 @@ class OpenStackCloud( self.cloud_config = cloud_config + def connect_as(self, **kwargs): + """Make a new OpenStackCloud object with new auth context. + + Take the existing settings from the current cloud and construct a new + OpenStackCloud object with some of the auth settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + cloud = shade.openstack_cloud(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as(username='different-user', password='') + # Work as different-user + servers = cloud2.list_servers() + + :param kwargs: keyword arguments can contain anything that would + normally go in an auth dict. They will override the same + settings from the parent cloud as appropriate. Entries + that do not want to be overridden can be ommitted. + """ + + config = os_client_config.OpenStackConfig( + app_name=self.cloud_config._app_name, + app_version=self.cloud_config._app_version, + load_yaml_config=False) + params = copy.deepcopy(self.cloud_config.config) + # Remove profile from current cloud so that overridding works + params.pop('profile', None) + + # Utility function to help with the stripping below. + def pop_keys(params, auth, name_key, id_key): + if name_key in auth or id_key in auth: + params['auth'].pop(name_key, None) + params['auth'].pop(id_key, None) + + # If there are user, project or domain settings in the incoming auth + # dict, strip out both id and name so that a user can say: + # cloud.connect_as(project_name='foo') + # and have that work with clouds that have a project_id set in their + # config. + for prefix in ('user', 'project'): + if prefix == 'user': + name_key = 'username' + else: + name_key = 'project_name' + id_key = '{prefix}_id'.format(prefix=prefix) + pop_keys(params, kwargs, name_key, id_key) + id_key = '{prefix}_domain_id'.format(prefix=prefix) + name_key = '{prefix}_domain_name'.format(prefix=prefix) + pop_keys(params, kwargs, name_key, id_key) + + for key, value in kwargs.items(): + params['auth'][key] = value + + # Closure to pass to OpenStackConfig to ensure the new cloud shares + # the Session with the current cloud. This will ensure that version + # discovery cache will be re-used. + def session_constructor(*args, **kwargs): + # We need to pass our current keystone session to the Session + # Constructor, otherwise the new auth plugin doesn't get used. + return keystoneauth1.session.Session(session=self.keystone_session) + + # Use cloud='defaults' so that we overlay settings properly + cloud_config = config.get_one_cloud( + cloud='defaults', + session_constructor=session_constructor, + **params) + # Override the cloud name so that logging/location work right + cloud_config.name = self.name + cloud_config.config['profile'] = self.name + # Use self.__class__ so that OperatorCloud will return an OperatorCloud + # instance. This should also help passthrough from sdk work better when + # we have it. + return self.__class__(cloud_config=cloud_config) + + def connect_as_project(self, project): + """Make a new OpenStackCloud object with a new project. + + Take the existing settings from the current cloud and construct a new + OpenStackCloud object with the project settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + cloud = shade.openstack_cloud(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as(dict(name='different-project')) + # Work in different-project + servers = cloud2.list_servers() + + :param project: Either a project name or a project dict as returned by + `list_projects`. + """ + auth = {} + if isinstance(project, dict): + auth['project_id'] = project.get('id') + auth['project_name'] = project.get('name') + if project.get('domain_id'): + auth['project_domain_id'] = project['domain_id'] + else: + auth['project_name'] = project + return self.connect_as(**auth) + def _make_cache(self, cache_class, expiration_time, arguments): return dogpile.cache.make_region( function_key_generator=self._make_cache_key diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py index 25d59e6db..5a0437be2 100644 --- a/shade/tests/functional/test_project.py +++ b/shade/tests/functional/test_project.py @@ -21,6 +21,8 @@ test_project Functional tests for `shade` project resource. """ +import pprint + from shade.exc import OpenStackCloudException from shade.tests.functional import base @@ -61,6 +63,22 @@ class TestProject(base.KeystoneBaseFunctionalTestCase): self.assertEqual(project_name, project['name']) self.assertEqual('test_create_project', project['description']) + user_id = self.operator_cloud.keystone_session.auth.get_access( + self.operator_cloud.keystone_session).user_id + + # Grant the current user access to the project + self.assertTrue(self.operator_cloud.grant_role( + 'Member', user=user_id, project=project['id'], wait=True)) + self.addCleanup( + self.operator_cloud.revoke_role, + 'Member', user=user_id, project=project['id'], wait=True) + + new_cloud = self.operator_cloud.connect_as_project(project) + self.add_info_on_exception( + 'new_cloud_config', pprint.pformat(new_cloud.cloud_config.config)) + location = new_cloud.current_location + self.assertEqual(project_name, location['project']['name']) + def test_update_project(self): project_name = self.new_project_name + '_update' diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 985853854..052a614cc 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -17,7 +17,6 @@ test_users Functional tests for `shade` user methods. """ -from shade import operator_cloud from shade import OpenStackCloudException from shade.tests.functional import base @@ -132,9 +131,16 @@ class TestUsers(base.KeystoneBaseFunctionalTestCase): self.addCleanup( self.operator_cloud.revoke_role, 'Member', user=user['id'], project='demo', wait=True) - self.assertIsNotNone(operator_cloud( - cloud=self._demo_name, - username=user_name, password='new_secret').service_catalog) + + new_cloud = self.operator_cloud.connect_as( + user_id=user['id'], + password='new_secret', + project_name='demo') + + self.assertIsNotNone(new_cloud) + location = new_cloud.current_location + self.assertEqual(location['project']['name'], 'demo') + self.assertIsNotNone(new_cloud.service_catalog) def test_users_and_groups(self): i_ver = self.operator_cloud.cloud_config.get_api_version('identity')