'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,
'activeCOW': True,
'isRollbackSource': False,
'pitRef': '3400000060080E500023BB3400631F335294A5A8',
- 'pitSequenceNumber': '19'
+ 'pitSequenceNumber': '19',
+ 'consistencyGroupId': '0000000000000000000000000000000000000000',
}
SNAPSHOT_VOLUME = {
'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 = []
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 [
{
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
'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):
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)
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',
thin_provisioned = pool['thinProvisioningCapable']
expected = {
+ 'consistencygroup_support': True,
'netapp_disk_encryption':
six.text_type(pool['encrypted']).lower(),
'netapp_eseries_flash_read_cache':
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):
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)
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",
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'
# 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'
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))
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"""
'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/{'
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
driver.ManageableVD,
driver.ExtendVD,
driver.TransferVD,
- driver.SnapshotVD):
+ driver.SnapshotVD,
+ driver.ConsistencyGroupVD):
"""NetApp E-Series FibreChannel volume driver."""
DRIVER_NAME = 'NetApp_FibreChannel_ESeries'
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)
driver.ManageableVD,
driver.ExtendVD,
driver.TransferVD,
- driver.SnapshotVD):
+ driver.SnapshotVD,
+ driver.ConsistencyGroupVD):
"""NetApp E-Series iSCSI volume driver."""
DRIVER_NAME = 'NetApp_iSCSI_ESeries'
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)
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():
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
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(
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:
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)
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."""
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
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]
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:
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())
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:
--- /dev/null
+---
+features:
+ - Support for Consistency Groups in the NetApp E-Series Volume Driver