diff --git a/glance_store/_drivers/swift/connection_manager.py b/glance_store/_drivers/swift/connection_manager.py new file mode 100644 index 00000000..6d57e309 --- /dev/null +++ b/glance_store/_drivers/swift/connection_manager.py @@ -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) diff --git a/glance_store/_drivers/swift/store.py b/glance_store/_drivers/swift/store.py index 99fb7a66..536d34c0 100644 --- a/glance_store/_drivers/swift/store.py +++ b/glance_store/_drivers/swift/store.py @@ -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://:@//" @@ -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__) diff --git a/glance_store/tests/unit/test_connection_manager.py b/glance_store/tests/unit/test_connection_manager.py new file mode 100644 index 00000000..52fc218d --- /dev/null +++ b/glance_store/tests/unit/test_connection_manager.py @@ -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) diff --git a/glance_store/tests/unit/test_opts.py b/glance_store/tests/unit/test_opts.py index 15bd045d..658fd07b 100644 --- a/glance_store/tests/unit/test_opts.py +++ b/glance_store/tests/unit/test_opts.py @@ -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', diff --git a/glance_store/tests/unit/test_swift_store.py b/glance_store/tests/unit/test_swift_store.py index ddfccfad..4b0d0d1a 100644 --- a/glance_store/tests/unit/test_swift_store.py +++ b/glance_store/tests/unit/test_swift_store.py @@ -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):