424 lines
18 KiB
Python
424 lines
18 KiB
Python
# Copyright 2014 Cloudbase Solutions Srl
|
|
#
|
|
# 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 copy
|
|
import os
|
|
|
|
import ddt
|
|
import mock
|
|
|
|
from cinder import exception
|
|
from cinder.image import image_utils
|
|
from cinder import test
|
|
from cinder import utils
|
|
from cinder.volume.drivers import remotefs
|
|
|
|
|
|
@ddt.ddt
|
|
class RemoteFsSnapDriverTestCase(test.TestCase):
|
|
|
|
_FAKE_CONTEXT = 'fake_context'
|
|
_FAKE_VOLUME_ID = '4f711859-4928-4cb7-801a-a50c37ceaccc'
|
|
_FAKE_VOLUME_NAME = 'volume-%s' % _FAKE_VOLUME_ID
|
|
_FAKE_VOLUME = {'id': _FAKE_VOLUME_ID,
|
|
'size': 1,
|
|
'provider_location': 'fake_share',
|
|
'name': _FAKE_VOLUME_NAME,
|
|
'status': 'available'}
|
|
_FAKE_MNT_POINT = '/mnt/fake_hash'
|
|
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT,
|
|
_FAKE_VOLUME_NAME)
|
|
_FAKE_SNAPSHOT_ID = '5g811859-4928-4cb7-801a-a50c37ceacba'
|
|
_FAKE_SNAPSHOT = {'context': _FAKE_CONTEXT,
|
|
'id': _FAKE_SNAPSHOT_ID,
|
|
'volume': _FAKE_VOLUME,
|
|
'volume_id': _FAKE_VOLUME_ID,
|
|
'status': 'available',
|
|
'volume_size': 1}
|
|
_FAKE_SNAPSHOT_PATH = (_FAKE_VOLUME_PATH + '.' + _FAKE_SNAPSHOT_ID)
|
|
|
|
def setUp(self):
|
|
super(RemoteFsSnapDriverTestCase, self).setUp()
|
|
self._driver = remotefs.RemoteFSSnapDriver()
|
|
self._driver._remotefsclient = mock.Mock()
|
|
self._driver._execute = mock.Mock()
|
|
self._driver._delete = mock.Mock()
|
|
|
|
def _test_delete_snapshot(self, volume_in_use=False,
|
|
stale_snapshot=False,
|
|
is_active_image=True):
|
|
# If the snapshot is not the active image, it is guaranteed that
|
|
# another snapshot exists having it as backing file.
|
|
|
|
fake_snapshot_name = os.path.basename(self._FAKE_SNAPSHOT_PATH)
|
|
fake_info = {'active': fake_snapshot_name,
|
|
self._FAKE_SNAPSHOT['id']: fake_snapshot_name}
|
|
fake_snap_img_info = mock.Mock()
|
|
fake_base_img_info = mock.Mock()
|
|
if stale_snapshot:
|
|
fake_snap_img_info.backing_file = None
|
|
else:
|
|
fake_snap_img_info.backing_file = self._FAKE_VOLUME_NAME
|
|
fake_snap_img_info.file_format = 'qcow2'
|
|
fake_base_img_info.backing_file = None
|
|
fake_base_img_info.file_format = 'raw'
|
|
|
|
self._driver._local_path_volume_info = mock.Mock(
|
|
return_value=mock.sentinel.fake_info_path)
|
|
self._driver._qemu_img_info = mock.Mock(
|
|
side_effect=[fake_snap_img_info, fake_base_img_info])
|
|
self._driver._local_volume_dir = mock.Mock(
|
|
return_value=self._FAKE_MNT_POINT)
|
|
|
|
self._driver._read_info_file = mock.Mock()
|
|
self._driver._write_info_file = mock.Mock()
|
|
self._driver._img_commit = mock.Mock()
|
|
self._driver._rebase_img = mock.Mock()
|
|
self._driver._ensure_share_writable = mock.Mock()
|
|
self._driver._delete_stale_snapshot = mock.Mock()
|
|
self._driver._delete_snapshot_online = mock.Mock()
|
|
|
|
expected_info = {
|
|
'active': fake_snapshot_name,
|
|
self._FAKE_SNAPSHOT_ID: fake_snapshot_name
|
|
}
|
|
|
|
if volume_in_use:
|
|
fake_snapshot = copy.deepcopy(self._FAKE_SNAPSHOT)
|
|
fake_snapshot['volume']['status'] = 'in-use'
|
|
|
|
self._driver._read_info_file.return_value = fake_info
|
|
|
|
self._driver._delete_snapshot(fake_snapshot)
|
|
if stale_snapshot:
|
|
self._driver._delete_stale_snapshot.assert_called_once_with(
|
|
fake_snapshot)
|
|
else:
|
|
expected_online_delete_info = {
|
|
'active_file': fake_snapshot_name,
|
|
'snapshot_file': fake_snapshot_name,
|
|
'base_file': self._FAKE_VOLUME_NAME,
|
|
'base_id': None,
|
|
'new_base_file': None
|
|
}
|
|
self._driver._delete_snapshot_online.assert_called_once_with(
|
|
self._FAKE_CONTEXT, fake_snapshot,
|
|
expected_online_delete_info)
|
|
|
|
elif is_active_image:
|
|
self._driver._read_info_file.return_value = fake_info
|
|
|
|
self._driver._delete_snapshot(self._FAKE_SNAPSHOT)
|
|
|
|
self._driver._img_commit.assert_called_once_with(
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
self.assertNotIn(self._FAKE_SNAPSHOT_ID, fake_info)
|
|
self._driver._write_info_file.assert_called_once_with(
|
|
mock.sentinel.fake_info_path, fake_info)
|
|
else:
|
|
fake_upper_snap_id = 'fake_upper_snap_id'
|
|
fake_upper_snap_path = (
|
|
self._FAKE_VOLUME_PATH + '-snapshot' + fake_upper_snap_id)
|
|
fake_upper_snap_name = os.path.basename(fake_upper_snap_path)
|
|
|
|
fake_backing_chain = [
|
|
{'filename': fake_upper_snap_name,
|
|
'backing-filename': fake_snapshot_name},
|
|
{'filename': fake_snapshot_name,
|
|
'backing-filename': self._FAKE_VOLUME_NAME},
|
|
{'filename': self._FAKE_VOLUME_NAME,
|
|
'backing-filename': None}]
|
|
|
|
fake_info[fake_upper_snap_id] = fake_upper_snap_name
|
|
fake_info[self._FAKE_SNAPSHOT_ID] = fake_snapshot_name
|
|
fake_info['active'] = fake_upper_snap_name
|
|
|
|
expected_info = copy.deepcopy(fake_info)
|
|
del expected_info[self._FAKE_SNAPSHOT_ID]
|
|
|
|
self._driver._read_info_file.return_value = fake_info
|
|
self._driver._get_backing_chain_for_path = mock.Mock(
|
|
return_value=fake_backing_chain)
|
|
|
|
self._driver._delete_snapshot(self._FAKE_SNAPSHOT)
|
|
|
|
self._driver._img_commit.assert_called_once_with(
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
self._driver._rebase_img.assert_called_once_with(
|
|
fake_upper_snap_path, self._FAKE_VOLUME_NAME,
|
|
fake_base_img_info.file_format)
|
|
self._driver._write_info_file.assert_called_once_with(
|
|
mock.sentinel.fake_info_path, expected_info)
|
|
|
|
def test_delete_snapshot_when_active_file(self):
|
|
self._test_delete_snapshot()
|
|
|
|
def test_delete_snapshot_in_use(self):
|
|
self._test_delete_snapshot(volume_in_use=True)
|
|
|
|
def test_delete_snapshot_in_use_stale_snapshot(self):
|
|
self._test_delete_snapshot(volume_in_use=True,
|
|
stale_snapshot=True)
|
|
|
|
def test_delete_snapshot_with_one_upper_file(self):
|
|
self._test_delete_snapshot(is_active_image=False)
|
|
|
|
def test_delete_stale_snapshot(self):
|
|
fake_snapshot_name = os.path.basename(self._FAKE_SNAPSHOT_PATH)
|
|
fake_snap_info = {
|
|
'active': self._FAKE_VOLUME_NAME,
|
|
self._FAKE_SNAPSHOT_ID: fake_snapshot_name
|
|
}
|
|
expected_info = {'active': self._FAKE_VOLUME_NAME}
|
|
|
|
self._driver._local_path_volume_info = mock.Mock(
|
|
return_value=mock.sentinel.fake_info_path)
|
|
self._driver._read_info_file = mock.Mock(
|
|
return_value=fake_snap_info)
|
|
self._driver._local_volume_dir = mock.Mock(
|
|
return_value=self._FAKE_MNT_POINT)
|
|
self._driver._write_info_file = mock.Mock()
|
|
|
|
self._driver._delete_stale_snapshot(self._FAKE_SNAPSHOT)
|
|
|
|
self._driver._delete.assert_called_once_with(self._FAKE_SNAPSHOT_PATH)
|
|
self._driver._write_info_file.assert_called_once_with(
|
|
mock.sentinel.fake_info_path, expected_info)
|
|
|
|
def test_do_create_snapshot(self):
|
|
self._driver._local_volume_dir = mock.Mock(
|
|
return_value=self._FAKE_VOLUME_PATH)
|
|
fake_backing_path = os.path.join(
|
|
self._driver._local_volume_dir(),
|
|
self._FAKE_VOLUME_NAME)
|
|
|
|
self._driver._execute = mock.Mock()
|
|
self._driver._set_rw_permissions = mock.Mock()
|
|
self._driver._qemu_img_info = mock.Mock(
|
|
return_value=mock.Mock(file_format=mock.sentinel.backing_fmt))
|
|
|
|
self._driver._do_create_snapshot(self._FAKE_SNAPSHOT,
|
|
self._FAKE_VOLUME_NAME,
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
command1 = ['qemu-img', 'create', '-f', 'qcow2', '-o',
|
|
'backing_file=%s' % fake_backing_path,
|
|
self._FAKE_SNAPSHOT_PATH]
|
|
command2 = ['qemu-img', 'rebase', '-u',
|
|
'-b', self._FAKE_VOLUME_NAME,
|
|
'-F', mock.sentinel.backing_fmt,
|
|
self._FAKE_SNAPSHOT_PATH]
|
|
|
|
self._driver._execute.assert_any_call(*command1, run_as_root=True)
|
|
self._driver._execute.assert_any_call(*command2, run_as_root=True)
|
|
|
|
def _test_create_snapshot(self, volume_in_use=False):
|
|
fake_snapshot = copy.deepcopy(self._FAKE_SNAPSHOT)
|
|
fake_snapshot_info = {}
|
|
fake_snapshot_file_name = os.path.basename(self._FAKE_SNAPSHOT_PATH)
|
|
|
|
self._driver._local_path_volume_info = mock.Mock(
|
|
return_value=mock.sentinel.fake_info_path)
|
|
self._driver._read_info_file = mock.Mock(
|
|
return_value=fake_snapshot_info)
|
|
self._driver._do_create_snapshot = mock.Mock()
|
|
self._driver._create_snapshot_online = mock.Mock()
|
|
self._driver._write_info_file = mock.Mock()
|
|
self._driver.get_active_image_from_info = mock.Mock(
|
|
return_value=self._FAKE_VOLUME_NAME)
|
|
self._driver._get_new_snap_path = mock.Mock(
|
|
return_value=self._FAKE_SNAPSHOT_PATH)
|
|
|
|
expected_snapshot_info = {
|
|
'active': fake_snapshot_file_name,
|
|
self._FAKE_SNAPSHOT_ID: fake_snapshot_file_name
|
|
}
|
|
|
|
if volume_in_use:
|
|
fake_snapshot['volume']['status'] = 'in-use'
|
|
expected_method_called = '_create_snapshot_online'
|
|
else:
|
|
fake_snapshot['volume']['status'] = 'available'
|
|
expected_method_called = '_do_create_snapshot'
|
|
|
|
self._driver._create_snapshot(fake_snapshot)
|
|
|
|
fake_method = getattr(self._driver, expected_method_called)
|
|
fake_method.assert_called_with(
|
|
fake_snapshot, self._FAKE_VOLUME_NAME,
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
self._driver._write_info_file.assert_called_with(
|
|
mock.sentinel.fake_info_path,
|
|
expected_snapshot_info)
|
|
|
|
def test_create_snapshot_volume_available(self):
|
|
self._test_create_snapshot()
|
|
|
|
def test_create_snapshot_volume_in_use(self):
|
|
self._test_create_snapshot(volume_in_use=True)
|
|
|
|
def test_create_snapshot_invalid_volume(self):
|
|
fake_snapshot = copy.deepcopy(self._FAKE_SNAPSHOT)
|
|
fake_snapshot['volume']['status'] = 'error'
|
|
|
|
self.assertRaises(exception.InvalidVolume,
|
|
self._driver._create_snapshot,
|
|
fake_snapshot)
|
|
|
|
@mock.patch('cinder.db.snapshot_get')
|
|
@mock.patch('time.sleep')
|
|
def test_create_snapshot_online_with_concurrent_delete(
|
|
self, mock_sleep, mock_snapshot_get):
|
|
self._driver._nova = mock.Mock()
|
|
|
|
# Test what happens when progress is so slow that someone
|
|
# decides to delete the snapshot while the last known status is
|
|
# "creating".
|
|
mock_snapshot_get.side_effect = [
|
|
{'status': 'creating', 'progress': '42%'},
|
|
{'status': 'creating', 'progress': '45%'},
|
|
{'status': 'deleting'},
|
|
]
|
|
|
|
with mock.patch.object(self._driver, '_do_create_snapshot') as \
|
|
mock_do_create_snapshot:
|
|
self.assertRaises(exception.RemoteFSConcurrentRequest,
|
|
self._driver._create_snapshot_online,
|
|
self._FAKE_SNAPSHOT,
|
|
self._FAKE_VOLUME_NAME,
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
|
|
mock_do_create_snapshot.assert_called_once_with(
|
|
self._FAKE_SNAPSHOT, self._FAKE_VOLUME_NAME,
|
|
self._FAKE_SNAPSHOT_PATH)
|
|
self.assertEqual([mock.call(1), mock.call(1)],
|
|
mock_sleep.call_args_list)
|
|
self.assertEqual(3, mock_snapshot_get.call_count)
|
|
mock_snapshot_get.assert_called_with(self._FAKE_SNAPSHOT['context'],
|
|
self._FAKE_SNAPSHOT['id'])
|
|
|
|
@mock.patch.object(utils, 'synchronized')
|
|
def _locked_volume_operation_test_helper(self, mock_synchronized, func,
|
|
expected_exception=False,
|
|
*args, **kwargs):
|
|
def mock_decorator(*args, **kwargs):
|
|
def mock_inner(f):
|
|
return f
|
|
return mock_inner
|
|
|
|
mock_synchronized.side_effect = mock_decorator
|
|
expected_lock = '%s-%s' % (self._driver.driver_prefix,
|
|
self._FAKE_VOLUME_ID)
|
|
|
|
if expected_exception:
|
|
self.assertRaises(expected_exception, func,
|
|
self._driver,
|
|
*args, **kwargs)
|
|
else:
|
|
ret_val = func(self._driver, *args, **kwargs)
|
|
|
|
mock_synchronized.assert_called_with(expected_lock,
|
|
external=False)
|
|
self.assertEqual(mock.sentinel.ret_val, ret_val)
|
|
|
|
def test_locked_volume_id_operation(self):
|
|
mock_volume = {'id': self._FAKE_VOLUME_ID}
|
|
|
|
@remotefs.locked_volume_id_operation
|
|
def synchronized_func(inst, volume):
|
|
return mock.sentinel.ret_val
|
|
|
|
self._locked_volume_operation_test_helper(func=synchronized_func,
|
|
volume=mock_volume)
|
|
|
|
def test_locked_volume_id_snapshot_operation(self):
|
|
mock_snapshot = {'volume': {'id': self._FAKE_VOLUME_ID}}
|
|
|
|
@remotefs.locked_volume_id_operation
|
|
def synchronized_func(inst, snapshot):
|
|
return mock.sentinel.ret_val
|
|
|
|
self._locked_volume_operation_test_helper(func=synchronized_func,
|
|
snapshot=mock_snapshot)
|
|
|
|
def test_locked_volume_id_operation_exception(self):
|
|
@remotefs.locked_volume_id_operation
|
|
def synchronized_func(inst):
|
|
return mock.sentinel.ret_val
|
|
|
|
self._locked_volume_operation_test_helper(
|
|
func=synchronized_func,
|
|
expected_exception=exception.VolumeBackendAPIException)
|
|
|
|
@mock.patch.object(image_utils, 'qemu_img_info')
|
|
@mock.patch('os.path.basename')
|
|
def _test_qemu_img_info(self, mock_basename,
|
|
mock_qemu_img_info, backing_file, basedir,
|
|
valid_backing_file=True):
|
|
fake_vol_name = 'fake_vol_name'
|
|
mock_info = mock_qemu_img_info.return_value
|
|
mock_info.image = mock.sentinel.image_path
|
|
mock_info.backing_file = backing_file
|
|
|
|
self._driver._VALID_IMAGE_EXTENSIONS = ['vhd', 'vhdx', 'raw', 'qcow2']
|
|
|
|
mock_basename.side_effect = [mock.sentinel.image_basename,
|
|
mock.sentinel.backing_file_basename]
|
|
|
|
if valid_backing_file:
|
|
img_info = self._driver._qemu_img_info_base(
|
|
mock.sentinel.image_path, fake_vol_name, basedir)
|
|
self.assertEqual(mock_info, img_info)
|
|
self.assertEqual(mock.sentinel.image_basename,
|
|
mock_info.image)
|
|
expected_basename_calls = [mock.call(mock.sentinel.image_path)]
|
|
if backing_file:
|
|
self.assertEqual(mock.sentinel.backing_file_basename,
|
|
mock_info.backing_file)
|
|
expected_basename_calls.append(mock.call(backing_file))
|
|
mock_basename.assert_has_calls(expected_basename_calls)
|
|
else:
|
|
self.assertRaises(exception.RemoteFSException,
|
|
self._driver._qemu_img_info_base,
|
|
mock.sentinel.image_path,
|
|
fake_vol_name, basedir)
|
|
|
|
mock_qemu_img_info.assert_called_with(mock.sentinel.image_path)
|
|
|
|
@ddt.data([None, '/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name', '/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name.vhd', '/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name.404f-404',
|
|
'/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name.tmp-snap-404f-404',
|
|
'/fake_basedir'])
|
|
@ddt.unpack
|
|
def test_qemu_img_info_valid_backing_file(self, backing_file, basedir):
|
|
self._test_qemu_img_info(backing_file=backing_file,
|
|
basedir=basedir)
|
|
|
|
@ddt.data(['/other_random_path', '/fake_basedir'],
|
|
['/other_basedir/cb2016/fake_vol_name', '/fake_basedir'],
|
|
['/fake_basedir/invalid_hash/fake_vol_name', '/fake_basedir'],
|
|
['/fake_basedir/cb2016/invalid_vol_name', '/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name.info', '/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name-random-suffix',
|
|
'/fake_basedir'],
|
|
['/fake_basedir/cb2016/fake_vol_name.invalidext',
|
|
'/fake_basedir'])
|
|
@ddt.unpack
|
|
def test_qemu_img_info_invalid_backing_file(self, backing_file, basedir):
|
|
self._test_qemu_img_info(backing_file=backing_file,
|
|
basedir=basedir,
|
|
valid_backing_file=False)
|