Merge "backup/swift: Add support sending service user token"
This commit is contained in:
@@ -57,6 +57,8 @@ from cinder.backup import chunkeddriver
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import interface
|
||||
from cinder import service_auth
|
||||
from cinder.utils import retry
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -141,6 +143,11 @@ swiftbackup_service_opts = [
|
||||
default=False,
|
||||
help='Bypass verification of server certificate when '
|
||||
'making SSL connection to Swift.'),
|
||||
cfg.BoolOpt('backup_swift_service_auth',
|
||||
default=False,
|
||||
help='Send a X-Service-Token header with service auth '
|
||||
'credentials. If enabled you also must set the '
|
||||
'service_user group and enable send_service_user_token.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -174,6 +181,21 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
def get_driver_options():
|
||||
return swiftbackup_service_opts
|
||||
|
||||
@retry(Exception, retries=CONF.backup_swift_retry_attempts,
|
||||
backoff_rate=CONF.backup_swift_retry_backoff)
|
||||
def _headers(self, headers=None):
|
||||
"""Add service token to headers if its enabled"""
|
||||
if not CONF.backup_swift_service_auth:
|
||||
return headers
|
||||
|
||||
result = headers or {}
|
||||
|
||||
sa_plugin = service_auth.get_service_auth_plugin()
|
||||
if sa_plugin is not None:
|
||||
result['X-Service-Token'] = sa_plugin.get_token()
|
||||
|
||||
return result
|
||||
|
||||
def initialize(self):
|
||||
self.swift_attempts = CONF.backup_swift_retry_attempts
|
||||
self.swift_backoff = CONF.backup_swift_retry_backoff
|
||||
@@ -274,11 +296,12 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
cacert=CONF.backup_swift_ca_cert_file)
|
||||
|
||||
class SwiftObjectWriter(object):
|
||||
def __init__(self, container, object_name, conn):
|
||||
def __init__(self, container, object_name, conn, headers_func=None):
|
||||
self.container = container
|
||||
self.object_name = object_name
|
||||
self.conn = conn
|
||||
self.data = bytearray()
|
||||
self.headers_func = headers_func
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -292,9 +315,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
def close(self):
|
||||
reader = io.BytesIO(self.data)
|
||||
try:
|
||||
headers = self.headers_func() if self.headers_func else None
|
||||
etag = self.conn.put_object(self.container, self.object_name,
|
||||
reader,
|
||||
content_length=len(self.data))
|
||||
content_length=len(self.data),
|
||||
headers=headers)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=err)
|
||||
md5 = secretutils.md5(self.data, usedforsecurity=False).hexdigest()
|
||||
@@ -306,10 +331,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
return md5
|
||||
|
||||
class SwiftObjectReader(object):
|
||||
def __init__(self, container, object_name, conn):
|
||||
def __init__(self, container, object_name, conn, headers_func=None):
|
||||
self.container = container
|
||||
self.object_name = object_name
|
||||
self.conn = conn
|
||||
self.headers_func = headers_func
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -319,8 +345,10 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
headers = self.headers_func() if self.headers_func else None
|
||||
(_resp, body) = self.conn.get_object(self.container,
|
||||
self.object_name)
|
||||
self.object_name,
|
||||
headers=headers)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=err)
|
||||
return body
|
||||
@@ -335,14 +363,15 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
existing container.
|
||||
"""
|
||||
try:
|
||||
self.conn.head_container(container)
|
||||
self.conn.head_container(container, headers=self._headers())
|
||||
except swift_exc.ClientException as e:
|
||||
if e.http_status == 404:
|
||||
try:
|
||||
storage_policy = CONF.backup_swift_create_storage_policy
|
||||
headers = ({'X-Storage-Policy': storage_policy}
|
||||
if storage_policy else None)
|
||||
self.conn.put_container(container, headers=headers)
|
||||
self.conn.put_container(container,
|
||||
headers=self._headers(headers))
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=err)
|
||||
return
|
||||
@@ -355,9 +384,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
def get_container_entries(self, container, prefix):
|
||||
"""Get container entry names"""
|
||||
try:
|
||||
headers = self._headers()
|
||||
swift_objects = self.conn.get_container(container,
|
||||
prefix=prefix,
|
||||
full_listing=True)[1]
|
||||
full_listing=True,
|
||||
headers=headers)[1]
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=err)
|
||||
swift_object_names = [swift_obj['name'] for swift_obj in swift_objects]
|
||||
@@ -369,7 +400,8 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
Returns a writer object that stores a chunk of volume data in a
|
||||
Swift object store.
|
||||
"""
|
||||
return self.SwiftObjectWriter(container, object_name, self.conn)
|
||||
return self.SwiftObjectWriter(container, object_name, self.conn,
|
||||
self._headers)
|
||||
|
||||
def get_object_reader(self, container, object_name, extra_metadata=None):
|
||||
"""Return reader object.
|
||||
@@ -377,12 +409,14 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
||||
Returns a reader object that retrieves a chunk of backed-up volume data
|
||||
from a Swift object store.
|
||||
"""
|
||||
return self.SwiftObjectReader(container, object_name, self.conn)
|
||||
return self.SwiftObjectReader(container, object_name, self.conn,
|
||||
self._headers)
|
||||
|
||||
def delete_object(self, container, object_name):
|
||||
"""Deletes a backup object from a Swift object store."""
|
||||
try:
|
||||
self.conn.delete_object(container, object_name)
|
||||
self.conn.delete_object(container, object_name,
|
||||
headers=self._headers())
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=err)
|
||||
|
||||
|
@@ -52,12 +52,7 @@ def reset_globals():
|
||||
_SERVICE_AUTH = None
|
||||
|
||||
|
||||
def get_auth_plugin(context, auth=None):
|
||||
if auth:
|
||||
user_auth = auth
|
||||
else:
|
||||
user_auth = context.get_auth_plugin()
|
||||
|
||||
def get_service_auth_plugin():
|
||||
if CONF.service_user.send_service_user_token:
|
||||
global _SERVICE_AUTH
|
||||
if not _SERVICE_AUTH:
|
||||
@@ -67,7 +62,19 @@ def get_auth_plugin(context, auth=None):
|
||||
# This can happen if no auth_type is specified, which probably
|
||||
# means there's no auth information in the [service_user] group
|
||||
raise exception.ServiceUserTokenNoAuth()
|
||||
return _SERVICE_AUTH
|
||||
return None
|
||||
|
||||
|
||||
def get_auth_plugin(context, auth=None):
|
||||
if auth:
|
||||
user_auth = auth
|
||||
else:
|
||||
user_auth = context.get_auth_plugin()
|
||||
|
||||
service_auth = get_service_auth_plugin()
|
||||
if service_auth is not None:
|
||||
return service_token.ServiceTokenAuthWrapper(
|
||||
user_auth=user_auth, service_auth=_SERVICE_AUTH)
|
||||
user_auth=user_auth, service_auth=service_auth)
|
||||
|
||||
return user_auth
|
||||
|
@@ -37,6 +37,7 @@ from cinder import db
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import objects
|
||||
from cinder import service_auth
|
||||
from cinder.tests.unit.backup import fake_swift_client
|
||||
from cinder.tests.unit.backup import fake_swift_client2
|
||||
from cinder.tests.unit import fake_constants as fake
|
||||
@@ -299,6 +300,35 @@ class BackupSwiftTestCase(test.TestCase):
|
||||
starting_backoff=ANY,
|
||||
cacert=ANY)
|
||||
|
||||
def _test_backup_swift_service_auth_headers_no_impact(self):
|
||||
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
||||
self.assertIsNone(service._headers())
|
||||
current = {'some': 'header'}
|
||||
self.assertEqual(service._headers(current), current)
|
||||
|
||||
def test_backup_swift_service_auth_headers_disabled(self):
|
||||
self._test_backup_swift_service_auth_headers_no_impact()
|
||||
|
||||
def test_backup_swift_service_auth_headers_partial_enabled(self):
|
||||
self.override_config('send_service_user_token', True,
|
||||
group='service_user')
|
||||
self._test_backup_swift_service_auth_headers_no_impact()
|
||||
|
||||
@mock.patch.object(service_auth, 'get_service_auth_plugin')
|
||||
def test_backup_swift_service_auth_headers_enabled(self, mock_plugin):
|
||||
class FakeServiceAuthPlugin:
|
||||
def get_token(self):
|
||||
return "fake"
|
||||
self.override_config('send_service_user_token', True,
|
||||
group='service_user')
|
||||
self.override_config('backup_swift_service_auth', True)
|
||||
mock_plugin.return_value = FakeServiceAuthPlugin()
|
||||
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
||||
expected = {'X-Service-Token': 'fake'}
|
||||
self.assertEqual(service._headers(), expected)
|
||||
expected = {'X-Service-Token': 'fake', 'some': 'header'}
|
||||
self.assertEqual(service._headers({'some': 'header'}), expected)
|
||||
|
||||
@mock.patch.object(fake_swift_client.FakeSwiftConnection, 'put_container')
|
||||
def test_default_backup_swift_create_storage_policy(self, mock_put):
|
||||
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
||||
|
@@ -37,7 +37,7 @@ class FakeSwiftConnection(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def head_container(self, container):
|
||||
def head_container(self, container, headers=None):
|
||||
if container in ['missing_container',
|
||||
'missing_container_socket_error_on_put']:
|
||||
raise swift.ClientException('fake exception',
|
||||
@@ -53,17 +53,17 @@ class FakeSwiftConnection(object):
|
||||
if container == 'missing_container_socket_error_on_put':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
|
||||
def get_container(self, container, **kwargs):
|
||||
def get_container(self, container, headers=None, **kwargs):
|
||||
fake_header = None
|
||||
fake_body = [{'name': 'backup_001'},
|
||||
{'name': 'backup_002'},
|
||||
{'name': 'backup_003'}]
|
||||
return fake_header, fake_body
|
||||
|
||||
def head_object(self, container, name):
|
||||
def head_object(self, container, name, headers=None):
|
||||
return {'etag': 'fake-md5-sum'}
|
||||
|
||||
def get_object(self, container, name):
|
||||
def get_object(self, container, name, headers=None):
|
||||
if container == 'socket_error_on_get':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
if 'metadata' in name:
|
||||
@@ -102,7 +102,7 @@ class FakeSwiftConnection(object):
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
return 'fake-md5-sum'
|
||||
|
||||
def delete_object(self, container, name):
|
||||
def delete_object(self, container, name, headers=None):
|
||||
if container == 'socket_error_on_delete':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
pass
|
||||
|
@@ -36,7 +36,7 @@ class FakeSwiftConnection2(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
def head_container(self, container):
|
||||
def head_container(self, container, headers=None):
|
||||
if container == 'missing_container':
|
||||
raise swift.ClientException('fake exception',
|
||||
http_status=http_client.NOT_FOUND)
|
||||
@@ -49,7 +49,7 @@ class FakeSwiftConnection2(object):
|
||||
def put_container(self, container, headers=None):
|
||||
pass
|
||||
|
||||
def get_container(self, container, **kwargs):
|
||||
def get_container(self, container, headers=None, **kwargs):
|
||||
fake_header = None
|
||||
container_dir = tempfile.gettempdir() + '/' + container
|
||||
fake_body = []
|
||||
@@ -62,10 +62,10 @@ class FakeSwiftConnection2(object):
|
||||
|
||||
return fake_header, fake_body
|
||||
|
||||
def head_object(self, container, name):
|
||||
def head_object(self, container, name, headers=None):
|
||||
return {'etag': 'fake-md5-sum'}
|
||||
|
||||
def get_object(self, container, name):
|
||||
def get_object(self, container, name, headers=None):
|
||||
if container == 'socket_error_on_get':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
object_path = tempfile.gettempdir() + '/' + container + '/' + name
|
||||
@@ -80,5 +80,5 @@ class FakeSwiftConnection2(object):
|
||||
object_file.write(reader.read())
|
||||
return md5(reader.read(), usedforsecurity=False).hexdigest()
|
||||
|
||||
def delete_object(self, container, name):
|
||||
def delete_object(self, container, name, headers=None):
|
||||
pass
|
||||
|
@@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The Swift backup driver now supports sending a X-Service-Token header with
|
||||
a service token when the new ``backup_swift_service_auth`` config option is
|
||||
enabled. Please note that you still need to configure the ``[service_user]``
|
||||
group and also set ``send_service_user_token`` to enable the behavior and not
|
||||
only the Swift backup driver option. Note ``send_service_user_token`` enables
|
||||
it globally and will also affect communication with Nova and Glance.
|
Reference in New Issue
Block a user