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
This commit is contained in:
parent
68e7ae469d
commit
6b7f476335
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Support for Consistency Groups in the NetApp E-Series Volume Driver
|
Loading…
Reference in New Issue
Block a user