From 6b7f47633567a1236732e46d9f4d7c80fa30f35c Mon Sep 17 00:00:00 2001 From: Michael Price Date: Fri, 8 Jan 2016 11:04:35 -0600 Subject: [PATCH] NetApp: Add Consistency Group support for E-Series Add Consistency Group support to the E-Series driver. This implementation utilizes the native Consistency Group feature available on the E-Series backend to support Cinder Consistency Groups. CGs and standalone snapshots both utilize snapshot groups. There is a limit of 3 snapshot groups per volume, so the number of standalone snapshots will be limited by the number of consistency groups that are created, and likewise the reverse. Each CG/Snapshot Group will support up to 32 snapshots, so each CG that a volume is a part of will reduce the number of available standalone snapshots that can be created by 32 (from a maximum of 96). Implements: blueprint netapp-eseries-consistency-groups Change-Id: Ib0fc9fa9abc6699f2971948d3d4c5e9902381072 --- .../volume/drivers/netapp/eseries/fakes.py | 81 +++- .../drivers/netapp/eseries/test_client.py | 156 ++++++ .../drivers/netapp/eseries/test_driver.py | 67 +++ .../drivers/netapp/eseries/test_library.py | 452 +++++++++++++++++- .../volume/drivers/netapp/eseries/client.py | 152 ++++++ .../drivers/netapp/eseries/fc_driver.py | 26 +- .../drivers/netapp/eseries/iscsi_driver.py | 26 +- .../volume/drivers/netapp/eseries/library.py | 351 ++++++++++++-- ...s-consistency-groups-4f6b2af2d20c94e9.yaml | 3 + 9 files changed, 1280 insertions(+), 34 deletions(-) create mode 100644 releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py index a745cd7c7..7a8ff129d 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py @@ -48,6 +48,15 @@ FAKE_CINDER_SNAPSHOT = { 'volume': FAKE_CINDER_VOLUME } +FAKE_CINDER_CG = { + 'id': '78f95b9d-3f02-4781-a512-1a1c951d48a2', +} + +FAKE_CINDER_CG_SNAPSHOT = { + 'id': '78f95b9d-4d13-4781-a512-1a1c951d6a6', + 'consistencygroup_id': FAKE_CINDER_CG['id'], +} + MULTIATTACH_HOST_GROUP = { 'clusterRef': '8500000060080E500023C7340036035F515B78FC', 'label': utils.MULTI_ATTACH_HOST_GROUP_NAME, @@ -693,7 +702,8 @@ SNAPSHOT_IMAGE = { 'activeCOW': True, 'isRollbackSource': False, 'pitRef': '3400000060080E500023BB3400631F335294A5A8', - 'pitSequenceNumber': '19' + 'pitSequenceNumber': '19', + 'consistencyGroupId': '0000000000000000000000000000000000000000', } SNAPSHOT_VOLUME = { @@ -1011,6 +1021,46 @@ FAKE_CLIENT_PARAMS = { 'password': 'rw', } +FAKE_CONSISTENCY_GROUP = { + 'cgRef': '2A000000600A0980006077F8008702F45480F41A', + 'label': '5BO5GPO4PFGRPMQWEXGTILSAUI', + 'repFullPolicy': 'failbasewrites', + 'fullWarnThreshold': 75, + 'autoDeleteLimit': 0, + 'rollbackPriority': 'medium', + 'uniqueSequenceNumber': [8940, 8941, 8942], + 'creationPendingStatus': 'none', + 'name': '5BO5GPO4PFGRPMQWEXGTILSAUI', + 'id': '2A000000600A0980006077F8008702F45480F41A' +} + +FAKE_CONSISTENCY_GROUP_MEMBER = { + 'consistencyGroupId': '2A000000600A0980006077F8008702F45480F41A', + 'volumeId': '02000000600A0980006077F8000002F55480F421', + 'volumeWwn': '600A0980006077F8000002F55480F421', + 'baseVolumeName': 'I5BHHNILUJGZHEUD4S36GCOQYA', + 'clusterSize': 65536, + 'totalRepositoryVolumes': 1, + 'totalRepositoryCapacity': '4294967296', + 'usedRepositoryCapacity': '5636096', + 'fullWarnThreshold': 75, + 'totalSnapshotImages': 3, + 'totalSnapshotVolumes': 2, + 'autoDeleteSnapshots': False, + 'autoDeleteLimit': 0, + 'pitGroupId': '33000000600A0980006077F8000002F85480F435', + 'repositoryVolume': '36000000600A0980006077F8000002F75480F435' +} +FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME = { + 'id': '2C00000060080E500034194F002C96A256BD50F9', + 'name': '6TRZHKDG75DVLBC2JU5J647RME', + 'cgViewRef': '2C00000060080E500034194F002C96A256BD50F9', + 'groupRef': '2A00000060080E500034194F0087969856BD2D67', + 'label': '6TRZHKDG75DVLBC2JU5J647RME', + 'viewTime': '1455221060', + 'viewSequenceNumber': '10', +} + def list_snapshot_groups(numGroups): snapshots = [] @@ -1179,9 +1229,12 @@ class FakeEseriesClient(object): def list_snapshot_images(self): return [SNAPSHOT_IMAGE] - def list_snapshot_image(self): + def list_snapshot_image(self, *args, **kwargs): return SNAPSHOT_IMAGE + def create_cg_snapshot_view(self, *args, **kwargs): + return SNAPSHOT_VOLUME + def list_host_types(self): return [ { @@ -1277,8 +1330,32 @@ class FakeEseriesClient(object): def restart_snapshot_volume(self, *args, **kwargs): pass + def create_consistency_group(self, *args, **kwargs): + return FAKE_CONSISTENCY_GROUP + + def delete_consistency_group(self, *args, **kwargs): + pass + + def list_consistency_groups(self, *args, **kwargs): + return [FAKE_CONSISTENCY_GROUP] + + def remove_consistency_group_member(self, *args, **kwargs): + pass + + def add_consistency_group_member(self, *args, **kwargs): + pass + def list_backend_store(self, key): return {} def save_backend_store(self, key, val): pass + + def create_consistency_group_snapshot(self, *args, **kwargs): + return [SNAPSHOT_IMAGE] + + def get_consistency_group_snapshots(self, *args, **kwargs): + return [SNAPSHOT_IMAGE] + + def delete_consistency_group_snapshot(self, *args, **kwargs): + pass diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py index a58ef5a11..5ebe3fc81 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py @@ -906,6 +906,162 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): 'DELETE', self.my_client.RESOURCE_PATHS['snapshot_image'], **{'object-id': fake_ref}) + def test_create_consistency_group(self): + invoke = self.mock_object(self.my_client, '_invoke') + name = 'fake' + + self.my_client.create_consistency_group(name) + + invoke.assert_called_once_with( + 'POST', self.my_client.RESOURCE_PATHS['cgroups'], mock.ANY) + + def test_list_consistency_group(self): + invoke = self.mock_object(self.my_client, '_invoke') + ref = 'fake' + + self.my_client.get_consistency_group(ref) + + invoke.assert_called_once_with( + 'GET', self.my_client.RESOURCE_PATHS['cgroup'], + **{'object-id': ref}) + + def test_list_consistency_groups(self): + invoke = self.mock_object(self.my_client, '_invoke') + + self.my_client.list_consistency_groups() + + invoke.assert_called_once_with( + 'GET', self.my_client.RESOURCE_PATHS['cgroups']) + + def test_delete_consistency_group(self): + invoke = self.mock_object(self.my_client, '_invoke') + ref = 'fake' + + self.my_client.delete_consistency_group(ref) + + invoke.assert_called_once_with( + 'DELETE', self.my_client.RESOURCE_PATHS['cgroup'], + **{'object-id': ref}) + + def test_add_consistency_group_member(self): + invoke = self.mock_object(self.my_client, '_invoke') + vol_id = eseries_fake.VOLUME['id'] + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.add_consistency_group_member(vol_id, cg_id) + + invoke.assert_called_once_with( + 'POST', self.my_client.RESOURCE_PATHS['cgroup_members'], + mock.ANY, **{'object-id': cg_id}) + + def test_remove_consistency_group_member(self): + invoke = self.mock_object(self.my_client, '_invoke') + vol_id = eseries_fake.VOLUME['id'] + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.remove_consistency_group_member(vol_id, cg_id) + + invoke.assert_called_once_with( + 'DELETE', self.my_client.RESOURCE_PATHS['cgroup_member'], + **{'object-id': cg_id, 'vol-id': vol_id}) + + def test_create_consistency_group_snapshot(self): + invoke = self.mock_object(self.my_client, '_invoke') + path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.create_consistency_group_snapshot(cg_id) + + invoke.assert_called_once_with('POST', path, **{'object-id': cg_id}) + + @ddt.data(0, 32) + def test_delete_consistency_group_snapshot(self, seq_num): + invoke = self.mock_object(self.my_client, '_invoke') + path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.delete_consistency_group_snapshot(cg_id, seq_num) + + invoke.assert_called_once_with( + 'DELETE', path, **{'object-id': cg_id, 'seq-num': seq_num}) + + def test_get_consistency_group_snapshots(self): + invoke = self.mock_object(self.my_client, '_invoke') + path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.get_consistency_group_snapshots(cg_id) + + invoke.assert_called_once_with( + 'GET', path, **{'object-id': cg_id}) + + def test_create_cg_snapshot_view(self): + cg_snap_view = copy.deepcopy( + eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME) + view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) + invoke = self.mock_object(self.my_client, '_invoke', mock.Mock( + return_value=cg_snap_view)) + list_views = self.mock_object( + self.my_client, 'list_cg_snapshot_views', + mock.Mock(return_value=[view])) + name = view['name'] + snap_id = view['basePIT'] + path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.my_client.create_cg_snapshot_view(cg_id, name, snap_id) + + invoke.assert_called_once_with( + 'POST', path, mock.ANY, **{'object-id': cg_id}) + list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef']) + + def test_create_cg_snapshot_view_not_found(self): + cg_snap_view = copy.deepcopy( + eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME) + view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) + invoke = self.mock_object(self.my_client, '_invoke', mock.Mock( + return_value=cg_snap_view)) + list_views = self.mock_object( + self.my_client, 'list_cg_snapshot_views', + mock.Mock(return_value=[view])) + del_view = self.mock_object(self.my_client, 'delete_cg_snapshot_view') + name = view['name'] + # Ensure we don't get a match on the retrieved views + snap_id = None + path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + + self.assertRaises( + exception.NetAppDriverException, + self.my_client.create_cg_snapshot_view, cg_id, name, snap_id) + + invoke.assert_called_once_with( + 'POST', path, mock.ANY, **{'object-id': cg_id}) + list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef']) + del_view.assert_called_once_with(cg_id, cg_snap_view['id']) + + def test_list_cg_snapshot_views(self): + invoke = self.mock_object(self.my_client, '_invoke') + path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot_views') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + view_id = 'id' + + self.my_client.list_cg_snapshot_views(cg_id, view_id) + + invoke.assert_called_once_with( + 'GET', path, **{'object-id': cg_id, 'view-id': view_id}) + + def test_delete_cg_snapshot_view(self): + invoke = self.mock_object(self.my_client, '_invoke') + path = self.my_client.RESOURCE_PATHS.get('cgroup_snap_view') + cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] + view_id = 'id' + + self.my_client.delete_cg_snapshot_view(cg_id, view_id) + + invoke.assert_called_once_with( + 'DELETE', path, **{'object-id': cg_id, 'view-id': view_id}) + @ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3', '01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4') def test_api_version_not_support_asup(self, api_version): diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py index 5ad1c499b..b49101526 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py @@ -488,3 +488,70 @@ class NetAppESeriesDriverTestCase(object): self.driver.extend_volume(self.fake_ret_vol, capacity) self.library.extend_volume.assert_called_with(self.fake_ret_vol, capacity) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'create_cgsnapshot', mock.Mock()) + def test_create_cgsnapshot(self): + cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) + snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) + + self.driver.create_cgsnapshot('ctx', cgsnapshot, snapshots) + + self.library.create_cgsnapshot.assert_called_with(cgsnapshot, + snapshots) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'delete_cgsnapshot', mock.Mock()) + def test_delete_cgsnapshot(self): + cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) + snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) + + self.driver.delete_cgsnapshot('ctx', cgsnapshot, snapshots) + + self.library.delete_cgsnapshot.assert_called_with(cgsnapshot, + snapshots) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'create_consistencygroup', mock.Mock()) + def test_create_consistencygroup(self): + cg = copy.deepcopy(fakes.FAKE_CINDER_CG) + + self.driver.create_consistencygroup('ctx', cg) + + self.library.create_consistencygroup.assert_called_with(cg) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'delete_consistencygroup', mock.Mock()) + def test_delete_consistencygroup(self): + cg = copy.deepcopy(fakes.FAKE_CINDER_CG) + volumes = copy.deepcopy([fakes.VOLUME]) + + self.driver.delete_consistencygroup('ctx', cg, volumes) + + self.library.delete_consistencygroup.assert_called_with(cg, volumes) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'update_consistencygroup', mock.Mock()) + def test_update_consistencygroup(self): + group = copy.deepcopy(fakes.FAKE_CINDER_CG) + + self.driver.update_consistencygroup('ctx', group, {}, {}) + + self.library.update_consistencygroup.assert_called_with(group, {}, {}) + + @mock.patch.object(library.NetAppESeriesLibrary, + 'create_consistencygroup_from_src', mock.Mock()) + def test_create_consistencygroup_from_src(self): + cg = copy.deepcopy(fakes.FAKE_CINDER_CG) + volumes = copy.deepcopy([fakes.VOLUME]) + source_vols = copy.deepcopy([fakes.VOLUME]) + cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) + source_cg = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) + snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) + + self.driver.create_consistencygroup_from_src( + 'ctx', cg, volumes, cgsnapshot, snapshots, source_cg, + source_vols) + + self.library.create_consistencygroup_from_src.assert_called_with( + cg, volumes, cgsnapshot, snapshots, source_cg, source_vols) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py index 8d7dd051e..b8ff66753 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py @@ -46,7 +46,7 @@ from cinder.zonemanager import utils as fczm_utils def get_fake_volume(): - """Return a fake Cinder Volume that can be used a parameter""" + """Return a fake Cinder Volume that can be used as a parameter""" return { 'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1, 'volume_name': 'lun1', 'host': 'hostname@backend#DDP', @@ -326,6 +326,7 @@ class NetAppEseriesLibraryTestCase(test.TestCase): thin_provisioned = pool['thinProvisioningCapable'] expected = { + 'consistencygroup_support': True, 'netapp_disk_encryption': six.text_type(pool['encrypted']).lower(), 'netapp_eseries_flash_read_cache': @@ -1063,6 +1064,219 @@ class NetAppEseriesLibraryTestCase(test.TestCase): fake_volume["id"]) self.assertEqual(2, library.LOG.error.call_count) + def test_create_consistencygroup(self): + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + expected = {'status': 'available'} + create_cg = self.mock_object(self.library, + '_create_consistency_group', + mock.Mock(return_value=expected)) + + actual = self.library.create_consistencygroup(fake_cg) + + create_cg.assert_called_once_with(fake_cg) + self.assertEqual(expected, actual) + + def test_create_consistency_group(self): + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + expected = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + create_cg = self.mock_object(self.library._client, + 'create_consistency_group', + mock.Mock(return_value=expected)) + + result = self.library._create_consistency_group(fake_cg) + + name = utils.convert_uuid_to_es_fmt(fake_cg['id']) + create_cg.assert_called_once_with(name) + self.assertEqual(expected, result) + + def test_delete_consistencygroup(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + volumes = [get_fake_volume()] * 3 + model_update = {'status': 'deleted'} + volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in + volumes] + delete_cg = self.mock_object(self.library._client, + 'delete_consistency_group') + updt_index = self.mock_object( + self.library, '_merge_soft_delete_changes') + delete_vol = self.mock_object(self.library, 'delete_volume') + self.mock_object(self.library, '_get_consistencygroup', + mock.Mock(return_value=cg)) + + result = self.library.delete_consistencygroup(fake_cg, volumes) + + self.assertEqual(len(volumes), delete_vol.call_count) + delete_cg.assert_called_once_with(cg['id']) + self.assertEqual((model_update, volume_update), result) + updt_index.assert_called_once_with(None, [cg['id']]) + + def test_delete_consistencygroup_index_update_failure(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + volumes = [get_fake_volume()] * 3 + model_update = {'status': 'deleted'} + volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in + volumes] + delete_cg = self.mock_object(self.library._client, + 'delete_consistency_group') + delete_vol = self.mock_object(self.library, 'delete_volume') + self.mock_object(self.library, '_get_consistencygroup', + mock.Mock(return_value=cg)) + + result = self.library.delete_consistencygroup(fake_cg, volumes) + + self.assertEqual(len(volumes), delete_vol.call_count) + delete_cg.assert_called_once_with(cg['id']) + self.assertEqual((model_update, volume_update), result) + + def test_delete_consistencygroup_not_found(self): + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + delete_cg = self.mock_object(self.library._client, + 'delete_consistency_group') + updt_index = self.mock_object( + self.library, '_merge_soft_delete_changes') + delete_vol = self.mock_object(self.library, 'delete_volume') + exc = exception.ConsistencyGroupNotFound(consistencygroup_id='') + self.mock_object(self.library, '_get_consistencygroup', + mock.Mock(side_effect=exc)) + + self.library.delete_consistencygroup(fake_cg, []) + + delete_cg.assert_not_called() + delete_vol.assert_not_called() + updt_index.assert_not_called() + + def test_get_consistencygroup(self): + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + name = utils.convert_uuid_to_es_fmt(fake_cg['id']) + cg['name'] = name + list_cgs = self.mock_object(self.library._client, + 'list_consistency_groups', + mock.Mock(return_value=[cg])) + + result = self.library._get_consistencygroup(fake_cg) + + self.assertEqual(cg, result) + list_cgs.assert_called_once_with() + + def test_get_consistencygroup_not_found(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + list_cgs = self.mock_object(self.library._client, + 'list_consistency_groups', + mock.Mock(return_value=[cg])) + + self.assertRaises(exception.ConsistencyGroupNotFound, + self.library._get_consistencygroup, + copy.deepcopy(eseries_fake.FAKE_CINDER_CG)) + + list_cgs.assert_called_once_with() + + def test_update_consistencygroup(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + vol = copy.deepcopy(eseries_fake.VOLUME) + volumes = [get_fake_volume()] * 3 + self.mock_object( + self.library, '_get_volume', mock.Mock(return_value=vol)) + self.mock_object(self.library, '_get_consistencygroup', + mock.Mock(return_value=cg)) + + self.library.update_consistencygroup(fake_cg, volumes, volumes) + + def test_create_consistencygroup_from_src(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + volumes = [cinder_utils.create_volume(self.ctxt) for i in range(3)] + src_volumes = [cinder_utils.create_volume(self.ctxt) for v in volumes] + update_cg = self.mock_object( + self.library, '_update_consistency_group_members') + create_cg = self.mock_object( + self.library, '_create_consistency_group', + mock.Mock(return_value=cg)) + self.mock_object( + self.library, '_create_volume_from_snapshot') + + self.mock_object( + self.library, '_get_snapshot', mock.Mock(return_value=snap)) + + self.library.create_consistencygroup_from_src( + fake_cg, volumes, None, None, None, src_volumes) + + create_cg.assert_called_once_with(fake_cg) + update_cg.assert_called_once_with(cg, volumes, []) + + def test_create_consistencygroup_from_src_cgsnapshot(self): + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) + fake_vol = cinder_utils.create_volume(self.ctxt) + cgsnap = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + volumes = [fake_vol] + snapshots = [cinder_utils.create_snapshot(self.ctxt, v['id']) for v + in volumes] + update_cg = self.mock_object( + self.library, '_update_consistency_group_members') + create_cg = self.mock_object( + self.library, '_create_consistency_group', + mock.Mock(return_value=cg)) + clone_vol = self.mock_object( + self.library, '_create_volume_from_snapshot') + + self.library.create_consistencygroup_from_src( + fake_cg, volumes, cgsnap, snapshots, None, None) + + create_cg.assert_called_once_with(fake_cg) + update_cg.assert_called_once_with(cg, volumes, []) + self.assertEqual(clone_vol.call_count, len(volumes)) + + @ddt.data({'consistencyGroupId': utils.NULL_REF}, + {'consistencyGroupId': None}, {'consistencyGroupId': '1'}, {}) + def test_is_cgsnapshot(self, snapshot_image): + if snapshot_image.get('consistencyGroupId'): + result = not (utils.NULL_REF == snapshot_image[ + 'consistencyGroupId']) + else: + result = False + + actual = self.library._is_cgsnapshot(snapshot_image) + + self.assertEqual(result, actual) + + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= + cinder_utils.ZeroIntervalLoopingCall) + def test_copy_volume_high_priority_readonly(self): + src_vol = copy.deepcopy(eseries_fake.VOLUME) + dst_vol = copy.deepcopy(eseries_fake.VOLUME) + vc = copy.deepcopy(eseries_fake.VOLUME_COPY_JOB) + self.mock_object(self.library._client, 'create_volume_copy_job', + mock.Mock(return_value=vc)) + self.mock_object(self.library._client, 'list_vol_copy_job', + mock.Mock(return_value=vc)) + delete_copy = self.mock_object(self.library._client, + 'delete_vol_copy_job') + + result = self.library._copy_volume_high_priority_readonly( + src_vol, dst_vol) + + self.assertIsNone(result) + delete_copy.assert_called_once_with(vc['volcopyRef']) + + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= + cinder_utils.ZeroIntervalLoopingCall) + def test_copy_volume_high_priority_readonly_job_create_failure(self): + src_vol = copy.deepcopy(eseries_fake.VOLUME) + dst_vol = copy.deepcopy(eseries_fake.VOLUME) + self.mock_object( + self.library._client, 'create_volume_copy_job', mock.Mock( + side_effect=exception.NetAppDriverException)) + + self.assertRaises( + exception.NetAppDriverException, + self.library._copy_volume_high_priority_readonly, src_vol, + dst_vol) + @ddt.ddt class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): @@ -1231,9 +1445,12 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): return_value = fake_created_volume) fake_cinder_volume = copy.deepcopy(eseries_fake.FAKE_CINDER_VOLUME) extend_vol = {'id': uuid.uuid4(), 'size': 10} + self.mock_object(self.library, '_create_volume_from_snapshot') self.library.create_cloned_volume(extend_vol, fake_cinder_volume) + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', + new = cinder_utils.ZeroIntervalLoopingCall) def test_create_volume_from_snapshot(self): fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) @@ -1273,6 +1490,8 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): self.library._client.delete_volume.assert_called_once_with( fake_dest_eseries_volume['volumeRef']) + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', + new = cinder_utils.ZeroIntervalLoopingCall) def test_create_volume_from_snapshot_copy_job_fails(self): fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) self.mock_object(self.library, "_schedule_and_create_volume", @@ -1305,6 +1524,8 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): self.library._client.delete_volume.assert_called_once_with( fake_dest_eseries_volume['volumeRef']) + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', + new = cinder_utils.ZeroIntervalLoopingCall) def test_create_volume_from_snapshot_fail_to_delete_snapshot_volume(self): fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) fake_dest_eseries_volume['volumeRef'] = 'fake_volume_ref' @@ -1334,6 +1555,42 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): # Ensure the volume we created is not cleaned up self.assertEqual(0, self.library._client.delete_volume.call_count) + def test_create_snapshot_volume_cgsnap(self): + image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) + self.mock_object(self.library, '_get_snapshot_group', mock.Mock( + return_value=grp)) + expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) + self.mock_object(self.library, '_is_cgsnapshot', mock.Mock( + return_value=True)) + create_view = self.mock_object( + self.library._client, 'create_cg_snapshot_view', + mock.Mock(return_value=expected)) + + result = self.library._create_snapshot_volume(image) + + self.assertEqual(expected, result) + create_view.assert_called_once_with(image['consistencyGroupId'], + mock.ANY, image['id']) + + def test_create_snapshot_volume(self): + image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) + self.mock_object(self.library, '_get_snapshot_group', mock.Mock( + return_value=grp)) + expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) + self.mock_object(self.library, '_is_cgsnapshot', mock.Mock( + return_value=False)) + create_view = self.mock_object( + self.library._client, 'create_snapshot_volume', + mock.Mock(return_value=expected)) + + result = self.library._create_snapshot_volume(image) + + self.assertEqual(expected, result) + create_view.assert_called_once_with( + image['pitRef'], mock.ANY, image['baseVol']) + def test_create_snapshot_group(self): label = 'label' @@ -1483,7 +1740,10 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): full_group = copy.deepcopy(snapshot_group) full_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT - snapshot_groups = [snapshot_group, reserved_group, full_group] + cgroup = copy.deepcopy(snapshot_group) + cgroup['consistencyGroup'] = True + + snapshot_groups = [snapshot_group, reserved_group, full_group, cgroup] get_call = self.mock_object( self.library, '_get_snapshot_groups_for_volume', mock.Mock( return_value=snapshot_groups)) @@ -1817,6 +2077,194 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): get_store.assert_not_called() save_store.assert_not_called() + def test_create_cgsnapshot(self): + fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + fake_vol = cinder_utils.create_volume(self.ctxt) + fake_snapshots = [cinder_utils.create_snapshot(self.ctxt, + fake_vol['id'])] + vol = copy.deepcopy(eseries_fake.VOLUME) + image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + image['baseVol'] = vol['id'] + cg_snaps = [image] + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + + for snap in cg_snaps: + snap['baseVol'] = vol['id'] + get_cg = self.mock_object( + self.library, '_get_consistencygroup_by_name', + mock.Mock(return_value=cg)) + get_vol = self.mock_object( + self.library, '_get_volume', + mock.Mock(return_value=vol)) + mk_snap = self.mock_object( + self.library._client, 'create_consistency_group_snapshot', + mock.Mock(return_value=cg_snaps)) + + model_update, snap_updt = self.library.create_cgsnapshot( + fake_cgsnapshot, fake_snapshots) + + self.assertIsNone(model_update) + for snap in cg_snaps: + self.assertIn({'id': fake_snapshots[0]['id'], + 'provider_id': snap['id'], + 'status': 'available'}, snap_updt) + self.assertEqual(len(cg_snaps), len(snap_updt)) + + get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( + fake_cgsnapshot['consistencygroup_id'])) + self.assertEqual(get_vol.call_count, len(fake_snapshots)) + mk_snap.assert_called_once_with(cg['id']) + + def test_create_cgsnapshot_cg_fail(self): + fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + fake_snapshots = [copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)] + self.mock_object( + self.library, '_get_consistencygroup_by_name', + mock.Mock(side_effect=exception.NetAppDriverException)) + + self.assertRaises( + exception.NetAppDriverException, + self.library.create_cgsnapshot, fake_cgsnapshot, fake_snapshots) + + def test_delete_cgsnapshot(self): + """Test the deletion of a cgsnapshot when a soft delete is required""" + fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + fake_vol = cinder_utils.create_volume(self.ctxt) + fake_snapshots = [cinder_utils.create_snapshot( + self.ctxt, fake_vol['id'])] + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + # Ensure that the snapshot to be deleted is not the oldest + cg_snap['pitSequenceNumber'] = str(max(cg['uniqueSequenceNumber'])) + cg_snaps = [cg_snap] + for snap in fake_snapshots: + snap['provider_id'] = cg_snap['id'] + vol = copy.deepcopy(eseries_fake.VOLUME) + for snap in cg_snaps: + snap['baseVol'] = vol['id'] + get_cg = self.mock_object( + self.library, '_get_consistencygroup_by_name', + mock.Mock(return_value=cg)) + self.mock_object( + self.library._client, 'delete_consistency_group_snapshot') + self.mock_object( + self.library._client, 'get_consistency_group_snapshots', + mock.Mock(return_value=cg_snaps)) + soft_del = self.mock_object( + self.library, '_soft_delete_cgsnapshot', + mock.Mock(return_value=(None, None))) + + # Mock the locking mechanism + model_update, snap_updt = self.library.delete_cgsnapshot( + fake_cgsnapshot, fake_snapshots) + + self.assertIsNone(model_update) + self.assertIsNone(snap_updt) + get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( + fake_cgsnapshot['consistencygroup_id'])) + soft_del.assert_called_once_with( + cg, cg_snap['pitSequenceNumber']) + + @ddt.data(True, False) + def test_soft_delete_cgsnapshot(self, bitset_exists): + """Test the soft deletion of a cgsnapshot""" + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + seq_num = 10 + cg_snap['pitSequenceNumber'] = seq_num + cg_snaps = [cg_snap] + self.mock_object( + self.library._client, 'delete_consistency_group_snapshot') + self.mock_object( + self.library._client, 'get_consistency_group_snapshots', + mock.Mock(return_value=cg_snaps)) + bitset = na_utils.BitSet(1) + index = {cg['id']: repr(bitset)} if bitset_exists else {} + bitset >>= len(cg_snaps) + updt = {cg['id']: repr(bitset)} + self.mock_object(self.library, '_get_soft_delete_map', mock.Mock( + return_value=index)) + save_map = self.mock_object( + self.library, '_merge_soft_delete_changes') + + model_update, snap_updt = self.library._soft_delete_cgsnapshot( + cg, seq_num) + + self.assertIsNone(model_update) + self.assertIsNone(snap_updt) + save_map.assert_called_once_with(updt, None) + + def test_delete_cgsnapshot_single(self): + """Test the backend deletion of the oldest cgsnapshot""" + fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + fake_vol = cinder_utils.create_volume(self.ctxt) + fake_snapshots = [cinder_utils.create_snapshot(self.ctxt, + fake_vol['id'])] + cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + cg_snaps = [cg_snap] + for snap in fake_snapshots: + snap['provider_id'] = cg_snap['id'] + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + cg['uniqueSequenceNumber'] = [cg_snap['pitSequenceNumber']] + vol = copy.deepcopy(eseries_fake.VOLUME) + for snap in cg_snaps: + snap['baseVol'] = vol['id'] + get_cg = self.mock_object( + self.library, '_get_consistencygroup_by_name', + mock.Mock(return_value=cg)) + del_snap = self.mock_object( + self.library._client, 'delete_consistency_group_snapshot', + mock.Mock(return_value=cg_snaps)) + + model_update, snap_updt = self.library.delete_cgsnapshot( + fake_cgsnapshot, fake_snapshots) + + self.assertIsNone(model_update) + self.assertIsNone(snap_updt) + get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( + fake_cgsnapshot['consistencygroup_id'])) + del_snap.assert_called_once_with(cg['id'], cg_snap[ + 'pitSequenceNumber']) + + def test_delete_cgsnapshot_snap_not_found(self): + fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) + fake_vol = cinder_utils.create_volume(self.ctxt) + fake_snapshots = [cinder_utils.create_snapshot( + self.ctxt, fake_vol['id'])] + cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) + cg_snaps = [cg_snap] + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + self.mock_object(self.library, '_get_consistencygroup_by_name', + mock.Mock(return_value=cg)) + self.mock_object( + self.library._client, 'delete_consistency_group_snapshot', + mock.Mock(return_value=cg_snaps)) + + self.assertRaises( + exception.CgSnapshotNotFound, + self.library.delete_cgsnapshot, fake_cgsnapshot, fake_snapshots) + + @ddt.data(0, 1, 10, 32) + def test_cleanup_cg_snapshots(self, count): + # Set the soft delete bit for 'count' snapshot images + bitset = na_utils.BitSet() + for i in range(count): + bitset.set(i) + cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) + # Define 32 snapshots for the CG + cg['uniqueSequenceNumber'] = list(range(32)) + cg_id = cg['id'] + del_snap = self.mock_object( + self.library._client, 'delete_consistency_group_snapshot') + expected_bitset = copy.deepcopy(bitset) >> count + expected_updt = {cg_id: repr(expected_bitset)} + + updt = self.library._cleanup_cg_snapshots( + cg_id, cg['uniqueSequenceNumber'], bitset) + + self.assertEqual(count, del_snap.call_count) + self.assertEqual(expected_updt, updt) + @ddt.data(False, True) def test_get_pool_operation_progress(self, expect_complete): """Validate the operation progress is interpreted correctly""" diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 49dcc5e66..0d869f9d5 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -126,6 +126,33 @@ class RestClient(WebserviceClient): 'snapshot_images': '/storage-systems/{system-id}/snapshot-images', 'snapshot_image': '/storage-systems/{system-id}/snapshot-images/{object-id}', + 'cgroup': + '/storage-systems/{system-id}/consistency-groups/{object-id}', + 'cgroups': '/storage-systems/{system-id}/consistency-groups', + 'cgroup_members': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/member-volumes', + 'cgroup_member': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/member-volumes/{vol-id}', + 'cgroup_snapshots': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/snapshots', + 'cgroup_snapshot': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/snapshots/{seq-num}', + 'cgroup_snapshots_by_seq': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/snapshots/{seq-num}', + 'cgroup_cgsnap_view': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/views/{seq-num}', + 'cgroup_cgsnap_views': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/views/', + 'cgroup_snapshot_views': + '/storage-systems/{system-id}/consistency-groups/{object-id}' + '/views/{view-id}/views', 'persistent-stores': '/storage-systems/{' 'system-id}/persistent-records/', 'persistent-store': '/storage-systems/{' @@ -447,6 +474,131 @@ class RestClient(WebserviceClient): data = {'name': label} return self._invoke('POST', path, data, **{'object-id': object_id}) + def create_consistency_group(self, name, warn_at_percent_full=75, + rollback_priority='medium', + full_policy='failbasewrites'): + """Define a new consistency group""" + path = self.RESOURCE_PATHS.get('cgroups') + data = { + 'name': name, + 'fullWarnThresholdPercent': warn_at_percent_full, + 'repositoryFullPolicy': full_policy, + # A non-zero threshold enables auto-deletion + 'autoDeleteThreshold': 0, + 'rollbackPriority': rollback_priority, + } + + return self._invoke('POST', path, data) + + def get_consistency_group(self, object_id): + """Retrieve the consistency group identified by object_id""" + path = self.RESOURCE_PATHS.get('cgroup') + + return self._invoke('GET', path, **{'object-id': object_id}) + + def list_consistency_groups(self): + """Retrieve all consistency groups defined on the array""" + path = self.RESOURCE_PATHS.get('cgroups') + + return self._invoke('GET', path) + + def delete_consistency_group(self, object_id): + path = self.RESOURCE_PATHS.get('cgroup') + + self._invoke('DELETE', path, **{'object-id': object_id}) + + def add_consistency_group_member(self, volume_id, cg_id, + repo_percent=20.0): + """Add a volume to a consistency group + + :param volume_id the eseries volume id + :param cg_id: the eseries cg id + :param repo_percent: percentage capacity of the volume to use for + capacity of the copy-on-write repository + """ + path = self.RESOURCE_PATHS.get('cgroup_members') + data = {'volumeId': volume_id, 'repositoryPercent': repo_percent} + + return self._invoke('POST', path, data, **{'object-id': cg_id}) + + def remove_consistency_group_member(self, volume_id, cg_id): + """Remove a volume from a consistency group""" + path = self.RESOURCE_PATHS.get('cgroup_member') + + self._invoke('DELETE', path, **{'object-id': cg_id, + 'vol-id': volume_id}) + + def create_consistency_group_snapshot(self, cg_id): + """Define a consistency group snapshot""" + path = self.RESOURCE_PATHS.get('cgroup_snapshots') + + return self._invoke('POST', path, **{'object-id': cg_id}) + + def delete_consistency_group_snapshot(self, cg_id, seq_num): + """Define a consistency group snapshot""" + path = self.RESOURCE_PATHS.get('cgroup_snapshot') + + return self._invoke('DELETE', path, **{'object-id': cg_id, + 'seq-num': seq_num}) + + def get_consistency_group_snapshots(self, cg_id): + """Retrieve all snapshots defined for a consistency group""" + path = self.RESOURCE_PATHS.get('cgroup_snapshots') + + return self._invoke('GET', path, **{'object-id': cg_id}) + + def create_cg_snapshot_view(self, cg_id, name, snap_id): + """Define a snapshot view for the cgsnapshot + + In order to define a snapshot view for a snapshot defined under a + consistency group, the view must be defined at the cgsnapshot + level. + + :param cg_id: E-Series cg identifier + :param name: the label for the view + :param snap_id: E-Series snapshot view to locate + :raise NetAppDriverException: if the snapshot view cannot be + located for the snapshot identified by snap_id + :return snapshot view for snapshot identified by snap_id + """ + path = self.RESOURCE_PATHS.get('cgroup_cgsnap_views') + + data = { + 'name': name, + 'accessMode': 'readOnly', + # Only define a view for this snapshot + 'pitId': snap_id, + } + # Define a view for the cgsnapshot + cgsnapshot_view = self._invoke( + 'POST', path, data, **{'object-id': cg_id}) + + # Retrieve the snapshot views associated with our cgsnapshot view + views = self.list_cg_snapshot_views(cg_id, cgsnapshot_view[ + 'cgViewRef']) + # Find the snapshot view defined for our snapshot + for view in views: + if view['basePIT'] == snap_id: + return view + else: + try: + self.delete_cg_snapshot_view(cg_id, cgsnapshot_view['id']) + finally: + raise exception.NetAppDriverException( + 'Unable to create snapshot view.') + + def list_cg_snapshot_views(self, cg_id, view_id): + path = self.RESOURCE_PATHS.get('cgroup_snapshot_views') + + return self._invoke('GET', path, **{'object-id': cg_id, + 'view-id': view_id}) + + def delete_cg_snapshot_view(self, cg_id, view_id): + path = self.RESOURCE_PATHS.get('cgroup_snap_view') + + return self._invoke('DELETE', path, **{'object-id': cg_id, + 'view-id': view_id}) + def get_pool_operation_progress(self, object_id): """Retrieve the progress long-running operations on a storage pool diff --git a/cinder/volume/drivers/netapp/eseries/fc_driver.py b/cinder/volume/drivers/netapp/eseries/fc_driver.py index 1efc87411..39319cf1c 100644 --- a/cinder/volume/drivers/netapp/eseries/fc_driver.py +++ b/cinder/volume/drivers/netapp/eseries/fc_driver.py @@ -30,7 +30,8 @@ class NetAppEseriesFibreChannelDriver(driver.BaseVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, - driver.SnapshotVD): + driver.SnapshotVD, + driver.ConsistencyGroupVD): """NetApp E-Series FibreChannel volume driver.""" DRIVER_NAME = 'NetApp_FibreChannel_ESeries' @@ -100,3 +101,26 @@ class NetAppEseriesFibreChannelDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup( + group, add_volumes, remove_volumes) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot, snapshots, source_cg, source_vols) diff --git a/cinder/volume/drivers/netapp/eseries/iscsi_driver.py b/cinder/volume/drivers/netapp/eseries/iscsi_driver.py index eee4a5d01..dcb66a439 100644 --- a/cinder/volume/drivers/netapp/eseries/iscsi_driver.py +++ b/cinder/volume/drivers/netapp/eseries/iscsi_driver.py @@ -32,7 +32,8 @@ class NetAppEseriesISCSIDriver(driver.BaseVD, driver.ManageableVD, driver.ExtendVD, driver.TransferVD, - driver.SnapshotVD): + driver.SnapshotVD, + driver.ConsistencyGroupVD): """NetApp E-Series iSCSI volume driver.""" DRIVER_NAME = 'NetApp_iSCSI_ESeries' @@ -100,3 +101,26 @@ class NetAppEseriesISCSIDriver(driver.BaseVD, def get_pool(self, volume): return self.library.get_pool(volume) + + def create_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.create_cgsnapshot(cgsnapshot, snapshots) + + def delete_cgsnapshot(self, context, cgsnapshot, snapshots): + return self.library.delete_cgsnapshot(cgsnapshot, snapshots) + + def create_consistencygroup(self, context, group): + return self.library.create_consistencygroup(group) + + def delete_consistencygroup(self, context, group, volumes): + return self.library.delete_consistencygroup(group, volumes) + + def update_consistencygroup(self, context, group, + add_volumes=None, remove_volumes=None): + return self.library.update_consistencygroup( + group, add_volumes, remove_volumes) + + def create_consistencygroup_from_src(self, context, group, volumes, + cgsnapshot=None, snapshots=None, + source_cg=None, source_vols=None): + return self.library.create_consistencygroup_from_src( + group, volumes, cgsnapshot, snapshots, source_cg, source_vols) diff --git a/cinder/volume/drivers/netapp/eseries/library.py b/cinder/volume/drivers/netapp/eseries/library.py index dc3e78f93..90cd85dc6 100644 --- a/cinder/volume/drivers/netapp/eseries/library.py +++ b/cinder/volume/drivers/netapp/eseries/library.py @@ -638,12 +638,10 @@ class NetAppESeriesLibrary(object): size = volume['size'] dst_vol = self._schedule_and_create_volume(label, size) + src_vol = None try: - src_vol = None - src_vol = self._client.create_snapshot_volume( - image['id'], utils.convert_uuid_to_es_fmt( - uuid.uuid4()), image['baseVol']) - self._copy_volume_high_prior_readonly(src_vol, dst_vol) + src_vol = self._create_snapshot_volume(image) + self._copy_volume_high_priority_readonly(src_vol, dst_vol) LOG.info(_LI("Created volume with label %s."), label) except exception.NetAppDriverException: with excutils.save_and_reraise_exception(): @@ -656,7 +654,8 @@ class NetAppESeriesLibrary(object): LOG.error(_LE("Failure restarting snap vol. Error: %s."), e) else: - LOG.warning(_LW("Snapshot volume not found.")) + LOG.warning(_LW("Snapshot volume creation failed for " + "snapshot %s."), image['id']) return dst_vol @@ -666,20 +665,19 @@ class NetAppESeriesLibrary(object): cinder_utils.synchronized(snapshot['id'])( self._create_volume_from_snapshot)(volume, es_snapshot) - def _copy_volume_high_prior_readonly(self, src_vol, dst_vol): + def _copy_volume_high_priority_readonly(self, src_vol, dst_vol): """Copies src volume to dest volume.""" LOG.info(_LI("Copying src vol %(src)s to dest vol %(dst)s."), {'src': src_vol['label'], 'dst': dst_vol['label']}) + job = None try: - job = None - job = self._client.create_volume_copy_job(src_vol['id'], - dst_vol['volumeRef']) - while True: + job = self._client.create_volume_copy_job( + src_vol['id'], dst_vol['volumeRef']) + + def wait_for_copy(): j_st = self._client.list_vol_copy_job(job['volcopyRef']) - if (j_st['status'] == 'inProgress' or j_st['status'] == - 'pending' or j_st['status'] == 'unknown'): - time.sleep(self.SLEEP_SECS) - continue + if (j_st['status'] in ['inProgress', 'pending', 'unknown']): + return if j_st['status'] == 'failed' or j_st['status'] == 'halted': LOG.error(_LE("Vol copy job status %s."), j_st['status']) raise exception.NetAppDriverException( @@ -687,7 +685,12 @@ class NetAppESeriesLibrary(object): dst_vol['label']) LOG.info(_LI("Vol copy job completed for dest %s."), dst_vol['label']) - break + raise loopingcall.LoopingCallDone() + + checker = loopingcall.FixedIntervalLoopingCall(wait_for_copy) + checker.start(interval=self.SLEEP_SECS, + initial_delay=self.SLEEP_SECS, + stop_on_exception=True).wait() finally: if job: try: @@ -702,13 +705,9 @@ class NetAppESeriesLibrary(object): def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" - snapshot = {'id': uuid.uuid4(), 'volume_id': src_vref['id'], - 'volume': src_vref} - group_name = (utils.convert_uuid_to_es_fmt(snapshot['id']) + - self.SNAPSHOT_VOL_COPY_SUFFIX) es_vol = self._get_volume(src_vref['id']) - es_snapshot = self._create_es_snapshot(es_vol, group_name) + es_snapshot = self._create_es_snapshot_for_clone(es_vol) try: self._create_volume_from_snapshot(volume, es_snapshot) @@ -717,7 +716,7 @@ class NetAppESeriesLibrary(object): self._client.delete_snapshot_group(es_snapshot['pitGroupRef']) except exception.NetAppDriverException: LOG.warning(_LW("Failure deleting temp snapshot %s."), - snapshot['id']) + es_snapshot['id']) def delete_volume(self, volume): """Deletes a volume.""" @@ -728,15 +727,27 @@ class NetAppESeriesLibrary(object): LOG.warning(_LW("Volume %s already deleted."), volume['id']) return - def _create_snapshot_volume(self, snapshot_id, label=None): + def _is_cgsnapshot(self, snapshot_image): + """Determine if an E-Series snapshot image is part of a cgsnapshot""" + cg_id = snapshot_image.get('consistencyGroupId') + # A snapshot that is not part of a consistency group may have a + # cg_id of either none or a string of all 0's, so we check for both + return not (cg_id is None or utils.NULL_REF == cg_id) + + def _create_snapshot_volume(self, image): """Creates snapshot volume for given group with snapshot_id.""" - image = self._get_snapshot(snapshot_id) group = self._get_snapshot_group(image['pitGroupRef']) + LOG.debug("Creating snap vol for group %s", group['label']) - if label is None: - label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) - return self._client.create_snapshot_volume(image['pitRef'], label, - image['baseVol']) + + label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) + + if self._is_cgsnapshot(image): + return self._client.create_cg_snapshot_view( + image['consistencyGroupId'], label, image['id']) + else: + return self._client.create_snapshot_volume( + image['pitRef'], label, image['baseVol']) def _create_snapshot_group(self, label, volume, percentage_capacity=20.0): """Define a new snapshot group for a volume @@ -802,6 +813,9 @@ class NetAppESeriesLibrary(object): groups = filter(lambda g: self.SNAPSHOT_VOL_COPY_SUFFIX not in g[ 'label'], groups_for_v) + # Filter out groups that are part of a consistency group + groups = filter(lambda g: not g['consistencyGroup'], groups) + # Find all groups with free snapshot capacity groups = [group for group in groups if group.get('snapshotCount') < self.MAX_SNAPSHOT_COUNT] @@ -833,6 +847,11 @@ class NetAppESeriesLibrary(object): else: return None + def _create_es_snapshot_for_clone(self, vol): + group_name = (utils.convert_uuid_to_es_fmt(uuid.uuid4()) + + self.SNAPSHOT_VOL_COPY_SUFFIX) + return self._create_es_snapshot(vol, group_name) + def _create_es_snapshot(self, vol, group_name=None): snap_grp, snap_image = None, None try: @@ -1520,6 +1539,8 @@ class NetAppESeriesLibrary(object): pool_ssc_info = ssc_stats[poolId] + pool_ssc_info['consistencygroup_support'] = True + pool_ssc_info[self.ENCRYPTION_UQ_SPEC] = ( six.text_type(pool['encrypted']).lower()) @@ -1724,6 +1745,280 @@ class NetAppESeriesLibrary(object): initial_delay=self.SLEEP_SECS, stop_on_exception=True).wait() + def create_cgsnapshot(self, cgsnapshot, snapshots): + """Creates a cgsnapshot.""" + cg_id = cgsnapshot['consistencygroup_id'] + cg_name = utils.convert_uuid_to_es_fmt(cg_id) + + # Retrieve the E-Series consistency group + es_cg = self._get_consistencygroup_by_name(cg_name) + + # Define an E-Series CG Snapshot + es_snaphots = self._client.create_consistency_group_snapshot( + es_cg['id']) + + # Build the snapshot updates + snapshot_updates = list() + for snap in snapshots: + es_vol = self._get_volume(snap['volume']['id']) + for es_snap in es_snaphots: + if es_snap['baseVol'] == es_vol['id']: + snapshot_updates.append({ + 'id': snap['id'], + # Directly track the backend snapshot ID + 'provider_id': es_snap['id'], + 'status': 'available' + }) + + return None, snapshot_updates + + def delete_cgsnapshot(self, cgsnapshot, snapshots): + """Deletes a cgsnapshot.""" + + cg_id = cgsnapshot['consistencygroup_id'] + cg_name = utils.convert_uuid_to_es_fmt(cg_id) + + # Retrieve the E-Series consistency group + es_cg = self._get_consistencygroup_by_name(cg_name) + + # Find the smallest sequence number defined on the group + min_seq_num = min(es_cg['uniqueSequenceNumber']) + + es_snapshots = self._client.get_consistency_group_snapshots( + es_cg['id']) + es_snap_ids = set(snap.get('provider_id') for snap in snapshots) + + # We need to find a single snapshot that is a part of the CG snap + seq_num = None + for snap in es_snapshots: + if snap['id'] in es_snap_ids: + seq_num = snap['pitSequenceNumber'] + break + + if seq_num is None: + raise exception.CgSnapshotNotFound(cgsnapshot_id=cg_id) + + # Perform a full backend deletion of the cgsnapshot + if int(seq_num) <= int(min_seq_num): + self._client.delete_consistency_group_snapshot( + es_cg['id'], seq_num) + return None, None + else: + # Perform a soft-delete, removing this snapshot from cinder + # management, and marking it as available for deletion. + return cinder_utils.synchronized(cg_id)( + self._soft_delete_cgsnapshot)( + es_cg, seq_num) + + def _soft_delete_cgsnapshot(self, es_cg, snap_seq_num): + """Mark a cgsnapshot as available for deletion from the backend. + + E-Series snapshots cannot be deleted out of order, as older + snapshots in the snapshot group are dependent on the newer + snapshots. A "soft delete" results in the cgsnapshot being removed + from Cinder management, with the snapshot marked as available for + deletion once all snapshots dependent on it are also deleted. + + :param es_cg: E-Series consistency group + :param snap_seq_num: unique sequence number of the cgsnapshot + :return an update to the snapshot index + """ + + index = self._get_soft_delete_map() + cg_ref = es_cg['id'] + if cg_ref in index: + bitset = na_utils.BitSet(int((index[cg_ref]))) + else: + bitset = na_utils.BitSet(0) + + seq_nums = ( + set([snap['pitSequenceNumber'] for snap in + self._client.get_consistency_group_snapshots(cg_ref)])) + + # Determine the relative index of the snapshot's sequence number + for i, seq_num in enumerate(sorted(seq_nums)): + if snap_seq_num == seq_num: + bitset.set(i) + break + + index_update = ( + self._cleanup_cg_snapshots(cg_ref, seq_nums, bitset)) + + self._merge_soft_delete_changes(index_update, None) + + return None, None + + def _cleanup_cg_snapshots(self, cg_ref, seq_nums, bitset): + """Delete cg snapshot images that are marked for removal + + The snapshot index tracks all snapshots that have been removed from + Cinder, and are therefore available for deletion when this operation + is possible. + + CG snapshots are tracked by unique sequence numbers that are + associated with 1 or more snapshot images. The sequence numbers are + tracked (relative to the 32 images allowed per group), within the + snapshot index. + + This method will purge CG snapshots that have been marked as + available for deletion within the backend persistent store. + + :param cg_ref: reference to an E-Series consistent group + :param seq_nums: set of unique sequence numbers associated with the + consistency group + :param bitset: the bitset representing which sequence numbers are + marked for deletion + :return update for the snapshot index + """ + deleted = 0 + # Order by their sequence number, from oldest to newest + for i, seq_num in enumerate(sorted(seq_nums)): + if bitset.is_set(i): + self._client.delete_consistency_group_snapshot(cg_ref, + seq_num) + deleted += 1 + else: + # Snapshots must be deleted in order, so if the current + # snapshot is not pending deletion, we don't want to + # process any more + break + + if deleted: + # We need to update the bitset to reflect the fact that older + # snapshots have been deleted, so snapshot relative indexes + # have now been updated. + bitset >>= deleted + + LOG.debug('Deleted %(count)s snapshot images from ' + 'consistency group: %(grp)s.', {'count': deleted, + 'grp': cg_ref}) + # Update the index + return {cg_ref: repr(bitset)} + + def create_consistencygroup(self, cinder_cg): + """Define a consistency group.""" + self._create_consistency_group(cinder_cg) + + return {'status': 'available'} + + def _create_consistency_group(self, cinder_cg): + """Define a new consistency group on the E-Series backend""" + name = utils.convert_uuid_to_es_fmt(cinder_cg['id']) + return self._client.create_consistency_group(name) + + def _get_consistencygroup(self, cinder_cg): + """Retrieve an E-Series consistency group""" + name = utils.convert_uuid_to_es_fmt(cinder_cg['id']) + return self._get_consistencygroup_by_name(name) + + def _get_consistencygroup_by_name(self, name): + """Retrieve an E-Series consistency group by name""" + + for cg in self._client.list_consistency_groups(): + if name == cg['name']: + return cg + + raise exception.ConsistencyGroupNotFound(consistencygroup_id=name) + + def delete_consistencygroup(self, group, volumes): + """Deletes a consistency group.""" + + volume_update = list() + + for volume in volumes: + LOG.info(_LI('Deleting volume %s.'), volume['id']) + volume_update.append({ + 'status': 'deleted', 'id': volume['id'], + }) + self.delete_volume(volume) + + try: + cg = self._get_consistencygroup(group) + except exception.ConsistencyGroupNotFound: + LOG.warning(_LW('Consistency group already deleted.')) + else: + self._client.delete_consistency_group(cg['id']) + try: + self._merge_soft_delete_changes(None, [cg['id']]) + except (exception.NetAppDriverException, + eseries_exc.WebServiceException): + LOG.warning(_LW('Unable to remove CG from the deletion map.')) + + model_update = {'status': 'deleted'} + + return model_update, volume_update + + def _update_consistency_group_members(self, es_cg, + add_volumes, remove_volumes): + """Add or remove consistency group members + + :param es_cg: The E-Series consistency group + :param add_volumes: A list of Cinder volumes to add to the + consistency group + :param remove_volumes: A list of Cinder volumes to remove from the + consistency group + :return None + """ + for volume in remove_volumes: + es_vol = self._get_volume(volume['id']) + LOG.info( + _LI('Removing volume %(v)s from consistency group %(''cg)s.'), + {'v': es_vol['label'], 'cg': es_cg['label']}) + self._client.remove_consistency_group_member(es_vol['id'], + es_cg['id']) + + for volume in add_volumes: + es_vol = self._get_volume(volume['id']) + LOG.info(_LI('Adding volume %(v)s to consistency group %(cg)s.'), + {'v': es_vol['label'], 'cg': es_cg['label']}) + self._client.add_consistency_group_member( + es_vol['id'], es_cg['id']) + + def update_consistencygroup(self, group, + add_volumes, remove_volumes): + """Add or remove volumes from an existing consistency group""" + cg = self._get_consistencygroup(group) + + self._update_consistency_group_members( + cg, add_volumes, remove_volumes) + + return None, None, None + + def create_consistencygroup_from_src(self, group, volumes, + cgsnapshot, snapshots, + source_cg, source_vols): + """Define a consistency group based on an existing group + + Define a new consistency group from a source consistency group. If + only a source_cg is provided, then clone each base volume and add + it to a new consistency group. If a cgsnapshot is provided, + clone each snapshot image to a new volume and add it to the cg. + + :param group: The new consistency group to define + :param volumes: The volumes to add to the consistency group + :param cgsnapshot: The cgsnapshot to base the group on + :param snapshots: The list of snapshots on the source cg + :param source_cg: The source consistency group + :param source_vols: The volumes added to the source cg + """ + cg = self._create_consistency_group(group) + if cgsnapshot: + for vol, snap in zip(volumes, snapshots): + image = self._get_snapshot(snap) + self._create_volume_from_snapshot(vol, image) + else: + for vol, src in zip(volumes, source_vols): + es_vol = self._get_volume(src['id']) + es_snapshot = self._create_es_snapshot_for_clone(es_vol) + try: + self._create_volume_from_snapshot(vol, es_snapshot) + finally: + self._delete_es_snapshot(es_snapshot) + + self._update_consistency_group_members(cg, volumes, []) + + return None, None + def _garbage_collect_tmp_vols(self): """Removes tmp vols with no snapshots.""" try: diff --git a/releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml b/releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml new file mode 100644 index 000000000..6a3bfb5be --- /dev/null +++ b/releasenotes/notes/netapp-eseries-consistency-groups-4f6b2af2d20c94e9.yaml @@ -0,0 +1,3 @@ +--- +features: + - Support for Consistency Groups in the NetApp E-Series Volume Driver