# 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 mock from cinder import exception from cinder import test from cinder.volume.drivers import remotefs class RemoteFsSnapDriverTestCase(test.TestCase): _FAKE_CONTEXT = 'fake_context' _FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc' _FAKE_VOLUME = {'id': '4f711859-4928-4cb7-801a-a50c37ceaccc', '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, '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, highest_file_exists=False): # If the snapshot is not the active image, it is guaranteed that # another snapshot exists having it as backing file. # If yet another file is backed by the file from the next level, # it means that the 'highest file' exists and it needs to be rebased. 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 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._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 if highest_file_exists: fake_highest_snap_id = 'fake_highest_snap_id' fake_highest_snap_path = ( self._FAKE_VOLUME_PATH + '-snapshot' + fake_highest_snap_id) fake_highest_snap_name = os.path.basename( fake_highest_snap_path) fake_highest_snap_info = { 'filename': fake_highest_snap_name, 'backing-filename': fake_upper_snap_name, } fake_backing_chain.insert(0, fake_highest_snap_info) fake_info['active'] = fake_highest_snap_name fake_info[fake_highest_snap_id] = fake_highest_snap_name else: fake_info['active'] = fake_upper_snap_name expected_info = copy.deepcopy(fake_info) expected_info[fake_upper_snap_id] = fake_snapshot_name del expected_info[self._FAKE_SNAPSHOT_ID] if not highest_file_exists: expected_info['active'] = fake_snapshot_name 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_any_call( fake_upper_snap_path) if highest_file_exists: self._driver._rebase_img.assert_called_once_with( fake_highest_snap_path, fake_snapshot_name, 'qcow2') 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_snapshot_with_two_or_more_upper_files(self): self._test_delete_snapshot(is_active_image=False, highest_file_exists=True) 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)