VMAX driver - Implement Tiramisu feature on VMAX
In Tiramisu, a group construct is used to manage the group of volumes to be replicated together for the ease of management. This patch adds this support to the VMAX driver. Change-Id: I9fffa0c6dc3092f3230cfa5da1ea5f3ff1e3151b Implements: blueprint vmax-replication-group
This commit is contained in:
parent
27fd333df9
commit
c6b0c4bca6
@ -26,6 +26,7 @@ import six
|
|||||||
|
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder import objects
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder.objects import group
|
from cinder.objects import group
|
||||||
from cinder.objects import group_snapshot
|
from cinder.objects import group_snapshot
|
||||||
@ -252,12 +253,12 @@ class VMAXCommonData(object):
|
|||||||
|
|
||||||
test_vol_grp_name_id_only = 'ec870a2f-6bf7-4152-aa41-75aad8e2ea96'
|
test_vol_grp_name_id_only = 'ec870a2f-6bf7-4152-aa41-75aad8e2ea96'
|
||||||
test_vol_grp_name = 'Grp_source_sg_%s' % test_vol_grp_name_id_only
|
test_vol_grp_name = 'Grp_source_sg_%s' % test_vol_grp_name_id_only
|
||||||
|
test_fo_vol_group = 'fo_vol_group_%s' % test_vol_grp_name_id_only
|
||||||
|
|
||||||
test_group_1 = group.Group(
|
test_group_1 = group.Group(
|
||||||
context=None, name=storagegroup_name_source,
|
context=None, name=storagegroup_name_source,
|
||||||
group_id='abc', size=1,
|
group_id='abc', size=1,
|
||||||
id=test_vol_grp_name_id_only,
|
id=test_vol_grp_name_id_only, status='available',
|
||||||
status='available',
|
|
||||||
provider_auth=None, volume_type_ids=['abc'],
|
provider_auth=None, volume_type_ids=['abc'],
|
||||||
group_type_id='grptypeid',
|
group_type_id='grptypeid',
|
||||||
volume_types=test_volume_type_list,
|
volume_types=test_volume_type_list,
|
||||||
@ -272,17 +273,21 @@ class VMAXCommonData(object):
|
|||||||
provider_auth=None, volume_type_ids=['abc'],
|
provider_auth=None, volume_type_ids=['abc'],
|
||||||
group_type_id='grptypeid',
|
group_type_id='grptypeid',
|
||||||
volume_types=test_volume_type_list,
|
volume_types=test_volume_type_list,
|
||||||
host=fake_host, provider_location=six.text_type(provider_location))
|
host=fake_host, provider_location=six.text_type(provider_location),
|
||||||
|
replication_status=fields.ReplicationStatus.DISABLED)
|
||||||
|
|
||||||
|
test_rep_group = fake_group.fake_group_obj(
|
||||||
|
context=ctx, name=storagegroup_name_source,
|
||||||
|
id=test_vol_grp_name_id_only, host=fake_host,
|
||||||
|
replication_status=fields.ReplicationStatus.ENABLED)
|
||||||
|
|
||||||
test_group = fake_group.fake_group_obj(
|
test_group = fake_group.fake_group_obj(
|
||||||
context=ctx, name=storagegroup_name_source,
|
context=ctx, name=storagegroup_name_source,
|
||||||
id='7634bda4-6950-436f-998c-37c3e01bad30', host=fake_host)
|
id=test_vol_grp_name_id_only, host=fake_host)
|
||||||
|
|
||||||
test_group_without_name = fake_group.fake_group_obj(
|
test_group_without_name = fake_group.fake_group_obj(
|
||||||
context=ctx,
|
context=ctx, name=None,
|
||||||
name=None,
|
id=test_vol_grp_name_id_only, host=fake_host)
|
||||||
id=test_vol_grp_name_id_only,
|
|
||||||
host=fake_host)
|
|
||||||
|
|
||||||
test_group_snapshot_1 = group_snapshot.GroupSnapshot(
|
test_group_snapshot_1 = group_snapshot.GroupSnapshot(
|
||||||
context=None, id='6560405d-b89a-4f79-9e81-ad1752f5a139',
|
context=None, id='6560405d-b89a-4f79-9e81-ad1752f5a139',
|
||||||
@ -300,6 +305,13 @@ class VMAXCommonData(object):
|
|||||||
status='available',
|
status='available',
|
||||||
group=test_group_failed)
|
group=test_group_failed)
|
||||||
|
|
||||||
|
test_volume_group_member = fake_volume.fake_volume_obj(
|
||||||
|
context=ctx, name='vol1', size=2, provider_auth=None,
|
||||||
|
provider_location=six.text_type(provider_location),
|
||||||
|
volume_type=test_volume_type, host=fake_host,
|
||||||
|
replication_driver_data=six.text_type(provider_location3),
|
||||||
|
group_id=test_vol_grp_name_id_only)
|
||||||
|
|
||||||
# masking view dict
|
# masking view dict
|
||||||
masking_view_dict = {
|
masking_view_dict = {
|
||||||
'array': array,
|
'array': array,
|
||||||
@ -441,6 +453,17 @@ class VMAXCommonData(object):
|
|||||||
"symmetrixId": array,
|
"symmetrixId": array,
|
||||||
"numSnapVXSnapshots": 1}]
|
"numSnapVXSnapshots": 1}]
|
||||||
|
|
||||||
|
sg_rdf_details = [{"storageGroupName": test_vol_grp_name,
|
||||||
|
"symmetrixId": array,
|
||||||
|
"modes": ["Synchronous"],
|
||||||
|
"rdfGroupNumber": rdf_group_no,
|
||||||
|
"states": ["Synchronized"]},
|
||||||
|
{"storageGroupName": test_fo_vol_group,
|
||||||
|
"symmetrixId": array,
|
||||||
|
"modes": ["Synchronous"],
|
||||||
|
"rdfGroupNumber": rdf_group_no,
|
||||||
|
"states": ["Failed Over"]}]
|
||||||
|
|
||||||
sg_list = {"storageGroupId": [storagegroup_name_f,
|
sg_list = {"storageGroupId": [storagegroup_name_f,
|
||||||
defaultstoragegroup_name]}
|
defaultstoragegroup_name]}
|
||||||
|
|
||||||
@ -498,7 +521,12 @@ class VMAXCommonData(object):
|
|||||||
"targetDevice": device_id2,
|
"targetDevice": device_id2,
|
||||||
"sourceDevice": device_id}}],
|
"sourceDevice": device_id}}],
|
||||||
"snapVXSrc": 'true',
|
"snapVXSrc": 'true',
|
||||||
"snapVXTgt": 'true'}}]}}
|
"snapVXTgt": 'true'},
|
||||||
|
"rdfInfo": {"RDFSession": [
|
||||||
|
{"SRDFStatus": "Ready",
|
||||||
|
"pairState": "Synchronized",
|
||||||
|
"remoteDeviceID": device_id2,
|
||||||
|
"remoteSymmetrixID": remote_array}]}}]}}
|
||||||
|
|
||||||
workloadtype = {"workloadId": ["OLTP", "OLTP_REP", "DSS", "DSS_REP"]}
|
workloadtype = {"workloadId": ["OLTP", "OLTP_REP", "DSS", "DSS_REP"]}
|
||||||
slo_details = {"sloId": ["Bronze", "Diamond", "Gold",
|
slo_details = {"sloId": ["Bronze", "Diamond", "Gold",
|
||||||
@ -741,15 +769,15 @@ class FakeRequestsSession(object):
|
|||||||
|
|
||||||
def _replication(self, url):
|
def _replication(self, url):
|
||||||
return_object = None
|
return_object = None
|
||||||
if 'rdf_group' in url:
|
if 'storagegroup' in url:
|
||||||
|
return_object = self._replication_sg(url)
|
||||||
|
elif 'rdf_group' in url:
|
||||||
if self.data.device_id in url:
|
if self.data.device_id in url:
|
||||||
return_object = self.data.rdf_group_vol_details
|
return_object = self.data.rdf_group_vol_details
|
||||||
elif self.data.rdf_group_no in url:
|
elif self.data.rdf_group_no in url:
|
||||||
return_object = self.data.rdf_group_details
|
return_object = self.data.rdf_group_details
|
||||||
else:
|
else:
|
||||||
return_object = self.data.rdf_group_list
|
return_object = self.data.rdf_group_list
|
||||||
elif 'storagegroup' in url:
|
|
||||||
return_object = self._replication_sg(url)
|
|
||||||
elif 'snapshot' in url:
|
elif 'snapshot' in url:
|
||||||
return_object = self.data.volume_snap_vx
|
return_object = self.data.volume_snap_vx
|
||||||
elif 'capabilities' in url:
|
elif 'capabilities' in url:
|
||||||
@ -760,6 +788,11 @@ class FakeRequestsSession(object):
|
|||||||
return_object = None
|
return_object = None
|
||||||
if 'generation' in url:
|
if 'generation' in url:
|
||||||
return_object = self.data.group_snap_vx
|
return_object = self.data.group_snap_vx
|
||||||
|
elif 'rdf_group' in url:
|
||||||
|
for sg in self.data.sg_rdf_details:
|
||||||
|
if sg['storageGroupName'] in url:
|
||||||
|
return_object = sg
|
||||||
|
break
|
||||||
elif 'storagegroup' in url:
|
elif 'storagegroup' in url:
|
||||||
return_object = self.data.sg_details_rep[0]
|
return_object = self.data.sg_details_rep[0]
|
||||||
return return_object
|
return return_object
|
||||||
@ -1247,19 +1280,6 @@ class VMAXUtilsTest(test.TestCase):
|
|||||||
vol_grp_name = self.utils.update_volume_group_name(group)
|
vol_grp_name = self.utils.update_volume_group_name(group)
|
||||||
self.assertEqual(ref_group_name, vol_grp_name)
|
self.assertEqual(ref_group_name, vol_grp_name)
|
||||||
|
|
||||||
def test_update_admin_metadata(self):
|
|
||||||
admin_metadata = {'targetVolumeName': '123456'}
|
|
||||||
ref_model_update = [{'id': '12345',
|
|
||||||
'admin_metadata': admin_metadata}]
|
|
||||||
volume_model_update = {'id': '12345'}
|
|
||||||
volumes_model_update = [volume_model_update]
|
|
||||||
key = 'targetVolumeName'
|
|
||||||
values = {}
|
|
||||||
values['12345'] = '123456'
|
|
||||||
self.utils.update_admin_metadata(
|
|
||||||
volumes_model_update, key, values)
|
|
||||||
self.assertEqual(ref_model_update, volumes_model_update)
|
|
||||||
|
|
||||||
def test_get_volume_group_utils(self):
|
def test_get_volume_group_utils(self):
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
array, extraspecs_dict = self.utils.get_volume_group_utils(
|
array, extraspecs_dict = self.utils.get_volume_group_utils(
|
||||||
@ -1313,6 +1333,38 @@ class VMAXUtilsTest(test.TestCase):
|
|||||||
volume_model_updates, volumes, 'abc')
|
volume_model_updates, volumes, 'abc')
|
||||||
self.assertEqual(ref_val, ret_val)
|
self.assertEqual(ref_val, ret_val)
|
||||||
|
|
||||||
|
def test_check_replication_matched(self):
|
||||||
|
# Check 1: Volume is not part of a group
|
||||||
|
self.utils.check_replication_matched(
|
||||||
|
self.data.test_volume, self.data.extra_specs)
|
||||||
|
group_volume = deepcopy(self.data.test_volume)
|
||||||
|
group_volume.group = self.data.test_group
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False):
|
||||||
|
# Check 2: Both volume and group have the same rep status
|
||||||
|
self.utils.check_replication_matched(
|
||||||
|
group_volume, self.data.extra_specs)
|
||||||
|
# Check 3: Volume and group have different rep status
|
||||||
|
with mock.patch.object(self.utils, 'is_replication_enabled',
|
||||||
|
return_value=True):
|
||||||
|
self.assertRaises(exception.InvalidInput,
|
||||||
|
self.utils.check_replication_matched,
|
||||||
|
group_volume, self.data.extra_specs)
|
||||||
|
|
||||||
|
def test_check_rep_status_enabled(self):
|
||||||
|
# Check 1: not replication enabled
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False):
|
||||||
|
self.utils.check_rep_status_enabled(self.data.test_group)
|
||||||
|
# Check 2: replication enabled, status enabled
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=True):
|
||||||
|
self.utils.check_rep_status_enabled(self.data.test_rep_group)
|
||||||
|
# Check 3: replication enabled, status disabled
|
||||||
|
self.assertRaises(exception.InvalidInput,
|
||||||
|
self.utils.check_rep_status_enabled,
|
||||||
|
self.data.test_group)
|
||||||
|
|
||||||
|
|
||||||
class VMAXRestTest(test.TestCase):
|
class VMAXRestTest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -2408,6 +2460,14 @@ class VMAXRestTest(test.TestCase):
|
|||||||
sg_value, qos_extra_spec, input_prop_dict)
|
sg_value, qos_extra_spec, input_prop_dict)
|
||||||
self.assertEqual(input_prop_dict, ret_prop_dict)
|
self.assertEqual(input_prop_dict, ret_prop_dict)
|
||||||
|
|
||||||
|
@mock.patch.object(rest.VMAXRest, 'modify_storage_group',
|
||||||
|
return_value=(202, VMAXCommonData.job_list[0]))
|
||||||
|
def test_set_storagegroup_srp(self, mock_mod):
|
||||||
|
self.rest.set_storagegroup_srp(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.srp2, self.data.extra_specs)
|
||||||
|
mock_mod.assert_called_once()
|
||||||
|
|
||||||
def test_get_rdf_group(self):
|
def test_get_rdf_group(self):
|
||||||
with mock.patch.object(self.rest, 'get_resource') as mock_get:
|
with mock.patch.object(self.rest, 'get_resource') as mock_get:
|
||||||
self.rest.get_rdf_group(self.data.array, self.data.rdf_group_no)
|
self.rest.get_rdf_group(self.data.array, self.data.rdf_group_no)
|
||||||
@ -2420,26 +2480,29 @@ class VMAXRestTest(test.TestCase):
|
|||||||
self.assertEqual(self.data.rdf_group_list, rdf_list)
|
self.assertEqual(self.data.rdf_group_list, rdf_list)
|
||||||
|
|
||||||
def test_get_rdf_group_volume(self):
|
def test_get_rdf_group_volume(self):
|
||||||
with mock.patch.object(self.rest, 'get_resource') as mock_get:
|
vol_details = self.data.private_vol_details['resultList']['result'][0]
|
||||||
|
with mock.patch.object(
|
||||||
|
self.rest, '_get_private_volume', return_value=vol_details
|
||||||
|
) as mock_get:
|
||||||
self.rest.get_rdf_group_volume(
|
self.rest.get_rdf_group_volume(
|
||||||
self.data.array, self.data.rdf_group_no, self.data.device_id)
|
self.data.array, self.data.device_id)
|
||||||
mock_get.assert_called_once_with(
|
mock_get.assert_called_once_with(
|
||||||
self.data.array, 'replication', 'rdf_group', "70/volume/00001")
|
self.data.array, self.data.device_id)
|
||||||
|
|
||||||
def test_are_vols_rdf_paired(self):
|
def test_are_vols_rdf_paired(self):
|
||||||
are_vols1, local_state, pair_state = self.rest.are_vols_rdf_paired(
|
are_vols1, local_state, pair_state = self.rest.are_vols_rdf_paired(
|
||||||
self.data.array, self.data.remote_array, self.data.device_id,
|
self.data.array, self.data.remote_array, self.data.device_id,
|
||||||
self.data.device_id2, self.data.rdf_group_no)
|
self.data.device_id2)
|
||||||
self.assertTrue(are_vols1)
|
self.assertTrue(are_vols1)
|
||||||
are_vols2, local_state, pair_state = self.rest.are_vols_rdf_paired(
|
are_vols2, local_state, pair_state = self.rest.are_vols_rdf_paired(
|
||||||
self.data.array, "00012345", self.data.device_id,
|
self.data.array, "00012345", self.data.device_id,
|
||||||
self.data.device_id2, self.data.rdf_group_no)
|
self.data.device_id2)
|
||||||
self.assertFalse(are_vols2)
|
self.assertFalse(are_vols2)
|
||||||
with mock.patch.object(self.rest, "get_rdf_group_volume",
|
with mock.patch.object(self.rest, "get_rdf_group_volume",
|
||||||
return_value=None):
|
return_value=None):
|
||||||
are_vols3, local, pair = self.rest.are_vols_rdf_paired(
|
are_vols3, local, pair = self.rest.are_vols_rdf_paired(
|
||||||
self.data.array, self.data.remote_array, self.data.device_id,
|
self.data.array, self.data.remote_array, self.data.device_id,
|
||||||
self.data.device_id2, self.data.rdf_group_no)
|
self.data.device_id2)
|
||||||
self.assertFalse(are_vols3)
|
self.assertFalse(are_vols3)
|
||||||
|
|
||||||
def test_get_rdf_group_number(self):
|
def test_get_rdf_group_number(self):
|
||||||
@ -2536,6 +2599,40 @@ class VMAXRestTest(test.TestCase):
|
|||||||
snap_name,
|
snap_name,
|
||||||
extra_specs)
|
extra_specs)
|
||||||
|
|
||||||
|
def test_get_storagegroup_rdf_details(self):
|
||||||
|
details = self.rest.get_storagegroup_rdf_details(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no)
|
||||||
|
self.assertEqual(self.data.sg_rdf_details[0], details)
|
||||||
|
|
||||||
|
def test_verify_rdf_state(self):
|
||||||
|
verify1 = self.rest._verify_rdf_state(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, 'Failover')
|
||||||
|
self.assertTrue(verify1)
|
||||||
|
verify2 = self.rest._verify_rdf_state(
|
||||||
|
self.data.array, self.data.test_fo_vol_group,
|
||||||
|
self.data.rdf_group_no, 'Establish')
|
||||||
|
self.assertTrue(verify2)
|
||||||
|
|
||||||
|
def test_modify_storagegroup_rdf(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.rest, 'modify_resource',
|
||||||
|
return_value=(202, self.data.job_list[0])) as mock_mod:
|
||||||
|
self.rest.modify_storagegroup_rdf(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, 'Failover',
|
||||||
|
self.data.extra_specs)
|
||||||
|
mock_mod.assert_called_once()
|
||||||
|
|
||||||
|
def test_delete_storagegroup_rdf(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.rest, 'delete_resource') as mock_del:
|
||||||
|
self.rest.delete_storagegroup_rdf(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no)
|
||||||
|
mock_del.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class VMAXProvisionTest(test.TestCase):
|
class VMAXProvisionTest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -2887,6 +2984,59 @@ class VMAXProvisionTest(test.TestCase):
|
|||||||
self.assertEqual(2, mock_sg.call_count)
|
self.assertEqual(2, mock_sg.call_count)
|
||||||
self.assertEqual(1, mock_create.call_count)
|
self.assertEqual(1, mock_create.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(rest.VMAXRest, 'create_resource',
|
||||||
|
return_value=(202, VMAXCommonData.job_list[0]))
|
||||||
|
def test_replicate_group(self, mock_create):
|
||||||
|
self.rest.replicate_group(
|
||||||
|
self.data.array, self.data.test_rep_group,
|
||||||
|
self.data.rdf_group_no, self.data.remote_array,
|
||||||
|
self.data.extra_specs)
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
|
||||||
|
def test_enable_group_replication(self):
|
||||||
|
with mock.patch.object(self.rest,
|
||||||
|
'modify_storagegroup_rdf') as mock_mod:
|
||||||
|
self.provision.enable_group_replication(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, self.data.extra_specs)
|
||||||
|
mock_mod.assert_called_once()
|
||||||
|
|
||||||
|
def test_disable_group_replication(self):
|
||||||
|
with mock.patch.object(self.rest,
|
||||||
|
'modify_storagegroup_rdf') as mock_mod:
|
||||||
|
self.provision.disable_group_replication(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, self.data.extra_specs)
|
||||||
|
mock_mod.assert_called_once()
|
||||||
|
|
||||||
|
def test_failover_group(self):
|
||||||
|
with mock.patch.object(self.rest,
|
||||||
|
'modify_storagegroup_rdf') as mock_fo:
|
||||||
|
# Failover
|
||||||
|
self.provision.failover_group(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, self.data.extra_specs)
|
||||||
|
mock_fo.assert_called_once_with(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, 'Failover', self.data.extra_specs)
|
||||||
|
mock_fo.reset_mock()
|
||||||
|
# Failback
|
||||||
|
self.provision.failover_group(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, self.data.extra_specs, False)
|
||||||
|
mock_fo.assert_called_once_with(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, 'Failback', self.data.extra_specs)
|
||||||
|
|
||||||
|
@mock.patch.object(rest.VMAXRest, 'modify_storagegroup_rdf')
|
||||||
|
@mock.patch.object(rest.VMAXRest, 'delete_storagegroup_rdf')
|
||||||
|
def test_delete_group_replication(self, mock_mod, mock_del):
|
||||||
|
self.provision.delete_group_replication(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
self.data.rdf_group_no, self.data.extra_specs)
|
||||||
|
mock_mod.assert_called_once()
|
||||||
|
mock_del.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class VMAXCommonTest(test.TestCase):
|
class VMAXCommonTest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -4178,15 +4328,14 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
group_snapshot,
|
group_snapshot,
|
||||||
snapshots)
|
snapshots)
|
||||||
|
|
||||||
def test_create_group(self):
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=True)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
side_effect=[False, False])
|
||||||
|
def test_create_group(self, mock_type, mock_cg_type):
|
||||||
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
context = None
|
model_update = self.common.create_group(None, self.data.test_group_1)
|
||||||
group = self.data.test_group_1
|
self.assertEqual(ref_model_update, model_update)
|
||||||
with mock.patch.object(
|
|
||||||
volume_utils, 'is_group_a_cg_snapshot_type',
|
|
||||||
return_value=True):
|
|
||||||
model_update = self.common.create_group(context, group)
|
|
||||||
self.assertEqual(ref_model_update, model_update)
|
|
||||||
|
|
||||||
def test_create_group_exception(self):
|
def test_create_group_exception(self):
|
||||||
context = None
|
context = None
|
||||||
@ -4196,8 +4345,7 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
return_value=True):
|
return_value=True):
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.common.create_group,
|
self.common.create_group,
|
||||||
context,
|
context, group)
|
||||||
group)
|
|
||||||
|
|
||||||
def test_delete_group_snapshot(self):
|
def test_delete_group_snapshot(self):
|
||||||
group_snapshot = self.data.test_group_snapshot_1
|
group_snapshot = self.data.test_group_snapshot_1
|
||||||
@ -4234,47 +4382,39 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
snapshots))
|
snapshots))
|
||||||
self.assertEqual(ref_model_update, model_update)
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
def test_update_group(self):
|
@mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=True)
|
||||||
|
def test_update_group(self, mock_cg_type, mock_type_check):
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
add_vols = [self.data.test_volume]
|
add_vols = [self.data.test_volume]
|
||||||
remove_vols = []
|
remove_vols = []
|
||||||
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
model_update, __, __ = self.common.update_group(group,
|
||||||
return_value=True):
|
add_vols,
|
||||||
model_update, __, __ = self.common.update_group(group,
|
remove_vols)
|
||||||
add_vols,
|
self.assertEqual(ref_model_update, model_update)
|
||||||
remove_vols)
|
|
||||||
self.assertEqual(ref_model_update, model_update)
|
|
||||||
|
|
||||||
|
@mock.patch.object(common.VMAXCommon, '_find_volume_group',
|
||||||
|
return_value=None)
|
||||||
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
return_value=True)
|
return_value=True)
|
||||||
def test_update_group_not_found(self, mock_check):
|
def test_update_group_not_found(self, mock_check, mock_grp):
|
||||||
group = self.data.test_group_1
|
self.assertRaises(exception.GroupNotFound, self.common.update_group,
|
||||||
add_vols = []
|
self.data.test_group_1, [], [])
|
||||||
remove_vols = []
|
|
||||||
with mock.patch.object(
|
|
||||||
self.common, '_find_volume_group',
|
|
||||||
return_value=None):
|
|
||||||
self.assertRaises(exception.GroupNotFound,
|
|
||||||
self.common.update_group,
|
|
||||||
group,
|
|
||||||
add_vols,
|
|
||||||
remove_vols)
|
|
||||||
|
|
||||||
|
@mock.patch.object(common.VMAXCommon, '_find_volume_group',
|
||||||
|
side_effect=exception.VolumeBackendAPIException)
|
||||||
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
return_value=True)
|
return_value=True)
|
||||||
def test_update_group_exception(self, mock_check):
|
def test_update_group_exception(self, mock_check, mock_grp):
|
||||||
group = self.data.test_group_1
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
add_vols = []
|
self.common.update_group,
|
||||||
remove_vols = []
|
self.data.test_group_1, [], [])
|
||||||
with mock.patch.object(
|
|
||||||
self.common, '_find_volume_group',
|
|
||||||
side_effect=exception.VolumeBackendAPIException):
|
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
|
||||||
self.common.update_group,
|
|
||||||
group, add_vols, remove_vols)
|
|
||||||
|
|
||||||
def test_delete_group(self):
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
|
||||||
|
def test_delete_group(self, mock_check):
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
volumes = [self.data.test_volume]
|
volumes = [self.data.test_volume]
|
||||||
context = None
|
context = None
|
||||||
@ -4287,7 +4427,8 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
context, group, volumes)
|
context, group, volumes)
|
||||||
self.assertEqual(ref_model_update, model_update)
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
def test_delete_group_success(self):
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
|
||||||
|
def test_delete_group_success(self, mock_check):
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
volumes = []
|
volumes = []
|
||||||
ref_model_update = {'status': fields.GroupStatus.DELETED}
|
ref_model_update = {'status': fields.GroupStatus.DELETED}
|
||||||
@ -4307,9 +4448,10 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
model_update, __ = self.common._delete_group(group, volumes)
|
model_update, __ = self.common._delete_group(group, volumes)
|
||||||
self.assertEqual(ref_model_update, model_update)
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=False)
|
||||||
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
return_value=True)
|
return_value=True)
|
||||||
def test_delete_group_failed(self, mock_check):
|
def test_delete_group_failed(self, mock_check, mock_type_check):
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
volumes = []
|
volumes = []
|
||||||
ref_model_update = {'status': fields.GroupStatus.ERROR_DELETING}
|
ref_model_update = {'status': fields.GroupStatus.ERROR_DELETING}
|
||||||
@ -4320,7 +4462,11 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
group, volumes)
|
group, volumes)
|
||||||
self.assertEqual(ref_model_update, model_update)
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
def test_create_group_from_src_success(self):
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=True)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False)
|
||||||
|
def test_create_group_from_src_success(self, mock_type, mock_cg_type):
|
||||||
context = None
|
context = None
|
||||||
group = self.data.test_group_1
|
group = self.data.test_group_1
|
||||||
group_snapshot = self.data.test_group_snapshot_1
|
group_snapshot = self.data.test_group_snapshot_1
|
||||||
@ -4329,13 +4475,11 @@ class VMAXCommonTest(test.TestCase):
|
|||||||
source_group = None
|
source_group = None
|
||||||
source_vols = []
|
source_vols = []
|
||||||
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
model_update, volumes_model_update = (
|
||||||
return_value=True):
|
self.common.create_group_from_src(
|
||||||
model_update, volumes_model_update = (
|
context, group, volumes,
|
||||||
self.common.create_group_from_src(
|
group_snapshot, snapshots,
|
||||||
context, group, volumes,
|
source_group, source_vols))
|
||||||
group_snapshot, snapshots,
|
|
||||||
source_group, source_vols))
|
|
||||||
self.assertEqual(ref_model_update, model_update)
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
|
|
||||||
@ -4565,6 +4709,27 @@ class VMAXFCTest(test.TestCase):
|
|||||||
mock_fo.assert_called_once_with([self.data.test_volume], None,
|
mock_fo.assert_called_once_with([self.data.test_volume], None,
|
||||||
None)
|
None)
|
||||||
|
|
||||||
|
def test_enable_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'enable_replication') as mock_er:
|
||||||
|
self.driver.enable_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_er.assert_called_once()
|
||||||
|
|
||||||
|
def test_disable_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'disable_replication') as mock_dr:
|
||||||
|
self.driver.disable_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_dr.assert_called_once()
|
||||||
|
|
||||||
|
def test_failover_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'failover_replication') as mock_fo:
|
||||||
|
self.driver.failover_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_fo.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class VMAXISCSITest(test.TestCase):
|
class VMAXISCSITest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -4804,6 +4969,27 @@ class VMAXISCSITest(test.TestCase):
|
|||||||
mock_fo.assert_called_once_with([self.data.test_volume], None,
|
mock_fo.assert_called_once_with([self.data.test_volume], None,
|
||||||
None)
|
None)
|
||||||
|
|
||||||
|
def test_enable_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'enable_replication') as mock_er:
|
||||||
|
self.driver.enable_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_er.assert_called_once()
|
||||||
|
|
||||||
|
def test_disable_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'disable_replication') as mock_dr:
|
||||||
|
self.driver.disable_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_dr.assert_called_once()
|
||||||
|
|
||||||
|
def test_failover_replication(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
self.common, 'failover_replication') as mock_fo:
|
||||||
|
self.driver.failover_replication(
|
||||||
|
self.data.ctx, self.data.test_group, [self.data.test_volume])
|
||||||
|
mock_fo.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class VMAXMaskingTest(test.TestCase):
|
class VMAXMaskingTest(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -5759,18 +5945,33 @@ class VMAXCommonReplicationTest(test.TestCase):
|
|||||||
self.common._get_replication_info()
|
self.common._get_replication_info()
|
||||||
self.assertTrue(self.common.replication_enabled)
|
self.assertTrue(self.common.replication_enabled)
|
||||||
|
|
||||||
def test_create_replicated_volume(self):
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=False)
|
||||||
|
@mock.patch.object(objects.Group, 'get_by_id',
|
||||||
|
return_value=VMAXCommonData.test_rep_group)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=True)
|
||||||
|
@mock.patch.object(utils.VMAXUtils, 'check_replication_matched',
|
||||||
|
return_value=True)
|
||||||
|
@mock.patch.object(masking.VMAXMasking, 'add_volume_to_storage_group')
|
||||||
|
@mock.patch.object(
|
||||||
|
common.VMAXCommon, '_replicate_volume',
|
||||||
|
return_value={
|
||||||
|
'replication_driver_data':
|
||||||
|
VMAXCommonData.test_volume.replication_driver_data})
|
||||||
|
def test_create_replicated_volume(self, mock_rep, mock_add, mock_match,
|
||||||
|
mock_check, mock_get, mock_cg):
|
||||||
extra_specs = deepcopy(self.extra_specs)
|
extra_specs = deepcopy(self.extra_specs)
|
||||||
extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f
|
extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f
|
||||||
vol_identifier = self.utils.get_volume_element_name(
|
vol_identifier = self.utils.get_volume_element_name(
|
||||||
self.data.test_volume.id)
|
self.data.test_volume.id)
|
||||||
with mock.patch.object(self.common, '_replicate_volume',
|
self.common.create_volume(self.data.test_volume)
|
||||||
return_value={}) as mock_rep:
|
volume_dict = self.data.provider_location
|
||||||
self.common.create_volume(self.data.test_volume)
|
mock_rep.assert_called_once_with(
|
||||||
volume_dict = self.data.provider_location
|
self.data.test_volume, vol_identifier, volume_dict,
|
||||||
mock_rep.assert_called_once_with(
|
extra_specs)
|
||||||
self.data.test_volume, vol_identifier, volume_dict,
|
# Add volume to replication group
|
||||||
extra_specs)
|
self.common.create_volume(self.data.test_volume_group_member)
|
||||||
|
mock_add.assert_called_once()
|
||||||
|
|
||||||
def test_create_cloned_replicated_volume(self):
|
def test_create_cloned_replicated_volume(self):
|
||||||
extra_specs = deepcopy(self.extra_specs)
|
extra_specs = deepcopy(self.extra_specs)
|
||||||
@ -6013,6 +6214,17 @@ class VMAXCommonReplicationTest(test.TestCase):
|
|||||||
self.common.failover_host,
|
self.common.failover_host,
|
||||||
volumes, secondary_id="default")
|
volumes, secondary_id="default")
|
||||||
|
|
||||||
|
@mock.patch.object(common.VMAXCommon, 'failover_replication',
|
||||||
|
return_value=({}, {}))
|
||||||
|
@mock.patch.object(common.VMAXCommon, '_failover_volume',
|
||||||
|
return_value={})
|
||||||
|
def test_failover_host_groups(self, mock_fv, mock_fg):
|
||||||
|
volumes = [self.data.test_volume_group_member]
|
||||||
|
group1 = self.data.test_group
|
||||||
|
self.common.failover_host(volumes, None, [group1])
|
||||||
|
mock_fv.assert_not_called()
|
||||||
|
mock_fg.assert_called_once()
|
||||||
|
|
||||||
def test_failover_volume(self):
|
def test_failover_volume(self):
|
||||||
ref_model_update = {
|
ref_model_update = {
|
||||||
'volume_id': self.data.test_volume.id,
|
'volume_id': self.data.test_volume.id,
|
||||||
@ -6215,3 +6427,164 @@ class VMAXCommonReplicationTest(test.TestCase):
|
|||||||
secondary_info = self.common.get_secondary_stats_info(
|
secondary_info = self.common.get_secondary_stats_info(
|
||||||
rep_config, array_info)
|
rep_config, array_info)
|
||||||
self.assertEqual(ref_info, secondary_info)
|
self.assertEqual(ref_info, secondary_info)
|
||||||
|
|
||||||
|
def test_replicate_group(self):
|
||||||
|
volume_model_update = {
|
||||||
|
'id': self.data.test_volume.id,
|
||||||
|
'provider_location': self.data.test_volume.provider_location}
|
||||||
|
vols_model_update = self.common._replicate_group(
|
||||||
|
self.data.array, [volume_model_update],
|
||||||
|
self.data.test_vol_grp_name, self.extra_specs)
|
||||||
|
ref_rep_data = six.text_type({'array': self.data.remote_array,
|
||||||
|
'device_id': self.data.device_id2})
|
||||||
|
ref_vol_update = {
|
||||||
|
'id': self.data.test_volume.id,
|
||||||
|
'provider_location': self.data.test_volume.provider_location,
|
||||||
|
'replication_driver_data': ref_rep_data,
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED}
|
||||||
|
self.assertEqual(ref_vol_update, vols_model_update[0])
|
||||||
|
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=False)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
side_effect=[True, True])
|
||||||
|
def test_create_replicaton_group(self, mock_type, mock_cg_type):
|
||||||
|
ref_model_update = {
|
||||||
|
'status': fields.GroupStatus.AVAILABLE,
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED}
|
||||||
|
model_update = self.common.create_group(None, self.data.test_group_1)
|
||||||
|
self.assertEqual(ref_model_update, model_update)
|
||||||
|
|
||||||
|
def test_enable_replication(self):
|
||||||
|
# Case 1: Group not replicated
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False):
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.common.enable_replication,
|
||||||
|
None, self.data.test_group,
|
||||||
|
[self.data.test_volume])
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=True):
|
||||||
|
# Case 2: Empty group
|
||||||
|
model_update, __ = self.common.enable_replication(
|
||||||
|
None, self.data.test_group, [])
|
||||||
|
self.assertEqual({}, model_update)
|
||||||
|
# Case 3: Successfully enabled
|
||||||
|
model_update, __ = self.common.enable_replication(
|
||||||
|
None, self.data.test_group, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.ENABLED,
|
||||||
|
model_update['replication_status'])
|
||||||
|
# Case 4: Exception
|
||||||
|
model_update, __ = self.common.enable_replication(
|
||||||
|
None, self.data.test_group_failed, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.ERROR,
|
||||||
|
model_update['replication_status'])
|
||||||
|
|
||||||
|
def test_disable_replication(self):
|
||||||
|
# Case 1: Group not replicated
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False):
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.common.disable_replication,
|
||||||
|
None, self.data.test_group,
|
||||||
|
[self.data.test_volume])
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=True):
|
||||||
|
# Case 2: Empty group
|
||||||
|
model_update, __ = self.common.disable_replication(
|
||||||
|
None, self.data.test_group, [])
|
||||||
|
self.assertEqual({}, model_update)
|
||||||
|
# Case 3: Successfully disabled
|
||||||
|
model_update, __ = self.common.disable_replication(
|
||||||
|
None, self.data.test_group, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.DISABLED,
|
||||||
|
model_update['replication_status'])
|
||||||
|
# Case 4: Exception
|
||||||
|
model_update, __ = self.common.disable_replication(
|
||||||
|
None, self.data.test_group_failed, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.ERROR,
|
||||||
|
model_update['replication_status'])
|
||||||
|
|
||||||
|
def test_failover_replication(self):
|
||||||
|
# Case 1: Group not replicated
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=False):
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.common.failover_replication,
|
||||||
|
None, self.data.test_group,
|
||||||
|
[self.data.test_volume])
|
||||||
|
with mock.patch.object(volume_utils, 'is_group_a_type',
|
||||||
|
return_value=True):
|
||||||
|
# Case 2: Empty group
|
||||||
|
model_update, __ = self.common.failover_replication(
|
||||||
|
None, self.data.test_group, [])
|
||||||
|
self.assertEqual({}, model_update)
|
||||||
|
# Case 3: Successfully failed over
|
||||||
|
model_update, __ = self.common.failover_replication(
|
||||||
|
None, self.data.test_group, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.FAILED_OVER,
|
||||||
|
model_update['replication_status'])
|
||||||
|
# Case 4: Successfully failed back
|
||||||
|
model_update, __ = self.common.failover_replication(
|
||||||
|
None, self.data.test_group, [self.data.test_volume],
|
||||||
|
secondary_backend_id='default')
|
||||||
|
self.assertEqual(fields.ReplicationStatus.ENABLED,
|
||||||
|
model_update['replication_status'])
|
||||||
|
# Case 5: Exception
|
||||||
|
model_update, __ = self.common.failover_replication(
|
||||||
|
None, self.data.test_group_failed, [self.data.test_volume])
|
||||||
|
self.assertEqual(fields.ReplicationStatus.ERROR,
|
||||||
|
model_update['replication_status'])
|
||||||
|
|
||||||
|
@mock.patch.object(utils.VMAXUtils, 'get_volume_group_utils',
|
||||||
|
return_value=(VMAXCommonData.array, []))
|
||||||
|
@mock.patch.object(common.VMAXCommon, '_cleanup_group_replication')
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=True)
|
||||||
|
def test_delete_replication_group(self, mock_check,
|
||||||
|
mock_cleanup, mock_utils):
|
||||||
|
self.common._delete_group(self.data.test_rep_group, [])
|
||||||
|
mock_cleanup.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(masking.VMAXMasking,
|
||||||
|
'remove_volumes_from_storage_group')
|
||||||
|
@mock.patch.object(utils.VMAXUtils, 'check_rep_status_enabled')
|
||||||
|
@mock.patch.object(common.VMAXCommon,
|
||||||
|
'_remove_remote_vols_from_volume_group')
|
||||||
|
@mock.patch.object(common.VMAXCommon, '_add_remote_vols_to_volume_group')
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_type', return_value=True)
|
||||||
|
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||||
|
return_value=True)
|
||||||
|
def test_update_replicated_group(self, mock_cg_type, mock_type_check,
|
||||||
|
mock_add, mock_remove, mock_check,
|
||||||
|
mock_rm):
|
||||||
|
add_vols = [self.data.test_volume]
|
||||||
|
remove_vols = [self.data.test_clone_volume]
|
||||||
|
self.common.update_group(
|
||||||
|
self.data.test_group_1, add_vols, remove_vols)
|
||||||
|
mock_add.assert_called_once()
|
||||||
|
mock_remove.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(masking.VMAXMasking,
|
||||||
|
'add_volumes_to_storage_group')
|
||||||
|
def test_add_remote_vols_to_volume_group(self, mock_add):
|
||||||
|
self.common._add_remote_vols_to_volume_group(
|
||||||
|
self.data.remote_array, [self.data.test_volume],
|
||||||
|
self.data.test_rep_group, self.data.rep_extra_specs)
|
||||||
|
mock_add.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(masking.VMAXMasking,
|
||||||
|
'remove_volumes_from_storage_group')
|
||||||
|
def test_remove_remote_vols_from_volume_group(self, mock_rm):
|
||||||
|
self.common._remove_remote_vols_from_volume_group(
|
||||||
|
self.data.remote_array, [self.data.test_volume],
|
||||||
|
self.data.test_rep_group, self.data.rep_extra_specs)
|
||||||
|
mock_rm.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(masking.VMAXMasking, 'remove_and_reset_members')
|
||||||
|
@mock.patch.object(masking.VMAXMasking,
|
||||||
|
'remove_volumes_from_storage_group')
|
||||||
|
def test_cleanup_group_replication(self, mock_rm, mock_rm_reset):
|
||||||
|
self.common._cleanup_group_replication(
|
||||||
|
self.data.array, self.data.test_vol_grp_name,
|
||||||
|
[self.data.device_id], self.extra_specs)
|
||||||
|
mock_rm.assert_called_once()
|
||||||
|
@ -22,7 +22,6 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
import six
|
import six
|
||||||
import uuid
|
|
||||||
|
|
||||||
from cinder import coordination
|
from cinder import coordination
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
@ -243,6 +242,7 @@ class VMAXCommon(object):
|
|||||||
:returns: model_update - dict
|
:returns: model_update - dict
|
||||||
"""
|
"""
|
||||||
model_update = {}
|
model_update = {}
|
||||||
|
rep_driver_data = {}
|
||||||
volume_id = volume.id
|
volume_id = volume.id
|
||||||
extra_specs = self._initial_setup(volume)
|
extra_specs = self._initial_setup(volume)
|
||||||
|
|
||||||
@ -253,24 +253,50 @@ class VMAXCommon(object):
|
|||||||
volume_dict = (self._create_volume(
|
volume_dict = (self._create_volume(
|
||||||
volume_name, volume_size, extra_specs))
|
volume_name, volume_size, extra_specs))
|
||||||
|
|
||||||
if volume.group_id is not None:
|
|
||||||
group_name = self.provision.get_or_create_volume_group(
|
|
||||||
extra_specs[utils.ARRAY], volume.group, extra_specs)
|
|
||||||
self.masking.add_volume_to_storage_group(
|
|
||||||
extra_specs[utils.ARRAY], volume_dict['device_id'],
|
|
||||||
group_name, volume_name, extra_specs)
|
|
||||||
|
|
||||||
# Set-up volume replication, if enabled
|
# Set-up volume replication, if enabled
|
||||||
if self.utils.is_replication_enabled(extra_specs):
|
if self.utils.is_replication_enabled(extra_specs):
|
||||||
rep_update = self._replicate_volume(volume, volume_name,
|
rep_update = self._replicate_volume(volume, volume_name,
|
||||||
volume_dict, extra_specs)
|
volume_dict, extra_specs)
|
||||||
|
rep_driver_data = rep_update['replication_driver_data']
|
||||||
model_update.update(rep_update)
|
model_update.update(rep_update)
|
||||||
|
|
||||||
|
# Add volume to group, if required
|
||||||
|
if volume.group_id is not None:
|
||||||
|
if (volume_utils.is_group_a_cg_snapshot_type(volume.group)
|
||||||
|
or volume.group.is_replicated):
|
||||||
|
self._add_new_volume_to_volume_group(
|
||||||
|
volume, volume_dict['device_id'], volume_name,
|
||||||
|
extra_specs, rep_driver_data)
|
||||||
|
|
||||||
LOG.info("Leaving create_volume: %(name)s. Volume dict: %(dict)s.",
|
LOG.info("Leaving create_volume: %(name)s. Volume dict: %(dict)s.",
|
||||||
{'name': volume_name, 'dict': volume_dict})
|
{'name': volume_name, 'dict': volume_dict})
|
||||||
model_update.update(
|
model_update.update(
|
||||||
{'provider_location': six.text_type(volume_dict)})
|
{'provider_location': six.text_type(volume_dict)})
|
||||||
return model_update
|
return model_update
|
||||||
|
|
||||||
|
def _add_new_volume_to_volume_group(self, volume, device_id, volume_name,
|
||||||
|
extra_specs, rep_driver_data=None):
|
||||||
|
"""Add a new volume to a volume group.
|
||||||
|
|
||||||
|
This may also be called after extending a replicated volume.
|
||||||
|
:param volume: the volume object
|
||||||
|
:param device_id: the device id
|
||||||
|
:param volume_name: the volume name
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
:param rep_driver_data: the replication driver data, optional
|
||||||
|
"""
|
||||||
|
self.utils.check_replication_matched(volume, extra_specs)
|
||||||
|
group_name = self.provision.get_or_create_volume_group(
|
||||||
|
extra_specs[utils.ARRAY], volume.group, extra_specs)
|
||||||
|
self.masking.add_volume_to_storage_group(
|
||||||
|
extra_specs[utils.ARRAY], device_id,
|
||||||
|
group_name, volume_name, extra_specs)
|
||||||
|
# Add remote volume to remote group, if required
|
||||||
|
if volume.group.is_replicated:
|
||||||
|
self._add_remote_vols_to_volume_group(
|
||||||
|
extra_specs[utils.ARRAY],
|
||||||
|
[volume], volume.group, extra_specs, rep_driver_data)
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
"""Creates a volume from a snapshot.
|
"""Creates a volume from a snapshot.
|
||||||
|
|
||||||
@ -757,7 +783,10 @@ class VMAXCommon(object):
|
|||||||
'max_over_subscription_ratio':
|
'max_over_subscription_ratio':
|
||||||
max_oversubscription_ratio,
|
max_oversubscription_ratio,
|
||||||
'reserved_percentage': reserved_percentage,
|
'reserved_percentage': reserved_percentage,
|
||||||
'replication_enabled': self.replication_enabled
|
'replication_enabled': self.replication_enabled,
|
||||||
|
'group_replication_enabled': self.replication_enabled,
|
||||||
|
'consistent_group_replication_enabled':
|
||||||
|
self.replication_enabled
|
||||||
}
|
}
|
||||||
if array_reserve_percent:
|
if array_reserve_percent:
|
||||||
if isinstance(reserved_percentage, int):
|
if isinstance(reserved_percentage, int):
|
||||||
@ -877,18 +906,8 @@ class VMAXCommon(object):
|
|||||||
device_id = name['keybindings']['DeviceID']
|
device_id = name['keybindings']['DeviceID']
|
||||||
else:
|
else:
|
||||||
device_id = None
|
device_id = None
|
||||||
element_name = self.utils.get_volume_element_name(
|
founddevice_id = self.rest.check_volume_device_id(
|
||||||
volume_name)
|
array, device_id, volume_name)
|
||||||
admin_metadata = {}
|
|
||||||
if 'admin_metadata' in volume:
|
|
||||||
admin_metadata = volume.admin_metadata
|
|
||||||
if 'targetVolumeName' in admin_metadata:
|
|
||||||
target_vol_name = admin_metadata['targetVolumeName']
|
|
||||||
founddevice_id = self.rest.check_volume_device_id(
|
|
||||||
array, target_vol_name, device_id)
|
|
||||||
else:
|
|
||||||
founddevice_id = self.rest.check_volume_device_id(
|
|
||||||
array, device_id, element_name)
|
|
||||||
|
|
||||||
if founddevice_id is None:
|
if founddevice_id is None:
|
||||||
LOG.debug("Volume %(volume_name)s not found on the array.",
|
LOG.debug("Volume %(volume_name)s not found on the array.",
|
||||||
@ -2227,7 +2246,7 @@ class VMAXCommon(object):
|
|||||||
"""
|
"""
|
||||||
are_vols_paired, local_vol_state, pair_state = (
|
are_vols_paired, local_vol_state, pair_state = (
|
||||||
self.rest.are_vols_rdf_paired(
|
self.rest.are_vols_rdf_paired(
|
||||||
array, remote_array, device_id, target_device, rdf_group))
|
array, remote_array, device_id, target_device))
|
||||||
if are_vols_paired:
|
if are_vols_paired:
|
||||||
# Break the sync relationship.
|
# Break the sync relationship.
|
||||||
self.provision.break_rdf_relationship(
|
self.provision.break_rdf_relationship(
|
||||||
@ -2304,8 +2323,11 @@ class VMAXCommon(object):
|
|||||||
:param secondary_id: the target backend
|
:param secondary_id: the target backend
|
||||||
:param groups: replication groups
|
:param groups: replication groups
|
||||||
:returns: secondary_id, volume_update_list, group_update_list
|
:returns: secondary_id, volume_update_list, group_update_list
|
||||||
|
:raises: VolumeBackendAPIException
|
||||||
"""
|
"""
|
||||||
volume_update_list = []
|
volume_update_list = []
|
||||||
|
group_update_list = []
|
||||||
|
group_fo = None
|
||||||
if secondary_id != 'default':
|
if secondary_id != 'default':
|
||||||
if not self.failover:
|
if not self.failover:
|
||||||
self.failover = True
|
self.failover = True
|
||||||
@ -2325,6 +2347,7 @@ class VMAXCommon(object):
|
|||||||
if self.failover:
|
if self.failover:
|
||||||
self.failover = False
|
self.failover = False
|
||||||
secondary_id = None
|
secondary_id = None
|
||||||
|
group_fo = 'default'
|
||||||
else:
|
else:
|
||||||
exception_message = (_(
|
exception_message = (_(
|
||||||
"Cannot failback backend %(backend)s- backend not "
|
"Cannot failback backend %(backend)s- backend not "
|
||||||
@ -2336,6 +2359,20 @@ class VMAXCommon(object):
|
|||||||
raise exception.VolumeBackendAPIException(
|
raise exception.VolumeBackendAPIException(
|
||||||
data=exception_message)
|
data=exception_message)
|
||||||
|
|
||||||
|
if groups:
|
||||||
|
for group in groups:
|
||||||
|
vol_list = []
|
||||||
|
for index, vol in enumerate(volumes):
|
||||||
|
if vol.group_id == group.id:
|
||||||
|
vol_list.append(volumes.pop(index))
|
||||||
|
grp_update, vol_updates = (
|
||||||
|
self.failover_replication(
|
||||||
|
None, group, vol_list, group_fo, host=True))
|
||||||
|
|
||||||
|
group_update_list.append({'group_id': group.id,
|
||||||
|
'updates': grp_update})
|
||||||
|
volume_update_list += vol_updates
|
||||||
|
|
||||||
for volume in volumes:
|
for volume in volumes:
|
||||||
extra_specs = self._initial_setup(volume)
|
extra_specs = self._initial_setup(volume)
|
||||||
if self.utils.is_replication_enabled(extra_specs):
|
if self.utils.is_replication_enabled(extra_specs):
|
||||||
@ -2357,7 +2394,7 @@ class VMAXCommon(object):
|
|||||||
volume_update_list.append(recovery)
|
volume_update_list.append(recovery)
|
||||||
|
|
||||||
LOG.info("Failover host complete.")
|
LOG.info("Failover host complete.")
|
||||||
return secondary_id, volume_update_list, []
|
return secondary_id, volume_update_list, group_update_list
|
||||||
|
|
||||||
def _failover_volume(self, vol, failover, extra_specs):
|
def _failover_volume(self, vol, failover, extra_specs):
|
||||||
"""Failover a volume.
|
"""Failover a volume.
|
||||||
@ -2460,8 +2497,7 @@ class VMAXCommon(object):
|
|||||||
target_device = remote_device
|
target_device = remote_device
|
||||||
are_vols_paired, local_vol_state, pair_state = (
|
are_vols_paired, local_vol_state, pair_state = (
|
||||||
self.rest.are_vols_rdf_paired(
|
self.rest.are_vols_rdf_paired(
|
||||||
array, remote_array, device_id,
|
array, remote_array, device_id, target_device))
|
||||||
target_device, rdf_group))
|
|
||||||
if not are_vols_paired:
|
if not are_vols_paired:
|
||||||
target_device = None
|
target_device = None
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
@ -2523,6 +2559,11 @@ class VMAXCommon(object):
|
|||||||
self.setup_volume_replication(
|
self.setup_volume_replication(
|
||||||
array, volume, device_id, extra_specs, target_device)
|
array, volume, device_id, extra_specs, target_device)
|
||||||
|
|
||||||
|
# Check if volume needs to be returned to volume group
|
||||||
|
if volume.group_id:
|
||||||
|
self._add_new_volume_to_volume_group(
|
||||||
|
volume, device_id, volume_name, extra_specs)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception_message = (_("Error extending volume. "
|
exception_message = (_("Error extending volume. "
|
||||||
"Error received was %(e)s") %
|
"Error received was %(e)s") %
|
||||||
@ -2676,7 +2717,8 @@ class VMAXCommon(object):
|
|||||||
|
|
||||||
return rep_extra_specs
|
return rep_extra_specs
|
||||||
|
|
||||||
def get_secondary_stats_info(self, rep_config, array_info):
|
@staticmethod
|
||||||
|
def get_secondary_stats_info(rep_config, array_info):
|
||||||
"""On failover, report on secondary array statistics.
|
"""On failover, report on secondary array statistics.
|
||||||
|
|
||||||
:param rep_config: the replication configuration
|
:param rep_config: the replication configuration
|
||||||
@ -2722,10 +2764,11 @@ class VMAXCommon(object):
|
|||||||
|
|
||||||
:param context: the context
|
:param context: the context
|
||||||
:param group: the group object to be created
|
:param group: the group object to be created
|
||||||
:returns: dict -- modelUpdate = {'status': 'available'}
|
:returns: dict -- modelUpdate
|
||||||
:raises: VolumeBackendAPIException, NotImplementedError
|
:raises: VolumeBackendAPIException, NotImplementedError
|
||||||
"""
|
"""
|
||||||
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
if (not volume_utils.is_group_a_cg_snapshot_type(group)
|
||||||
|
and not group.is_replicated):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
@ -2742,6 +2785,15 @@ class VMAXCommon(object):
|
|||||||
self.interval, self.retries)
|
self.interval, self.retries)
|
||||||
self.provision.create_volume_group(
|
self.provision.create_volume_group(
|
||||||
array, vol_grp_name, interval_retries_dict)
|
array, vol_grp_name, interval_retries_dict)
|
||||||
|
if group.is_replicated:
|
||||||
|
LOG.debug("Group: %(group)s is a replication group.",
|
||||||
|
{'group': group.id})
|
||||||
|
# Create remote group
|
||||||
|
__, remote_array = self.get_rdf_details(array)
|
||||||
|
self.provision.create_volume_group(
|
||||||
|
remote_array, vol_grp_name, interval_retries_dict)
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED})
|
||||||
except Exception:
|
except Exception:
|
||||||
exception_message = (_("Failed to create generic volume group:"
|
exception_message = (_("Failed to create generic volume group:"
|
||||||
" %(volGrpName)s.")
|
" %(volGrpName)s.")
|
||||||
@ -2763,7 +2815,8 @@ class VMAXCommon(object):
|
|||||||
"""
|
"""
|
||||||
LOG.info("Delete generic volume group: %(group)s.",
|
LOG.info("Delete generic volume group: %(group)s.",
|
||||||
{'group': group.id})
|
{'group': group.id})
|
||||||
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
if (not volume_utils.is_group_a_cg_snapshot_type(group)
|
||||||
|
and not group.is_replicated):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
model_update, volumes_model_update = self._delete_group(
|
model_update, volumes_model_update = self._delete_group(
|
||||||
group, volumes)
|
group, volumes)
|
||||||
@ -2800,33 +2853,36 @@ class VMAXCommon(object):
|
|||||||
intervals_retries_dict = self.utils.get_intervals_retries_dict(
|
intervals_retries_dict = self.utils.get_intervals_retries_dict(
|
||||||
self.interval, self.retries)
|
self.interval, self.retries)
|
||||||
deleted_volume_device_ids = []
|
deleted_volume_device_ids = []
|
||||||
|
|
||||||
|
# Remove replication for group, if applicable
|
||||||
|
if group.is_replicated:
|
||||||
|
self._cleanup_group_replication(
|
||||||
|
array, vol_grp_name, volume_device_ids,
|
||||||
|
intervals_retries_dict)
|
||||||
try:
|
try:
|
||||||
# If there are no volumes in sg then delete it
|
if volume_device_ids:
|
||||||
if not volume_device_ids:
|
# First remove all the volumes from the SG
|
||||||
self.rest.delete_storage_group(array, vol_grp_name)
|
self.masking.remove_volumes_from_storage_group(
|
||||||
model_update = {'status': fields.GroupStatus.DELETED}
|
array, volume_device_ids, vol_grp_name,
|
||||||
volumes_model_update = self.utils.update_volume_model_updates(
|
intervals_retries_dict)
|
||||||
volumes_model_update, volumes, group.id, status='deleted')
|
for vol in volumes:
|
||||||
return model_update, volumes_model_update
|
for extraspecs_dict in extraspecs_dict_list:
|
||||||
# First remove all the volumes from the SG
|
if (vol.volume_type_id in
|
||||||
self.masking.remove_volumes_from_storage_group(
|
extraspecs_dict['volumeTypeId']):
|
||||||
array, volume_device_ids, vol_grp_name, intervals_retries_dict)
|
extraspecs = extraspecs_dict.get(
|
||||||
for vol in volumes:
|
utils.EXTRA_SPECS)
|
||||||
for extraspecs_dict in extraspecs_dict_list:
|
device_id = self._find_device_on_array(
|
||||||
if vol.volume_type_id in extraspecs_dict['volumeTypeId']:
|
vol, extraspecs)
|
||||||
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
|
if device_id in volume_device_ids:
|
||||||
device_id = self._find_device_on_array(vol,
|
self.masking.remove_and_reset_members(
|
||||||
extraspecs)
|
array, vol, device_id, vol.name,
|
||||||
if device_id in volume_device_ids:
|
extraspecs, False)
|
||||||
self._remove_vol_and_cleanup_replication(
|
self._delete_from_srp(
|
||||||
array, device_id,
|
array, device_id, "group vol", extraspecs)
|
||||||
vol.name, extraspecs, vol)
|
else:
|
||||||
self._delete_from_srp(
|
LOG.debug("Volume not found on the array.")
|
||||||
array, device_id, "group vol", extraspecs)
|
# Add the device id to the deleted list
|
||||||
else:
|
deleted_volume_device_ids.append(device_id)
|
||||||
LOG.debug("Volume not present in storage group.")
|
|
||||||
# Add the device id to the deleted list
|
|
||||||
deleted_volume_device_ids.append(device_id)
|
|
||||||
# Once all volumes are deleted then delete the SG
|
# Once all volumes are deleted then delete the SG
|
||||||
self.rest.delete_storage_group(array, vol_grp_name)
|
self.rest.delete_storage_group(array, vol_grp_name)
|
||||||
model_update = {'status': fields.GroupStatus.DELETED}
|
model_update = {'status': fields.GroupStatus.DELETED}
|
||||||
@ -2865,6 +2921,38 @@ class VMAXCommon(object):
|
|||||||
|
|
||||||
return model_update, volumes_model_update
|
return model_update, volumes_model_update
|
||||||
|
|
||||||
|
def _cleanup_group_replication(
|
||||||
|
self, array, vol_grp_name, volume_device_ids, extra_specs):
|
||||||
|
"""Cleanup remote replication.
|
||||||
|
|
||||||
|
Break and delete the rdf replication relationship and
|
||||||
|
delete the remote storage group and member devices.
|
||||||
|
:param array: the array serial number
|
||||||
|
:param vol_grp_name: the volume group name
|
||||||
|
:param volume_device_ids: the device ids of the local volumes
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
rdf_group_no, remote_array = self.get_rdf_details(array)
|
||||||
|
# Delete replication for group, if applicable
|
||||||
|
if volume_device_ids:
|
||||||
|
self.provision.delete_group_replication(
|
||||||
|
array, vol_grp_name, rdf_group_no, extra_specs)
|
||||||
|
remote_device_ids = self._get_members_of_volume_group(
|
||||||
|
remote_array, vol_grp_name)
|
||||||
|
# Remove volumes from remote replication group
|
||||||
|
if remote_device_ids:
|
||||||
|
self.masking.remove_volumes_from_storage_group(
|
||||||
|
remote_array, remote_device_ids, vol_grp_name, extra_specs)
|
||||||
|
for device_id in remote_device_ids:
|
||||||
|
# Make sure they are not members of any other storage groups
|
||||||
|
self.masking.remove_and_reset_members(
|
||||||
|
remote_array, None, device_id, 'target_vol',
|
||||||
|
extra_specs, False)
|
||||||
|
self._delete_from_srp(
|
||||||
|
remote_array, device_id, "group vol", extra_specs)
|
||||||
|
# Once all volumes are deleted then delete the SG
|
||||||
|
self.rest.delete_storage_group(remote_array, vol_grp_name)
|
||||||
|
|
||||||
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
"""Creates a generic volume group snapshot.
|
"""Creates a generic volume group snapshot.
|
||||||
|
|
||||||
@ -3053,7 +3141,8 @@ class VMAXCommon(object):
|
|||||||
"This adds and/or removes volumes from "
|
"This adds and/or removes volumes from "
|
||||||
"a generic volume group.",
|
"a generic volume group.",
|
||||||
{'group': group.id})
|
{'group': group.id})
|
||||||
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
if (not volume_utils.is_group_a_cg_snapshot_type(group)
|
||||||
|
and not group.is_replicated):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
array, __ = self.utils.get_volume_group_utils(
|
array, __ = self.utils.get_volume_group_utils(
|
||||||
@ -3065,25 +3154,35 @@ class VMAXCommon(object):
|
|||||||
remove_device_ids = self._get_volume_device_ids(remove_vols, array)
|
remove_device_ids = self._get_volume_device_ids(remove_vols, array)
|
||||||
vol_grp_name = None
|
vol_grp_name = None
|
||||||
try:
|
try:
|
||||||
volume_group = self._find_volume_group(
|
volume_group = self._find_volume_group(array, group)
|
||||||
array, group)
|
|
||||||
if volume_group:
|
if volume_group:
|
||||||
if 'name' in volume_group:
|
if 'name' in volume_group:
|
||||||
vol_grp_name = volume_group['name']
|
vol_grp_name = volume_group['name']
|
||||||
if vol_grp_name is None:
|
if vol_grp_name is None:
|
||||||
raise exception.GroupNotFound(
|
raise exception.GroupNotFound(group_id=group.id)
|
||||||
group_id=group.id)
|
|
||||||
interval_retries_dict = self.utils.get_intervals_retries_dict(
|
interval_retries_dict = self.utils.get_intervals_retries_dict(
|
||||||
self.interval, self.retries)
|
self.interval, self.retries)
|
||||||
# Add volume(s) to the group
|
# Add volume(s) to the group
|
||||||
if add_device_ids:
|
if add_device_ids:
|
||||||
|
self.utils.check_rep_status_enabled(group)
|
||||||
|
for vol in add_vols:
|
||||||
|
extra_specs = self._initial_setup(vol)
|
||||||
|
self.utils.check_replication_matched(vol, extra_specs)
|
||||||
self.masking.add_volumes_to_storage_group(
|
self.masking.add_volumes_to_storage_group(
|
||||||
array, add_device_ids, vol_grp_name, interval_retries_dict)
|
array, add_device_ids, vol_grp_name, interval_retries_dict)
|
||||||
|
if group.is_replicated:
|
||||||
|
# Add remote volumes to remote storage group
|
||||||
|
self._add_remote_vols_to_volume_group(
|
||||||
|
array, add_vols, group, interval_retries_dict)
|
||||||
# Remove volume(s) from the group
|
# Remove volume(s) from the group
|
||||||
if remove_device_ids:
|
if remove_device_ids:
|
||||||
self.masking.remove_volumes_from_storage_group(
|
self.masking.remove_volumes_from_storage_group(
|
||||||
array, remove_device_ids,
|
array, remove_device_ids,
|
||||||
vol_grp_name, interval_retries_dict)
|
vol_grp_name, interval_retries_dict)
|
||||||
|
if group.is_replicated:
|
||||||
|
# Remove remote volumes from the remote storage group
|
||||||
|
self._remove_remote_vols_from_volume_group(
|
||||||
|
array, remove_vols, group, interval_retries_dict)
|
||||||
except exception.GroupNotFound:
|
except exception.GroupNotFound:
|
||||||
raise
|
raise
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -3096,6 +3195,57 @@ class VMAXCommon(object):
|
|||||||
|
|
||||||
return model_update, None, None
|
return model_update, None, None
|
||||||
|
|
||||||
|
def _add_remote_vols_to_volume_group(
|
||||||
|
self, array, volumes, group,
|
||||||
|
extra_specs, rep_driver_data=None):
|
||||||
|
"""Add the remote volumes to their volume group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param volumes: list of volumes
|
||||||
|
:param group: the id of the group
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
:param rep_driver_data: replication driver data, optional
|
||||||
|
"""
|
||||||
|
remote_device_list = []
|
||||||
|
__, remote_array = self.get_rdf_details(array)
|
||||||
|
for vol in volumes:
|
||||||
|
try:
|
||||||
|
remote_loc = ast.literal_eval(vol.replication_driver_data)
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
remote_loc = ast.literal_eval(rep_driver_data)
|
||||||
|
founddevice_id = self.rest.check_volume_device_id(
|
||||||
|
remote_array, remote_loc['device_id'], vol.id)
|
||||||
|
if founddevice_id is not None:
|
||||||
|
remote_device_list.append(founddevice_id)
|
||||||
|
group_name = self.provision.get_or_create_volume_group(
|
||||||
|
remote_array, group, extra_specs)
|
||||||
|
self.masking.add_volumes_to_storage_group(
|
||||||
|
remote_array, remote_device_list, group_name, extra_specs)
|
||||||
|
LOG.info("Added volumes to remote volume group.")
|
||||||
|
|
||||||
|
def _remove_remote_vols_from_volume_group(
|
||||||
|
self, array, volumes, group, extra_specs):
|
||||||
|
"""Remove the remote volumes from their volume group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param volumes: list of volumes
|
||||||
|
:param group: the id of the group
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
remote_device_list = []
|
||||||
|
__, remote_array = self.get_rdf_details(array)
|
||||||
|
for vol in volumes:
|
||||||
|
remote_loc = ast.literal_eval(vol.replication_driver_data)
|
||||||
|
founddevice_id = self.rest.check_volume_device_id(
|
||||||
|
remote_array, remote_loc['device_id'], vol.id)
|
||||||
|
if founddevice_id is not None:
|
||||||
|
remote_device_list.append(founddevice_id)
|
||||||
|
group_name = self.provision.get_or_create_volume_group(
|
||||||
|
array, group, extra_specs)
|
||||||
|
self.masking.remove_volumes_from_storage_group(
|
||||||
|
remote_array, remote_device_list, group_name, extra_specs)
|
||||||
|
LOG.info("Removed volumes from remote volume group.")
|
||||||
|
|
||||||
def _get_volume_device_ids(self, volumes, array):
|
def _get_volume_device_ids(self, volumes, array):
|
||||||
"""Get volume device ids from volume.
|
"""Get volume device ids from volume.
|
||||||
|
|
||||||
@ -3159,7 +3309,6 @@ class VMAXCommon(object):
|
|||||||
tgt_name = self.utils.update_volume_group_name(group)
|
tgt_name = self.utils.update_volume_group_name(group)
|
||||||
self.create_group(context, group)
|
self.create_group(context, group)
|
||||||
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
snap_name = None
|
|
||||||
try:
|
try:
|
||||||
array, extraspecs_dict_list = (
|
array, extraspecs_dict_list = (
|
||||||
self.utils.get_volume_group_utils(
|
self.utils.get_volume_group_utils(
|
||||||
@ -3178,8 +3327,8 @@ class VMAXCommon(object):
|
|||||||
if volume.volume_type_id in (
|
if volume.volume_type_id in (
|
||||||
extraspecs_dict['volumeTypeId']):
|
extraspecs_dict['volumeTypeId']):
|
||||||
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
|
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
|
||||||
# Create a random UUID and use it as volume name
|
target_volume_name = (
|
||||||
target_volume_name = six.text_type(uuid.uuid4())
|
self.utils.get_volume_element_name(volume.id))
|
||||||
volume_dict = self.provision.create_volume_from_sg(
|
volume_dict = self.provision.create_volume_from_sg(
|
||||||
array, target_volume_name,
|
array, target_volume_name,
|
||||||
tgt_name, volume_size, extraspecs)
|
tgt_name, volume_size, extraspecs)
|
||||||
@ -3209,7 +3358,6 @@ class VMAXCommon(object):
|
|||||||
self.provision.link_and_break_replica(
|
self.provision.link_and_break_replica(
|
||||||
array, vol_grp_name, tgt_name, snap_name,
|
array, vol_grp_name, tgt_name, snap_name,
|
||||||
interval_retries_dict, delete_snapshot=create_snapshot)
|
interval_retries_dict, delete_snapshot=create_snapshot)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
exception_message = (_("Failed to create vol grp %(volGrpName)s"
|
exception_message = (_("Failed to create vol grp %(volGrpName)s"
|
||||||
" from source %(grpSnapshot)s.")
|
" from source %(grpSnapshot)s.")
|
||||||
@ -3220,16 +3368,203 @@ class VMAXCommon(object):
|
|||||||
volumes_model_update = self.utils.update_volume_model_updates(
|
volumes_model_update = self.utils.update_volume_model_updates(
|
||||||
volumes_model_update, volumes, group.id, model_update['status'])
|
volumes_model_update, volumes, group.id, model_update['status'])
|
||||||
|
|
||||||
# Update the provider_location
|
# Update the provider_location & replication status
|
||||||
for volume_model_update in volumes_model_update:
|
for volume_model_update in volumes_model_update:
|
||||||
if volume_model_update['id'] in dict_volume_dicts:
|
if volume_model_update['id'] in dict_volume_dicts:
|
||||||
volume_model_update.update(
|
volume_model_update.update(
|
||||||
{'provider_location': six.text_type(
|
{'provider_location': six.text_type(
|
||||||
dict_volume_dicts[volume_model_update['id']])})
|
dict_volume_dicts[volume_model_update['id']])})
|
||||||
|
if group.is_replicated:
|
||||||
# Update the volumes_model_update with admin_metadata
|
volumes_model_update = self._replicate_group(
|
||||||
self.utils.update_admin_metadata(volumes_model_update,
|
array, volumes_model_update,
|
||||||
key='targetVolumeName',
|
tgt_name, interval_retries_dict)
|
||||||
values=target_volume_names)
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED})
|
||||||
|
|
||||||
return model_update, volumes_model_update
|
return model_update, volumes_model_update
|
||||||
|
|
||||||
|
def _replicate_group(self, array, volumes_model_update,
|
||||||
|
group_name, extra_specs):
|
||||||
|
"""Replicate a cloned volume group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param volumes_model_update: the volumes model updates
|
||||||
|
:param group_name: the group name
|
||||||
|
:param extra_specs: the extra specs
|
||||||
|
:return: volumes_model_update
|
||||||
|
"""
|
||||||
|
rdf_group_no, remote_array = self.get_rdf_details(array)
|
||||||
|
self.rest.replicate_group(
|
||||||
|
array, group_name, rdf_group_no, remote_array, extra_specs)
|
||||||
|
# Need to set SRP to None for generic volume group - Not set
|
||||||
|
# automatically, and a volume can only be in one storage group
|
||||||
|
# managed by FAST
|
||||||
|
self.rest.set_storagegroup_srp(array, group_name, "None", extra_specs)
|
||||||
|
for volume_model_update in volumes_model_update:
|
||||||
|
vol_id = volume_model_update['id']
|
||||||
|
loc = ast.literal_eval(volume_model_update['provider_location'])
|
||||||
|
src_device_id = loc['device_id']
|
||||||
|
rdf_vol_details = self.rest.get_rdf_group_volume(
|
||||||
|
array, src_device_id)
|
||||||
|
tgt_device_id = rdf_vol_details['remoteDeviceID']
|
||||||
|
element_name = self.utils.get_volume_element_name(vol_id)
|
||||||
|
self.rest.rename_volume(remote_array, tgt_device_id, element_name)
|
||||||
|
rep_update = {'device_id': tgt_device_id, 'array': remote_array}
|
||||||
|
volume_model_update.update(
|
||||||
|
{'replication_driver_data': six.text_type(rep_update),
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED})
|
||||||
|
return volumes_model_update
|
||||||
|
|
||||||
|
def enable_replication(self, context, group, volumes):
|
||||||
|
"""Enable replication for a group.
|
||||||
|
|
||||||
|
Replication is enabled on replication-enabled groups by default.
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
if not group.is_replicated:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
model_update = {}
|
||||||
|
if not volumes:
|
||||||
|
# Return if empty group
|
||||||
|
return model_update, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
vol_grp_name = None
|
||||||
|
extra_specs = self._initial_setup(volumes[0])
|
||||||
|
array = extra_specs[utils.ARRAY]
|
||||||
|
volume_group = self._find_volume_group(array, group)
|
||||||
|
if volume_group:
|
||||||
|
if 'name' in volume_group:
|
||||||
|
vol_grp_name = volume_group['name']
|
||||||
|
if vol_grp_name is None:
|
||||||
|
raise exception.GroupNotFound(group_id=group.id)
|
||||||
|
|
||||||
|
rdf_group_no, _ = self.get_rdf_details(array)
|
||||||
|
self.provision.enable_group_replication(
|
||||||
|
array, vol_grp_name, rdf_group_no, extra_specs)
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED})
|
||||||
|
except Exception as e:
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ERROR})
|
||||||
|
LOG.error("Error enabling replication on group %(group)s. "
|
||||||
|
"Exception received: %(e)s.",
|
||||||
|
{'group': group.id, 'e': e})
|
||||||
|
|
||||||
|
return model_update, None
|
||||||
|
|
||||||
|
def disable_replication(self, context, group, volumes):
|
||||||
|
"""Disable replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
if not group.is_replicated:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
model_update = {}
|
||||||
|
if not volumes:
|
||||||
|
# Return if empty group
|
||||||
|
return model_update, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
vol_grp_name = None
|
||||||
|
extra_specs = self._initial_setup(volumes[0])
|
||||||
|
array = extra_specs[utils.ARRAY]
|
||||||
|
volume_group = self._find_volume_group(array, group)
|
||||||
|
if volume_group:
|
||||||
|
if 'name' in volume_group:
|
||||||
|
vol_grp_name = volume_group['name']
|
||||||
|
if vol_grp_name is None:
|
||||||
|
raise exception.GroupNotFound(group_id=group.id)
|
||||||
|
|
||||||
|
rdf_group_no, _ = self.get_rdf_details(array)
|
||||||
|
self.provision.disable_group_replication(
|
||||||
|
array, vol_grp_name, rdf_group_no, extra_specs)
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.DISABLED})
|
||||||
|
except Exception as e:
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ERROR})
|
||||||
|
LOG.error("Error disabling replication on group %(group)s. "
|
||||||
|
"Exception received: %(e)s.",
|
||||||
|
{'group': group.id, 'e': e})
|
||||||
|
|
||||||
|
return model_update, None
|
||||||
|
|
||||||
|
def failover_replication(self, context, group, volumes,
|
||||||
|
secondary_backend_id=None, host=False):
|
||||||
|
"""Failover replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:param secondary_backend_id: the secondary backend id - default None
|
||||||
|
:param host: flag to indicate if whole host is being failed over
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
if not group.is_replicated:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
model_update = {}
|
||||||
|
vol_model_updates = []
|
||||||
|
if not volumes:
|
||||||
|
# Return if empty group
|
||||||
|
return model_update, vol_model_updates
|
||||||
|
|
||||||
|
try:
|
||||||
|
vol_grp_name = None
|
||||||
|
extra_specs = self._initial_setup(volumes[0])
|
||||||
|
array = extra_specs[utils.ARRAY]
|
||||||
|
volume_group = self._find_volume_group(array, group)
|
||||||
|
if volume_group:
|
||||||
|
if 'name' in volume_group:
|
||||||
|
vol_grp_name = volume_group['name']
|
||||||
|
if vol_grp_name is None:
|
||||||
|
raise exception.GroupNotFound(group_id=group.id)
|
||||||
|
|
||||||
|
rdf_group_no, _ = self.get_rdf_details(array)
|
||||||
|
# As we only support a single replication target, ignore
|
||||||
|
# any secondary_backend_id which is not 'default'
|
||||||
|
failover = False if secondary_backend_id == 'default' else True
|
||||||
|
self.provision.failover_group(
|
||||||
|
array, vol_grp_name, rdf_group_no, extra_specs, failover)
|
||||||
|
if failover:
|
||||||
|
model_update.update({
|
||||||
|
'replication_status':
|
||||||
|
fields.ReplicationStatus.FAILED_OVER})
|
||||||
|
vol_rep_status = fields.ReplicationStatus.FAILED_OVER
|
||||||
|
else:
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ENABLED})
|
||||||
|
vol_rep_status = fields.ReplicationStatus.ENABLED
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
model_update.update({
|
||||||
|
'replication_status': fields.ReplicationStatus.ERROR})
|
||||||
|
vol_rep_status = fields.ReplicationStatus.ERROR
|
||||||
|
LOG.error("Error failover replication on group %(group)s. "
|
||||||
|
"Exception received: %(e)s.",
|
||||||
|
{'group': group.id, 'e': e})
|
||||||
|
|
||||||
|
for vol in volumes:
|
||||||
|
loc = vol.provider_location
|
||||||
|
rep_data = vol.replication_driver_data
|
||||||
|
if vol_rep_status != fields.ReplicationStatus.ERROR:
|
||||||
|
loc = vol.replication_driver_data
|
||||||
|
rep_data = vol.provider_location
|
||||||
|
update = {'id': vol.id,
|
||||||
|
'replication_status': vol_rep_status,
|
||||||
|
'provider_location': loc,
|
||||||
|
'replication_driver_data': rep_data}
|
||||||
|
if host:
|
||||||
|
update = {'volume_id': vol.id, 'updates': update}
|
||||||
|
vol_model_updates.append(update)
|
||||||
|
|
||||||
|
return model_update, vol_model_updates
|
||||||
|
@ -82,9 +82,10 @@ class VMAXFCDriver(driver.FibreChannelDriver):
|
|||||||
- Support for volume replication
|
- Support for volume replication
|
||||||
- Support for live migration
|
- Support for live migration
|
||||||
- Support for Generic Volume Group
|
- Support for Generic Volume Group
|
||||||
|
3.1.0 - Support for replication groups (Tiramisu)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.1.0"
|
||||||
|
|
||||||
# ThirdPartySystems wiki
|
# ThirdPartySystems wiki
|
||||||
CI_WIKI_NAME = "EMC_VMAX_CI"
|
CI_WIKI_NAME = "EMC_VMAX_CI"
|
||||||
@ -465,8 +466,9 @@ class VMAXFCDriver(driver.FibreChannelDriver):
|
|||||||
|
|
||||||
:param context: the context
|
:param context: the context
|
||||||
:param group: the group object
|
:param group: the group object
|
||||||
|
:returns: model_update
|
||||||
"""
|
"""
|
||||||
self.common.create_group(context, group)
|
return self.common.create_group(context, group)
|
||||||
|
|
||||||
def delete_group(self, context, group, volumes):
|
def delete_group(self, context, group, volumes):
|
||||||
"""Deletes a generic volume group.
|
"""Deletes a generic volume group.
|
||||||
@ -526,3 +528,36 @@ class VMAXFCDriver(driver.FibreChannelDriver):
|
|||||||
return self.common.create_group_from_src(
|
return self.common.create_group_from_src(
|
||||||
context, group, volumes, group_snapshot, snapshots, source_group,
|
context, group, volumes, group_snapshot, snapshots, source_group,
|
||||||
source_vols)
|
source_vols)
|
||||||
|
|
||||||
|
def enable_replication(self, context, group, volumes):
|
||||||
|
"""Enable replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
return self.common.enable_replication(context, group, volumes)
|
||||||
|
|
||||||
|
def disable_replication(self, context, group, volumes):
|
||||||
|
"""Disable replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
return self.common.disable_replication(context, group, volumes)
|
||||||
|
|
||||||
|
def failover_replication(self, context, group, volumes,
|
||||||
|
secondary_backend_id=None):
|
||||||
|
"""Failover replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:param secondary_backend_id: the secondary backend id - default None
|
||||||
|
:returns: model_update, vol_model_updates
|
||||||
|
"""
|
||||||
|
return self.common.failover_replication(
|
||||||
|
context, group, volumes, secondary_backend_id)
|
||||||
|
@ -87,9 +87,10 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
|
|||||||
- Support for volume replication
|
- Support for volume replication
|
||||||
- Support for live migration
|
- Support for live migration
|
||||||
- Support for Generic Volume Group
|
- Support for Generic Volume Group
|
||||||
|
3.1.0 - Support for replication groups (Tiramisu)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.1.0"
|
||||||
|
|
||||||
# ThirdPartySystems wiki
|
# ThirdPartySystems wiki
|
||||||
CI_WIKI_NAME = "EMC_VMAX_CI"
|
CI_WIKI_NAME = "EMC_VMAX_CI"
|
||||||
@ -412,8 +413,9 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
|
|||||||
|
|
||||||
:param context: the context
|
:param context: the context
|
||||||
:param group: the group object
|
:param group: the group object
|
||||||
|
:returns: model_update
|
||||||
"""
|
"""
|
||||||
self.common.create_group(context, group)
|
return self.common.create_group(context, group)
|
||||||
|
|
||||||
def delete_group(self, context, group, volumes):
|
def delete_group(self, context, group, volumes):
|
||||||
"""Deletes a generic volume group.
|
"""Deletes a generic volume group.
|
||||||
@ -473,3 +475,36 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
|
|||||||
return self.common.create_group_from_src(
|
return self.common.create_group_from_src(
|
||||||
context, group, volumes, group_snapshot, snapshots, source_group,
|
context, group, volumes, group_snapshot, snapshots, source_group,
|
||||||
source_vols)
|
source_vols)
|
||||||
|
|
||||||
|
def enable_replication(self, context, group, volumes):
|
||||||
|
"""Enable replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
return self.common.enable_replication(context, group, volumes)
|
||||||
|
|
||||||
|
def disable_replication(self, context, group, volumes):
|
||||||
|
"""Disable replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:returns: model_update, None
|
||||||
|
"""
|
||||||
|
return self.common.disable_replication(context, group, volumes)
|
||||||
|
|
||||||
|
def failover_replication(self, context, group, volumes,
|
||||||
|
secondary_backend_id=None):
|
||||||
|
"""Failover replication for a group.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param group: the group object
|
||||||
|
:param volumes: the list of volumes
|
||||||
|
:param secondary_backend_id: the secondary backend id - default None
|
||||||
|
:returns: model_update, vol_model_updates
|
||||||
|
"""
|
||||||
|
return self.common.failover_replication(
|
||||||
|
context, group, volumes, secondary_backend_id)
|
||||||
|
@ -596,3 +596,67 @@ class VMAXProvision(object):
|
|||||||
timer = loopingcall.FixedIntervalLoopingCall(_unlink_grp)
|
timer = loopingcall.FixedIntervalLoopingCall(_unlink_grp)
|
||||||
rc = timer.start(interval=UNLINK_INTERVAL).wait()
|
rc = timer.start(interval=UNLINK_INTERVAL).wait()
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
|
def enable_group_replication(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, extra_specs):
|
||||||
|
"""Resume rdf replication on a storage group.
|
||||||
|
|
||||||
|
Replication is enabled by default. This allows resuming
|
||||||
|
replication on a suspended group.
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storagegroup name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
action = "Resume"
|
||||||
|
self.rest.modify_storagegroup_rdf(
|
||||||
|
array, storagegroup_name, rdf_group_num, action, extra_specs)
|
||||||
|
|
||||||
|
def disable_group_replication(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, extra_specs):
|
||||||
|
"""Suspend rdf replication on a storage group.
|
||||||
|
|
||||||
|
This does not delete the rdf pairs, that can only be done
|
||||||
|
by deleting the group. This method suspends all i/o activity
|
||||||
|
on the rdf links.
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storagegroup name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
action = "Suspend"
|
||||||
|
self.rest.modify_storagegroup_rdf(
|
||||||
|
array, storagegroup_name, rdf_group_num, action, extra_specs)
|
||||||
|
|
||||||
|
def failover_group(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, extra_specs, failover=True):
|
||||||
|
"""Failover or failback replication on a storage group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storagegroup name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
:param failover: flag to indicate failover/ failback
|
||||||
|
"""
|
||||||
|
action = "Failover" if failover else "Failback"
|
||||||
|
self.rest.modify_storagegroup_rdf(
|
||||||
|
array, storagegroup_name, rdf_group_num, action, extra_specs)
|
||||||
|
|
||||||
|
def delete_group_replication(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, extra_specs):
|
||||||
|
"""Split replication for a group and delete the pairs.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storage group name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
action = "Split"
|
||||||
|
LOG.debug("Splitting remote replication for group %(sg)s",
|
||||||
|
{'sg': storagegroup_name})
|
||||||
|
self.rest.modify_storagegroup_rdf(
|
||||||
|
array, storagegroup_name, rdf_group_num, action, extra_specs)
|
||||||
|
LOG.debug("Deleting remote replication for group %(sg)s",
|
||||||
|
{'sg': storagegroup_name})
|
||||||
|
self.rest.delete_storagegroup_rdf(
|
||||||
|
array, storagegroup_name, rdf_group_num)
|
||||||
|
@ -697,14 +697,15 @@ class VMAXRest(object):
|
|||||||
volume_dict = {'array': array, 'device_id': device_id}
|
volume_dict = {'array': array, 'device_id': device_id}
|
||||||
return volume_dict
|
return volume_dict
|
||||||
|
|
||||||
def check_volume_device_id(self, array, device_id, element_name):
|
def check_volume_device_id(self, array, device_id, volume_id):
|
||||||
"""Check if the identifiers match for a given volume.
|
"""Check if the identifiers match for a given volume.
|
||||||
|
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
:param device_id: the device id
|
:param device_id: the device id
|
||||||
:param element_name: name associated with cinder, e.g.OS-<cinderUUID>
|
:param volume_id: cinder volume id
|
||||||
:return: found_device_id
|
:returns: found_device_id
|
||||||
"""
|
"""
|
||||||
|
element_name = self.utils.get_volume_element_name(volume_id)
|
||||||
found_device_id = None
|
found_device_id = None
|
||||||
vol_details = self.get_volume(array, device_id)
|
vol_details = self.get_volume(array, device_id)
|
||||||
if vol_details:
|
if vol_details:
|
||||||
@ -860,6 +861,22 @@ class VMAXRest(object):
|
|||||||
data=exception_message)
|
data=exception_message)
|
||||||
return property_dict
|
return property_dict
|
||||||
|
|
||||||
|
def set_storagegroup_srp(
|
||||||
|
self, array, storagegroup_name, srp_name, extra_specs):
|
||||||
|
"""Modify a storage group's srp value.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storage group name
|
||||||
|
:param srp_name: the srp pool name
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
payload = {"editStorageGroupActionParam": {
|
||||||
|
"editStorageGroupSRPParam": {"srpId": srp_name}}}
|
||||||
|
status_code, job = self.modify_storage_group(
|
||||||
|
array, storagegroup_name, payload)
|
||||||
|
self.wait_for_job("Set storage group srp", status_code,
|
||||||
|
job, extra_specs)
|
||||||
|
|
||||||
def get_vmax_default_storage_group(
|
def get_vmax_default_storage_group(
|
||||||
self, array, srp, slo, workload,
|
self, array, srp, slo, workload,
|
||||||
do_disable_compression=False, is_re=False):
|
do_disable_compression=False, is_re=False):
|
||||||
@ -1796,41 +1813,43 @@ class VMAXRest(object):
|
|||||||
"""
|
"""
|
||||||
return self.get_resource(array, REPLICATION, 'rdf_group')
|
return self.get_resource(array, REPLICATION, 'rdf_group')
|
||||||
|
|
||||||
def get_rdf_group_volume(self, array, rdf_number, device_id):
|
def get_rdf_group_volume(self, array, src_device_id):
|
||||||
"""Get specific volume details, from an RDF group.
|
"""Get the RDF details for a volume.
|
||||||
|
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
:param rdf_number: the rdf group number
|
:param src_device_id: the source device id
|
||||||
:param device_id: the device id
|
:returns: rdf_session
|
||||||
"""
|
"""
|
||||||
resource_name = "%(rdf)s/volume/%(dev)s" % {
|
rdf_session = None
|
||||||
'rdf': rdf_number, 'dev': device_id}
|
volume = self._get_private_volume(array, src_device_id)
|
||||||
return self.get_resource(array, REPLICATION, 'rdf_group',
|
try:
|
||||||
resource_name)
|
rdf_session = volume['rdfInfo']['RDFSession'][0]
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
LOG.warning("Cannot locate source RDF volume %s", src_device_id)
|
||||||
|
return rdf_session
|
||||||
|
|
||||||
def are_vols_rdf_paired(self, array, remote_array, device_id,
|
def are_vols_rdf_paired(self, array, remote_array,
|
||||||
target_device, rdf_group):
|
device_id, target_device):
|
||||||
"""Check if a pair of volumes are RDF paired.
|
"""Check if a pair of volumes are RDF paired.
|
||||||
|
|
||||||
:param array: the array serial number
|
:param array: the array serial number
|
||||||
:param remote_array: the remote array serial number
|
:param remote_array: the remote array serial number
|
||||||
:param device_id: the device id
|
:param device_id: the device id
|
||||||
:param target_device: the target device id
|
:param target_device: the target device id
|
||||||
:param rdf_group: the rdf group
|
:returns: paired -- bool, local_vol_state, rdf_pair_state
|
||||||
:returns: paired -- bool, state -- string
|
|
||||||
"""
|
"""
|
||||||
paired, local_vol_state, rdf_pair_state = False, '', ''
|
paired, local_vol_state, rdf_pair_state = False, '', ''
|
||||||
volume = self.get_rdf_group_volume(array, rdf_group, device_id)
|
rdf_session = self.get_rdf_group_volume(array, device_id)
|
||||||
if volume:
|
if rdf_session:
|
||||||
remote_volume = volume['remoteVolumeName']
|
remote_volume = rdf_session['remoteDeviceID']
|
||||||
remote_symm = volume['remoteSymmetrixId']
|
remote_symm = rdf_session['remoteSymmetrixID']
|
||||||
if (remote_volume == target_device
|
if (remote_volume == target_device
|
||||||
and remote_array == remote_symm):
|
and remote_array == remote_symm):
|
||||||
paired = True
|
paired = True
|
||||||
local_vol_state = volume['localVolumeState']
|
local_vol_state = rdf_session['SRDFStatus']
|
||||||
rdf_pair_state = volume['rdfpairState']
|
rdf_pair_state = rdf_session['pairState']
|
||||||
else:
|
else:
|
||||||
LOG.warning("Cannot locate source RDF volume %s", device_id)
|
LOG.warning("Cannot locate RDF session for volume %s", device_id)
|
||||||
return paired, local_vol_state, rdf_pair_state
|
return paired, local_vol_state, rdf_pair_state
|
||||||
|
|
||||||
def get_rdf_group_number(self, array, rdf_group_label):
|
def get_rdf_group_number(self, array, rdf_group_label):
|
||||||
@ -1843,8 +1862,9 @@ class VMAXRest(object):
|
|||||||
number = None
|
number = None
|
||||||
rdf_list = self.get_rdf_group_list(array)
|
rdf_list = self.get_rdf_group_list(array)
|
||||||
if rdf_list and rdf_list.get('rdfGroupID'):
|
if rdf_list and rdf_list.get('rdfGroupID'):
|
||||||
number = [rdf['rdfgNumber'] for rdf in rdf_list['rdfGroupID']
|
number_list = [rdf['rdfgNumber'] for rdf in rdf_list['rdfGroupID']
|
||||||
if rdf['label'] == rdf_group_label][0]
|
if rdf['label'] == rdf_group_label]
|
||||||
|
number = number_list[0] if len(number_list) > 0 else None
|
||||||
if number:
|
if number:
|
||||||
rdf_group = self.get_rdf_group(array, number)
|
rdf_group = self.get_rdf_group(array, number)
|
||||||
if not rdf_group:
|
if not rdf_group:
|
||||||
@ -2023,3 +2043,105 @@ class VMAXRest(object):
|
|||||||
% {'sg_name': source_sg_id, 'snap_id': snap_name})
|
% {'sg_name': source_sg_id, 'snap_id': snap_name})
|
||||||
return self.delete_resource(
|
return self.delete_resource(
|
||||||
array, REPLICATION, 'storagegroup', resource_name)
|
array, REPLICATION, 'storagegroup', resource_name)
|
||||||
|
|
||||||
|
def get_storagegroup_rdf_details(self, array, storagegroup_name,
|
||||||
|
rdf_group_num):
|
||||||
|
"""Get the remote replication details of a storage group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storage group name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
"""
|
||||||
|
resource_name = ("%(sg_name)s/rdf_group/%(rdf_num)s"
|
||||||
|
% {'sg_name': storagegroup_name,
|
||||||
|
'rdf_num': rdf_group_num})
|
||||||
|
return self.get_resource(array, REPLICATION, 'storagegroup',
|
||||||
|
resource_name=resource_name)
|
||||||
|
|
||||||
|
def replicate_group(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, remote_array, extra_specs):
|
||||||
|
"""Create a target group on the remote array and enable replication.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the name of the group
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param remote_array: the remote array serial number
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
resource_name = ("storagegroup/%(sg_name)s/rdf_group"
|
||||||
|
% {'sg_name': storagegroup_name})
|
||||||
|
payload = {"executionOption": "ASYNCHRONOUS",
|
||||||
|
"replicationMode": "Synchronous",
|
||||||
|
"remoteSymmId": remote_array,
|
||||||
|
"remoteStorageGroupName": storagegroup_name,
|
||||||
|
"rdfgNumber": rdf_group_num, "establish": 'true'}
|
||||||
|
status_code, job = self.create_resource(
|
||||||
|
array, REPLICATION, resource_name, payload)
|
||||||
|
self.wait_for_job('Create storage group rdf', status_code,
|
||||||
|
job, extra_specs)
|
||||||
|
|
||||||
|
def _verify_rdf_state(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, action):
|
||||||
|
"""Verify if a storage group requires the requested state change.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the storage group name
|
||||||
|
:param rdf_group_num: the rdf group number
|
||||||
|
:param action: the requested action
|
||||||
|
:returns: bool
|
||||||
|
"""
|
||||||
|
mod_rqd = False
|
||||||
|
sg_rdf_details = self.get_storagegroup_rdf_details(
|
||||||
|
array, storagegroup_name, rdf_group_num)
|
||||||
|
if sg_rdf_details:
|
||||||
|
state_list = sg_rdf_details['states']
|
||||||
|
for state in state_list:
|
||||||
|
if (action.lower() in ["establish", "failback", "resume"] and
|
||||||
|
state.lower() in ["suspended", "failed over"]):
|
||||||
|
mod_rqd = True
|
||||||
|
break
|
||||||
|
elif (action.lower() in ["split", "failover", "suspend"] and
|
||||||
|
state.lower() in ["synchronized", "syncinprog"]):
|
||||||
|
mod_rqd = True
|
||||||
|
break
|
||||||
|
return mod_rqd
|
||||||
|
|
||||||
|
def modify_storagegroup_rdf(self, array, storagegroup_name,
|
||||||
|
rdf_group_num, action, extra_specs):
|
||||||
|
"""Modify the rdf state of a storage group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the name of the storage group
|
||||||
|
:param rdf_group_num: the number of the rdf group
|
||||||
|
:param action: the required action
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
"""
|
||||||
|
# Check if group is in valid state for desired action
|
||||||
|
mod_reqd = self._verify_rdf_state(array, storagegroup_name,
|
||||||
|
rdf_group_num, action)
|
||||||
|
if mod_reqd:
|
||||||
|
payload = {"executionOption": "ASYNCHRONOUS", "action": action}
|
||||||
|
resource_name = ('%(sg_name)s/rdf_group/%(rdf_num)s'
|
||||||
|
% {'sg_name': storagegroup_name,
|
||||||
|
'rdf_num': rdf_group_num})
|
||||||
|
|
||||||
|
status_code, job = self.modify_resource(
|
||||||
|
array, REPLICATION, 'storagegroup', payload,
|
||||||
|
resource_name=resource_name)
|
||||||
|
|
||||||
|
self.wait_for_job('Modify storagegroup rdf',
|
||||||
|
status_code, job, extra_specs)
|
||||||
|
|
||||||
|
def delete_storagegroup_rdf(self, array, storagegroup_name,
|
||||||
|
rdf_group_num):
|
||||||
|
"""Delete the rdf pairs for a storage group.
|
||||||
|
|
||||||
|
:param array: the array serial number
|
||||||
|
:param storagegroup_name: the name of the storage group
|
||||||
|
:param rdf_group_num: the number of the rdf group
|
||||||
|
"""
|
||||||
|
resource_name = ('%(sg_name)s/rdf_group/%(rdf_num)s'
|
||||||
|
% {'sg_name': storagegroup_name,
|
||||||
|
'rdf_num': rdf_group_num})
|
||||||
|
self.delete_resource(
|
||||||
|
array, REPLICATION, 'storagegroup', resource_name=resource_name)
|
||||||
|
@ -559,22 +559,6 @@ class VMAXUtils(object):
|
|||||||
default_dict[RETRIES] = retries
|
default_dict[RETRIES] = retries
|
||||||
return default_dict
|
return default_dict
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_admin_metadata(volumes_model_update, key, values):
|
|
||||||
"""Update the volume_model_updates with admin metadata.
|
|
||||||
|
|
||||||
:param volumes_model_update: List of volume model updates
|
|
||||||
:param key: Key to be updated in the admin_metadata
|
|
||||||
:param values: Dictionary of values per volume id
|
|
||||||
"""
|
|
||||||
for volume_model_update in volumes_model_update:
|
|
||||||
volume_id = volume_model_update['id']
|
|
||||||
if volume_id in values:
|
|
||||||
admin_metadata = {}
|
|
||||||
admin_metadata.update({key: values[volume_id]})
|
|
||||||
volume_model_update.update(
|
|
||||||
{'admin_metadata': admin_metadata})
|
|
||||||
|
|
||||||
def get_volume_group_utils(self, group, interval, retries):
|
def get_volume_group_utils(self, group, interval, retries):
|
||||||
"""Standard utility for generic volume groups.
|
"""Standard utility for generic volume groups.
|
||||||
|
|
||||||
@ -639,7 +623,7 @@ class VMAXUtils(object):
|
|||||||
:returns: group_name -- formatted name + id
|
:returns: group_name -- formatted name + id
|
||||||
"""
|
"""
|
||||||
group_name = ""
|
group_name = ""
|
||||||
if group.name is not None:
|
if group.name is not None and group.name != group.id:
|
||||||
group_name = (
|
group_name = (
|
||||||
self.truncate_string(
|
self.truncate_string(
|
||||||
group.name, TRUNCATE_27) + "_")
|
group.name, TRUNCATE_27) + "_")
|
||||||
@ -685,3 +669,44 @@ class VMAXUtils(object):
|
|||||||
new_pool['pool_name'] = new_pool_name
|
new_pool['pool_name'] = new_pool_name
|
||||||
pools.append(new_pool)
|
pools.append(new_pool)
|
||||||
return pools
|
return pools
|
||||||
|
|
||||||
|
def check_replication_matched(self, volume, extra_specs):
|
||||||
|
"""Check volume type and group type.
|
||||||
|
|
||||||
|
This will make sure they do not conflict with each other.
|
||||||
|
:param volume: volume to be checked
|
||||||
|
:param extra_specs: the extra specifications
|
||||||
|
:raises: InvalidInput
|
||||||
|
"""
|
||||||
|
# If volume is not a member of group, skip this check anyway.
|
||||||
|
if not volume.group:
|
||||||
|
return
|
||||||
|
vol_is_re = self.is_replication_enabled(extra_specs)
|
||||||
|
group_is_re = volume.group.is_replicated
|
||||||
|
|
||||||
|
if not (vol_is_re == group_is_re):
|
||||||
|
msg = _('Replication should be enabled or disabled for both '
|
||||||
|
'volume or group. Volume replication status: '
|
||||||
|
'%(vol_status)s, group replication status: '
|
||||||
|
'%(group_status)s') % {
|
||||||
|
'vol_status': vol_is_re, 'group_status': group_is_re}
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_rep_status_enabled(group):
|
||||||
|
"""Check replication status for group.
|
||||||
|
|
||||||
|
Group status must be enabled before proceeding with certain
|
||||||
|
operations.
|
||||||
|
:param group: the group object
|
||||||
|
:raises: InvalidInput
|
||||||
|
"""
|
||||||
|
if group.is_replicated:
|
||||||
|
if group.replication_status != fields.ReplicationStatus.ENABLED:
|
||||||
|
msg = (_('Replication status should be %s for '
|
||||||
|
'replication-enabled group.')
|
||||||
|
% fields.ReplicationStatus.ENABLED)
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
else:
|
||||||
|
LOG.debug('Replication is not enabled on group %s, '
|
||||||
|
'skip status check.', group.id)
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add consistent replication group support in Dell EMC VMAX cinder driver.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user