diff --git a/glance_store/_drivers/swift/store.py b/glance_store/_drivers/swift/store.py index ef3fcd4b..bf42d050 100644 --- a/glance_store/_drivers/swift/store.py +++ b/glance_store/_drivers/swift/store.py @@ -33,6 +33,10 @@ try: except ImportError: swiftclient = None +from keystoneclient.auth.identity import v3 as ks_v3 +from keystoneclient import session as ks_session +from keystoneclient.v3 import client as ks_client + import glance_store from glance_store._drivers.swift import connection_manager from glance_store._drivers.swift import utils as sutils @@ -140,7 +144,7 @@ _SWIFT_OPTS = [ ] -def swift_retry_iter(resp_iter, length, store, location, context): +def swift_retry_iter(resp_iter, length, store, location, manager): if not length and isinstance(resp_iter, six.BytesIO): if six.PY3: # On Python 3, io.BytesIO does not have a len attribute, instead @@ -185,9 +189,9 @@ def swift_retry_iter(resp_iter, length, store, location, context): 'max_retries': retry_count, 'start': bytes_read, 'end': length}) - (_resp_headers, resp_iter) = store._get_object(location, None, - bytes_read, - context=context) + (_resp_headers, resp_iter) = store._get_object(location, + manager, + bytes_read) else: break @@ -440,16 +444,14 @@ class BaseStore(driver.Store): reason=msg) super(BaseStore, self).configure(re_raise_bsc=re_raise_bsc) - def _get_object(self, location, connection=None, start=None, context=None): - if not connection: - connection = self.get_connection(location, context=context) + def _get_object(self, location, manager, start=None): headers = {} if start is not None: bytes_range = 'bytes=%d-' % start headers = {'Range': bytes_range} try: - resp_headers, resp_body = connection.get_object( + resp_headers, resp_body = manager.get_connection().get_object( location.container, location.obj, resp_chunk_size=self.CHUNKSIZE, headers=headers) except swiftclient.ClientException as e: @@ -466,21 +468,26 @@ class BaseStore(driver.Store): def get(self, location, connection=None, offset=0, chunk_size=None, context=None): location = location.store_location - (resp_headers, resp_body) = self._get_object(location, connection, - context=context) + # initialize manager to receive valid connections + allow_retry = \ + self.conf.glance_store.swift_store_retry_get_count > 0 + with get_manager_for_store(self, location, context, + allow_reauth=allow_retry) as manager: + (resp_headers, resp_body) = self._get_object(location, + manager=manager) - class ResponseIndexable(glance_store.Indexable): - def another(self): - try: - return next(self.wrapped) - except StopIteration: - return '' + class ResponseIndexable(glance_store.Indexable): + def another(self): + try: + return next(self.wrapped) + except StopIteration: + return '' - length = int(resp_headers.get('content-length', 0)) - if self.conf.glance_store.swift_store_retry_get_count > 0: - resp_body = swift_retry_iter(resp_body, length, - self, location, context) - return (ResponseIndexable(resp_body, length), length) + length = int(resp_headers.get('content-length', 0)) + if allow_retry: + resp_body = swift_retry_iter(resp_body, length, + self, location, manager=manager) + return ResponseIndexable(resp_body, length), length def get_size(self, location, connection=None, context=None): location = location.store_location @@ -516,138 +523,143 @@ class BaseStore(driver.Store): @capabilities.check def add(self, image_id, image_file, image_size, - connection=None, context=None, verifier=None): + context=None, verifier=None): location = self.create_location(image_id, context=context) - if not connection: - connection = self.get_connection(location, context=context) + # initialize a manager with re-auth if image need to be splitted + need_chunks = (image_size == 0) or ( + image_size >= self.large_object_size) + with get_manager_for_store(self, location, context, + allow_reauth=need_chunks) as manager: - self._create_container_if_missing(location.container, connection) + self._create_container_if_missing(location.container, + manager.get_connection()) - LOG.debug("Adding image object '%(obj_name)s' " - "to Swift" % dict(obj_name=location.obj)) - try: - if image_size > 0 and image_size < self.large_object_size: - # Image size is known, and is less than large_object_size. - # Send to Swift with regular PUT. - if verifier: - checksum = hashlib.md5() - reader = ChunkReader(image_file, checksum, - image_size, verifier) - obj_etag = connection.put_object(location.container, - location.obj, - reader, - content_length=image_size) - else: - obj_etag = connection.put_object(location.container, - location.obj, image_file, - content_length=image_size) - else: - # Write the image into Swift in chunks. - chunk_id = 1 - if image_size > 0: - total_chunks = str(int( - math.ceil(float(image_size) / - float(self.large_object_chunk_size)))) - else: - # image_size == 0 is when we don't know the size - # of the image. This can occur with older clients - # that don't inspect the payload size. - LOG.debug("Cannot determine image size. Adding as a " - "segmented object to Swift.") - total_chunks = '?' - - checksum = hashlib.md5() - written_chunks = [] - combined_chunks_size = 0 - while True: - chunk_size = self.large_object_chunk_size - if image_size == 0: - content_length = None + LOG.debug("Adding image object '%(obj_name)s' " + "to Swift" % dict(obj_name=location.obj)) + try: + if not need_chunks: + # Image size is known, and is less than large_object_size. + # Send to Swift with regular PUT. + if verifier: + checksum = hashlib.md5() + reader = ChunkReader(image_file, checksum, + image_size, verifier) + obj_etag = manager.get_connection().put_object( + location.container, location.obj, + reader, content_length=image_size) else: - left = image_size - combined_chunks_size - if left == 0: + obj_etag = manager.get_connection().put_object( + location.container, location.obj, + image_file, content_length=image_size) + else: + # Write the image into Swift in chunks. + chunk_id = 1 + if image_size > 0: + total_chunks = str(int( + math.ceil(float(image_size) / + float(self.large_object_chunk_size)))) + else: + # image_size == 0 is when we don't know the size + # of the image. This can occur with older clients + # that don't inspect the payload size. + LOG.debug("Cannot determine image size. Adding as a " + "segmented object to Swift.") + total_chunks = '?' + + checksum = hashlib.md5() + written_chunks = [] + combined_chunks_size = 0 + while True: + chunk_size = self.large_object_chunk_size + if image_size == 0: + content_length = None + else: + left = image_size - combined_chunks_size + if left == 0: + break + if chunk_size > left: + chunk_size = left + content_length = chunk_size + + chunk_name = "%s-%05d" % (location.obj, chunk_id) + reader = ChunkReader(image_file, checksum, chunk_size, + verifier) + if reader.is_zero_size is True: + LOG.debug('Not writing zero-length chunk.') break - if chunk_size > left: - chunk_size = left - content_length = chunk_size + try: + chunk_etag = manager.get_connection().put_object( + location.container, chunk_name, reader, + content_length=content_length) + written_chunks.append(chunk_name) + except Exception: + # Delete orphaned segments from swift backend + with excutils.save_and_reraise_exception(): + LOG.exception(_("Error during chunked upload " + "to backend, deleting stale " + "chunks")) + self._delete_stale_chunks( + manager.get_connection(), + location.container, + written_chunks) - chunk_name = "%s-%05d" % (location.obj, chunk_id) - reader = ChunkReader(image_file, checksum, chunk_size, - verifier) - if reader.is_zero_size is True: - LOG.debug('Not writing zero-length chunk.') - break - try: - chunk_etag = connection.put_object( - location.container, chunk_name, reader, - content_length=content_length) - written_chunks.append(chunk_name) - except Exception: - # Delete orphaned segments from swift backend - with excutils.save_and_reraise_exception(): - LOG.exception(_("Error during chunked upload to " - "backend, deleting stale chunks")) - self._delete_stale_chunks(connection, - location.container, - written_chunks) + bytes_read = reader.bytes_read + msg = ("Wrote chunk %(chunk_name)s (%(chunk_id)d/" + "%(total_chunks)s) of length %(bytes_read)d " + "to Swift returning MD5 of content: " + "%(chunk_etag)s" % + {'chunk_name': chunk_name, + 'chunk_id': chunk_id, + 'total_chunks': total_chunks, + 'bytes_read': bytes_read, + 'chunk_etag': chunk_etag}) + LOG.debug(msg) - bytes_read = reader.bytes_read - msg = ("Wrote chunk %(chunk_name)s (%(chunk_id)d/" - "%(total_chunks)s) of length %(bytes_read)d " - "to Swift returning MD5 of content: " - "%(chunk_etag)s" % - {'chunk_name': chunk_name, - 'chunk_id': chunk_id, - 'total_chunks': total_chunks, - 'bytes_read': bytes_read, - 'chunk_etag': chunk_etag}) - LOG.debug(msg) + chunk_id += 1 + combined_chunks_size += bytes_read - chunk_id += 1 - combined_chunks_size += bytes_read + # In the case we have been given an unknown image size, + # set the size to the total size of the combined chunks. + if image_size == 0: + image_size = combined_chunks_size - # In the case we have been given an unknown image size, - # set the size to the total size of the combined chunks. - if image_size == 0: - image_size = combined_chunks_size + # Now we write the object manifest and return the + # manifest's etag... + manifest = "%s/%s-" % (location.container, location.obj) + headers = {'ETag': hashlib.md5(b"").hexdigest(), + 'X-Object-Manifest': manifest} - # Now we write the object manifest and return the - # manifest's etag... - manifest = "%s/%s-" % (location.container, location.obj) - headers = {'ETag': hashlib.md5(b"").hexdigest(), - 'X-Object-Manifest': manifest} + # The ETag returned for the manifest is actually the + # MD5 hash of the concatenated checksums of the strings + # of each chunk...so we ignore this result in favour of + # the MD5 of the entire image file contents, so that + # users can verify the image file contents accordingly + manager.get_connection().put_object(location.container, + location.obj, + None, headers=headers) + obj_etag = checksum.hexdigest() - # The ETag returned for the manifest is actually the - # MD5 hash of the concatenated checksums of the strings - # of each chunk...so we ignore this result in favour of - # the MD5 of the entire image file contents, so that - # users can verify the image file contents accordingly - connection.put_object(location.container, location.obj, - None, headers=headers) - obj_etag = checksum.hexdigest() + # NOTE: We return the user and key here! Have to because + # location is used by the API server to return the actual + # image data. We *really* should consider NOT returning + # the location attribute from GET /images/ and + # GET /images/details + if sutils.is_multiple_swift_store_accounts_enabled(self.conf): + include_creds = False + else: + include_creds = True + return (location.get_uri(credentials_included=include_creds), + image_size, obj_etag, {}) + except swiftclient.ClientException as e: + if e.http_status == http_client.CONFLICT: + msg = _("Swift already has an image at this location") + raise exceptions.Duplicate(message=msg) - # NOTE: We return the user and key here! Have to because - # location is used by the API server to return the actual - # image data. We *really* should consider NOT returning - # the location attribute from GET /images/ and - # GET /images/details - if sutils.is_multiple_swift_store_accounts_enabled(self.conf): - include_creds = False - else: - include_creds = True - - return (location.get_uri(credentials_included=include_creds), - image_size, obj_etag, {}) - except swiftclient.ClientException as e: - if e.http_status == http_client.CONFLICT: - msg = _("Swift already has an image at this location") - raise exceptions.Duplicate(message=msg) - - msg = (_(u"Failed to add object to Swift.\n" - "Got error from Swift: %s.") - % encodeutils.exception_to_unicode(e)) - LOG.error(msg) - raise glance_store.BackendException(msg) + msg = (_(u"Failed to add object to Swift.\n" + "Got error from Swift: %s.") + % encodeutils.exception_to_unicode(e)) + LOG.error(msg) + raise glance_store.BackendException(msg) @capabilities.check def delete(self, location, connection=None, context=None): @@ -766,7 +778,13 @@ class BaseStore(driver.Store): :return: swiftclient connection that allows to request container and others """ - raise NotImplementedError() + # initialize a connection + return swiftclient.Connection( + preauthurl=storage_url, + preauthtoken=auth_token, + insecure=self.insecure, + ssl_compression=self.ssl_compression, + cacert=self.cacert) class SingleTenantStore(BaseStore): @@ -905,6 +923,39 @@ class SingleTenantStore(BaseStore): auth_version=self.auth_version, os_options=os_options, ssl_compression=self.ssl_compression, cacert=self.cacert) + def init_client(self, location, context=None): + """Initialize keystone client with swift service user credentials""" + # prepare swift admin credentials + if not location.user: + reason = _("Location is missing user:password information.") + LOG.info(reason) + raise exceptions.BadStoreUri(message=reason) + + auth_url = location.swift_url + if not auth_url.endswith('/'): + auth_url += '/' + + try: + tenant_name, user = location.user.split(':') + except ValueError: + reason = (_("Badly formed tenant:user '%(user)s' in " + "Swift URI") % {'user': location.user}) + LOG.info(reason) + raise exceptions.BadStoreUri(message=reason) + + # initialize a keystone plugin for swift admin with creds + password = ks_v3.Password(auth_url=auth_url, + username=user, + password=location.key, + project_name=tenant_name, + user_domain_id=self.user_domain_id, + user_domain_name=self.user_domain_name, + project_domain_id=self.project_domain_id, + project_domain_name=self.project_domain_name) + sess = ks_session.Session(auth=password) + + return ks_client.Client(session=sess) + class MultiTenantStore(BaseStore): EXAMPLE_URL = "swift:////" @@ -1000,6 +1051,73 @@ class MultiTenantStore(BaseStore): ssl_compression=self.ssl_compression, cacert=self.cacert) + def init_client(self, location, context=None): + # read client parameters from config files + ref_params = sutils.SwiftParams(self.conf).params + default_ref = self.conf.glance_store.default_swift_reference + default_swift_reference = ref_params.get(default_ref) + if not default_swift_reference: + reason = _("default_swift_reference %s is required.") % default_ref + LOG.error(reason) + raise exceptions.BadStoreConfiguration(message=reason) + + auth_address = default_swift_reference.get('auth_address') + user = default_swift_reference.get('user') + key = default_swift_reference.get('key') + user_domain_id = default_swift_reference.get('user_domain_id') + user_domain_name = default_swift_reference.get('user_domain_name') + project_domain_id = default_swift_reference.get('project_domain_id') + project_domain_name = default_swift_reference.get( + 'project_domain_name') + + # create client for multitenant user(trustor) + trustor_auth = ks_v3.Token(auth_url=auth_address, + token=context.auth_token, + project_id=context.tenant) + trustor_sess = ks_session.Session(auth=trustor_auth) + trustor_client = ks_client.Client(session=trustor_sess) + auth_ref = trustor_client.session.auth.get_auth_ref(trustor_sess) + roles = [t['name'] for t in auth_ref['roles']] + + # create client for trustee - glance user specified in swift config + tenant_name, user = user.split(':') + password = ks_v3.Password(auth_url=auth_address, + username=user, + password=key, + project_name=tenant_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + trustee_sess = ks_session.Session(auth=password) + trustee_client = ks_client.Client(session=trustee_sess) + + # request glance user id - we will use it as trustee user + trustee_user_id = trustee_client.session.get_user_id() + + # create trust for trustee user + trust_id = trustor_client.trusts.create( + trustee_user=trustee_user_id, trustor_user=context.user, + project=context.tenant, impersonation=True, + role_names=roles + ).id + # initialize a new client with trust and trustee credentials + # create client for glance trustee user + client_password = ks_v3.Password( + auth_url=auth_address, + username=user, + password=key, + trust_id=trust_id, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name + ) + # now we can authenticate against KS + # as trustee of user who provided token + client_sess = ks_session.Session(auth=client_password) + return ks_client.Client(session=client_sess) + class ChunkReader(object): def __init__(self, fd, checksum, total, verifier=None): diff --git a/glance_store/tests/unit/test_swift_store.py b/glance_store/tests/unit/test_swift_store.py index 40d1a5a6..cf1e3d3a 100644 --- a/glance_store/tests/unit/test_swift_store.py +++ b/glance_store/tests/unit/test_swift_store.py @@ -22,6 +22,7 @@ import mock import tempfile import uuid +from keystoneclient import exceptions as ks_exceptions from oslo_config import cfg from oslo_utils import encodeutils from oslo_utils import units @@ -35,6 +36,7 @@ from six.moves import range import swiftclient from glance_store._drivers.swift import store as swift +from glance_store._drivers.swift import utils as sutils from glance_store import backend from glance_store import BackendException from glance_store import capabilities @@ -233,6 +235,12 @@ def stub_out_swiftclient(stubs, swift_store_auth_version): class SwiftTests(object): + def mock_keystone_client(self): + # mock keystone client functions to avoid dependency errors + swift.ks_v3 = mock.MagicMock() + swift.ks_session = mock.MagicMock() + swift.ks_client = mock.MagicMock() + @property def swift_store_user(self): return 'tenant:user1' @@ -285,10 +293,13 @@ class SwiftTests(object): resp_full = b''.join([chunk for chunk in image_swift.wrapped]) resp_half = resp_full[:len(resp_full) // 2] resp_half = six.BytesIO(resp_half) + manager = swift.get_manager_for_store(self.store, loc.store_location, + ctxt) + image_swift.wrapped = swift.swift_retry_iter(resp_half, image_size, self.store, loc.store_location, - ctxt) + manager) self.assertEqual(image_size, 5120) expected_data = b"*" * FIVE_KB @@ -337,6 +348,7 @@ class SwiftTests(object): def test_add(self): """Test that we can add an image via the swift backend.""" moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() expected_swift_size = FIVE_KB @@ -374,6 +386,7 @@ class SwiftTests(object): conf['default_swift_reference'] = 'store_2' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -424,6 +437,7 @@ class SwiftTests(object): conf['default_swift_reference'] = variation self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(image_id, image_swift, @@ -454,6 +468,7 @@ class SwiftTests(object): conf['swift_store_container'] = 'noexist' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -500,6 +515,7 @@ class SwiftTests(object): conf['swift_store_container'] = 'noexist' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(expected_image_id, @@ -545,6 +561,8 @@ class SwiftTests(object): conf['swift_store_multiple_containers_seed'] = 2 self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() + self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(expected_image_id, @@ -579,6 +597,7 @@ class SwiftTests(object): conf['swift_store_multiple_containers_seed'] = 2 self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() expected_image_id = str(uuid.uuid4()) expected_container = 'randomname_' + expected_image_id[:2] @@ -895,6 +914,7 @@ class SwiftTests(object): conf = copy.deepcopy(SWIFT_CONF) self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -934,6 +954,7 @@ class SwiftTests(object): conf = copy.deepcopy(SWIFT_CONF) self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -960,7 +981,7 @@ class SwiftTests(object): loc = location.get_location_from_uri(uri, conf=self.conf) self.store.delete(loc) - self.assertRaises(exceptions.NotFound, self.store.get, loc) + self.assertRaises(ks_exceptions.NotFound, self.store.get, loc) def test_delete_non_existing(self): """ @@ -1109,6 +1130,81 @@ class SwiftTests(object): self.assertRaises(NotImplementedError, swift.get_manager_for_store, store, loc) + @mock.patch("glance_store._drivers.swift.store.ks_v3") + @mock.patch("glance_store._drivers.swift.store.ks_session") + @mock.patch("glance_store._drivers.swift.store.ks_client") + def test_init_client_multi_tenant(self, + mock_client, mock_session, mock_v3): + """Test that keystone client was initialized correctly""" + # initialize store and connection parameters + self.config(swift_store_multi_tenant=True) + store = Store(self.conf) + store.configure() + ref_params = sutils.SwiftParams(self.conf).params + default_ref = self.conf.glance_store.default_swift_reference + default_swift_reference = ref_params.get(default_ref) + # prepare client and session + trustee_session = mock.MagicMock() + trustor_session = mock.MagicMock() + main_session = mock.MagicMock() + trustee_client = mock.MagicMock() + trustee_client.session.get_user_id.return_value = 'fake_user' + trustor_client = mock.MagicMock() + trustor_client.session.auth.get_auth_ref.return_value = { + 'roles': [{'name': 'fake_role'}] + } + trustor_client.trusts.create.return_value = mock.MagicMock( + id='fake_trust') + main_client = mock.MagicMock() + mock_session.Session.side_effect = [trustor_session, trustee_session, + main_session] + mock_client.Client.side_effect = [trustor_client, trustee_client, + main_client] + # initialize client + ctxt = mock.MagicMock() + client = store.init_client(location=mock.MagicMock(), context=ctxt) + # test trustor usage + mock_v3.Token.assert_called_once_with( + auth_url=default_swift_reference.get('auth_address'), + token=ctxt.auth_token, + project_id=ctxt.tenant + ) + mock_session.Session.assert_any_call(auth=mock_v3.Token()) + mock_client.Client.assert_any_call(session=trustor_session) + # test trustee usage and trust creation + tenant_name, user = default_swift_reference.get('user').split(':') + mock_v3.Password.assert_any_call( + auth_url=default_swift_reference.get('auth_address'), + username=user, + password=default_swift_reference.get('key'), + project_name=tenant_name, + user_domain_id=default_swift_reference.get('user_domain_id'), + user_domain_name=default_swift_reference.get('user_domain_name'), + project_domain_id=default_swift_reference.get('project_domain_id'), + project_domain_name=default_swift_reference.get( + 'project_domain_name') + ) + mock_session.Session.assert_any_call(auth=mock_v3.Password()) + mock_client.Client.assert_any_call(session=trustee_session) + trustor_client.trusts.create.assert_called_once_with( + trustee_user='fake_user', trustor_user=ctxt.user, + project=ctxt.tenant, impersonation=True, + role_names=['fake_role'] + ) + mock_v3.Password.assert_any_call( + auth_url=default_swift_reference.get('auth_address'), + username=user, + password=default_swift_reference.get('key'), + trust_id='fake_trust', + user_domain_id=default_swift_reference.get('user_domain_id'), + user_domain_name=default_swift_reference.get('user_domain_name'), + project_domain_id=default_swift_reference.get('project_domain_id'), + project_domain_name=default_swift_reference.get( + 'project_domain_name') + ) + mock_client.Client.assert_any_call(session=main_session) + self.assertEqual(main_client, client) + class TestStoreAuthV1(base.StoreBaseTest, SwiftTests, test_store_capabilities.TestStoreCapabilitiesChecking): @@ -1133,6 +1229,7 @@ class TestStoreAuthV1(base.StoreBaseTest, SwiftTests, moxfixture = self.useFixture(moxstubout.MoxStubout()) self.stubs = moxfixture.stubs stub_out_swiftclient(self.stubs, conf['swift_store_auth_version']) + self.mock_keystone_client() self.store = Store(self.conf) self.config(**conf) self.store.configure() @@ -1171,6 +1268,33 @@ class TestStoreAuthV3(TestStoreAuthV1): conf['swift_store_user'] = 'tenant:user1' return conf + @mock.patch("glance_store._drivers.swift.store.ks_v3") + @mock.patch("glance_store._drivers.swift.store.ks_session") + @mock.patch("glance_store._drivers.swift.store.ks_client") + def test_init_client_single_tenant(self, + mock_client, mock_session, mock_v3): + """Test that keystone client was initialized correctly""" + # initialize client + store = Store(self.conf) + store.configure() + uri = "swift://%s:key@auth_address/glance/%s" % ( + self.swift_store_user, FAKE_UUID) + loc = location.get_location_from_uri(uri, conf=self.conf) + ctxt = mock.MagicMock() + store.init_client(location=loc.store_location, context=ctxt) + # check that keystone was initialized correctly + tenant = None if store.auth_version == '1' else "tenant" + username = "tenant:user1" if store.auth_version == '1' else "user1" + mock_v3.Password.assert_called_once_with( + auth_url=loc.store_location.swift_url + '/', + username=username, password="key", + project_name=tenant, + project_domain_id=None, project_domain_name=None, + user_domain_id=None, user_domain_name=None,) + mock_session.Session.assert_called_once_with(auth=mock_v3.Password()) + mock_client.Client.assert_called_once_with( + session=mock_session.Session()) + class FakeConnection(object): def __init__(self, authurl=None, user=None, key=None, retries=5, diff --git a/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml b/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml new file mode 100644 index 00000000..7a1bae88 --- /dev/null +++ b/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml @@ -0,0 +1,12 @@ +--- +prelude: > + Prevent Unauthorized errors during uploading or + donwloading data to Swift store. +features: + - Allow glance_store to refresh token when upload or download data to Swift + store. glance_store identifies if token is going to expire soon when + executing request to Swift and refresh the token. For multi-tenant swift + store glance_store uses trusts, for single-tenant swift store glance_store + uses credentials from swift store configurations. Please also note that + this feature is enabled if and only if Keystone V3 API is available + and enabled. \ No newline at end of file