Implement swift store connection manager
The patch defines implementation of swift connection manager for swift driver. It allows to receive swift connections and update them if user token is going to expire soon. Connection manager for single tenant store uses swift service user credentials to receive new token. Connection manager for multi-tenant store uses trusts to receive new token and initialize a connection. Please note that this is first part of bp implementation that defines framework and helpers for re-authentication. Implementation of keystoneclient initialization and enabling of re-authentication for swift store available in next patch. Implements bp prevention-of-401-in-swift-driver Change-Id: I61b0fcfe284bdfbf4c0558178318c69617ec6127
This commit is contained in:
parent
48927adfdd
commit
142cf34c2f
|
@ -0,0 +1,199 @@
|
|||
# Copyright 2010-2015 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Connection Manager for Swift connections that responsible for providing
|
||||
connection with valid credentials and updated token"""
|
||||
|
||||
import logging
|
||||
|
||||
from keystoneclient import exceptions as ks_exceptions
|
||||
|
||||
from glance_store import exceptions
|
||||
from glance_store.i18n import _
|
||||
from glance_store.i18n import _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SwiftConnectionManager(object):
|
||||
"""Connection Manager class responsible for initializing and managing
|
||||
swiftclient connections in store. The instance of that class can provide
|
||||
swift connections with a valid(and refreshed) user token if the token is
|
||||
going to expire soon.
|
||||
"""
|
||||
|
||||
AUTH_HEADER_NAME = 'X-Auth-Token'
|
||||
|
||||
def __init__(self, store, store_location, context=None,
|
||||
allow_reauth=False):
|
||||
"""Initialize manager with parameters required to establish connection.
|
||||
|
||||
Initialize store and prepare it for interacting with swift. Also
|
||||
initialize keystone client that need to be used for authentication if
|
||||
allow_reauth is True.
|
||||
The method invariant is the following: if method was executed
|
||||
successfully and self.allow_reauth is True users can safely request
|
||||
valid(no expiration) swift connections any time. Otherwise, connection
|
||||
manager initialize a connection once and always returns that connection
|
||||
to users.
|
||||
|
||||
:param store: store that provides connections
|
||||
:param store_location: image location in store
|
||||
:param context: user context to access data in Swift
|
||||
:param allow_reauth: defines if re-authentication need to be executed
|
||||
when a user request the connection
|
||||
"""
|
||||
self._client = None
|
||||
self.store = store
|
||||
self.location = store_location
|
||||
self.context = context
|
||||
self.allow_reauth = allow_reauth
|
||||
self.storage_url = self._get_storage_url()
|
||||
self.connection = self._init_connection()
|
||||
|
||||
def get_connection(self):
|
||||
"""Get swift client connection.
|
||||
|
||||
Returns swift client connection. If allow_reauth is True and
|
||||
connection token is going to expire soon then the method returns
|
||||
updated connection.
|
||||
The method invariant is the following: if self.allow_reauth is False
|
||||
then the method returns the same connection for every call. So the
|
||||
connection may expire. If self.allow_reauth is True the returned
|
||||
swift connection is always valid and cannot expire at least for
|
||||
swift_store_expire_soon_interval.
|
||||
"""
|
||||
if self.allow_reauth:
|
||||
# we are refreshing token only and if only connection manager
|
||||
# re-authentication is allowed. Token refreshing is setup by
|
||||
# connection manager users. Also we disable re-authentication
|
||||
# if there is not way to execute it (cannot initialize trusts for
|
||||
# multi-tenant or auth_version is not 3)
|
||||
auth_ref = self.client.session.auth.get_auth_ref(
|
||||
self.client.session)
|
||||
# if connection token is going to expire soon (keystone checks
|
||||
# is token is going to expire or expired already)
|
||||
if auth_ref.will_expire_soon(
|
||||
self.store.conf.glance_store.swift_store_expire_soon_interval
|
||||
):
|
||||
LOG.info(_LI("Requesting new token for swift connection."))
|
||||
# request new token with session and client provided by store
|
||||
auth_token = self.client.session.get_auth_headers().get(
|
||||
self.AUTH_HEADER_NAME)
|
||||
LOG.info(_LI("Token has been successfully requested. "
|
||||
"Refreshing swift connection."))
|
||||
# initialize new switclient connection with fresh token
|
||||
self.connection = self.store.get_store_connection(
|
||||
auth_token, self.storage_url)
|
||||
return self.connection
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Return keystone client to request a new token.
|
||||
|
||||
Initialize a client lazily from the method provided by glance_store.
|
||||
The method invariant is the following: if client cannot be
|
||||
initialized raise exception otherwise return initialized client that
|
||||
can be used for re-authentication any time.
|
||||
"""
|
||||
if self._client is None:
|
||||
self._client = self._init_client()
|
||||
return self._client
|
||||
|
||||
def _init_connection(self):
|
||||
"""Initialize and return valid Swift connection."""
|
||||
auth_token = self.client.session.get_auth_headers().get(
|
||||
self.AUTH_HEADER_NAME)
|
||||
return self.store.get_store_connection(
|
||||
auth_token, self.storage_url)
|
||||
|
||||
def _init_client(self):
|
||||
"""Initialize Keystone client."""
|
||||
return self.store.init_client(location=self.location,
|
||||
context=self.context)
|
||||
|
||||
def _get_storage_url(self):
|
||||
"""Request swift storage url."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
|
||||
class SingleTenantConnectionManager(SwiftConnectionManager):
|
||||
def _get_storage_url(self):
|
||||
return self.client.session.get_endpoint(
|
||||
service_type=self.store.service_type,
|
||||
interface=self.store.endpoint_type,
|
||||
region_name=self.store.region
|
||||
)
|
||||
|
||||
def _init_connection(self):
|
||||
if self.store.auth_version == '3':
|
||||
return super(SingleTenantConnectionManager,
|
||||
self)._init_connection()
|
||||
else:
|
||||
# no re-authentication for v1 and v2
|
||||
self.allow_reauth = False
|
||||
# use good old connection initialization
|
||||
return self.store.get_connection(self.location, self.context)
|
||||
|
||||
|
||||
class MultiTenantConnectionManager(SwiftConnectionManager):
|
||||
|
||||
def __init__(self, store, store_location, context=None,
|
||||
allow_reauth=False):
|
||||
# no context - no party
|
||||
if context is None:
|
||||
reason = _("Multi-tenant Swift storage requires a user context.")
|
||||
raise exceptions.BadStoreConfiguration(store_name="swift",
|
||||
reason=reason)
|
||||
super(MultiTenantConnectionManager, self).__init__(
|
||||
store, store_location, context, allow_reauth)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._client and self.client.trust_id:
|
||||
# client has been initialized - need to cleanup resources
|
||||
LOG.info(_LI("Revoking trust %s"), self.client.trust_id)
|
||||
self.client.trusts.delete(self.client.trust_id)
|
||||
|
||||
def _get_storage_url(self):
|
||||
try:
|
||||
return self.store._get_endpoint(self.context)
|
||||
except (exceptions.BadStoreConfiguration,
|
||||
ks_exceptions.EndpointNotFound) as e:
|
||||
LOG.debug("Cannot obtain endpoint from context: %s. Use location "
|
||||
"value from database to obtain swift_url.", e)
|
||||
return self.location.swift_url
|
||||
|
||||
def _init_connection(self):
|
||||
if self.allow_reauth:
|
||||
try:
|
||||
return super(MultiTenantConnectionManager,
|
||||
self)._init_connection()
|
||||
except Exception as e:
|
||||
LOG.debug("Cannot initialize swift connection for multi-tenant"
|
||||
" store with trustee token: %s. Using user token for"
|
||||
" connection initialization.", e)
|
||||
# for multi-tenant store we have a token, so we can use it
|
||||
# for connection initialization but we cannot fetch new token
|
||||
# with client
|
||||
self.allow_reauth = False
|
||||
|
||||
return self.store.get_store_connection(
|
||||
self.context.auth_token, self.storage_url)
|
|
@ -33,6 +33,7 @@ except ImportError:
|
|||
swiftclient = None
|
||||
|
||||
import glance_store
|
||||
from glance_store._drivers.swift import connection_manager
|
||||
from glance_store._drivers.swift import utils as sutils
|
||||
from glance_store import capabilities
|
||||
from glance_store import driver
|
||||
|
@ -119,7 +120,22 @@ _SWIFT_OPTS = [
|
|||
'compressed format, eg qcow2.')),
|
||||
cfg.IntOpt('swift_store_retry_get_count', default=0,
|
||||
help=_('The number of times a Swift download will be retried '
|
||||
'before the request fails.'))
|
||||
'before the request fails.')),
|
||||
cfg.IntOpt('swift_store_expire_soon_interval', default=60,
|
||||
help=_('The period of time (in seconds) before token expiration'
|
||||
'when glance_store will try to reques new user token. '
|
||||
'Default value 60 sec means that if token is going to '
|
||||
'expire in 1 min then glance_store request new user '
|
||||
'token.')),
|
||||
cfg.BoolOpt('swift_store_use_trusts', default=True,
|
||||
help=_('If set to True create a trust for each add/get '
|
||||
'request to Multi-tenant store in order to prevent '
|
||||
'authentication token to be expired during '
|
||||
'uploading/downloading data. If set to False then user '
|
||||
'token is used for Swift connection (so no overhead on '
|
||||
'trust creation). Please note that this '
|
||||
'option is considered only and only if '
|
||||
'swift_store_multi_tenant=True'))
|
||||
]
|
||||
|
||||
|
||||
|
@ -728,6 +744,29 @@ class BaseStore(driver.Store):
|
|||
def create_location(self, image_id, context=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def init_client(self, location, context=None):
|
||||
"""Initialize and return client to authorize against keystone
|
||||
|
||||
The method invariant is the following: it always returns Keystone
|
||||
client that can be used to receive fresh token in any time. Otherwise
|
||||
it raises appropriate exception.
|
||||
:param location: swift location data
|
||||
:param context: user context (it is not required if user grants are
|
||||
specified for single tenant store)
|
||||
:return correctly initialized keystone client
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_store_connection(self, auth_token, storage_url):
|
||||
"""Get initialized swift connection
|
||||
|
||||
:param auth_token: auth token
|
||||
:param storage_url: swift storage url
|
||||
:return: swiftclient connection that allows to request container and
|
||||
others
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SingleTenantStore(BaseStore):
|
||||
EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
|
||||
|
@ -983,3 +1022,34 @@ class ChunkReader(object):
|
|||
if self.verifier:
|
||||
self.verifier.update(result)
|
||||
return result
|
||||
|
||||
|
||||
def get_manager_for_store(store, store_location,
|
||||
context=None,
|
||||
allow_reauth=False):
|
||||
"""Return appropriate connection manager for store
|
||||
|
||||
The method detects store type (singletenant or multitenant) and returns
|
||||
appropriate connection manager (singletenant or multitenant) that allows
|
||||
to request swiftclient connections.
|
||||
:param store: store that needs swift connections
|
||||
:param store_location: StoreLocation object that define image location
|
||||
:param context: user context
|
||||
:param allow_reauth: defines if we allow re-authentication when user token
|
||||
is expired and refresh swift connection
|
||||
:return: connection manager for store
|
||||
"""
|
||||
if store.__class__ == SingleTenantStore:
|
||||
return connection_manager.SingleTenantConnectionManager(
|
||||
store, store_location, context, allow_reauth)
|
||||
elif store.__class__ == MultiTenantStore:
|
||||
# if global toggle is turned off then do not allow re-authentication
|
||||
# with trusts
|
||||
if not store.conf.glance_store.swift_store_use_trusts:
|
||||
allow_reauth = False
|
||||
return connection_manager.MultiTenantConnectionManager(
|
||||
store, store_location, context, allow_reauth)
|
||||
else:
|
||||
raise NotImplementedError(_("There is no Connection Manager "
|
||||
"implemented for %s class.") %
|
||||
store.__class__.__name__)
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright 2014 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from glance_store._drivers.swift import connection_manager
|
||||
from glance_store._drivers.swift import store as swift_store
|
||||
from glance_store import exceptions
|
||||
from glance_store.tests import base
|
||||
|
||||
|
||||
class TestConnectionManager(base.StoreBaseTest):
|
||||
def setUp(self):
|
||||
super(TestConnectionManager, self).setUp()
|
||||
self.client = mock.MagicMock()
|
||||
self.client.session.get_auth_headers.return_value = {
|
||||
connection_manager.SwiftConnectionManager.AUTH_HEADER_NAME:
|
||||
"fake_token"}
|
||||
|
||||
self.location = mock.create_autospec(swift_store.StoreLocation)
|
||||
self.context = mock.MagicMock()
|
||||
self.conf = mock.MagicMock()
|
||||
|
||||
def prepare_store(self, multi_tenant=False):
|
||||
if multi_tenant:
|
||||
store = mock.create_autospec(swift_store.MultiTenantStore,
|
||||
conf=self.conf)
|
||||
else:
|
||||
store = mock.create_autospec(swift_store.SingleTenantStore,
|
||||
service_type="swift",
|
||||
endpoint_type="internal",
|
||||
region=None,
|
||||
conf=self.conf,
|
||||
auth_version='3')
|
||||
|
||||
store.init_client.return_value = self.client
|
||||
return store
|
||||
|
||||
def test_basic_single_tenant_cm_init(self):
|
||||
store = self.prepare_store()
|
||||
manager = connection_manager.SingleTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location, None)
|
||||
self.client.session.get_endpoint.assert_called_once_with(
|
||||
service_type=store.service_type,
|
||||
interface=store.endpoint_type,
|
||||
region_name=store.region
|
||||
)
|
||||
store.get_store_connection.assert_called_once_with(
|
||||
"fake_token", manager.storage_url
|
||||
)
|
||||
|
||||
def test_basic_multi_tenant_cm_init(self):
|
||||
store = self.prepare_store(multi_tenant=True)
|
||||
manager = connection_manager.MultiTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
context=self.context
|
||||
)
|
||||
store._get_endpoint.assert_called_once_with(self.context)
|
||||
store.get_store_connection.assert_called_once_with(
|
||||
self.context.auth_token, manager.storage_url)
|
||||
|
||||
def test_basis_multi_tenant_no_context(self):
|
||||
store = self.prepare_store(multi_tenant=True)
|
||||
self.assertRaises(exceptions.BadStoreConfiguration,
|
||||
connection_manager.MultiTenantConnectionManager,
|
||||
store=store, store_location=self.location)
|
||||
|
||||
def test_multi_tenant_client_cm_with_client_creation_fails(self):
|
||||
store = self.prepare_store(multi_tenant=True)
|
||||
store.init_client.side_effect = [Exception]
|
||||
manager = connection_manager.MultiTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
context=self.context,
|
||||
allow_reauth=True
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location,
|
||||
self.context)
|
||||
store._get_endpoint.assert_called_once_with(self.context)
|
||||
store.get_store_connection.assert_called_once_with(
|
||||
self.context.auth_token, manager.storage_url)
|
||||
self.assertFalse(manager.allow_reauth)
|
||||
|
||||
def test_multi_tenant_client_cm_with_no_expiration(self):
|
||||
store = self.prepare_store(multi_tenant=True)
|
||||
manager = connection_manager.MultiTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
context=self.context,
|
||||
allow_reauth=True
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location,
|
||||
self.context)
|
||||
store._get_endpoint.assert_called_once_with(self.context)
|
||||
# return the same connection because it should not be expired
|
||||
auth_ref = mock.MagicMock()
|
||||
self.client.session.auth.get_auth_ref.return_value = auth_ref
|
||||
auth_ref.will_expire_soon.return_value = False
|
||||
manager.get_connection()
|
||||
# check that we don't update connection
|
||||
store.get_store_connection.assert_called_once_with("fake_token",
|
||||
manager.storage_url)
|
||||
self.client.session.get_auth_headers.assert_called_once_with()
|
||||
|
||||
def test_multi_tenant_client_cm_with_expiration(self):
|
||||
store = self.prepare_store(multi_tenant=True)
|
||||
manager = connection_manager.MultiTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
context=self.context,
|
||||
allow_reauth=True
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location,
|
||||
self.context)
|
||||
store._get_endpoint.assert_called_once_with(self.context)
|
||||
# return the same connection because it should not be expired
|
||||
auth_ref = mock.MagicMock()
|
||||
self.client.session.auth.get_auth_ref.return_value = auth_ref
|
||||
auth_ref.will_expire_soon.return_value = True
|
||||
manager.get_connection()
|
||||
# check that we don't update connection
|
||||
self.assertEqual(2, store.get_store_connection.call_count)
|
||||
self.assertEqual(2, self.client.session.get_auth_headers.call_count)
|
||||
|
||||
def test_single_tenant_client_cm_with_no_expiration(self):
|
||||
store = self.prepare_store()
|
||||
manager = connection_manager.SingleTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
allow_reauth=True
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location, None)
|
||||
self.client.session.get_endpoint.assert_called_once_with(
|
||||
service_type=store.service_type,
|
||||
interface=store.endpoint_type,
|
||||
region_name=store.region
|
||||
)
|
||||
# return the same connection because it should not be expired
|
||||
auth_ref = mock.MagicMock()
|
||||
self.client.session.auth.get_auth_ref.return_value = auth_ref
|
||||
auth_ref.will_expire_soon.return_value = False
|
||||
manager.get_connection()
|
||||
# check that we don't update connection
|
||||
store.get_store_connection.assert_called_once_with("fake_token",
|
||||
manager.storage_url)
|
||||
self.client.session.get_auth_headers.assert_called_once_with()
|
||||
|
||||
def test_single_tenant_client_cm_with_expiration(self):
|
||||
store = self.prepare_store()
|
||||
manager = connection_manager.SingleTenantConnectionManager(
|
||||
store=store,
|
||||
store_location=self.location,
|
||||
allow_reauth=True
|
||||
)
|
||||
store.init_client.assert_called_once_with(self.location, None)
|
||||
self.client.session.get_endpoint.assert_called_once_with(
|
||||
service_type=store.service_type,
|
||||
interface=store.endpoint_type,
|
||||
region_name=store.region
|
||||
)
|
||||
# return the same connection because it should not be expired
|
||||
auth_ref = mock.MagicMock()
|
||||
self.client.session.auth.get_auth_ref.return_value = auth_ref
|
||||
auth_ref.will_expire_soon.return_value = True
|
||||
manager.get_connection()
|
||||
# check that we don't update connection
|
||||
self.assertEqual(2, store.get_store_connection.call_count)
|
||||
self.assertEqual(2, self.client.session.get_auth_headers.call_count)
|
|
@ -86,6 +86,7 @@ class OptsTestCase(base.StoreBaseTest):
|
|||
's3_store_large_object_chunk_size',
|
||||
's3_store_thread_pools',
|
||||
's3_store_enable_proxy',
|
||||
'swift_store_expire_soon_interval',
|
||||
's3_store_proxy_host',
|
||||
's3_store_proxy_port',
|
||||
's3_store_proxy_user',
|
||||
|
@ -112,6 +113,7 @@ class OptsTestCase(base.StoreBaseTest):
|
|||
'swift_store_retry_get_count',
|
||||
'swift_store_service_type',
|
||||
'swift_store_ssl_compression',
|
||||
'swift_store_use_trusts',
|
||||
'swift_store_user',
|
||||
'vmware_api_insecure',
|
||||
'vmware_api_retry_count',
|
||||
|
|
|
@ -1078,6 +1078,37 @@ class SwiftTests(object):
|
|||
self.assertEqual(container_headers['X-Container-Write'],
|
||||
'frank:*,jim:*')
|
||||
|
||||
@mock.patch("glance_store._drivers.swift."
|
||||
"connection_manager.MultiTenantConnectionManager")
|
||||
def test_get_connection_manager_multi_tenant(self, manager_class):
|
||||
manager = mock.MagicMock()
|
||||
manager_class.return_value = manager
|
||||
self.config(swift_store_multi_tenant=True)
|
||||
store = Store(self.conf)
|
||||
store.configure()
|
||||
loc = mock.MagicMock()
|
||||
swift.get_manager_for_store(store, loc)
|
||||
self.assertEqual(swift.get_manager_for_store(store, loc),
|
||||
manager)
|
||||
|
||||
@mock.patch("glance_store._drivers.swift."
|
||||
"connection_manager.SingleTenantConnectionManager")
|
||||
def test_get_connection_manager_single_tenant(self, manager_class):
|
||||
manager = mock.MagicMock()
|
||||
manager_class.return_value = manager
|
||||
store = Store(self.conf)
|
||||
store.configure()
|
||||
loc = mock.MagicMock()
|
||||
swift.get_manager_for_store(store, loc)
|
||||
self.assertEqual(swift.get_manager_for_store(store, loc),
|
||||
manager)
|
||||
|
||||
def test_get_connection_manager_failed(self):
|
||||
store = mock.MagicMock()
|
||||
loc = mock.MagicMock()
|
||||
self.assertRaises(NotImplementedError, swift.get_manager_for_store,
|
||||
store, loc)
|
||||
|
||||
|
||||
class TestStoreAuthV1(base.StoreBaseTest, SwiftTests,
|
||||
test_store_capabilities.TestStoreCapabilitiesChecking):
|
||||
|
|
Loading…
Reference in New Issue