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 import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import interface
|
from cinder import interface
|
||||||
|
from cinder import service_auth
|
||||||
|
from cinder.utils import retry
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -141,6 +143,11 @@ swiftbackup_service_opts = [
|
|||||||
default=False,
|
default=False,
|
||||||
help='Bypass verification of server certificate when '
|
help='Bypass verification of server certificate when '
|
||||||
'making SSL connection to Swift.'),
|
'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
|
CONF = cfg.CONF
|
||||||
@@ -174,6 +181,21 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
def get_driver_options():
|
def get_driver_options():
|
||||||
return swiftbackup_service_opts
|
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):
|
def initialize(self):
|
||||||
self.swift_attempts = CONF.backup_swift_retry_attempts
|
self.swift_attempts = CONF.backup_swift_retry_attempts
|
||||||
self.swift_backoff = CONF.backup_swift_retry_backoff
|
self.swift_backoff = CONF.backup_swift_retry_backoff
|
||||||
@@ -274,11 +296,12 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
cacert=CONF.backup_swift_ca_cert_file)
|
cacert=CONF.backup_swift_ca_cert_file)
|
||||||
|
|
||||||
class SwiftObjectWriter(object):
|
class SwiftObjectWriter(object):
|
||||||
def __init__(self, container, object_name, conn):
|
def __init__(self, container, object_name, conn, headers_func=None):
|
||||||
self.container = container
|
self.container = container
|
||||||
self.object_name = object_name
|
self.object_name = object_name
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.data = bytearray()
|
self.data = bytearray()
|
||||||
|
self.headers_func = headers_func
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@@ -292,9 +315,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
def close(self):
|
def close(self):
|
||||||
reader = io.BytesIO(self.data)
|
reader = io.BytesIO(self.data)
|
||||||
try:
|
try:
|
||||||
|
headers = self.headers_func() if self.headers_func else None
|
||||||
etag = self.conn.put_object(self.container, self.object_name,
|
etag = self.conn.put_object(self.container, self.object_name,
|
||||||
reader,
|
reader,
|
||||||
content_length=len(self.data))
|
content_length=len(self.data),
|
||||||
|
headers=headers)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
raise exception.SwiftConnectionFailed(reason=err)
|
raise exception.SwiftConnectionFailed(reason=err)
|
||||||
md5 = secretutils.md5(self.data, usedforsecurity=False).hexdigest()
|
md5 = secretutils.md5(self.data, usedforsecurity=False).hexdigest()
|
||||||
@@ -306,10 +331,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
return md5
|
return md5
|
||||||
|
|
||||||
class SwiftObjectReader(object):
|
class SwiftObjectReader(object):
|
||||||
def __init__(self, container, object_name, conn):
|
def __init__(self, container, object_name, conn, headers_func=None):
|
||||||
self.container = container
|
self.container = container
|
||||||
self.object_name = object_name
|
self.object_name = object_name
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
self.headers_func = headers_func
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@@ -319,8 +345,10 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
try:
|
try:
|
||||||
|
headers = self.headers_func() if self.headers_func else None
|
||||||
(_resp, body) = self.conn.get_object(self.container,
|
(_resp, body) = self.conn.get_object(self.container,
|
||||||
self.object_name)
|
self.object_name,
|
||||||
|
headers=headers)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
raise exception.SwiftConnectionFailed(reason=err)
|
raise exception.SwiftConnectionFailed(reason=err)
|
||||||
return body
|
return body
|
||||||
@@ -335,14 +363,15 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
existing container.
|
existing container.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.conn.head_container(container)
|
self.conn.head_container(container, headers=self._headers())
|
||||||
except swift_exc.ClientException as e:
|
except swift_exc.ClientException as e:
|
||||||
if e.http_status == 404:
|
if e.http_status == 404:
|
||||||
try:
|
try:
|
||||||
storage_policy = CONF.backup_swift_create_storage_policy
|
storage_policy = CONF.backup_swift_create_storage_policy
|
||||||
headers = ({'X-Storage-Policy': storage_policy}
|
headers = ({'X-Storage-Policy': storage_policy}
|
||||||
if storage_policy else None)
|
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:
|
except socket.error as err:
|
||||||
raise exception.SwiftConnectionFailed(reason=err)
|
raise exception.SwiftConnectionFailed(reason=err)
|
||||||
return
|
return
|
||||||
@@ -355,9 +384,11 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
def get_container_entries(self, container, prefix):
|
def get_container_entries(self, container, prefix):
|
||||||
"""Get container entry names"""
|
"""Get container entry names"""
|
||||||
try:
|
try:
|
||||||
|
headers = self._headers()
|
||||||
swift_objects = self.conn.get_container(container,
|
swift_objects = self.conn.get_container(container,
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
full_listing=True)[1]
|
full_listing=True,
|
||||||
|
headers=headers)[1]
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
raise exception.SwiftConnectionFailed(reason=err)
|
raise exception.SwiftConnectionFailed(reason=err)
|
||||||
swift_object_names = [swift_obj['name'] for swift_obj in swift_objects]
|
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
|
Returns a writer object that stores a chunk of volume data in a
|
||||||
Swift object store.
|
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):
|
def get_object_reader(self, container, object_name, extra_metadata=None):
|
||||||
"""Return reader object.
|
"""Return reader object.
|
||||||
@@ -377,12 +409,14 @@ class SwiftBackupDriver(chunkeddriver.ChunkedBackupDriver):
|
|||||||
Returns a reader object that retrieves a chunk of backed-up volume data
|
Returns a reader object that retrieves a chunk of backed-up volume data
|
||||||
from a Swift object store.
|
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):
|
def delete_object(self, container, object_name):
|
||||||
"""Deletes a backup object from a Swift object store."""
|
"""Deletes a backup object from a Swift object store."""
|
||||||
try:
|
try:
|
||||||
self.conn.delete_object(container, object_name)
|
self.conn.delete_object(container, object_name,
|
||||||
|
headers=self._headers())
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
raise exception.SwiftConnectionFailed(reason=err)
|
raise exception.SwiftConnectionFailed(reason=err)
|
||||||
|
|
||||||
|
|||||||
@@ -52,12 +52,7 @@ def reset_globals():
|
|||||||
_SERVICE_AUTH = None
|
_SERVICE_AUTH = None
|
||||||
|
|
||||||
|
|
||||||
def get_auth_plugin(context, auth=None):
|
def get_service_auth_plugin():
|
||||||
if auth:
|
|
||||||
user_auth = auth
|
|
||||||
else:
|
|
||||||
user_auth = context.get_auth_plugin()
|
|
||||||
|
|
||||||
if CONF.service_user.send_service_user_token:
|
if CONF.service_user.send_service_user_token:
|
||||||
global _SERVICE_AUTH
|
global _SERVICE_AUTH
|
||||||
if not _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
|
# This can happen if no auth_type is specified, which probably
|
||||||
# means there's no auth information in the [service_user] group
|
# means there's no auth information in the [service_user] group
|
||||||
raise exception.ServiceUserTokenNoAuth()
|
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(
|
return service_token.ServiceTokenAuthWrapper(
|
||||||
user_auth=user_auth, service_auth=_SERVICE_AUTH)
|
user_auth=user_auth, service_auth=service_auth)
|
||||||
|
|
||||||
return user_auth
|
return user_auth
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from cinder import db
|
|||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import objects
|
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_client
|
||||||
from cinder.tests.unit.backup import fake_swift_client2
|
from cinder.tests.unit.backup import fake_swift_client2
|
||||||
from cinder.tests.unit import fake_constants as fake
|
from cinder.tests.unit import fake_constants as fake
|
||||||
@@ -299,6 +300,35 @@ class BackupSwiftTestCase(test.TestCase):
|
|||||||
starting_backoff=ANY,
|
starting_backoff=ANY,
|
||||||
cacert=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')
|
@mock.patch.object(fake_swift_client.FakeSwiftConnection, 'put_container')
|
||||||
def test_default_backup_swift_create_storage_policy(self, mock_put):
|
def test_default_backup_swift_create_storage_policy(self, mock_put):
|
||||||
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class FakeSwiftConnection(object):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def head_container(self, container):
|
def head_container(self, container, headers=None):
|
||||||
if container in ['missing_container',
|
if container in ['missing_container',
|
||||||
'missing_container_socket_error_on_put']:
|
'missing_container_socket_error_on_put']:
|
||||||
raise swift.ClientException('fake exception',
|
raise swift.ClientException('fake exception',
|
||||||
@@ -53,17 +53,17 @@ class FakeSwiftConnection(object):
|
|||||||
if container == 'missing_container_socket_error_on_put':
|
if container == 'missing_container_socket_error_on_put':
|
||||||
raise socket.error(111, 'ECONNREFUSED')
|
raise socket.error(111, 'ECONNREFUSED')
|
||||||
|
|
||||||
def get_container(self, container, **kwargs):
|
def get_container(self, container, headers=None, **kwargs):
|
||||||
fake_header = None
|
fake_header = None
|
||||||
fake_body = [{'name': 'backup_001'},
|
fake_body = [{'name': 'backup_001'},
|
||||||
{'name': 'backup_002'},
|
{'name': 'backup_002'},
|
||||||
{'name': 'backup_003'}]
|
{'name': 'backup_003'}]
|
||||||
return fake_header, fake_body
|
return fake_header, fake_body
|
||||||
|
|
||||||
def head_object(self, container, name):
|
def head_object(self, container, name, headers=None):
|
||||||
return {'etag': 'fake-md5-sum'}
|
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':
|
if container == 'socket_error_on_get':
|
||||||
raise socket.error(111, 'ECONNREFUSED')
|
raise socket.error(111, 'ECONNREFUSED')
|
||||||
if 'metadata' in name:
|
if 'metadata' in name:
|
||||||
@@ -102,7 +102,7 @@ class FakeSwiftConnection(object):
|
|||||||
raise socket.error(111, 'ECONNREFUSED')
|
raise socket.error(111, 'ECONNREFUSED')
|
||||||
return 'fake-md5-sum'
|
return 'fake-md5-sum'
|
||||||
|
|
||||||
def delete_object(self, container, name):
|
def delete_object(self, container, name, headers=None):
|
||||||
if container == 'socket_error_on_delete':
|
if container == 'socket_error_on_delete':
|
||||||
raise socket.error(111, 'ECONNREFUSED')
|
raise socket.error(111, 'ECONNREFUSED')
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class FakeSwiftConnection2(object):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.tempdir = tempfile.mkdtemp()
|
self.tempdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
def head_container(self, container):
|
def head_container(self, container, headers=None):
|
||||||
if container == 'missing_container':
|
if container == 'missing_container':
|
||||||
raise swift.ClientException('fake exception',
|
raise swift.ClientException('fake exception',
|
||||||
http_status=http_client.NOT_FOUND)
|
http_status=http_client.NOT_FOUND)
|
||||||
@@ -49,7 +49,7 @@ class FakeSwiftConnection2(object):
|
|||||||
def put_container(self, container, headers=None):
|
def put_container(self, container, headers=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_container(self, container, **kwargs):
|
def get_container(self, container, headers=None, **kwargs):
|
||||||
fake_header = None
|
fake_header = None
|
||||||
container_dir = tempfile.gettempdir() + '/' + container
|
container_dir = tempfile.gettempdir() + '/' + container
|
||||||
fake_body = []
|
fake_body = []
|
||||||
@@ -62,10 +62,10 @@ class FakeSwiftConnection2(object):
|
|||||||
|
|
||||||
return fake_header, fake_body
|
return fake_header, fake_body
|
||||||
|
|
||||||
def head_object(self, container, name):
|
def head_object(self, container, name, headers=None):
|
||||||
return {'etag': 'fake-md5-sum'}
|
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':
|
if container == 'socket_error_on_get':
|
||||||
raise socket.error(111, 'ECONNREFUSED')
|
raise socket.error(111, 'ECONNREFUSED')
|
||||||
object_path = tempfile.gettempdir() + '/' + container + '/' + name
|
object_path = tempfile.gettempdir() + '/' + container + '/' + name
|
||||||
@@ -80,5 +80,5 @@ class FakeSwiftConnection2(object):
|
|||||||
object_file.write(reader.read())
|
object_file.write(reader.read())
|
||||||
return md5(reader.read(), usedforsecurity=False).hexdigest()
|
return md5(reader.read(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
def delete_object(self, container, name):
|
def delete_object(self, container, name, headers=None):
|
||||||
pass
|
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