From 0215fcc022d60608a0d887dd6510496ab2162f5b Mon Sep 17 00:00:00 2001 From: Chuck Fouts Date: Thu, 30 Mar 2017 15:09:19 -0400 Subject: [PATCH] NetApp cDOT: Support generic groups for file Adding support for generic volume groups to NetApp cDOT's NFS driver. CG methods are moved to the 7-mode NFS driver. Announcement was made in a previous release for the removal of the NetApp 7-mode drivers. Generic groups will only be implemented for the NetApp cDOT drivers. Change-Id: I5edf62ccfbbb7a5f3e64e0baebe9eb4a955cbdf5 Implements: blueprint netapp-add-generic-group-support-cdot --- .../volume/drivers/netapp/dataontap/fakes.py | 76 ++++++ .../netapp/dataontap/test_nfs_7mode.py | 161 +++++++++++++ .../drivers/netapp/dataontap/test_nfs_base.py | 181 -------------- .../netapp/dataontap/test_nfs_cmode.py | 225 +++++++++++++++++- .../drivers/netapp/dataontap/nfs_7mode.py | 149 ++++++++++++ .../drivers/netapp/dataontap/nfs_base.py | 152 ------------ .../drivers/netapp/dataontap/nfs_cmode.py | 168 ++++++++++++- ...c-group-support-cdot-9bebd13356694e13.yaml | 4 + 8 files changed, 775 insertions(+), 341 deletions(-) create mode 100644 releasenotes/notes/netapp-add-generic-group-support-cdot-9bebd13356694e13.yaml diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 3b9be0336fe..7732f4673bb 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -457,6 +457,82 @@ CG_VOLUME_SNAPSHOT = { } +VG_VOLUME_NAME = 'fake_vg_volume' +VG_GROUP_NAME = 'fake_volume_group' +VG_POOL_NAME = 'cdot' +SOURCE_VG_VOLUME_NAME = 'fake_source_vg_volume' +VG_VOLUME_ID = 'fake_vg_volume_id' +VG_VOLUME_SIZE = 100 +SOURCE_VG_VOLUME_ID = 'fake_source_vg_volume_id' +VOLUME_GROUP_NAME = 'fake_vg' +SOURCE_VOLUME_GROUP_ID = 'fake_source_vg_id' +VOLUME_GROUP_ID = 'fake_vg_id' +VG_SNAPSHOT_ID = 'fake_vg_snapshot_id' +VG_SNAPSHOT_NAME = 'snapshot-' + VG_SNAPSHOT_ID +VG_VOLUME_SNAPSHOT_ID = 'fake_vg_volume_snapshot_id' + +VG_LUN_METADATA = { + 'OsType': None, + 'Path': '/vol/aggr1/fake_vg_volume', + 'SpaceReserved': 'true', + 'Qtree': None, + 'Volume': POOL_NAME, +} + +SOURCE_VG_VOLUME = { + 'name': SOURCE_VG_VOLUME_NAME, + 'size': VG_VOLUME_SIZE, + 'id': SOURCE_VG_VOLUME_ID, + 'host': 'hostname@backend#cdot', + 'volumegroup_id': None, + 'status': 'fake_status', + 'provider_location': PROVIDER_LOCATION, +} + +VG_VOLUME = { + 'name': VG_VOLUME_NAME, + 'size': 100, + 'id': VG_VOLUME_ID, + 'host': 'hostname@backend#' + VG_POOL_NAME, + 'volumegroup_id': VOLUME_GROUP_ID, + 'status': 'fake_status', + 'provider_location': PROVIDER_LOCATION, +} + +SOURCE_VOLUME_GROUP = { + 'id': SOURCE_VOLUME_GROUP_ID, + 'status': 'fake_status', +} + +VOLUME_GROUP = { + 'id': VOLUME_GROUP_ID, + 'status': 'fake_status', + 'name': VG_GROUP_NAME, +} + +VG_CONTEXT = {} + +VG_SNAPSHOT = { + 'id': VG_SNAPSHOT_ID, + 'name': VG_SNAPSHOT_NAME, + 'volume_size': VG_VOLUME_SIZE, + 'volumegroup_id': VOLUME_GROUP_ID, + 'status': 'fake_status', + 'volume_id': 'fake_source_volume_id', + 'volume': VG_VOLUME, +} + +VG_VOLUME_SNAPSHOT = { + 'name': VG_SNAPSHOT_NAME, + 'volume_size': VG_VOLUME_SIZE, + 'vgsnapshot_id': VG_SNAPSHOT_ID, + 'id': VG_VOLUME_SNAPSHOT_ID, + 'status': 'fake_status', + 'volume_id': VG_VOLUME_ID, + +} + + class test_volume(object): pass 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 ab8d9844984..44ca0877354 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 @@ -20,6 +20,8 @@ import mock from os_brick.remotefs import remotefs as remotefs_brick from oslo_utils import units +from cinder import exception +from cinder.objects import fields from cinder import test from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake from cinder.tests.unit.volume.drivers.netapp import fakes as na_fakes @@ -265,3 +267,162 @@ class NetApp7modeNfsDriverTestCase(test.TestCase): result = self.driver._get_backing_flexvol_names() self.assertEqual('path', result[0]) + + def test_create_consistency_group(self): + model_update = self.driver.create_consistencygroup( + fake.CG_CONTEXT, fake.CONSISTENCY_GROUP) + self.assertEqual('available', model_update['status']) + + 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) + + @ddt.data(None, + {'replication_status': fields.ReplicationStatus.ENABLED}) + def test_create_consistencygroup_from_src(self, volume_model_update): + volume_model_update = volume_model_update or {} + volume_model_update.update( + {'provider_location': fake.PROVIDER_LOCATION}) + mock_create_volume_from_snapshot = self.mock_object( + self.driver, 'create_volume_from_snapshot', + return_value=volume_model_update) + + 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])) + + expected_volumes_model_updates = [{'id': fake.VOLUME['id']}] + expected_volumes_model_updates[0].update(volume_model_update) + mock_create_volume_from_snapshot.assert_called_once_with( + fake.VOLUME, fake.SNAPSHOT) + self.assertIsNone(model_update) + self.assertEqual(expected_volumes_model_updates, volumes_model_update) + + @ddt.data(None, + {'replication_status': fields.ReplicationStatus.ENABLED}) + def test_create_consistencygroup_from_src_source_vols( + self, volume_model_update): + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + 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') + self.mock_object(self.driver, '_get_volume_model_update', + return_value=volume_model_update) + + 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.NFS_VOLUME])) + + expected_volumes_model_updates = [{ + 'id': fake.NFS_VOLUME['id'], + 'provider_location': fake.PROVIDER_LOCATION, + }] + if volume_model_update: + expected_volumes_model_updates[0].update(volume_model_update) + mock_get_snapshot_flexvols.assert_called_once_with( + [fake.NFS_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.NFS_VOLUME['name'], fake.VOLUME['name'], + fake.NFS_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.assertEqual(expected_volumes_model_updates, 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_flexvol_names_from_hosts') + 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_create_cgsnapshot_busy_snapshot(self): + snapshot = fake.CG_SNAPSHOT + snapshot['volume'] = fake.CG_VOLUME + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + 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') + mock_busy.side_effect = exception.SnapshotIsBusy(snapshot['name']) + mock_mark_snapshot_for_deletion = self.mock_object( + self.driver.zapi_client, 'mark_snapshot_for_deletion') + + 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_not_called() + mock_mark_snapshot_for_deletion.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', 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_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py index 798393ec604..ea9d18f7830 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 @@ -919,11 +919,6 @@ class NetAppNfsDriverTestCase(test.TestCase): 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( @@ -966,160 +961,6 @@ class NetAppNfsDriverTestCase(test.TestCase): 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) - - @ddt.data(None, - {'replication_status': fields.ReplicationStatus.ENABLED}) - def test_create_consistencygroup_from_src(self, volume_model_update): - volume_model_update = volume_model_update or {} - volume_model_update.update( - {'provider_location': fake.PROVIDER_LOCATION}) - mock_create_volume_from_snapshot = self.mock_object( - self.driver, 'create_volume_from_snapshot', - return_value=volume_model_update) - - 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])) - - expected_volumes_model_updates = [{'id': fake.VOLUME['id']}] - expected_volumes_model_updates[0].update(volume_model_update) - mock_create_volume_from_snapshot.assert_called_once_with( - fake.VOLUME, fake.SNAPSHOT) - self.assertIsNone(model_update) - self.assertEqual(expected_volumes_model_updates, volumes_model_update) - - @ddt.data(None, - {'replication_status': fields.ReplicationStatus.ENABLED}) - def test_create_consistencygroup_from_src_source_vols( - self, volume_model_update): - mock_get_snapshot_flexvols = self.mock_object( - self.driver, '_get_flexvol_names_from_hosts') - 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') - self.mock_object(self.driver, '_get_volume_model_update', - return_value=volume_model_update) - - 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.NFS_VOLUME])) - - expected_volumes_model_updates = [{ - 'id': fake.NFS_VOLUME['id'], - 'provider_location': fake.PROVIDER_LOCATION, - }] - if volume_model_update: - expected_volumes_model_updates[0].update(volume_model_update) - mock_get_snapshot_flexvols.assert_called_once_with( - [fake.NFS_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.NFS_VOLUME['name'], fake.VOLUME['name'], - fake.NFS_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.assertEqual(expected_volumes_model_updates, 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_flexvol_names_from_hosts') - 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_create_cgsnapshot_busy_snapshot(self): - snapshot = fake.CG_SNAPSHOT - snapshot['volume'] = fake.CG_VOLUME - mock_get_snapshot_flexvols = self.mock_object( - self.driver, '_get_flexvol_names_from_hosts') - 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') - mock_busy.side_effect = exception.SnapshotIsBusy(snapshot['name']) - mock_mark_snapshot_for_deletion = self.mock_object( - self.zapi_client, 'mark_snapshot_for_deletion') - - 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_not_called() - mock_mark_snapshot_for_deletion.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', 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) - def test_check_for_setup_error(self): super_check_for_setup_error = self.mock_object( nfs.NfsDriver, 'check_for_setup_error') @@ -1144,25 +985,3 @@ class NetAppNfsDriverTestCase(test.TestCase): mock.call(mock_call_snap_cleanup, loopingcalls.ONE_MINUTE, loopingcalls.ONE_MINUTE), mock.call(mock_call_ems_logging, loopingcalls.ONE_HOUR)]) - - def test_delete_snapshots_marked_for_deletion(self): - snapshots = [{ - 'name': fake.SNAPSHOT_NAME, - 'volume_name': fake.VOLUME['name'] - }] - mock_get_flexvol_names = self.mock_object( - self.driver, '_get_backing_flexvol_names') - mock_get_flexvol_names.return_value = [fake.VOLUME['name']] - mock_get_snapshots_marked = self.mock_object( - self.zapi_client, 'get_snapshots_marked_for_deletion') - mock_get_snapshots_marked.return_value = snapshots - mock_delete_snapshot = self.mock_object( - self.zapi_client, 'delete_snapshot') - - self.driver._delete_snapshots_marked_for_deletion() - - mock_get_flexvol_names.assert_called_once_with() - mock_get_snapshots_marked.assert_called_once_with( - [fake.VOLUME['name']]) - mock_delete_snapshot.assert_called_once_with( - fake.VOLUME['name'], fake.SNAPSHOT_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 0a50a36cf98..52b1ad2fc48 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 @@ -26,6 +26,7 @@ from oslo_utils import units from cinder import exception from cinder.image import image_utils +from cinder.objects import fields from cinder import test from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake from cinder.tests.unit.volume.drivers.netapp.dataontap.utils import fakes as \ @@ -161,7 +162,7 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): 'netapp_aggregate': 'aggr1', 'netapp_raid_type': 'raid_dp', 'netapp_disk_type': 'SSD', - 'consistencygroup_support': True, + 'consistent_group_snapshot_enabled': True, }, } mock_get_ssc = self.mock_object(self.driver.ssc_library, @@ -234,6 +235,7 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): 'netapp_raid_type': 'raid_dp', 'netapp_disk_type': 'SSD', 'consistencygroup_support': True, + 'consistent_group_snapshot_enabled': True, 'replication_enabled': False, }] if replication_backends: @@ -1421,16 +1423,16 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.assertEqual('dev1', actual_active) self.assertEqual([], vol_updates) - def test_delete_cgsnapshot(self): + def test_delete_group_snapshot(self): mock_delete_backing_file = self.mock_object( self.driver, '_delete_backing_file_for_snapshot') - snapshots = [fake.CG_SNAPSHOT] + snapshots = [fake.VG_SNAPSHOT] model_update, snapshots_model_update = ( - self.driver.delete_cgsnapshot( - fake.CG_CONTEXT, fake.CG_SNAPSHOT, snapshots)) + self.driver.delete_group_snapshot( + fake.VG_CONTEXT, fake.VG_SNAPSHOT, snapshots)) - mock_delete_backing_file.assert_called_once_with(fake.CG_SNAPSHOT) + mock_delete_backing_file.assert_called_once_with(fake.VG_SNAPSHOT) self.assertIsNone(model_update) self.assertIsNone(snapshots_model_update) @@ -1467,3 +1469,214 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.driver._get_backing_flexvol_names() mock_ssc_library.assert_called_once_with() + + def test_create_group(self): + + model_update = self.driver.create_group( + fake.VG_CONTEXT, fake.VOLUME_GROUP) + + self.assertEqual('available', model_update['status']) + + def test_update_group(self): + + model_update, add_volumes_update, remove_volumes_update = ( + self.driver.update_group(fake.VG_CONTEXT, "foo")) + + self.assertIsNone(add_volumes_update) + self.assertIsNone(remove_volumes_update) + + @ddt.data(None, + {'replication_status': fields.ReplicationStatus.ENABLED}) + def test_create_group_from_src(self, volume_model_update): + volume_model_update = volume_model_update or {} + volume_model_update.update( + {'provider_location': fake.PROVIDER_LOCATION}) + mock_create_volume_from_snapshot = self.mock_object( + self.driver, 'create_volume_from_snapshot', + return_value=volume_model_update) + + model_update, volumes_model_update = ( + self.driver.create_group_from_src( + fake.VG_CONTEXT, fake.VOLUME_GROUP, [fake.VOLUME], + group_snapshot=fake.VG_SNAPSHOT, + sorted_snapshots=[fake.SNAPSHOT])) + + expected_volumes_model_updates = [{'id': fake.VOLUME['id']}] + expected_volumes_model_updates[0].update(volume_model_update) + mock_create_volume_from_snapshot.assert_called_once_with( + fake.VOLUME, fake.SNAPSHOT) + self.assertIsNone(model_update) + self.assertEqual(expected_volumes_model_updates, volumes_model_update) + + @ddt.data(None, + {'replication_status': fields.ReplicationStatus.ENABLED}) + def test_create_group_from_src_source_vols(self, volume_model_update): + self.driver.zapi_client = mock.Mock() + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + mock_get_snapshot_flexvols.return_value = (set([fake.VG_POOL_NAME])) + mock_clone_backing_file = self.mock_object( + self.driver, '_clone_backing_file_for_volume') + fake_snapshot_name = 'snapshot-temp-' + fake.VOLUME_GROUP['id'] + mock_busy = self.mock_object( + self.driver.zapi_client, 'wait_for_busy_snapshot') + self.mock_object(self.driver, '_get_volume_model_update', + return_value=volume_model_update) + + model_update, volumes_model_update = ( + self.driver.create_group_from_src( + fake.VG_CONTEXT, fake.VOLUME_GROUP, [fake.VG_VOLUME], + source_group=fake.VOLUME_GROUP, + sorted_source_vols=[fake.SOURCE_VG_VOLUME])) + + expected_volumes_model_updates = [{ + 'id': fake.VG_VOLUME['id'], + 'provider_location': fake.PROVIDER_LOCATION, + }] + if volume_model_update: + expected_volumes_model_updates[0].update(volume_model_update) + mock_get_snapshot_flexvols.assert_called_once_with( + [fake.SOURCE_VG_VOLUME['host']]) + self.driver.zapi_client.create_cg_snapshot.assert_called_once_with( + set([fake.VG_POOL_NAME]), fake_snapshot_name) + mock_clone_backing_file.assert_called_once_with( + fake.SOURCE_VG_VOLUME['name'], fake.VG_VOLUME['name'], + fake.SOURCE_VG_VOLUME['id'], source_snapshot=fake_snapshot_name) + mock_busy.assert_called_once_with( + fake.VG_POOL_NAME, fake_snapshot_name) + self.driver.zapi_client.delete_snapshot.assert_called_once_with( + fake.VG_POOL_NAME, fake_snapshot_name) + self.assertIsNone(model_update) + self.assertEqual(expected_volumes_model_updates, volumes_model_update) + + def test_create_group_from_src_invalid_parms(self): + model_update, volumes_model_update = ( + self.driver.create_group_from_src( + fake.VG_CONTEXT, fake.VOLUME_GROUP, [fake.VOLUME])) + + self.assertIn('error', model_update['status']) + + def test_create_group_snapshot_raise_exception(self): + mock_is_cg_snapshot = self.mock_object( + volume_utils, 'is_group_a_cg_snapshot_type', return_value=True) + mock__get_flexvol_names = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + + self.mock_object(self.driver.zapi_client, 'create_cg_snapshot', + side_effect=netapp_api.NaApiError) + + self.assertRaises(exception.NetAppDriverException, + self.driver.create_group_snapshot, + fake.VG_CONTEXT, + fake.VOLUME_GROUP, + [fake.VG_SNAPSHOT]) + + mock_is_cg_snapshot.assert_called_once_with(fake.VOLUME_GROUP) + mock__get_flexvol_names.assert_called_once_with( + [fake.VG_SNAPSHOT['volume']['host']]) + + def test_create_group_snapshot(self): + mock_is_cg_snapshot = self.mock_object( + volume_utils, 'is_group_a_cg_snapshot_type', return_value=False) + mock__clone_backing_file_for_volume = self.mock_object( + self.driver, '_clone_backing_file_for_volume') + + model_update, snapshots_model_update = ( + self.driver.create_group_snapshot(fake.VG_CONTEXT, + fake.VOLUME_GROUP, + [fake.SNAPSHOT])) + + self.assertIsNone(model_update) + self.assertIsNone(snapshots_model_update) + mock_is_cg_snapshot.assert_called_once_with(fake.VOLUME_GROUP) + mock__clone_backing_file_for_volume.assert_called_once_with( + fake.SNAPSHOT['volume_name'], fake.SNAPSHOT['name'], + fake.SNAPSHOT['volume_id'], is_snapshot=True) + + def test_create_consistent_group_snapshot(self): + mock_is_cg_snapshot = self.mock_object( + volume_utils, 'is_group_a_cg_snapshot_type', return_value=True) + + self.driver.zapi_client = mock.Mock() + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + mock_get_snapshot_flexvols.return_value = (set([fake.VG_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') + + model_update, snapshots_model_update = ( + self.driver.create_group_snapshot(fake.VG_CONTEXT, + fake.VOLUME_GROUP, + [fake.VG_SNAPSHOT])) + + self.assertIsNone(model_update) + self.assertIsNone(snapshots_model_update) + mock_is_cg_snapshot.assert_called_once_with(fake.VOLUME_GROUP) + mock_get_snapshot_flexvols.assert_called_once_with( + [fake.VG_SNAPSHOT['volume']['host']]) + self.driver.zapi_client.create_cg_snapshot.assert_called_once_with( + set([fake.VG_POOL_NAME]), fake.VOLUME_GROUP_ID) + mock_clone_backing_file.assert_called_once_with( + fake.VG_SNAPSHOT['volume']['name'], fake.VG_SNAPSHOT['name'], + fake.VG_SNAPSHOT['volume']['id'], + source_snapshot=fake.VOLUME_GROUP_ID) + mock_busy.assert_called_once_with( + fake.VG_POOL_NAME, fake.VOLUME_GROUP_ID) + self.driver.zapi_client.delete_snapshot.assert_called_once_with( + fake.VG_POOL_NAME, fake.VOLUME_GROUP_ID) + + def test_create_group_snapshot_busy_snapshot(self): + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + self.driver.zapi_client = mock.Mock() + snapshot = fake.VG_SNAPSHOT + snapshot['volume'] = fake.VG_VOLUME + mock_get_snapshot_flexvols = self.mock_object( + self.driver, '_get_flexvol_names_from_hosts') + mock_get_snapshot_flexvols.return_value = (set([fake.VG_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') + mock_busy.side_effect = exception.SnapshotIsBusy(snapshot['name']) + mock_mark_snapshot_for_deletion = self.mock_object( + self.driver.zapi_client, 'mark_snapshot_for_deletion') + + self.driver.create_group_snapshot( + fake.VG_CONTEXT, fake.VG_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.VG_POOL_NAME]), fake.VG_SNAPSHOT_ID) + mock_clone_backing_file.assert_called_once_with( + snapshot['volume']['name'], snapshot['name'], + snapshot['volume']['id'], source_snapshot=fake.VG_SNAPSHOT_ID) + mock_busy.assert_called_once_with( + fake.VG_POOL_NAME, fake.VG_SNAPSHOT_ID) + self.driver.zapi_client.delete_snapshot.assert_not_called() + mock_mark_snapshot_for_deletion.assert_called_once_with( + fake.VG_POOL_NAME, fake.VG_SNAPSHOT_ID) + + def test_delete_group_volume_delete_failure(self): + self.mock_object(self.driver, '_delete_file', side_effect=Exception) + + model_update, volumes = self.driver.delete_group( + fake.VG_CONTEXT, fake.VOLUME_GROUP, [fake.VG_VOLUME]) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('error_deleting', volumes[0]['status']) + + def test_delete_group(self): + mock_delete_file = self.mock_object( + self.driver, '_delete_file') + + model_update, volumes = self.driver.delete_group( + fake.VG_CONTEXT, fake.VOLUME_GROUP, [fake.VG_VOLUME]) + + self.assertEqual('deleted', model_update['status']) + self.assertEqual('deleted', volumes[0]['status']) + mock_delete_file.assert_called_once_with( + fake.VG_VOLUME_ID, fake.VG_VOLUME_NAME) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py index 878b4d5d1f6..3283dc11f15 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py @@ -30,6 +30,7 @@ import six from cinder import exception from cinder.i18n import _ from cinder import interface +from cinder.objects import fields from cinder import utils from cinder.volume.drivers.netapp.dataontap.client import client_7mode from cinder.volume.drivers.netapp.dataontap import nfs_base @@ -281,3 +282,151 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): LOG.debug("Snapshot %s deletion successful", snapshot['name']) return None, None + + @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. + + :returns: Hard-coded model update for consistency group model. + """ + model_update = {'status': fields.ConsistencyGroupStatus.AVAILABLE} + return model_update + + @utils.trace_method + def delete_consistencygroup(self, context, group, volumes): + """Driver entry point for deleting a consistency group. + + :returns: Updated consistency group model and list of volume models + for the volumes that were deleted. + """ + model_update = {'status': fields.ConsistencyGroupStatus.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("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. + + :returns: 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_flexvol_names_from_hosts(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: + try: + self.zapi_client.wait_for_busy_snapshot( + flexvol_name, cgsnapshot['id']) + self.zapi_client.delete_snapshot( + flexvol_name, cgsnapshot['id']) + except exception.SnapshotIsBusy: + self.zapi_client.mark_snapshot_for_deletion( + flexvol_name, cgsnapshot['id']) + + return None, None + + @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. + + :returns: An implicit update for the volumes model that is + interpreted by the manager as a successful operation. + """ + LOG.debug("VOLUMES %s ", ', '.join([vol['id'] for vol in volumes])) + model_update = None + volumes_model_update = [] + + if cgsnapshot: + vols = zip(volumes, snapshots) + + for volume, snapshot in vols: + update = self.create_volume_from_snapshot( + volume, snapshot) + update['id'] = volume['id'] + volumes_model_update.append(update) + + elif source_cg and source_vols: + hosts = [source_vol['host'] for source_vol in source_vols] + flexvols = self._get_flexvol_names_from_hosts(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) + volume_model_update = ( + self._get_volume_model_update(volume) or {}) + volume_model_update.update({ + 'id': volume['id'], + 'provider_location': source_vol['provider_location'], + }) + volumes_model_update.append(volume_model_update) + + # 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("Unexpected set of parameters received when " + "creating consistency group from source.") + model_update = {'status': fields.ConsistencyGroupStatus.ERROR} + + return model_update, volumes_model_update diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index 4ea22b8c187..91fe1fdf888 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -1070,155 +1070,3 @@ class NetAppNfsDriver(driver.ManageableVD, vol_path = os.path.join(volume['provider_location'], vol_str) LOG.info('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("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_flexvol_names_from_hosts(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: - try: - self.zapi_client.wait_for_busy_snapshot( - flexvol_name, cgsnapshot['id']) - self.zapi_client.delete_snapshot( - flexvol_name, cgsnapshot['id']) - except exception.SnapshotIsBusy: - self.zapi_client.mark_snapshot_for_deletion( - 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 - volumes_model_update = [] - - if cgsnapshot: - vols = zip(volumes, snapshots) - - for volume, snapshot in vols: - update = self.create_volume_from_snapshot( - volume, snapshot) - update['id'] = volume['id'] - volumes_model_update.append(update) - - elif source_cg and source_vols: - hosts = [source_vol['host'] for source_vol in source_vols] - flexvols = self._get_flexvol_names_from_hosts(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) - volume_model_update = ( - self._get_volume_model_update(volume) or {}) - volume_model_update.update({ - 'id': volume['id'], - 'provider_location': source_vol['provider_location'], - }) - volumes_model_update.append(volume_model_update) - - # 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("Unexpected set of parameters received when " - "creating consistency group from source.") - model_update = {} - model_update['status'] = 'error' - - return model_update, volumes_model_update diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index 0d0d0a3ea11..593c209294d 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -269,6 +269,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, # Add driver capabilities and config info pool['QoS_support'] = True pool['consistencygroup_support'] = True + pool['consistent_group_snapshot_enabled'] = True pool['multiattach'] = False # Add up-to-date capacity info @@ -733,8 +734,8 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, return flexvols @utils.trace_method - def delete_cgsnapshot(self, context, cgsnapshot, snapshots): - """Delete files backing each snapshot in the cgsnapshot. + def delete_group_snapshot(self, context, group_snapshot, snapshots): + """Delete files backing each snapshot in the group snapshot. :return: An implicit update of snapshot models that the manager will interpret and subsequently set the model state to deleted. @@ -744,3 +745,166 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, LOG.debug("Snapshot %s deletion successful", snapshot['name']) return None, None + + @utils.trace_method + def create_group(self, context, group): + """Driver entry point for creating a generic volume group. + + ONTAP does not maintain an actual group construct. As a result, no + communtication to the backend is necessary for generic volume group + creation. + + :returns: Hard-coded model update for generic volume group model. + """ + model_update = {'status': fields.GroupStatus.AVAILABLE} + return model_update + + @utils.trace_method + def delete_group(self, context, group, volumes): + """Driver entry point for deleting a generic volume group. + + :returns: Updated group model and list of volume models for the volumes + that were deleted. + """ + model_update = {'status': fields.GroupStatus.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': fields.GroupStatus.ERROR_DELETING}) + LOG.exception("Volume %(vol)s in the group could not be " + "deleted.", {'vol': volume}) + return model_update, volumes_model_update + + @utils.trace_method + def update_group(self, context, group, add_volumes=None, + remove_volumes=None): + """Driver entry point for updating a generic volume group. + + Since no actual group 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_group_snapshot(self, context, group_snapshot, snapshots): + """Creates a Cinder group snapshot object. + + The Cinder group snapshot object is created by making use of an ONTAP + consistency group snapshot in order to provide write-order consistency + for a set of flexvols snapshots. First, a list of the flexvols backing + the given Cinder group must be gathered. An ONTAP group-snapshot of + these flexvols will create a snapshot copy of all the Cinder volumes in + the generic volume group. For each Cinder volume in the group, it is + then necessary to clone its backing file from the ONTAP cg-snapshot. + The naming convention used to for the clones is what indicates the + clone's role as a Cinder snapshot and its inclusion in a Cinder group. + The ONTAP cg-snapshot of the flexvols is deleted after the cloning + operation is completed. + + :returns: An implicit update for the group snapshot and snapshot models + that is then used by the manager to set the models to + available. + """ + try: + if volume_utils.is_group_a_cg_snapshot_type(group_snapshot): + self._create_consistent_group_snapshot(group_snapshot, + snapshots) + else: + for snapshot in snapshots: + self._clone_backing_file_for_volume( + snapshot['volume_name'], snapshot['name'], + snapshot['volume_id'], is_snapshot=True) + except Exception as ex: + err_msg = (_("Create group snapshot failed (%s).") % ex) + LOG.exception(err_msg, resource=group_snapshot) + raise exception.NetAppDriverException(err_msg) + + return None, None + + def _create_consistent_group_snapshot(self, group_snapshot, snapshots): + hosts = [snapshot['volume']['host'] for snapshot in snapshots] + flexvols = self._get_flexvol_names_from_hosts(hosts) + + # Create snapshot for backing flexvol + self.zapi_client.create_cg_snapshot(flexvols, group_snapshot['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=group_snapshot['id']) + + # Delete backing flexvol snapshots + for flexvol_name in flexvols: + try: + self.zapi_client.wait_for_busy_snapshot( + flexvol_name, group_snapshot['id']) + self.zapi_client.delete_snapshot( + flexvol_name, group_snapshot['id']) + except exception.SnapshotIsBusy: + self.zapi_client.mark_snapshot_for_deletion( + flexvol_name, group_snapshot['id']) + + @utils.trace_method + def create_group_from_src(self, context, group, volumes, + group_snapshot=None, sorted_snapshots=None, + source_group=None, sorted_source_vols=None): + """Creates a group from a group snapshot or a group of cinder vols. + + :returns: An implicit update for the volumes model that is + interpreted by the manager as a successful operation. + """ + LOG.debug("VOLUMES %s ", ', '.join([vol['id'] for vol in volumes])) + model_update = None + volumes_model_update = [] + + if group_snapshot: + vols = zip(volumes, sorted_snapshots) + + for volume, snapshot in vols: + update = self.create_volume_from_snapshot( + volume, snapshot) + update['id'] = volume['id'] + volumes_model_update.append(update) + + elif source_group and sorted_source_vols: + hosts = [source_vol['host'] for source_vol in sorted_source_vols] + flexvols = self._get_flexvol_names_from_hosts(hosts) + + # Create snapshot for backing flexvol + snapshot_name = 'snapshot-temp-' + source_group['id'] + self.zapi_client.create_cg_snapshot(flexvols, snapshot_name) + + # Start clone process for new volumes + vols = zip(volumes, sorted_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) + volume_model_update = ( + self._get_volume_model_update(volume) or {}) + volume_model_update.update({ + 'id': volume['id'], + 'provider_location': source_vol['provider_location'], + }) + volumes_model_update.append(volume_model_update) + + # 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("Unexpected set of parameters received when " + "creating group from source.") + model_update = {'status': fields.GroupStatus.ERROR} + + return model_update, volumes_model_update diff --git a/releasenotes/notes/netapp-add-generic-group-support-cdot-9bebd13356694e13.yaml b/releasenotes/notes/netapp-add-generic-group-support-cdot-9bebd13356694e13.yaml new file mode 100644 index 00000000000..b298edebca2 --- /dev/null +++ b/releasenotes/notes/netapp-add-generic-group-support-cdot-9bebd13356694e13.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added generic volume group capability to NetApp cDot drivers with + support for write consistent group snapshots.