479 lines
20 KiB
Python
479 lines
20 KiB
Python
# 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.
|
|
"""Tests for the base chunkedbackupdriver class."""
|
|
|
|
import json
|
|
import uuid
|
|
|
|
import mock
|
|
from oslo_config import cfg
|
|
from oslo_utils import units
|
|
|
|
from cinder.backup import chunkeddriver as cbd
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder import objects
|
|
from cinder.objects import fields
|
|
from cinder import test
|
|
|
|
|
|
CONF = cfg.CONF
|
|
|
|
TEST_DATA = ('abcdefhijklmnopqrstuvwxyz' * 10).encode('utf-8')
|
|
|
|
|
|
class ConcreteChunkedDriver(cbd.ChunkedBackupDriver):
|
|
def __init__(self, ctxt):
|
|
super(ConcreteChunkedDriver, self).__init__(
|
|
ctxt, 1, 1, 'container', False)
|
|
|
|
def _generate_object_name_prefix(self, backup):
|
|
return 'test-'
|
|
|
|
def delete_object(self, container, object_name):
|
|
return True
|
|
|
|
def get_container_entries(self, container, prefix):
|
|
return ['{}{}'.format(container, prefix)]
|
|
|
|
def get_extra_metadata(self, backup, volume):
|
|
return "{}extra_metadata".format(volume.id)
|
|
|
|
def get_object_reader(self, *args, **kwargs):
|
|
return TestObjectReader(*args, **kwargs)
|
|
|
|
def get_object_writer(self, *args, **kwargs):
|
|
return TestObjectWriter(self, *args, **kwargs)
|
|
|
|
def put_container(self, bucket):
|
|
pass
|
|
|
|
def update_container_name(self, backup, bucket):
|
|
return None
|
|
|
|
|
|
class TestObjectWriter(object):
|
|
def __init__(self, container, filename, extra_metadata=None):
|
|
self.container = container
|
|
self.filename = filename
|
|
self.extra_metadata = extra_metadata
|
|
self.written_data = None
|
|
self.write_count = 0
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
pass
|
|
|
|
def write(self, data):
|
|
self.written_data = data
|
|
self.write_count += 1
|
|
|
|
|
|
class TestObjectReader(object):
|
|
def __init__(self, container, filename, extra_metadata=None):
|
|
self.container = container
|
|
self.filename = filename
|
|
self.extra_metadata = extra_metadata
|
|
self.written_data = None
|
|
metadata = {}
|
|
metadata['version'] = 1
|
|
metadata['backup_id'] = 'backupid'
|
|
metadata['volume_id'] = 'volumeid'
|
|
metadata['backup_name'] = 'backup_name'
|
|
metadata['backup_description'] = 'backup_description'
|
|
metadata['objects'] = ['obj1']
|
|
metadata['parent_id'] = 'parent_id'
|
|
metadata['extra_metadata'] = 'extra_metadata'
|
|
metadata['chunk_size'] = 1
|
|
metadata['sha256s'] = ['sha']
|
|
metadata['volume_meta'] = json.dumps(metadata)
|
|
metadata['version'] = '1.0.0'
|
|
self.metadata = metadata
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
pass
|
|
|
|
def read(self):
|
|
return json.dumps(self.metadata).encode('utf-8')
|
|
|
|
|
|
class ChunkedDriverTestCase(test.TestCase):
|
|
|
|
def _create_backup_db_entry(self, volume_id=str(uuid.uuid4()),
|
|
restore_volume_id=None,
|
|
display_name='test_backup',
|
|
display_description='this is a test backup',
|
|
container='volumebackups',
|
|
status=fields.BackupStatus.CREATING,
|
|
size=1,
|
|
object_count=0,
|
|
project_id=str(uuid.uuid4()),
|
|
service=None,
|
|
temp_volume_id=None,
|
|
temp_snapshot_id=None,
|
|
snapshot_id=None,
|
|
metadata=None,
|
|
parent_id=None,
|
|
encryption_key_id=None):
|
|
"""Create a backup entry in the DB.
|
|
|
|
Return the entry ID
|
|
"""
|
|
kwargs = {}
|
|
kwargs['volume_id'] = volume_id
|
|
kwargs['restore_volume_id'] = restore_volume_id
|
|
kwargs['user_id'] = str(uuid.uuid4())
|
|
kwargs['project_id'] = project_id
|
|
kwargs['host'] = 'testhost'
|
|
kwargs['availability_zone'] = '1'
|
|
kwargs['display_name'] = display_name
|
|
kwargs['display_description'] = display_description
|
|
kwargs['container'] = container
|
|
kwargs['status'] = status
|
|
kwargs['fail_reason'] = ''
|
|
kwargs['service'] = service or CONF.backup_driver
|
|
kwargs['snapshot_id'] = snapshot_id
|
|
kwargs['parent_id'] = parent_id
|
|
kwargs['size'] = size
|
|
kwargs['object_count'] = object_count
|
|
kwargs['temp_volume_id'] = temp_volume_id
|
|
kwargs['temp_snapshot_id'] = temp_snapshot_id
|
|
kwargs['metadata'] = metadata or {}
|
|
kwargs['encryption_key_id'] = encryption_key_id
|
|
kwargs['service_metadata'] = 'test_metadata'
|
|
backup = objects.Backup(context=self.ctxt, **kwargs)
|
|
backup.create()
|
|
return backup
|
|
|
|
def _create_volume_db_entry(self, display_name='test_volume',
|
|
display_description='this is a test volume',
|
|
status='backing-up',
|
|
previous_status='available',
|
|
size=1,
|
|
host='testhost',
|
|
encryption_key_id=None):
|
|
"""Create a volume entry in the DB.
|
|
|
|
Return the entry ID
|
|
"""
|
|
vol = {}
|
|
vol['size'] = size
|
|
vol['host'] = host
|
|
vol['user_id'] = str(uuid.uuid4())
|
|
vol['project_id'] = str(uuid.uuid4())
|
|
vol['status'] = status
|
|
vol['display_name'] = display_name
|
|
vol['display_description'] = display_description
|
|
vol['attach_status'] = fields.VolumeAttachStatus.DETACHED
|
|
vol['availability_zone'] = '1'
|
|
vol['previous_status'] = previous_status
|
|
vol['encryption_key_id'] = encryption_key_id
|
|
volume = objects.Volume(context=self.ctxt, **vol)
|
|
volume.create()
|
|
return volume.id
|
|
|
|
def setUp(self):
|
|
super(ChunkedDriverTestCase, self).setUp()
|
|
self.ctxt = context.get_admin_context()
|
|
self.driver = ConcreteChunkedDriver(self.ctxt)
|
|
self.driver.compressor = None
|
|
self.volume = self._create_volume_db_entry()
|
|
self.backup = self._create_backup_db_entry(volume_id=self.volume)
|
|
|
|
def test_get_compressor_none(self):
|
|
for algo in ['None', 'Off', 'No']:
|
|
self.assertIsNone(self.driver._get_compressor(algo))
|
|
|
|
def test_get_compressor_zlib(self):
|
|
for algo in ['zlib', 'gzip']:
|
|
self.assertTrue('zlib' in str(self.driver._get_compressor(algo)))
|
|
|
|
def test_get_compressor_bz(self):
|
|
for algo in ['bz2', 'bzip2']:
|
|
self.assertTrue('bz' in str(self.driver._get_compressor(algo)))
|
|
|
|
def test_get_compressor_invalid(self):
|
|
self.assertRaises(ValueError, self.driver._get_compressor, 'winzip')
|
|
|
|
def test_create_container(self):
|
|
self.assertEqual(self.backup.container,
|
|
self.driver._create_container(self.backup))
|
|
|
|
def test_create_container_default(self):
|
|
self.backup.container = None
|
|
self.assertEqual('container',
|
|
self.driver._create_container(self.backup))
|
|
|
|
def test_create_container_new_container(self):
|
|
with mock.patch.object(self.driver, 'update_container_name',
|
|
return_value='new_and_improved'):
|
|
self.assertEqual('new_and_improved',
|
|
self.driver._create_container(self.backup))
|
|
|
|
def test_generate_object_names(self):
|
|
obj_names = self.driver._generate_object_names(self.backup)
|
|
self.assertTrue(len(obj_names) == 1)
|
|
self.assertEqual('{}{}'.format(self.backup.container,
|
|
self.backup.service_metadata),
|
|
obj_names[0])
|
|
|
|
def test_metadata_filename(self):
|
|
filename = self.driver._metadata_filename(self.backup)
|
|
self.assertEqual('{}_metadata'.format(self.backup.service_metadata),
|
|
filename)
|
|
|
|
def test_sha256_filename(self):
|
|
filename = self.driver._sha256_filename(self.backup)
|
|
self.assertEqual('{}_sha256file'.format(self.backup.service_metadata),
|
|
filename)
|
|
|
|
def test_write_metadata(self):
|
|
obj_writer = TestObjectWriter('', '')
|
|
with mock.patch.object(self.driver, 'get_object_writer',
|
|
return_value=obj_writer):
|
|
self.driver._write_metadata(self.backup, 'volid', 'contain_name',
|
|
['obj1'], 'volume_meta',
|
|
extra_metadata='extra_metadata')
|
|
|
|
self.assertIsNotNone(obj_writer.written_data)
|
|
written_data = obj_writer.written_data.decode('utf-8')
|
|
metadata = json.loads(written_data)
|
|
self.assertEqual(self.driver.DRIVER_VERSION,
|
|
metadata.get('version'))
|
|
self.assertEqual(self.backup.id, metadata.get('backup_id'))
|
|
self.assertEqual('volid', metadata.get('volume_id'))
|
|
self.assertEqual(self.backup.display_name,
|
|
metadata.get('backup_name'))
|
|
self.assertEqual(self.backup.display_description,
|
|
metadata.get('backup_description'))
|
|
self.assertEqual(['obj1'], metadata.get('objects'))
|
|
self.assertEqual(self.backup.parent_id, metadata.get('parent_id'))
|
|
self.assertEqual('volume_meta', metadata.get('volume_meta'))
|
|
self.assertEqual('extra_metadata', metadata.get('extra_metadata'))
|
|
|
|
def test_write_sha256file(self):
|
|
obj_writer = TestObjectWriter('', '')
|
|
with mock.patch.object(self.driver, 'get_object_writer',
|
|
return_value=obj_writer):
|
|
self.driver._write_sha256file(self.backup, 'volid', 'contain_name',
|
|
['sha'])
|
|
|
|
self.assertIsNotNone(obj_writer.written_data)
|
|
written_data = obj_writer.written_data.decode('utf-8')
|
|
metadata = json.loads(written_data)
|
|
self.assertEqual(self.driver.DRIVER_VERSION,
|
|
metadata.get('version'))
|
|
self.assertEqual(self.backup.id, metadata.get('backup_id'))
|
|
self.assertEqual('volid', metadata.get('volume_id'))
|
|
self.assertEqual(self.backup.display_name,
|
|
metadata.get('backup_name'))
|
|
self.assertEqual(self.backup.display_description,
|
|
metadata.get('backup_description'))
|
|
self.assertEqual(self.driver.sha_block_size_bytes,
|
|
metadata.get('chunk_size'))
|
|
self.assertEqual(['sha'], metadata.get('sha256s'))
|
|
|
|
def test_read_metadata(self):
|
|
obj_reader = TestObjectReader('', '')
|
|
with mock.patch.object(self.driver, 'get_object_reader',
|
|
return_value=obj_reader):
|
|
metadata = self.driver._read_metadata(self.backup)
|
|
|
|
self.assertIsNotNone(obj_reader.metadata)
|
|
expected = obj_reader.metadata
|
|
self.assertEqual(expected['version'], metadata['version'])
|
|
self.assertEqual(expected['backup_id'], metadata['backup_id'])
|
|
self.assertEqual(expected['volume_id'], metadata['volume_id'])
|
|
self.assertEqual(expected['backup_name'], metadata['backup_name'])
|
|
self.assertEqual(expected['backup_description'],
|
|
metadata['backup_description'])
|
|
self.assertEqual(expected['objects'], metadata['objects'])
|
|
self.assertEqual(expected['parent_id'], metadata['parent_id'])
|
|
self.assertEqual(expected['volume_meta'], metadata['volume_meta'])
|
|
self.assertEqual(expected['extra_metadata'],
|
|
metadata['extra_metadata'])
|
|
|
|
def test_read_sha256file(self):
|
|
obj_reader = TestObjectReader('', '')
|
|
with mock.patch.object(self.driver, 'get_object_reader',
|
|
return_value=obj_reader):
|
|
metadata = self.driver._read_sha256file(self.backup)
|
|
|
|
self.assertIsNotNone(obj_reader.metadata)
|
|
expected = obj_reader.metadata
|
|
self.assertEqual(expected['version'], metadata['version'])
|
|
self.assertEqual(expected['backup_id'], metadata['backup_id'])
|
|
self.assertEqual(expected['volume_id'], metadata['volume_id'])
|
|
self.assertEqual(expected['backup_name'], metadata['backup_name'])
|
|
self.assertEqual(expected['backup_description'],
|
|
metadata['backup_description'])
|
|
self.assertEqual(expected['chunk_size'], metadata['chunk_size'])
|
|
self.assertEqual(expected['sha256s'], metadata['sha256s'])
|
|
|
|
def test_prepare_backup(self):
|
|
(object_meta, object_sha256, extra_metadata, container,
|
|
volume_size_bytes) = self.driver._prepare_backup(self.backup)
|
|
|
|
self.assertDictEqual({'id': 1,
|
|
'list': [],
|
|
'prefix': 'test-',
|
|
'volume_meta': None,
|
|
'extra_metadata': "{}extra_metadata".format(
|
|
self.volume),
|
|
},
|
|
object_meta)
|
|
self.assertDictEqual({'id': 1,
|
|
'sha256s': [],
|
|
'prefix': 'test-',
|
|
},
|
|
object_sha256)
|
|
self.assertEqual(extra_metadata, object_meta['extra_metadata'])
|
|
self.assertEqual(self.backup.container, container)
|
|
self.assertEqual(self.backup.size * units.Gi, volume_size_bytes)
|
|
|
|
def test_prepare_backup_invalid_size(self):
|
|
volume = self._create_volume_db_entry(size=0)
|
|
backup = self._create_backup_db_entry(volume_id=volume)
|
|
|
|
self.assertRaises(exception.InvalidVolume,
|
|
self.driver._prepare_backup,
|
|
backup)
|
|
|
|
def test_backup_chunk(self):
|
|
(object_meta, object_sha256, extra_metadata, container,
|
|
volume_size_bytes) = self.driver._prepare_backup(self.backup)
|
|
|
|
obj_writer = TestObjectWriter('', '')
|
|
with mock.patch.object(self.driver, 'get_object_writer',
|
|
return_value=obj_writer):
|
|
self.driver._backup_chunk(self.backup,
|
|
self.backup.container,
|
|
TEST_DATA,
|
|
0,
|
|
object_meta,
|
|
extra_metadata)
|
|
|
|
self.assertEqual(TEST_DATA, obj_writer.written_data)
|
|
self.assertEqual(1, len(object_meta['list']))
|
|
self.assertEqual(2, object_meta['id'])
|
|
|
|
chunk = object_meta['list'][0]['test--00001']
|
|
self.assertEqual('b4bc937908ab6be6039b6d4141200de8', chunk['md5'])
|
|
self.assertEqual(0, chunk['offset'])
|
|
self.assertEqual(len(TEST_DATA), chunk['length'])
|
|
|
|
def test_finalize_backup(self):
|
|
(object_meta, object_sha256, extra_metadata, container,
|
|
volume_size_bytes) = self.driver._prepare_backup(self.backup)
|
|
|
|
obj_writer = TestObjectWriter('', '')
|
|
with mock.patch.object(self.driver, 'get_object_writer',
|
|
return_value=obj_writer):
|
|
self.driver._backup_chunk(self.backup,
|
|
self.backup.container,
|
|
TEST_DATA,
|
|
0,
|
|
object_meta,
|
|
extra_metadata)
|
|
|
|
self.driver._finalize_backup(self.backup,
|
|
self.backup.container,
|
|
object_meta,
|
|
object_sha256)
|
|
|
|
self.assertEqual(1, self.backup.object_count)
|
|
|
|
def test_backup_metadata(self):
|
|
object_meta = {}
|
|
|
|
self.driver._backup_metadata(self.backup, object_meta)
|
|
self.assertTrue('volume_meta' in object_meta.keys())
|
|
|
|
# Too much that we mostly don't care about for UT purposes. Just spot
|
|
# check a few things
|
|
metadata = json.loads(object_meta['volume_meta'])
|
|
self.assertTrue('volume-base-metadata' in metadata.keys())
|
|
self.assertEqual(self.volume, metadata['volume-base-metadata']['id'])
|
|
self.assertEqual(1, metadata['volume-base-metadata']['size'])
|
|
self.assertEqual('test_volume',
|
|
metadata['volume-base-metadata']['display_name'])
|
|
self.assertEqual('testhost', metadata['volume-base-metadata']['host'])
|
|
|
|
@mock.patch('cinder.volume.utils.notify_about_backup_usage')
|
|
def test_send_progress_end(self, mock_notify):
|
|
obj_meta = {}
|
|
self.driver._send_progress_end(self.ctxt, self.backup, obj_meta)
|
|
|
|
self.assertEqual(100, obj_meta.get('backup_percent', 0))
|
|
self.assertTrue(mock_notify.called)
|
|
|
|
@mock.patch('cinder.volume.utils.notify_about_backup_usage')
|
|
def test_send_progress_notification(self, mock_notify):
|
|
obj_meta = {}
|
|
self.driver._send_progress_notification(
|
|
self.ctxt, self.backup, obj_meta, 1, 2)
|
|
|
|
self.assertEqual(50, obj_meta.get('backup_percent', 0))
|
|
self.assertTrue(mock_notify.called)
|
|
|
|
@mock.patch('cinder.volume.utils.notify_about_backup_usage')
|
|
def test_backup(self, mock_notify):
|
|
volume_file = mock.Mock()
|
|
volume_file.tell.side_effect = [0, len(TEST_DATA)]
|
|
volume_file.read.side_effect = [TEST_DATA, b'']
|
|
obj_writer = TestObjectWriter('', '')
|
|
with mock.patch.object(self.driver, 'get_object_writer',
|
|
return_value=obj_writer):
|
|
self.driver.backup(self.backup, volume_file)
|
|
|
|
mock_notify.assert_called()
|
|
|
|
def test_backup_invalid_size(self):
|
|
self.driver.chunk_size_bytes = 999
|
|
self.driver.sha_block_size_bytes = 1024
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.backup,
|
|
self.backup,
|
|
mock.Mock())
|
|
|
|
def test_restore(self):
|
|
volume_file = mock.Mock()
|
|
restore_test = mock.Mock()
|
|
self.driver._restore_v1 = restore_test
|
|
|
|
# Create a second backup
|
|
backup = self._create_backup_db_entry(
|
|
self.volume, parent_id=self.backup.id)
|
|
|
|
with mock.patch.object(self.driver, 'put_metadata') as mock_put:
|
|
self.driver.restore(backup, self.volume, volume_file)
|
|
self.assertEqual(2, mock_put.call_count)
|
|
|
|
restore_test.assert_called()
|
|
|
|
def test_delete_backup(self):
|
|
with mock.patch.object(self.driver, 'delete_object') as mock_delete:
|
|
self.driver.delete_backup(self.backup)
|
|
|
|
mock_delete.assert_called()
|
|
self.assertEqual(1, mock_delete.call_count)
|
|
mock_delete.assert_called_once_with(
|
|
self.backup.container,
|
|
self.backup.container + self.backup.service_metadata)
|