diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py index fdc7cc17227..cc441c68931 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -4346,3 +4346,18 @@ class NetAppCmodeClientTestCase(test.TestCase): mock_send_request.assert_called_once_with('lun-copy-cancel', api_args, enable_tunneling=False) + + def test_rename_file(self): + self.mock_object(self.client.connection, 'send_request') + + orig_file_name = '/vol/fake_vol/volume-%s' % self.fake_volume + new_file_name = '/vol/fake_vol/new-volume-%s' % self.fake_volume + + self.client.rename_file(orig_file_name, new_file_name) + + api_args = { + 'from-path': orig_file_name, + 'to-path': new_file_name, + } + self.client.connection.send_request.assert_called_once_with( + 'file-rename-file', api_args) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 926de74e28b..8bb208aa985 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -348,6 +348,26 @@ SNAPSHOT = { 'id': 'fake_id' } +SNAPSHOT_VOLUME = { + 'id': VOLUME_ID, + 'name': VOLUME_NAME +} + +LUN_WITH_METADATA = { + 'handle': 'vserver_fake:/vol/fake_flexvol/volume-fake-uuid', + 'name': 'volume-fake-uuid', + 'size': 20971520, + 'metadata': { + 'Vserver': 'vserver_fake', + 'Volume': 'fake_flexvol', + 'Qtree': None, + 'Path': '/vol/fake_flexvol/volume-fake-uuid', + 'OsType': 'linux', + 'SpaceReserved': 'false', + 'UUID': 'fake-uuid' + } +} + VOLUME_REF = {'name': 'fake_vref_name', 'size': 42} FAKE_CMODE_VOLUMES = ['open123', 'mixed', 'open321'] diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py index 11560aad3f7..eaf13c4492d 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -19,6 +19,7 @@ from unittest import mock import ddt +import six from cinder import exception from cinder.objects import fields @@ -1432,3 +1433,247 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): fake_vol, fake.DEST_HOST_STRING, fake.BACKEND_NAME, fake.VSERVER_NAME) self.assertEqual({}, result) + + def test_revert_to_snapshot(self): + mock__revert_to_snapshot = self.mock_object(self.library, + '_revert_to_snapshot') + + self.library.revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + mock__revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) + + self.library.revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + def test_revert_to_snapshot_revert_failed(self): + self.mock_object(self.library, '_revert_to_snapshot', + side_effect=Exception) + + self.assertRaises(exception.VolumeBackendAPIException, + self.library.revert_to_snapshot, + fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) + + def test__revert_to_snapshot(self): + lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'], + fake.LUN_WITH_METADATA['name'], + fake.LUN_WITH_METADATA['size'], + fake.LUN_WITH_METADATA['metadata']) + lun_name = lun_obj.name + new_lun_name = 'new-%s' % fake.SNAPSHOT['name'] + flexvol_name = lun_obj.metadata['Volume'] + + mock__clone_snapshot = self.mock_object( + self.library, '_clone_snapshot', return_value=new_lun_name) + mock__get_lun_from_table = self.mock_object( + self.library, '_get_lun_from_table', return_value=lun_obj) + mock__swap_luns = self.mock_object(self.library, '_swap_luns') + mock_destroy_lun = self.mock_object(self.library.zapi_client, + 'destroy_lun') + + self.library._revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + mock__clone_snapshot.assert_called_once_with(fake.SNAPSHOT['name']) + mock__get_lun_from_table.assert_called_once_with( + fake.SNAPSHOT_VOLUME['name']) + mock__swap_luns.assert_called_once_with(lun_name, new_lun_name, + flexvol_name) + mock_destroy_lun.assert_not_called() + + @ddt.data(False, True) + def test__revert_to_snapshot_swap_exception(self, delete_lun_exception): + lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'], + fake.LUN_WITH_METADATA['name'], + fake.LUN_WITH_METADATA['size'], + fake.LUN_WITH_METADATA['metadata']) + new_lun_name = 'new-%s' % fake.SNAPSHOT['name'] + flexvol_name = lun_obj.metadata['Volume'] + new_lun_path = '/vol/%s/%s' % (flexvol_name, new_lun_name) + side_effect = Exception if delete_lun_exception else lambda: True + + self.mock_object( + self.library, '_clone_snapshot', return_value=new_lun_name) + self.mock_object( + self.library, '_get_lun_from_table', return_value=lun_obj) + swap_exception = exception.VolumeBackendAPIException(data="data") + self.mock_object(self.library, '_swap_luns', + side_effect=swap_exception) + mock_destroy_lun = self.mock_object(self.library.zapi_client, + 'destroy_lun', + side_effect=side_effect) + self.assertRaises(exception.VolumeBackendAPIException, + self.library._revert_to_snapshot, + fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + mock_destroy_lun.assert_called_once_with(new_lun_path) + + def test__clone_snapshot(self): + lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'], + fake.LUN_WITH_METADATA['name'], + fake.LUN_WITH_METADATA['size'], + fake.LUN_WITH_METADATA['metadata']) + new_snap_name = 'new-%s' % fake.SNAPSHOT['name'] + snapshot_path = lun_obj.metadata['Path'] + flexvol_name = lun_obj.metadata['Volume'] + block_count = 40960 + + mock__get_lun_from_table = self.mock_object( + self.library, '_get_lun_from_table', return_value=lun_obj) + mock__get_lun_block_count = self.mock_object( + self.library, '_get_lun_block_count', return_value=block_count) + mock_create_lun = self.mock_object(self.library.zapi_client, + 'create_lun') + mock__clone_lun = self.mock_object(self.library, '_clone_lun') + + self.library._clone_snapshot(fake.SNAPSHOT['name']) + + mock__get_lun_from_table.assert_called_once_with(fake.SNAPSHOT['name']) + mock__get_lun_block_count.assert_called_once_with(snapshot_path) + mock_create_lun.assert_called_once_with(flexvol_name, new_snap_name, + six.text_type(lun_obj.size), + lun_obj.metadata) + mock__clone_lun.assert_called_once_with(fake.SNAPSHOT['name'], + new_snap_name, + block_count=block_count) + + def test__clone_snapshot_invalid_block_count(self): + lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'], + fake.LUN_WITH_METADATA['name'], + fake.LUN_WITH_METADATA['size'], + fake.LUN_WITH_METADATA['metadata']) + + self.mock_object(self.library, '_get_lun_from_table', + return_value=lun_obj) + self.mock_object(self.library, '_get_lun_block_count', + return_value=0) + + self.assertRaises(exception.VolumeBackendAPIException, + self.library._clone_snapshot, + fake.SNAPSHOT['name']) + + def test__clone_snapshot_clone_exception(self): + lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'], + fake.LUN_WITH_METADATA['name'], + fake.LUN_WITH_METADATA['size'], + fake.LUN_WITH_METADATA['metadata']) + new_snap_name = 'new-%s' % fake.SNAPSHOT['name'] + snapshot_path = lun_obj.metadata['Path'] + flexvol_name = lun_obj.metadata['Volume'] + new_lun_path = '/vol/%s/%s' % (flexvol_name, new_snap_name) + block_count = 40960 + + mock__get_lun_from_table = self.mock_object( + self.library, '_get_lun_from_table', return_value=lun_obj) + mock__get_lun_block_count = self.mock_object( + self.library, '_get_lun_block_count', return_value=block_count) + mock_create_lun = self.mock_object(self.library.zapi_client, + 'create_lun') + side_effect = exception.VolumeBackendAPIException(data='data') + mock__clone_lun = self.mock_object(self.library, '_clone_lun', + side_effect=side_effect) + mock_destroy_lun = self.mock_object(self.library.zapi_client, + 'destroy_lun') + + self.assertRaises(exception.VolumeBackendAPIException, + self.library._clone_snapshot, + fake.SNAPSHOT['name']) + + mock__get_lun_from_table.assert_called_once_with(fake.SNAPSHOT['name']) + mock__get_lun_block_count.assert_called_once_with(snapshot_path) + mock_create_lun.assert_called_once_with(flexvol_name, new_snap_name, + six.text_type(lun_obj.size), + lun_obj.metadata) + mock__clone_lun.assert_called_once_with(fake.SNAPSHOT['name'], + new_snap_name, + block_count=block_count) + mock_destroy_lun.assert_called_once_with(new_lun_path) + + def test__swap_luns(self): + original_lun = fake.LUN_WITH_METADATA['name'] + new_lun = 'new-%s' % fake.SNAPSHOT['name'] + flexvol = fake.LUN_WITH_METADATA['metadata']['Volume'] + + tmp_lun = 'tmp-%s' % original_lun + + path = "/vol/%s/%s" % (flexvol, original_lun) # original path + tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun) + new_path = "/vol/%s/%s" % (flexvol, new_lun) + + mock_move_lun = self.mock_object( + self.library.zapi_client, 'move_lun', return_value=True) + mock_destroy_lun = self.mock_object( + self.library.zapi_client, 'destroy_lun', return_value=True) + + self.library._swap_luns(original_lun, new_lun, flexvol) + + mock_move_lun.assert_has_calls([ + mock.call(path, tmp_path), + mock.call(new_path, path) + ]) + mock_destroy_lun.assert_called_once_with(tmp_path) + + @ddt.data((True, False), (False, False), (False, True)) + @ddt.unpack + def test__swap_luns_move_exception(self, first_move_exception, + move_back_exception): + original_lun = fake.LUN_WITH_METADATA['name'] + new_lun = 'new-%s' % fake.SNAPSHOT['name'] + flexvol = fake.LUN_WITH_METADATA['metadata']['Volume'] + side_effect = Exception + + def _side_effect_skip(): + return True + + if not first_move_exception and not move_back_exception: + side_effect = [_side_effect_skip, Exception, _side_effect_skip] + elif not first_move_exception: + side_effect = [_side_effect_skip, Exception, Exception] + + tmp_lun = 'tmp-%s' % original_lun + + path = "/vol/%s/%s" % (flexvol, original_lun) # original path + tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun) + new_path = "/vol/%s/%s" % (flexvol, new_lun) + + mock_move_lun = self.mock_object(self.library.zapi_client, + 'move_lun', + side_effect=side_effect) + + self.assertRaises(exception.VolumeBackendAPIException, + self.library._swap_luns, + original_lun, + new_lun, + flexvol) + + if first_move_exception: + mock_move_lun.assert_called_once_with(path, tmp_path) + else: + mock_move_lun.assert_has_calls([ + mock.call(path, tmp_path), + mock.call(new_path, path), + mock.call(tmp_path, path) + ]) + + def test__swap_luns_destroy_exception(self): + original_lun = fake.LUN_WITH_METADATA['name'] + new_lun = 'new-%s' % fake.SNAPSHOT['name'] + flexvol = fake.LUN_WITH_METADATA['metadata']['Volume'] + + tmp_lun = 'tmp-%s' % original_lun + + path = "/vol/%s/%s" % (flexvol, original_lun) + tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun) + new_path = "/vol/%s/%s" % (flexvol, new_lun) + + mock_move_lun = self.mock_object( + self.library.zapi_client, 'move_lun', return_value=True) + mock_destroy_lun = self.mock_object( + self.library.zapi_client, 'destroy_lun', side_effect=Exception) + + self.library._swap_luns(original_lun, new_lun, flexvol) + + mock_move_lun.assert_has_calls([ + mock.call(path, tmp_path), + mock.call(new_path, path) + ]) + mock_destroy_lun.assert_called_once_with(tmp_path) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_fc_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_fc_cmode.py new file mode 100644 index 00000000000..7a35f8be8c9 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_fc_cmode.py @@ -0,0 +1,51 @@ +# 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. +"""Mock unit tests for NetApp Data ONTAP FibreChannel storage systems.""" + +from unittest import mock + +from cinder import context +from cinder.tests.unit import test +from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake +import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes +from cinder.volume.drivers.netapp.dataontap import fc_cmode + + +class NetAppCmodeFibreChannelDriverTestCase(test.TestCase): + + def setUp(self): + super(NetAppCmodeFibreChannelDriverTestCase, self).setUp() + + kwargs = { + 'configuration': self.get_config_base(), + 'host': 'openstack@netappblock', + } + self.library = fc_cmode.NetAppCmodeFibreChannelDriver(**kwargs) + self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client + self.mock_request = mock.Mock() + self.ctxt = context.RequestContext('fake', 'fake', auth_token=True) + + def get_config_base(self): + return na_fakes.create_configuration() + + def test_revert_to_snapshot(self): + mock_revert_to_snapshot = self.mock_object(self.library.library, + 'revert_to_snapshot') + + self.library.revert_to_snapshot(self.ctxt, fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) + + mock_revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_iscsi_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_iscsi_cmode.py new file mode 100644 index 00000000000..8933fabaad1 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_iscsi_cmode.py @@ -0,0 +1,52 @@ +# 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. +"""Mock unit tests for NetApp Data ONTAP (C-mode) iSCSI storage systems.""" + +from unittest import mock + + +from cinder import context +from cinder.tests.unit import test +from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake +import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes +from cinder.volume.drivers.netapp.dataontap import iscsi_cmode + + +class NetAppCmodeFibreChannelDriverTestCase(test.TestCase): + + def setUp(self): + super(NetAppCmodeFibreChannelDriverTestCase, self).setUp() + + kwargs = { + 'configuration': self.get_config_base(), + 'host': 'openstack@netappblock', + } + self.library = iscsi_cmode.NetAppCmodeISCSIDriver(**kwargs) + self.library.zapi_client = mock.Mock() + self.zapi_client = self.library.zapi_client + self.mock_request = mock.Mock() + self.ctxt = context.RequestContext('fake', 'fake', auth_token=True) + + def get_config_base(self): + return na_fakes.create_configuration() + + def test_revert_to_snapshot(self): + mock_revert_to_snapshot = self.mock_object(self.library.library, + 'revert_to_snapshot') + + self.library.revert_to_snapshot(self.ctxt, fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) + + mock_revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py index ff2846b93f3..550da728a86 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py @@ -1167,3 +1167,38 @@ class NetAppNfsDriverTestCase(test.TestCase): self.driver.update_migrated_volume, self.ctxt, fake.test_volume, mock.sentinel.new_volume, mock.sentinel.original_status) + + @ddt.data({'is_flexgroup': False, 'is_flexgroup_supported': False}, + {'is_flexgroup': False, 'is_flexgroup_supported': True}, + {'is_flexgroup': True, 'is_flexgroup_supported': False}, + {'is_flexgroup': True, 'is_flexgroup_supported': True}) + @ddt.unpack + def test_revert_to_snapshot(self, is_flexgroup, is_flexgroup_supported): + context = {} + + self.mock_object(self.driver, + '_is_flexgroup', + return_value=is_flexgroup) + self.mock_object(self.driver, + '_is_flexgroup_clone_file_supported', + return_value=is_flexgroup_supported) + mock_revert_to_snapshot = self.mock_object( + remotefs.RemoteFSSnapDriverDistributed, 'revert_to_snapshot') + mock__revert_to_snapshot = self.mock_object(self.driver, + '_revert_to_snapshot') + + self.driver.revert_to_snapshot(context, fake.SNAPSHOT_VOLUME, + fake.SNAPSHOT) + + if is_flexgroup and not is_flexgroup_supported: + mock_revert_to_snapshot.assert_called_once_with( + context, fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + mock__revert_to_snapshot.assert_not_called() + else: + mock__revert_to_snapshot.assert_called_once_with( + fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + mock_revert_to_snapshot.assert_not_called() + + def test__revert_to_snapshot(self): + self.assertRaises(NotImplementedError, self.driver._revert_to_snapshot, + fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py index e66ed15f563..0704cee2d8c 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py @@ -2200,3 +2200,131 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): mock_migrate_volume_ontap_assisted.assert_not_called() self.assertFalse(migrated) self.assertEqual({}, updates) + + def test__revert_to_snapshot(self): + mock_clone_backing_file_for_volume = self.mock_object( + self.driver, '_clone_backing_file_for_volume') + mock_get_export_ip_path = self.mock_object( + self.driver, '_get_export_ip_path', + return_value=(fake.SHARE_IP, fake.EXPORT_PATH)) + mock_get_vserver_for_ip = self.mock_object( + self.driver, '_get_vserver_for_ip', return_value=fake.VSERVER_NAME) + mock_get_vol_by_junc_vserver = self.mock_object( + self.driver.zapi_client, 'get_vol_by_junc_vserver', + return_value=fake.FLEXVOL) + mock_swap_files = self.mock_object(self.driver, '_swap_files') + mock_delete_file = self.mock_object(self.driver.zapi_client, + 'delete_file') + + self.driver._revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + mock_clone_backing_file_for_volume.assert_called_once_with( + fake.SNAPSHOT['name'], + 'new-%s' % fake.SNAPSHOT['name'], + fake.SNAPSHOT_VOLUME['id'], + is_snapshot=False) + mock_get_export_ip_path.assert_called_once_with( + volume_id=fake.SNAPSHOT_VOLUME['id']) + mock_get_vserver_for_ip.assert_called_once_with(fake.SHARE_IP) + mock_get_vol_by_junc_vserver.assert_called_once_with( + fake.VSERVER_NAME, fake.EXPORT_PATH) + mock_swap_files.assert_called_once_with( + fake.FLEXVOL, fake.SNAPSHOT_VOLUME['name'], + 'new-%s' % fake.SNAPSHOT['name']) + mock_delete_file.assert_not_called() + + @ddt.data(False, True) + def test__revert_to_snapshot_swap_exception(self, delete_exception): + new_snap_name = 'new-%s' % fake.SNAPSHOT['name'] + new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_snap_name) + + self.mock_object(self.driver, '_clone_backing_file_for_volume') + self.mock_object(self.driver, '_get_export_ip_path', + return_value=(fake.SHARE_IP, fake.EXPORT_PATH)) + self.mock_object(self.driver, '_get_vserver_for_ip', + return_value=fake.VSERVER_NAME) + self.mock_object(self.driver.zapi_client, 'get_vol_by_junc_vserver', + return_value=fake.FLEXVOL) + swap_exception = exception.VolumeBackendAPIException(data="data") + self.mock_object(self.driver, '_swap_files', + side_effect=swap_exception) + side_effect = Exception if delete_exception else lambda: True + mock_delete_file = self.mock_object(self.driver.zapi_client, + 'delete_file', + side_effect=side_effect) + + self.assertRaises(exception.VolumeBackendAPIException, + self.driver._revert_to_snapshot, + fake.SNAPSHOT_VOLUME, fake.SNAPSHOT) + + mock_delete_file.assert_called_once_with(new_file_path) + + def test__swap_files(self): + new_file = 'new-%s' % fake.SNAPSHOT['name'] + new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_file) + original_file_path = '/vol/%s/%s' % (fake.FLEXVOL, fake.VOLUME_NAME) + tmp_file_path = '/vol/%s/tmp-%s' % (fake.FLEXVOL, fake.VOLUME_NAME) + + mock_rename_file = self.mock_object( + self.driver.zapi_client, 'rename_file') + mock_delete_file = self.mock_object( + self.driver.zapi_client, 'delete_file') + + self.driver._swap_files(fake.FLEXVOL, fake.VOLUME_NAME, new_file) + + mock_rename_file.assert_has_calls([ + mock.call(original_file_path, tmp_file_path), + mock.call(new_file_path, original_file_path)]) + + mock_delete_file.assert_called_once_with(tmp_file_path) + + @ddt.data((True, False), (False, False), (False, True)) + @ddt.unpack + def test__swap_files_rename_exception(self, first_exception, + rollback_exception): + new_file = 'new-%s' % fake.SNAPSHOT['name'] + new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_file) + original_file_path = '/vol/%s/%s' % (fake.FLEXVOL, fake.VOLUME_NAME) + tmp_file_path = '/vol/%s/tmp-%s' % (fake.FLEXVOL, fake.VOLUME_NAME) + side_effect = None + + def _skip_side_effect(): + return True + + if not first_exception and not rollback_exception: + side_effect = [_skip_side_effect, + exception.VolumeBackendAPIException(data="data"), + _skip_side_effect] + elif not first_exception and rollback_exception: + side_effect = [_skip_side_effect, + exception.VolumeBackendAPIException(data="data"), + exception.VolumeBackendAPIException(data="data")] + else: + side_effect = exception.VolumeBackendAPIException(data="data") + + mock_rename_file = self.mock_object(self.driver.zapi_client, + 'rename_file', + side_effect=side_effect) + + self.assertRaises( + na_utils.NetAppDriverException, + self.driver._swap_files, fake.FLEXVOL, fake.VOLUME_NAME, new_file) + + if not first_exception: + mock_rename_file.assert_has_calls([ + mock.call(original_file_path, tmp_file_path), + mock.call(new_file_path, original_file_path), + mock.call(tmp_file_path, original_file_path)]) + else: + mock_rename_file.assert_called_once_with(original_file_path, + tmp_file_path) + + def test__swap_files_delete_exception(self): + new_file = 'new-%s' % fake.SNAPSHOT['name'] + + self.mock_object(self.driver.zapi_client, 'rename_file') + side_effect = exception.VolumeBackendAPIException(data="data") + self.mock_object(self.driver.zapi_client, 'delete_file', + side_effect=side_effect) + + self.driver._swap_files(fake.FLEXVOL, fake.VOLUME_NAME, new_file) diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py index 3ffea0effac..45bbcd43e27 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -60,6 +60,7 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary, 2.0.0 - Add support for QoS minimums specs Add support for dynamic Adaptive QoS policy group creation 3.0.0 - Add support for Intra-cluster Storage assisted volume migration + Add support for revert to snapshot """ @@ -809,3 +810,127 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary, """Migrate Cinder volume to the specified pool or vserver.""" return self.migrate_volume_ontap_assisted( volume, host, self.backend_name, self.configuration.netapp_vserver) + + def revert_to_snapshot(self, volume, snapshot): + """Driver entry point for reverting volume to snapshot.""" + try: + self._revert_to_snapshot(volume, snapshot) + except Exception: + raise exception.VolumeBackendAPIException( + "Revert snapshot failed.") + + def _revert_to_snapshot(self, volume, snapshot): + """Sets up all required resources for _swap_luns. + + If _swap_luns fails, the cloned LUN is destroyed. + """ + new_lun_name = self._clone_snapshot(snapshot["name"]) + + LOG.debug("Cloned from snapshot: %s.", new_lun_name) + + lun = self._get_lun_from_table(volume["name"]) + volume_path = lun.metadata["Path"] + seg = volume_path.split("/") + lun_name = seg[-1] + flexvol_name = seg[2] + + try: + self._swap_luns(lun_name, new_lun_name, flexvol_name) + except Exception: + LOG.error("Swapping LUN from %s to %s failed.", lun_name, + new_lun_name) + with excutils.save_and_reraise_exception(): + try: + LOG.debug("Deleting temporary reverted LUN %s.", + new_lun_name) + new_lun_path = "/vol/%s/%s" % (flexvol_name, new_lun_name) + self.zapi_client.destroy_lun(new_lun_path) + except Exception: + LOG.error("Failure deleting temporary reverted LUN %s. " + "A manual deletion is required.", new_lun_name) + + def _clone_snapshot(self, snapshot_name): + """Returns the name of the LUN cloned from snapshot. + + Creates a LUN with same metadata as original LUN and then clones + from snapshot. If clone operation fails, the new LUN is deleted. + """ + snapshot_lun = self._get_lun_from_table(snapshot_name) + snapshot_path = snapshot_lun.metadata["Path"] + lun_name = snapshot_path.split("/")[-1] + flexvol_name = snapshot_path.split("/")[2] + + LOG.info("Cloning LUN %s from snapshot %s in volume %s.", lun_name, + snapshot_name, flexvol_name) + + metadata = snapshot_lun.metadata + + block_count = self._get_lun_block_count(snapshot_path) + if block_count == 0: + msg = _("%s cannot be reverted using clone operation" + " as it contains no blocks.") + raise exception.VolumeBackendAPIException(data=msg % snapshot_name) + + new_snap_name = "new-%s" % snapshot_name + + self.zapi_client.create_lun( + flexvol_name, new_snap_name, + six.text_type(snapshot_lun.size), metadata) + try: + self._clone_lun(snapshot_name, new_snap_name, + block_count=block_count) + return new_snap_name + except Exception: + with excutils.save_and_reraise_exception(): + try: + new_lun_path = "/vol/%s/%s" % (flexvol_name, new_snap_name) + self.zapi_client.destroy_lun(new_lun_path) + except Exception: + LOG.error("Failure deleting temporary reverted LUN %s. " + "A manual deletion is required.", new_snap_name) + + def _swap_luns(self, original_lun, new_lun, flexvol_name): + """Swaps cloned and original LUNs using a temporary LUN. + + Moves the original LUN to a temporary path, then moves the cloned LUN + to the original path (if this fails, moves the temporary LUN back as + original LUN) and finally destroys the LUN with temporary path. + """ + tmp_lun = "tmp-%s" % original_lun + + original_path = "/vol/%s/%s" % (flexvol_name, original_lun) + tmp_path = "/vol/%s/%s" % (flexvol_name, tmp_lun) + new_path = "/vol/%s/%s" % (flexvol_name, new_lun) + + LOG.debug("Original Path: %s.", original_path) + LOG.debug("Temporary Path: %s.", tmp_path) + LOG.debug("New Path %s.", new_path) + + try: + self.zapi_client.move_lun(original_path, tmp_path) + except Exception: + msg = _("Failure moving original LUN from %s to %s." % + (original_path, tmp_path)) + raise exception.VolumeBackendAPIException(data=msg) + + try: + self.zapi_client.move_lun(new_path, original_path) + except Exception: + LOG.debug("Move temporary reverted LUN failed. Moving back " + "original LUN to original path.") + try: + self.zapi_client.move_lun(tmp_path, original_path) + except Exception: + LOG.error("Could not move original LUN path from %s to %s. " + "Cinder may lose the volume management. Please, you " + "should move it back manually.", + tmp_path, original_path) + + msg = _("Failure moving temporary reverted LUN from %s to %s.") + raise exception.VolumeBackendAPIException( + data=msg % (new_path, original_path)) + try: + self.zapi_client.destroy_lun(tmp_path) + except Exception: + LOG.error("Failure deleting old LUN %s. A manual deletion " + "is required.", tmp_lun) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index aa3e654cd90..4ac04cdbaa0 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -1696,6 +1696,17 @@ class Client(client_base.Client): } self.connection.send_request('volume-rename', api_args) + def rename_file(self, orig_file_name, new_file_name): + """Rename a volume file.""" + LOG.debug("Renaming the file %(original)s to %(new)s.", + {'original': orig_file_name, 'new': new_file_name}) + + api_args = { + 'from-path': orig_file_name, + 'to-path': new_file_name, + } + self.connection.send_request('file-rename-file', api_args) + def mount_flexvol(self, flexvol_name, junction_path=None): """Mounts a volume on a junction path.""" api_args = { diff --git a/cinder/volume/drivers/netapp/dataontap/fc_cmode.py b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py index f8b6d39df39..cfb64ff159a 100644 --- a/cinder/volume/drivers/netapp/dataontap/fc_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/fc_cmode.py @@ -35,6 +35,7 @@ class NetAppCmodeFibreChannelDriver(driver.BaseVD, 1.0.0 - Driver development before Wallaby 2.0.0 - Wallaby driver version bump 3.0.0 - Add support for Intra-cluster Storage assisted volume migration + Add support for revert to snapshot """ @@ -155,3 +156,6 @@ class NetAppCmodeFibreChannelDriver(driver.BaseVD, def migrate_volume(self, context, volume, host): return self.library.migrate_volume(context, volume, host) + + def revert_to_snapshot(self, context, volume, snapshot): + return self.library.revert_to_snapshot(volume, snapshot) diff --git a/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py index 6d4443ef54b..f581ec3dad7 100644 --- a/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/iscsi_cmode.py @@ -138,3 +138,6 @@ class NetAppCmodeISCSIDriver(driver.BaseVD, def migrate_volume(self, context, volume, host): return self.library.migrate_volume(context, volume, host) + + def revert_to_snapshot(self, context, volume, snapshot): + return self.library.revert_to_snapshot(volume, snapshot) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index 541ed1b1f27..3b8a32266d2 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -392,6 +392,19 @@ class NetAppNfsDriver(driver.ManageableVD, return nfs_server_ip + ':' + export_path + def revert_to_snapshot(self, context, volume, snapshot): + """Revert a volume to a given snapshot. + + For a FlexGroup pool, the operation relies on the NFS generic driver + because the ONTAP clone file is not supported by FlexGroup yet. + """ + if (self._is_flexgroup(vol_id=snapshot['volume_id']) and + not self._is_flexgroup_clone_file_supported()): + super(NetAppNfsDriver, self).revert_to_snapshot(context, volume, + snapshot) + else: + self._revert_to_snapshot(volume, snapshot) + def _clone_backing_file_for_volume(self, volume_name, clone_name, volume_id, share=None, is_snapshot=False, @@ -399,6 +412,9 @@ class NetAppNfsDriver(driver.ManageableVD, """Clone backing file for Cinder volume.""" raise NotImplementedError() + def _revert_to_snapshot(self, volume, snapshot): + raise NotImplementedError() + def _is_flexgroup(self, vol_id=None, host=None): """Discover if a given volume is a FlexGroup or not""" raise NotImplementedError() diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index 6ed2d073c97..7b1cbbdddd8 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -63,6 +63,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, Add support for dynamic Adaptive QoS policy group creation Implement FlexGroup pool 3.0.0 - Add support for Intra-cluster Storage assisted volume migration + Add support for revert to snapshot """ @@ -224,6 +225,80 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, qos_policy_group_is_adaptive, target_path) + def _revert_to_snapshot(self, volume, snapshot): + """Clone volume from snapshot to perform the file name swap.""" + new_snap_name = 'new-%s' % snapshot['name'] + self._clone_backing_file_for_volume(snapshot['name'], + new_snap_name, + snapshot['volume_id'], + is_snapshot=False) + + (host_ip, junction_path) = self._get_export_ip_path( + volume_id=volume['id']) + vserver = self._get_vserver_for_ip(host_ip) + flexvol_name = self.zapi_client.get_vol_by_junc_vserver(vserver, + junction_path) + + try: + self._swap_files(flexvol_name, volume['name'], new_snap_name) + except Exception: + LOG.error("Swapping temporary reverted volume name from %s to %s " + "failed.", new_snap_name, volume['name']) + with excutils.save_and_reraise_exception(): + try: + LOG.debug("Deleting temporary reverted volume file %s.", + new_snap_name) + file_path = '/vol/%s/%s' % (flexvol_name, new_snap_name) + self.zapi_client.delete_file(file_path) + except Exception: + LOG.error("Could not delete temporary reverted volume %s. " + "A manual deletion is required.", new_snap_name) + + def _swap_files(self, flexvol_name, original_file, new_file): + """Swaps cloned and original files using a temporary file. + + Renames the original file path to a temporary path, then changes the + cloned file path to the original path (if this fails, change the + temporary file path back as original path) and finally deletes the + file with temporary path. + """ + prefix_path_on_backend = '/vol/' + flexvol_name + '/' + + new_file_path = prefix_path_on_backend + new_file + original_file_path = prefix_path_on_backend + original_file + tmp_file_path = prefix_path_on_backend + 'tmp-%s' % original_file + + try: + self.zapi_client.rename_file(original_file_path, tmp_file_path) + except exception.VolumeBackendAPIException: + msg = _("Could not rename original volume from %s to %s.") + raise na_utils.NetAppDriverException(msg % (original_file_path, + tmp_file_path)) + + try: + self.zapi_client.rename_file(new_file_path, original_file_path) + except exception.VolumeBackendAPIException: + try: + LOG.debug("Revert volume failed. Rolling back to its original" + " name.") + self.zapi_client.rename_file(tmp_file_path, original_file_path) + except exception.VolumeBackendAPIException: + LOG.error("Could not rollback original volume name from %s " + "to %s. Cinder may lose the volume management. " + "Please, you should rename it back manually.", + tmp_file_path, original_file_path) + + msg = _("Could not rename temporary reverted volume from %s " + "to original volume name %s.") + raise na_utils.NetAppDriverException(msg % (new_file_path, + original_file_path)) + + try: + self.zapi_client.delete_file(tmp_file_path) + except exception.VolumeBackendAPIException: + LOG.error("Could not delete old volume %s. A manual deletion is " + "required.", tmp_file_path) + def _clone_backing_file_for_volume(self, volume_name, clone_name, volume_id, share=None, is_snapshot=False, diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 0c1002098fb..b64ca67fd40 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -903,7 +903,7 @@ driver.linbit_linstor=missing driver.lvm=complete driver.macrosan=missing driver.nec=complete -driver.netapp_ontap=missing +driver.netapp_ontap=complete driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing diff --git a/releasenotes/notes/netapp-ontap-add-revert-to-snapshot-ce20810bcf094fce.yaml b/releasenotes/notes/netapp-ontap-add-revert-to-snapshot-ce20810bcf094fce.yaml new file mode 100644 index 00000000000..4d92a3e17c3 --- /dev/null +++ b/releasenotes/notes/netapp-ontap-add-revert-to-snapshot-ce20810bcf094fce.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + NetApp ONTAP driver: Added support to Revert to Snapshot for the iSCSI, FC + and NFS drivers with FlexVol pool. This feature does not support FlexGroups + and is limited to revert only to the most recent snapshot of a given + Cinder volume.