Merge "Implement re-authentication for swift driver"
This commit is contained in:
commit
6c4ae678f5
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue