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,8 +468,13 @@ 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):
@ -477,10 +484,10 @@ class BaseStore(driver.Store):
return ''
length = int(resp_headers.get('content-length', 0))
if self.conf.glance_store.swift_store_retry_get_count > 0:
if allow_retry:
resp_body = swift_retry_iter(resp_body, length,
self, location, context)
return (ResponseIndexable(resp_body, length), 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,31 +523,34 @@ 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:
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 = connection.put_object(location.container,
location.obj,
reader,
content_length=image_size)
obj_etag = manager.get_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)
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
@ -578,16 +588,18 @@ class BaseStore(driver.Store):
LOG.debug('Not writing zero-length chunk.')
break
try:
chunk_etag = connection.put_object(
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(connection,
LOG.exception(_("Error during chunked upload "
"to backend, deleting stale "
"chunks"))
self._delete_stale_chunks(
manager.get_connection(),
location.container,
written_chunks)
@ -622,7 +634,8 @@ class BaseStore(driver.Store):
# 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,
manager.get_connection().put_object(location.container,
location.obj,
None, headers=headers)
obj_etag = checksum.hexdigest()
@ -635,7 +648,6 @@ class BaseStore(driver.Store):
include_creds = False
else:
include_creds = True
return (location.get_uri(credentials_included=include_creds),
image_size, obj_etag, {})
except swiftclient.ClientException as e:
@ -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.