From 389188c5ea9c048af927297dea08a8c9cc9506f6 Mon Sep 17 00:00:00 2001 From: Chuck Fouts Date: Thu, 19 May 2016 20:41:07 -0400 Subject: [PATCH] NetApp: Add Consistency Group Support for NFS Adds consistency group support for the NetApp NFS driver. Implements: blueprint netapp-nfs-consistencygroup-support DocImpact Change-Id: I79369e3817f8345b29b90179279de1d7c7f1ca0a --- .../dataontap/client/test_client_7mode.py | 7 +- .../dataontap/client/test_client_base.py | 32 ++++ .../dataontap/client/test_client_cmode.py | 17 +- .../volume/drivers/netapp/dataontap/fakes.py | 5 +- .../netapp/dataontap/test_block_base.py | 16 +- .../netapp/dataontap/test_nfs_7mode.py | 32 +++- .../drivers/netapp/dataontap/test_nfs_base.py | 181 ++++++++++++++++-- .../netapp/dataontap/test_nfs_cmode.py | 80 +++++--- .../drivers/netapp/dataontap/block_base.py | 23 +-- .../netapp/dataontap/client/client_7mode.py | 15 +- .../netapp/dataontap/client/client_base.py | 24 +++ .../netapp/dataontap/client/client_cmode.py | 13 +- .../drivers/netapp/dataontap/nfs_7mode.py | 30 ++- .../drivers/netapp/dataontap/nfs_base.py | 159 ++++++++++++++- .../drivers/netapp/dataontap/nfs_cmode.py | 63 ++++-- ...stency-group-support-83eccc2da91ee19b.yaml | 3 + 16 files changed, 577 insertions(+), 123 deletions(-) create mode 100644 releasenotes/notes/netapp-nfs-consistency-group-support-83eccc2da91ee19b.yaml diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py index f47388b9c2e..0323da07275 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py @@ -507,7 +507,9 @@ class NetApp7modeClientTestCase(test.TestCase): self.connection.invoke_successfully.side_effect = [ fake_clone_id_response, fake_clone_list_response] - self.client.clone_file(expected_src_path, expected_dest_path) + self.client.clone_file(expected_src_path, + expected_dest_path, + source_snapshot=fake.CG_SNAPSHOT_ID) __, _args, _kwargs = self.connection.invoke_successfully.mock_calls[0] actual_request = _args[0] @@ -519,6 +521,9 @@ class NetApp7modeClientTestCase(test.TestCase): self.assertEqual(expected_src_path, actual_src_path) self.assertEqual(expected_dest_path, actual_dest_path) + self.assertEqual( + fake.CG_SNAPSHOT_ID, + actual_request.get_child_by_name('snapshot-name').get_content()) self.assertEqual(actual_request.get_child_by_name( 'destination-exists'), None) self.assertTrue(enable_tunneling) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py index f6db0a7318b..4fefb80750c 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -19,6 +19,7 @@ import uuid from lxml import etree import mock import six +import time from cinder import exception from cinder import test @@ -537,6 +538,15 @@ class NetAppBaseClientTestCase(test.TestCase): self.client._commit_cg_snapshot.assert_called_once_with( fake.CONSISTENCY_GROUP_ID) + def test_create_cg_snapshot_no_id(self): + self.mock_object(self.client, '_start_cg_snapshot', mock.Mock( + return_value=None)) + + self.assertRaises(exception.VolumeBackendAPIException, + self.client.create_cg_snapshot, + [fake.CG_VOLUME_NAME], + fake.CG_SNAPSHOT_NAME) + def test_start_cg_snapshot(self): snapshot_init = { 'snapshot': fake.CG_SNAPSHOT_NAME, @@ -559,3 +569,25 @@ class NetAppBaseClientTestCase(test.TestCase): self.client.send_request.assert_called_once_with( 'cg-commit', {'cg-id': snapshot_commit['cg-id']}) + + def test_wait_for_busy_snapshot_raise_exception(self): + BUSY_SNAPSHOT = dict(fake.SNAPSHOT) + BUSY_SNAPSHOT['busy'] = True + + # Need to mock sleep as it is called by @utils.retry + self.mock_object(time, 'sleep') + mock_get_snapshot = self.mock_object( + self.client, 'get_snapshot', + mock.Mock(return_value=BUSY_SNAPSHOT) + ) + + self.assertRaises(exception.SnapshotIsBusy, + self.client.wait_for_busy_snapshot, + fake.FLEXVOL, fake.SNAPSHOT_NAME) + + calls = [ + mock.call(fake.FLEXVOL, fake.SNAPSHOT_NAME), + mock.call(fake.FLEXVOL, fake.SNAPSHOT_NAME), + mock.call(fake.FLEXVOL, fake.SNAPSHOT_NAME), + ] + mock_get_snapshot.assert_has_calls(calls) 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 7b493c7ebf3..e9fe65d103e 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 @@ -999,7 +999,8 @@ class NetAppCmodeClientTestCase(test.TestCase): self.connection.get_api_version.return_value = (1, 20) self.client.clone_file(expected_flex_vol, expected_src_path, - expected_dest_path, self.vserver) + expected_dest_path, self.vserver, + source_snapshot=fake.CG_SNAPSHOT_ID) __, _args, __ = self.connection.invoke_successfully.mock_calls[0] actual_request = _args[0] @@ -1013,6 +1014,9 @@ class NetAppCmodeClientTestCase(test.TestCase): self.assertEqual(expected_flex_vol, actual_flex_vol) self.assertEqual(expected_src_path, actual_src_path) self.assertEqual(expected_dest_path, actual_dest_path) + req_snapshot_child = actual_request.get_child_by_name('snapshot-name') + self.assertEqual(fake.CG_SNAPSHOT_ID, req_snapshot_child.get_content()) + self.assertEqual(actual_request.get_child_by_name( 'destination-exists'), None) @@ -3147,3 +3151,14 @@ class NetAppCmodeClientTestCase(test.TestCase): fake_client.VOLUME_NAME) self.assertEqual(expected_prov_opts, actual_prov_opts) + + def test_wait_for_busy_snapshot(self): + mock_get_snapshot = self.mock_object( + self.client, 'get_snapshot', + mock.Mock(return_value=fake.SNAPSHOT) + ) + + self.client.wait_for_busy_snapshot(fake.FLEXVOL, fake.SNAPSHOT_NAME) + + mock_get_snapshot.assert_called_once_with(fake.FLEXVOL, + fake.SNAPSHOT_NAME) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index dbbac893ca0..2b4ca3e8c5b 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -387,6 +387,7 @@ FAKE_7MODE_POOLS = [ CG_VOLUME_NAME = 'fake_cg_volume' CG_GROUP_NAME = 'fake_consistency_group' +CG_POOL_NAME = 'cdot' SOURCE_CG_VOLUME_NAME = 'fake_source_cg_volume' CG_VOLUME_ID = 'fake_cg_volume_id' CG_VOLUME_SIZE = 100 @@ -419,7 +420,7 @@ CG_VOLUME = { 'name': CG_VOLUME_NAME, 'size': 100, 'id': CG_VOLUME_ID, - 'host': 'hostname@backend#cdot', + 'host': 'hostname@backend#' + CG_POOL_NAME, 'consistencygroup_id': CONSISTENCY_GROUP_ID, 'status': 'fake_status', } @@ -435,6 +436,8 @@ CONSISTENCY_GROUP = { 'name': CG_GROUP_NAME, } +CG_CONTEXT = {} + CG_SNAPSHOT = { 'id': CG_SNAPSHOT_ID, 'name': CG_SNAPSHOT_NAME, diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py index 253bda3b93f..5ac3f94bbd7 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py @@ -1370,7 +1370,8 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=fake.POOL_NAME)) mock_clone_lun = self.mock_object(self.library, '_clone_lun') - mock_busy = self.mock_object(self.library, '_handle_busy_snapshot') + mock_busy = self.mock_object( + self.zapi_client, 'wait_for_busy_snapshot') self.library.create_cgsnapshot(fake.CG_SNAPSHOT, [snapshot]) @@ -1499,16 +1500,3 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): } mock_clone_source_to_destination.assert_called_once_with( clone_source_to_destination_args, fake.VOLUME) - - def test_handle_busy_snapshot(self): - self.mock_object(block_base, 'LOG') - mock_get_snapshot = self.mock_object( - self.zapi_client, 'get_snapshot', - mock.Mock(return_value=fake.SNAPSHOT) - ) - - self.library._handle_busy_snapshot(fake.FLEXVOL, fake.SNAPSHOT_NAME) - - self.assertEqual(1, block_base.LOG.info.call_count) - mock_get_snapshot.assert_called_once_with(fake.FLEXVOL, - fake.SNAPSHOT_NAME) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py index 82f62b5f081..5dd7e6e836f 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py @@ -81,7 +81,8 @@ class NetApp7modeNfsDriverTestCase(test.TestCase): mock_get_actual_path_for_export.assert_called_once_with( fake.EXPORT_PATH) self.driver.zapi_client.clone_file.assert_called_once_with( - 'fake_path/' + fake.FLEXVOL, 'fake_path/fake_clone') + 'fake_path/' + fake.FLEXVOL, 'fake_path/fake_clone', + None) @ddt.data({'nfs_sparsed_volumes': True}, {'nfs_sparsed_volumes': False}) @@ -115,6 +116,7 @@ class NetApp7modeNfsDriverTestCase(test.TestCase): expected = [{'pool_name': '192.168.99.24:/fake/export/path', 'QoS_support': False, + 'consistencygroup_support': True, 'thick_provisioning_support': thick, 'thin_provisioning_support': not thick, 'free_capacity_gb': 12.0, @@ -171,3 +173,31 @@ class NetApp7modeNfsDriverTestCase(test.TestCase): fake.NFS_SHARE) self.assertEqual(expected, result) + + def test_delete_cgsnapshot(self): + mock_delete_file = self.mock_object(self.driver, '_delete_file') + + model_update, snapshots_model_update = ( + self.driver.delete_cgsnapshot( + fake.CG_CONTEXT, fake.CG_SNAPSHOT, [fake.SNAPSHOT])) + + mock_delete_file.assert_called_once_with( + fake.SNAPSHOT['volume_id'], fake.SNAPSHOT['name']) + self.assertIsNone(model_update) + self.assertIsNone(snapshots_model_update) + + def test_get_snapshot_backing_flexvol_names(self): + snapshots = [ + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume1'}}, + {'volume': {'host': 'hostA@192.168.1.01#/fake/volume2'}}, + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume3'}}, + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume1'}}, + ] + + hosts = [snap['volume']['host'] for snap in snapshots] + flexvols = self.driver._get_backing_flexvol_names(hosts) + + self.assertEqual(3, len(flexvols)) + self.assertIn('volume1', flexvols) + self.assertIn('volume2', flexvols) + self.assertIn('volume3', flexvols) 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 9e68320da92..4f8d5f1ee11 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 @@ -69,6 +69,9 @@ class NetAppNfsDriverTestCase(test.TestCase): self.driver = nfs_base.NetAppNfsDriver(**kwargs) self.driver.db = mock.Mock() + self.driver.zapi_client = mock.Mock() + self.zapi_client = self.driver.zapi_client + @mock.patch.object(nfs.NfsDriver, 'do_setup') @mock.patch.object(na_utils, 'check_flags') def test_do_setup(self, mock_check_flags, mock_super_do_setup): @@ -125,7 +128,6 @@ class NetAppNfsDriverTestCase(test.TestCase): def test_get_capacity_info_ipv4_share(self): expected = fake.CAPACITY_VALUES - self.driver.zapi_client = mock.Mock() get_capacity = self.driver.zapi_client.get_flexvol_capacity get_capacity.return_value = fake.CAPACITIES @@ -137,7 +139,6 @@ class NetAppNfsDriverTestCase(test.TestCase): def test_get_capacity_info_ipv6_share(self): expected = fake.CAPACITY_VALUES - self.driver.zapi_client = mock.Mock() get_capacity = self.driver.zapi_client.get_flexvol_capacity get_capacity.return_value = fake.CAPACITIES @@ -322,8 +323,7 @@ class NetAppNfsDriverTestCase(test.TestCase): fake.SNAPSHOT['volume_name'], fake.SNAPSHOT['name'], fake.SNAPSHOT['volume_id'], is_snapshot=True) - @ddt.data(True, False) - def test_delete_snapshot(self, volume_present): + def test_delete_snapshot(self): updates = { 'name': fake.SNAPSHOT_NAME, 'volume_size': fake.SIZE, @@ -332,24 +332,12 @@ class NetAppNfsDriverTestCase(test.TestCase): 'busy': False, } snapshot = fake_snapshot.fake_snapshot_obj(self.ctxt, **updates) - self.mock_object(self.driver, '_get_provider_location', - mock.Mock(return_value=fake.SNAPSHOT_MOUNT)) - self.mock_object(self.driver, '_volume_not_present', - mock.Mock(return_value=volume_present)) - self.mock_object(self.driver, '_execute') - self.mock_object(self.driver, '_get_volume_path', - mock.Mock(return_value='fake')) - self.driver._execute_as_root = True + self.mock_object(self.driver, '_delete_file') - retval = self.driver.delete_snapshot(snapshot) + self.driver.delete_snapshot(snapshot) - if volume_present: - self.assertTrue(retval) - self.driver._execute.assert_not_called() - else: - self.assertIsNone(retval) - self.driver._execute.assert_called_once_with( - 'rm', 'fake', run_as_root=True) + self.driver._delete_file.assert_called_once_with(snapshot.volume_id, + snapshot.name) def test__get_volume_location(self): volume_id = fake.VOLUME_ID @@ -931,3 +919,156 @@ class NetAppNfsDriverTestCase(test.TestCase): self.driver.manage_existing_get_size, volume, vol_ref) + + def test_create_consistency_group(self): + model_update = self.driver.create_consistencygroup( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP) + self.assertEqual('available', model_update['status']) + + @ddt.data(True, False) + def test_delete_file(self, volume_not_present): + mock_get_provider_location = self.mock_object( + self.driver, '_get_provider_location') + mock_get_provider_location.return_value = fake.NFS_SHARE + mock_volume_not_present = self.mock_object( + self.driver, '_volume_not_present') + mock_volume_not_present.return_value = volume_not_present + mock_get_volume_path = self.mock_object( + self.driver, '_get_volume_path') + mock_get_volume_path.return_value = fake.PATH + mock_delete = self.mock_object(self.driver, '_delete') + + self.driver._delete_file(fake.CG_VOLUME_ID, fake.CG_VOLUME_NAME) + + mock_get_provider_location.assert_called_once_with(fake.CG_VOLUME_ID) + mock_volume_not_present.assert_called_once_with( + fake.NFS_SHARE, fake.CG_VOLUME_NAME) + if not volume_not_present: + mock_get_volume_path.assert_called_once_with( + fake.NFS_SHARE, fake.CG_VOLUME_NAME) + mock_delete.assert_called_once_with(fake.PATH) + + def test_delete_file_volume_not_present(self): + mock_get_provider_location = self.mock_object( + self.driver, '_get_provider_location') + mock_get_provider_location.return_value = fake.NFS_SHARE + mock_volume_not_present = self.mock_object( + self.driver, '_volume_not_present') + mock_volume_not_present.return_value = True + mock_get_volume_path = self.mock_object( + self.driver, '_get_volume_path') + mock_delete = self.mock_object(self.driver, '_delete') + + self.driver._delete_file(fake.CG_VOLUME_ID, fake.CG_VOLUME_NAME) + + mock_get_provider_location.assert_called_once_with(fake.CG_VOLUME_ID) + mock_volume_not_present.assert_called_once_with( + fake.NFS_SHARE, fake.CG_VOLUME_NAME) + mock_get_volume_path.assert_not_called() + mock_delete.assert_not_called() + + def test_update_consistencygroup(self): + model_update, add_volumes_update, remove_volumes_update = ( + self.driver.update_consistencygroup(fake.CG_CONTEXT, "foo")) + self.assertIsNone(add_volumes_update) + self.assertIsNone(remove_volumes_update) + + def test_create_consistencygroup_from_src(self): + mock_create_volume_from_snapshot = self.mock_object( + self.driver, 'create_volume_from_snapshot') + + model_update, volumes_model_update = ( + self.driver.create_consistencygroup_from_src( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP, [fake.VOLUME], + cgsnapshot=fake.CG_SNAPSHOT, snapshots=[fake.SNAPSHOT])) + + mock_create_volume_from_snapshot.assert_called_once_with( + fake.VOLUME, fake.SNAPSHOT) + self.assertIsNone(model_update) + self.assertIsNone(volumes_model_update) + + def test_create_consistencygroup_from_src_source_vols(self): + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_backing_flexvol_names') + mock_get_snapshot_flexvols.return_value = (set([fake.CG_POOL_NAME])) + mock_clone_backing_file = self.mock_object( + self.driver, '_clone_backing_file_for_volume') + fake_snapshot_name = 'snapshot-temp-' + fake.CONSISTENCY_GROUP['id'] + mock_busy = self.mock_object( + self.driver.zapi_client, 'wait_for_busy_snapshot') + + model_update, volumes_model_update = ( + self.driver.create_consistencygroup_from_src( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP, [fake.VOLUME], + source_cg=fake.CONSISTENCY_GROUP, + source_vols=[fake.CG_VOLUME])) + + mock_get_snapshot_flexvols.assert_called_once_with( + [fake.CG_VOLUME['host']]) + self.driver.zapi_client.create_cg_snapshot.assert_called_once_with( + set([fake.CG_POOL_NAME]), fake_snapshot_name) + mock_clone_backing_file.assert_called_once_with( + fake.CG_VOLUME['name'], fake.VOLUME['name'], fake.CG_VOLUME['id'], + source_snapshot=fake_snapshot_name) + mock_busy.assert_called_once_with( + fake.CG_POOL_NAME, fake_snapshot_name) + self.driver.zapi_client.delete_snapshot.assert_called_once_with( + fake.CG_POOL_NAME, fake_snapshot_name) + self.assertIsNone(model_update) + self.assertIsNone(volumes_model_update) + + def test_create_consistencygroup_from_src_invalid_parms(self): + + model_update, volumes_model_update = ( + self.driver.create_consistencygroup_from_src( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP, [fake.VOLUME])) + + self.assertIn('error', model_update['status']) + + def test_create_cgsnapshot(self): + snapshot = fake.CG_SNAPSHOT + snapshot['volume'] = fake.CG_VOLUME + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_backing_flexvol_names') + mock_get_snapshot_flexvols.return_value = (set([fake.CG_POOL_NAME])) + mock_clone_backing_file = self.mock_object( + self.driver, '_clone_backing_file_for_volume') + mock_busy = self.mock_object( + self.driver.zapi_client, 'wait_for_busy_snapshot') + + self.driver.create_cgsnapshot( + fake.CG_CONTEXT, fake.CG_SNAPSHOT, [snapshot]) + + mock_get_snapshot_flexvols.assert_called_once_with( + [snapshot['volume']['host']]) + self.driver.zapi_client.create_cg_snapshot.assert_called_once_with( + set([fake.CG_POOL_NAME]), fake.CG_SNAPSHOT_ID) + mock_clone_backing_file.assert_called_once_with( + snapshot['volume']['name'], snapshot['name'], + snapshot['volume']['id'], source_snapshot=fake.CG_SNAPSHOT_ID) + mock_busy.assert_called_once_with( + fake.CG_POOL_NAME, fake.CG_SNAPSHOT_ID) + self.driver.zapi_client.delete_snapshot.assert_called_once_with( + fake.CG_POOL_NAME, fake.CG_SNAPSHOT_ID) + + def test_delete_consistencygroup_volume_delete_failure(self): + self.mock_object(self.driver, '_delete_file', + mock.Mock(side_effect=Exception)) + + model_update, volumes = self.driver.delete_consistencygroup( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP, [fake.CG_VOLUME]) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('error_deleting', volumes[0]['status']) + + def test_delete_consistencygroup(self): + mock_delete_file = self.mock_object( + self.driver, '_delete_file') + + model_update, volumes = self.driver.delete_consistencygroup( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP, [fake.CG_VOLUME]) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('deleted', volumes[0]['status']) + mock_delete_file.assert_called_once_with( + fake.CG_VOLUME_ID, fake.CG_VOLUME_NAME) 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 2cdb6c0f233..30a2a5595aa 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 @@ -158,6 +158,7 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): 'netapp_aggregate': 'aggr1', 'netapp_raid_type': 'raid_dp', 'netapp_disk_type': 'SSD', + 'consistencygroup_support': True, }, } mock_get_ssc = self.mock_object(self.driver.ssc_library, @@ -221,6 +222,7 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): 'netapp_aggregate': 'aggr1', 'netapp_raid_type': 'raid_dp', 'netapp_disk_type': 'SSD', + 'consistencygroup_support': True, }] self.assertEqual(expected, result) @@ -471,22 +473,21 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): assert_called_once_with('fake_qos_policy_group_info')) def test_delete_backing_file_for_volume(self): - mock_filer_delete = self.mock_object(self.driver, - '_delete_volume_on_filer') + mock_filer_delete = self.mock_object(self.driver, '_delete_file') mock_super_delete = self.mock_object(nfs_base.NetAppNfsDriver, 'delete_volume') self.driver._delete_backing_file_for_volume(fake.NFS_VOLUME) - mock_filer_delete.assert_called_once_with(fake.NFS_VOLUME) + mock_filer_delete.assert_called_once_with( + fake.NFS_VOLUME['id'], fake.NFS_VOLUME['name']) self.assertEqual(0, mock_super_delete.call_count) @ddt.data(True, False) def test_delete_backing_file_for_volume_exception_path(self, super_exc): mock_exception_log = self.mock_object(nfs_cmode.LOG, 'exception') exception_call_count = 2 if super_exc else 1 - mock_filer_delete = self.mock_object(self.driver, - '_delete_volume_on_filer') + mock_filer_delete = self.mock_object(self.driver, '_delete_file') mock_filer_delete.side_effect = [Exception] mock_super_delete = self.mock_object(nfs_base.NetAppNfsDriver, 'delete_volume') @@ -495,20 +496,11 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.driver._delete_backing_file_for_volume(fake.NFS_VOLUME) - mock_filer_delete.assert_called_once_with(fake.NFS_VOLUME) + mock_filer_delete.assert_called_once_with( + fake.NFS_VOLUME['id'], fake.NFS_VOLUME['name']) mock_super_delete.assert_called_once_with(fake.NFS_VOLUME) self.assertEqual(exception_call_count, mock_exception_log.call_count) - def test_delete_volume_on_filer(self): - mock_get_vs_ip = self.mock_object(self.driver, '_get_export_ip_path') - mock_get_vs_ip.return_value = (fake.VSERVER_NAME, '/%s' % fake.FLEXVOL) - mock_zapi_delete = self.driver.zapi_client.delete_file - - self.driver._delete_volume_on_filer(fake.NFS_VOLUME) - - mock_zapi_delete.assert_called_once_with( - '/vol/%s/%s' % (fake.FLEXVOL, fake.NFS_VOLUME['name'])) - def test_delete_snapshot(self): mock_get_location = self.mock_object(self.driver, '_get_provider_location') @@ -521,22 +513,21 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): mock_delete_backing.assert_called_once_with(fake.test_snapshot) def test_delete_backing_file_for_snapshot(self): - mock_filer_delete = self.mock_object( - self.driver, '_delete_snapshot_on_filer') + mock_filer_delete = self.mock_object(self.driver, '_delete_file') mock_super_delete = self.mock_object(nfs_base.NetAppNfsDriver, 'delete_snapshot') self.driver._delete_backing_file_for_snapshot(fake.test_snapshot) - mock_filer_delete.assert_called_once_with(fake.test_snapshot) + mock_filer_delete.assert_called_once_with( + fake.test_snapshot['volume_id'], fake.test_snapshot['name']) self.assertEqual(0, mock_super_delete.call_count) @ddt.data(True, False) def test_delete_backing_file_for_snapshot_exception_path(self, super_exc): mock_exception_log = self.mock_object(nfs_cmode.LOG, 'exception') exception_call_count = 2 if super_exc else 1 - mock_filer_delete = self.mock_object( - self.driver, '_delete_snapshot_on_filer') + mock_filer_delete = self.mock_object(self.driver, '_delete_file') mock_filer_delete.side_effect = [Exception] mock_super_delete = self.mock_object(nfs_base.NetAppNfsDriver, 'delete_snapshot') @@ -545,16 +536,18 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.driver._delete_backing_file_for_snapshot(fake.test_snapshot) - mock_filer_delete.assert_called_once_with(fake.test_snapshot) + mock_filer_delete.assert_called_once_with( + fake.test_snapshot['volume_id'], fake.test_snapshot['name']) mock_super_delete.assert_called_once_with(fake.test_snapshot) self.assertEqual(exception_call_count, mock_exception_log.call_count) - def test_delete_snapshot_on_filer(self): + def test_delete_file(self): mock_get_vs_ip = self.mock_object(self.driver, '_get_export_ip_path') mock_get_vs_ip.return_value = (fake.VSERVER_NAME, '/%s' % fake.FLEXVOL) mock_zapi_delete = self.driver.zapi_client.delete_file - self.driver._delete_snapshot_on_filer(fake.test_snapshot) + self.driver._delete_file( + fake.test_snapshot['volume_id'], fake.test_snapshot['name']) mock_zapi_delete.assert_called_once_with( '/vol/%s/%s' % (fake.FLEXVOL, fake.test_snapshot['name'])) @@ -1341,3 +1334,42 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.assertEqual('dev1', self.driver.failed_over_backend_name) self.assertEqual('dev1', actual_active) self.assertEqual([], vol_updates) + + def test_delete_cgsnapshot(self): + mock_delete_backing_file = self.mock_object( + self.driver, '_delete_backing_file_for_snapshot') + snapshots = [fake.CG_SNAPSHOT] + + model_update, snapshots_model_update = ( + self.driver.delete_cgsnapshot( + fake.CG_CONTEXT, fake.CG_SNAPSHOT, snapshots)) + + mock_delete_backing_file.assert_called_once_with(fake.CG_SNAPSHOT) + self.assertIsNone(model_update) + self.assertIsNone(snapshots_model_update) + + def test_get_snapshot_backing_flexvol_names(self): + snapshots = [ + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume1'}}, + {'volume': {'host': 'hostA@192.168.1.01#/fake/volume2'}}, + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume3'}}, + {'volume': {'host': 'hostA@192.168.99.25#/fake/volume1'}}, + ] + + ssc = { + 'volume1': {'pool_name': '/fake/volume1', }, + 'volume2': {'pool_name': '/fake/volume2', }, + 'volume3': {'pool_name': '/fake/volume3', }, + } + + mock_get_ssc = self.mock_object(self.driver.ssc_library, 'get_ssc') + mock_get_ssc.return_value = ssc + + hosts = [snap['volume']['host'] for snap in snapshots] + flexvols = self.driver._get_backing_flexvol_names(hosts) + + mock_get_ssc.assert_called_once_with() + self.assertEqual(3, len(flexvols)) + self.assertIn('volume1', flexvols) + self.assertIn('volume2', flexvols) + self.assertIn('volume3', flexvols) diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index b736c9b1883..d9d1b3952d8 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -1081,32 +1081,11 @@ class NetAppBlockStorageLibrary(object): source_snapshot=cgsnapshot['id']) for flexvol in flexvols: - self._handle_busy_snapshot(flexvol, cgsnapshot['id']) + self.zapi_client.wait_for_busy_snapshot(flexvol, cgsnapshot['id']) self.zapi_client.delete_snapshot(flexvol, cgsnapshot['id']) return None, None - @utils.retry(exception.SnapshotIsBusy) - def _handle_busy_snapshot(self, flexvol, snapshot_name): - """Checks for and handles a busy snapshot. - - If a snapshot is not busy, take no action. If a snapshot is busy for - reasons other than a clone dependency, raise immediately. Otherwise, - since we always start a clone split operation after cloning a share, - wait up to a minute for a clone dependency to clear before giving up. - """ - snapshot = self.zapi_client.get_snapshot(flexvol, snapshot_name) - if not snapshot['busy']: - LOG.info(_LI("Backing consistency group snapshot %s " - "available for deletion"), snapshot_name) - return - else: - LOG.debug('Snapshot %(snap)s for vol %(vol)s is busy, waiting ' - 'for volume clone dependency to clear.', - {'snap': snapshot_name, 'vol': flexvol}) - - raise exception.SnapshotIsBusy(snapshot_name=snapshot_name) - def delete_cgsnapshot(self, cgsnapshot, snapshots): """Delete LUNs backing each snapshot in the cgsnapshot. diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py index 3aef869d1fb..90f7cbae08b 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py @@ -326,14 +326,19 @@ class Client(client_base.Client): raise exception.NotFound(_('No storage path found for export path %s') % (export_path)) - def clone_file(self, src_path, dest_path): + def clone_file(self, src_path, dest_path, source_snapshot=None): LOG.debug("Cloning with src %(src_path)s, dest %(dest_path)s", {'src_path': src_path, 'dest_path': dest_path}) + zapi_args = { + 'source-path': src_path, + 'destination-path': dest_path, + 'no-snap': 'true', + } + if source_snapshot: + zapi_args['snapshot-name'] = source_snapshot + clone_start = netapp_api.NaElement.create_node_with_children( - 'clone-start', - **{'source-path': src_path, - 'destination-path': dest_path, - 'no-snap': 'true'}) + 'clone-start', **zapi_args) result = self.connection.invoke_successfully(clone_start, enable_tunneling=True) clone_id_el = result.get_child_by_name('clone-id') diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index 797492b3074..858fc3e2f9e 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -433,3 +433,27 @@ class Client(object): def _commit_cg_snapshot(self, cg_id): snapshot_commit = {'cg-id': cg_id} self.send_request('cg-commit', snapshot_commit) + + def get_snapshot(self, volume_name, snapshot_name): + """Gets a single snapshot.""" + raise NotImplementedError() + + @utils.retry(exception.SnapshotIsBusy) + def wait_for_busy_snapshot(self, flexvol, snapshot_name): + """Checks for and handles a busy snapshot. + + If a snapshot is busy, for reasons other than cloning, an exception is + raised immediately. Otherwise, wait for a period of time for the clone + dependency to finish before giving up. If the snapshot is not busy then + no action is taken and the method exits. + """ + snapshot = self.get_snapshot(flexvol, snapshot_name) + if not snapshot['busy']: + LOG.debug("Backing consistency group snapshot %s available for " + "deletion.", snapshot_name) + return + else: + LOG.debug("Snapshot %(snap)s for vol %(vol)s is busy, waiting " + "for volume clone dependency to clear.", + {"snap": snapshot_name, "vol": flexvol}) + raise exception.SnapshotIsBusy(snapshot_name=snapshot_name) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index c2b1853853b..1e9464aa89d 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -633,13 +633,15 @@ class Client(client_base.Client): "%(junction)s ") % msg_fmt) def clone_file(self, flex_vol, src_path, dest_path, vserver, - dest_exists=False, is_snapshot=False): + dest_exists=False, source_snapshot=None, + is_snapshot=False): """Clones file on vserver.""" LOG.debug("Cloning with params volume %(volume)s, src %(src_path)s, " - "dest %(dest_path)s, vserver %(vserver)s", + "dest %(dest_path)s, vserver %(vserver)s," + "source_snapshot %(source_snapshot)s", {'volume': flex_vol, 'src_path': src_path, - 'dest_path': dest_path, 'vserver': vserver}) - + 'dest_path': dest_path, 'vserver': vserver, + 'source_snapshot': source_snapshot}) zapi_args = { 'volume': flex_vol, 'source-path': src_path, @@ -647,9 +649,10 @@ class Client(client_base.Client): } if is_snapshot and self.features.BACKUP_CLONE_PARAM: zapi_args['is-backup'] = 'true' + if source_snapshot: + zapi_args['snapshot-name'] = source_snapshot clone_create = netapp_api.NaElement.create_node_with_children( 'clone-create', **zapi_args) - major, minor = self.connection.get_api_version() if major == 1 and minor >= 20 and dest_exists: clone_create.add_new_child('destination-exists', 'true') diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py index b43a53a88ff..aaa98317222 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py @@ -35,6 +35,7 @@ from cinder.volume.drivers.netapp.dataontap import nfs_base from cinder.volume.drivers.netapp.dataontap.performance import perf_7mode from cinder.volume.drivers.netapp import options as na_opts from cinder.volume.drivers.netapp import utils as na_utils +from cinder.volume import utils as volume_utils LOG = logging.getLogger(__name__) @@ -80,17 +81,17 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): def _clone_backing_file_for_volume(self, volume_name, clone_name, volume_id, share=None, - is_snapshot=False): + is_snapshot=False, + source_snapshot=None): """Clone backing file for Cinder volume. :param: is_snapshot Not used, present for method signature consistency """ - (_host_ip, export_path) = self._get_export_ip_path(volume_id, share) storage_path = self.zapi_client.get_actual_path_for_export(export_path) target_path = '%s/%s' % (storage_path, clone_name) self.zapi_client.clone_file('%s/%s' % (storage_path, volume_name), - target_path) + target_path, source_snapshot) def _update_volume_stats(self): """Retrieve stats info from vserver.""" @@ -138,6 +139,7 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): pool['utilization'] = na_utils.round_down(utilization, '0.01') pool['filter_function'] = filter_function pool['goodness_function'] = goodness_function + pool['consistencygroup_support'] = True pools.append(pool) @@ -216,3 +218,25 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): """Set QoS policy on backend from volume type information.""" # 7-mode DOT does not support QoS. return + + def _get_backing_flexvol_names(self, hosts): + """Returns a set of flexvol names.""" + flexvols = set() + for host in hosts: + pool_name = volume_utils.extract_host(host, level='pool') + flexvol_name = pool_name.rsplit('/', 1)[1] + flexvols.add(flexvol_name) + return flexvols + + @utils.trace_method + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + """Delete files backing each snapshot in the cgsnapshot. + + :return: An implicit update of snapshot models that the manager will + interpret and subsequently set the model state to deleted. + """ + for snapshot in snapshots: + self._delete_file(snapshot['volume_id'], snapshot['name']) + LOG.debug("Snapshot %s deletion successful", snapshot['name']) + + return None, None diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index 3653348138e..462f4be2b8b 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -235,13 +235,19 @@ class NetAppNfsDriver(driver.ManageableVD, def delete_snapshot(self, snapshot): """Deletes a snapshot.""" - nfs_mount = self._get_provider_location(snapshot.volume_id) + self._delete_file(snapshot.volume_id, snapshot.name) - if self._volume_not_present(nfs_mount, snapshot.name): - return True + def _delete_file(self, file_id, file_name): + nfs_share = self._get_provider_location(file_id) - self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name), - run_as_root=self._execute_as_root) + if self._volume_not_present(nfs_share, file_name): + LOG.debug('File %(file_name)s not found when attempting to delete ' + 'from share %(share)s', + {'file_name': file_name, 'share': nfs_share}) + return + + path = self._get_volume_path(nfs_share, file_name) + self._delete(path) def _get_volume_location(self, volume_id): """Returns NFS mount address as :.""" @@ -251,10 +257,15 @@ class NetAppNfsDriver(driver.ManageableVD, def _clone_backing_file_for_volume(self, volume_name, clone_name, volume_id, share=None, - is_snapshot=False): + is_snapshot=False, + source_snapshot=None): """Clone backing file for Cinder volume.""" raise NotImplementedError() + def _get_backing_flexvol_names(self, hosts): + """Returns a set of flexvol names.""" + raise NotImplementedError() + def _get_provider_location(self, volume_id): """Returns provider location for given volume.""" volume = self.db.volume_get(self._context, volume_id) @@ -1001,3 +1012,139 @@ class NetAppNfsDriver(driver.ManageableVD, vol_path = os.path.join(volume['provider_location'], vol_str) LOG.info(_LI("Cinder NFS volume with current path \"%(cr)s\" is " "no longer being managed."), {'cr': vol_path}) + + @utils.trace_method + def create_consistencygroup(self, context, group): + """Driver entry point for creating a consistency group. + + ONTAP does not maintain an actual CG construct. As a result, no + communtication to the backend is necessary for consistency group + creation. + + :return: Hard-coded model update for consistency group model. + """ + model_update = {'status': 'available'} + return model_update + + @utils.trace_method + def delete_consistencygroup(self, context, group, volumes): + """Driver entry point for deleting a consistency group. + + :return: Updated consistency group model and list of volume models + for the volumes that were deleted. + """ + model_update = {'status': 'deleted'} + volumes_model_update = [] + for volume in volumes: + try: + self._delete_file(volume['id'], volume['name']) + volumes_model_update.append( + {'id': volume['id'], 'status': 'deleted'}) + except Exception: + volumes_model_update.append( + {'id': volume['id'], 'status': 'error_deleting'}) + LOG.exception(_LE("Volume %(vol)s in the consistency group " + "could not be deleted."), {'vol': volume}) + return model_update, volumes_model_update + + @utils.trace_method + def update_consistencygroup(self, context, group, add_volumes=None, + remove_volumes=None): + """Driver entry point for updating a consistency group. + + Since no actual CG construct is ever created in ONTAP, it is not + necessary to update any metadata on the backend. Since this is a NO-OP, + there is guaranteed to be no change in any of the volumes' statuses. + """ + return None, None, None + + @utils.trace_method + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + """Creates a Cinder cgsnapshot object. + + The Cinder cgsnapshot object is created by making use of an ONTAP CG + snapshot in order to provide write-order consistency for a set of + backing flexvols. First, a list of the flexvols backing the given + Cinder volumes in the CG is determined. An ONTAP CG snapshot of the + flexvols creates a write-order consistent snapshot of each backing + flexvol. For each Cinder volume in the CG, it is then necessary to + clone its volume from the ONTAP CG snapshot. The naming convention + used to create the clones indicates the clone's role as a Cinder + snapshot and its inclusion in a Cinder CG snapshot. The ONTAP CG + snapshots, of each backing flexvol, are deleted after the cloning + operation is completed. + + :return: An implicit update for the cgsnapshot and snapshot models that + is then used by the manager to set the models to available. + """ + + hosts = [snapshot['volume']['host'] for snapshot in snapshots] + flexvols = self._get_backing_flexvol_names(hosts) + + # Create snapshot for backing flexvol + self.zapi_client.create_cg_snapshot(flexvols, cgsnapshot['id']) + + # Start clone process for snapshot files + for snapshot in snapshots: + self._clone_backing_file_for_volume( + snapshot['volume']['name'], snapshot['name'], + snapshot['volume']['id'], source_snapshot=cgsnapshot['id']) + + # Delete backing flexvol snapshots + for flexvol_name in flexvols: + self.zapi_client.wait_for_busy_snapshot( + flexvol_name, cgsnapshot['id']) + self.zapi_client.delete_snapshot(flexvol_name, cgsnapshot['id']) + + return None, None + + @utils.trace_method + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + """Delete files backing each snapshot in the cgsnapshot.""" + raise NotImplementedError() + + @utils.trace_method + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + """Creates a CG from a either a cgsnapshot or group of cinder vols. + + :return: An implicit update for the volumes model that is + interpreted by the manager as a successful operation. + """ + LOG.debug("VOLUMES %s ", [dict(vol) for vol in volumes]) + model_update = None + + if cgsnapshot: + vols = zip(volumes, snapshots) + + for volume, snapshot in vols: + self.create_volume_from_snapshot(volume, snapshot) + + elif source_cg and source_vols: + hosts = [source_vol['host'] for source_vol in source_vols] + flexvols = self._get_backing_flexvol_names(hosts) + + # Create snapshot for backing flexvol + snapshot_name = 'snapshot-temp-' + source_cg['id'] + self.zapi_client.create_cg_snapshot(flexvols, snapshot_name) + + # Start clone process for new volumes + vols = zip(volumes, source_vols) + for volume, source_vol in vols: + self._clone_backing_file_for_volume( + source_vol['name'], volume['name'], + source_vol['id'], source_snapshot=snapshot_name) + + # Delete backing flexvol snapshots + for flexvol_name in flexvols: + self.zapi_client.wait_for_busy_snapshot( + flexvol_name, snapshot_name) + self.zapi_client.delete_snapshot(flexvol_name, snapshot_name) + else: + LOG.error(_LE("Unexpected set of parameters received when " + "creating consistency group from source.")) + model_update = {} + model_update['status'] = 'error' + + return model_update, None diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index 75c9a7c0b46..d4046e913d1 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -170,7 +170,8 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, def _clone_backing_file_for_volume(self, volume_name, clone_name, volume_id, share=None, - is_snapshot=False): + is_snapshot=False, + source_snapshot=None): """Clone backing file for Cinder volume.""" (vserver, exp_volume) = self._get_vserver_and_exp_vol(volume_id, share) self.zapi_client.clone_file(exp_volume, volume_name, clone_name, @@ -237,6 +238,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, # Add driver capabilities and config info pool['QoS_support'] = True + pool['consistencygroup_support'] = True # Add up-to-date capacity info nfs_share = ssc_vol_info['pool_name'] @@ -366,6 +368,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, return ssc_vol_name return None + @utils.trace_method def delete_volume(self, volume): """Deletes a logical volume.""" self._delete_backing_file_for_volume(volume) @@ -383,9 +386,9 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, """Deletes file on nfs share that backs a cinder volume.""" try: LOG.debug('Deleting backing file for volume %s.', volume['id']) - self._delete_volume_on_filer(volume) + self._delete_file(volume['id'], volume['name']) except Exception: - LOG.exception(_LE('Could not do delete of volume %s on filer, ' + LOG.exception(_LE('Could not delete volume %s on backend, ' 'falling back to exec of "rm" command.'), volume['id']) try: @@ -394,43 +397,35 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, LOG.exception(_LE('Exec of "rm" command on backing file for ' '%s was unsuccessful.'), volume['id']) - def _delete_volume_on_filer(self, volume): - (_vserver, flexvol) = self._get_export_ip_path(volume_id=volume['id']) - path_on_filer = '/vol' + flexvol + '/' + volume['name'] - LOG.debug('Attempting to delete backing file %s for volume %s on ' - 'filer.', path_on_filer, volume['id']) - self.zapi_client.delete_file(path_on_filer) + def _delete_file(self, file_id, file_name): + (_vserver, flexvol) = self._get_export_ip_path(volume_id=file_id) + path_on_backend = '/vol' + flexvol + '/' + file_name + LOG.debug('Attempting to delete file %(path)s for ID %(file_id)s on ' + 'backend.', {'path': path_on_backend, 'file_id': file_id}) + self.zapi_client.delete_file(path_on_backend) @utils.trace_method def delete_snapshot(self, snapshot): """Deletes a snapshot.""" self._delete_backing_file_for_snapshot(snapshot) - @utils.trace_method def _delete_backing_file_for_snapshot(self, snapshot): """Deletes file on nfs share that backs a cinder volume.""" try: LOG.debug('Deleting backing file for snapshot %s.', snapshot['id']) - self._delete_snapshot_on_filer(snapshot) + self._delete_file(snapshot['volume_id'], snapshot['name']) except Exception: - LOG.exception(_LE('Could not do delete of snapshot %s on filer, ' + LOG.exception(_LE('Could not delete snapshot %s on backend, ' 'falling back to exec of "rm" command.'), snapshot['id']) try: + # delete_file_from_share super(NetAppCmodeNfsDriver, self).delete_snapshot(snapshot) except Exception: LOG.exception(_LE('Exec of "rm" command on backing file for' ' %s was unsuccessful.'), snapshot['id']) @utils.trace_method - def _delete_snapshot_on_filer(self, snapshot): - (_vserver, flexvol) = self._get_export_ip_path( - volume_id=snapshot['volume_id']) - path_on_filer = '/vol' + flexvol + '/' + snapshot['name'] - LOG.debug('Attempting to delete backing file %s for snapshot %s ' - 'on filer.', path_on_filer, snapshot['id']) - self.zapi_client.delete_file(path_on_filer) - def copy_image_to_volume(self, context, volume, image_service, image_id): """Fetch the image from image_service and write it to the volume.""" copy_success = False @@ -653,6 +648,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, if os.path.exists(dst_img_local): self._delete_file_at_path(dst_img_local) + @utils.trace_method def unmanage(self, volume): """Removes the specified volume from Cinder management. @@ -678,3 +674,30 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, """Failover a backend to a secondary replication target.""" return self._failover_host(volumes, secondary_id=secondary_id) + + def _get_backing_flexvol_names(self, hosts): + """Returns a set of flexvol names.""" + flexvols = set() + ssc = self.ssc_library.get_ssc() + + for host in hosts: + pool_name = volume_utils.extract_host(host, level='pool') + + for flexvol_name, ssc_volume_data in ssc.items(): + if ssc_volume_data['pool_name'] == pool_name: + flexvols.add(flexvol_name) + + return flexvols + + @utils.trace_method + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + """Delete files backing each snapshot in the cgsnapshot. + + :return: An implicit update of snapshot models that the manager will + interpret and subsequently set the model state to deleted. + """ + for snapshot in snapshots: + self._delete_backing_file_for_snapshot(snapshot) + LOG.debug("Snapshot %s deletion successful", snapshot['name']) + + return None, None diff --git a/releasenotes/notes/netapp-nfs-consistency-group-support-83eccc2da91ee19b.yaml b/releasenotes/notes/netapp-nfs-consistency-group-support-83eccc2da91ee19b.yaml new file mode 100644 index 00000000000..4852b5d7c43 --- /dev/null +++ b/releasenotes/notes/netapp-nfs-consistency-group-support-83eccc2da91ee19b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added Cinder consistency group for the NetApp NFS driver.