Merge "Implement re-authentication for swift driver"

This commit is contained in:
Jenkins 2016-03-01 09:35:09 +00:00 committed by Gerrit Code Review
commit 6c4ae678f5
3 changed files with 398 additions and 144 deletions

View File

@ -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/<ID> 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/<ID> 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://<SWIFT_URL>/<CONTAINER>/<FILE>"
@ -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):

View File

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

View File

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