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:
kairat_kushaev 2015-12-01 13:40:08 +03:00
parent 48927adfdd
commit 142cf34c2f
5 changed files with 487 additions and 1 deletions

View File

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

View File

@ -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__)

View File

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

View File

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

View File

@ -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):