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:
Michael Price 2016-01-08 11:04:35 -06:00
parent 68e7ae469d
commit 6b7f476335
9 changed files with 1280 additions and 34 deletions

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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"""

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -0,0 +1,3 @@
---
features:
- Support for Consistency Groups in the NetApp E-Series Volume Driver