VMAX driver - Implement Generic Volume Group feature

This patch adds Consistency Group capability to Generic
Volume Groups in the VMAX driver.

Change-Id: I1564f12e052b3c7e9a45826b3f1f707011e3c634
Partially-Implements: blueprint vmax-generic-volume-group
This commit is contained in:
Helen Walsh 2017-06-15 11:09:27 +00:00
parent dd065f8e19
commit 1ee279bd90
9 changed files with 1707 additions and 15 deletions

View File

@ -27,7 +27,11 @@ import six
from cinder import context
from cinder import exception
from cinder.objects import fields
from cinder.objects import group
from cinder.objects import group_snapshot
from cinder.objects import volume_type
from cinder import test
from cinder.tests.unit import fake_group
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.volume.drivers.dell_emc.vmax import common
@ -37,6 +41,7 @@ from cinder.volume.drivers.dell_emc.vmax import masking
from cinder.volume.drivers.dell_emc.vmax import provision
from cinder.volume.drivers.dell_emc.vmax import rest
from cinder.volume.drivers.dell_emc.vmax import utils
from cinder.volume import utils as volume_utils
from cinder.volume import volume_types
from cinder.zonemanager import utils as fczm_utils
@ -75,6 +80,11 @@ class VMAXCommonData(object):
rdf_group_name = '23_24_007'
rdf_group_no = '70'
u4v_version = '84'
storagegroup_name_source = 'Grp_source_sg'
storagegroup_name_target = 'Grp_target_sg'
group_snapshot_name = 'Grp_snapshot'
target_group_name = 'Grp_target'
storagegroup_name_with_id = 'GrpId_group_name'
# connector info
wwpn1 = "123456789012345"
@ -194,6 +204,53 @@ class VMAXCommonData(object):
rep_extra_specs['retries'] = 0
rep_extra_specs['srp'] = srp2
test_volume_type_1 = volume_type.VolumeType(
id='abc', name='abc',
extra_specs=extra_specs
)
test_volume_type_list = volume_type.VolumeTypeList(
objects=[test_volume_type_1])
test_group_1 = group.Group(
context=None, name=storagegroup_name_source,
group_id='abc', size=1,
id='12345', status='available',
provider_auth=None, volume_type_ids=['abc'],
group_type_id='grptypeid',
volume_types=test_volume_type_list,
host=fake_host, provider_location=six.text_type(provider_location))
test_group_failed = group.Group(
context=None, name=failed_resource,
group_id='abc', size=1,
id='12345', status='available',
provider_auth=None, volume_type_ids=['abc'],
group_type_id='grptypeid',
volume_types=test_volume_type_list,
host=fake_host, provider_location=six.text_type(provider_location))
test_group = fake_group.fake_group_obj(
context=ctx, name=storagegroup_name_source,
id='12345', host=fake_host)
test_group_without_name = fake_group.fake_group_obj(
context=ctx, name=None,
id='12345', host=fake_host)
test_vol_grp_name = 'Grp_source_sg_12345'
test_vol_grp_name_id_only = '12345'
test_group_snapshot_1 = group_snapshot.GroupSnapshot(
context=None, id='123456',
group_id='12345', name=group_snapshot_name,
group_type_id='grptypeid', status='available',
group=test_group_1)
test_group_snapshot_failed = group_snapshot.GroupSnapshot(
context=None, id='123456',
group_id='12345', name=failed_resource,
group_type_id='grptypeid', status='available',
group=test_group_failed)
# masking view dict
masking_view_dict = {
'array': array,
@ -325,9 +382,21 @@ class VMAXCommonData(object):
"maskingview": [masking_view_name_i], }
]
sg_details_rep = [{"childNames": [],
"numDevicesNonGk": 2,
"isLinkTarget": False,
"rdf": False,
"capacityGB": 2.0,
"name": storagegroup_name_source,
"snapVXSnapshots": ['12345'],
"symmetrixId": array,
"numSnapVXSnapshots": 1}]
sg_list = {"storageGroupId": [storagegroup_name_f,
defaultstoragegroup_name]}
sg_list_rep = [storagegroup_name_with_id]
srp_details = {"srpSloDemandId": ["Bronze", "Diamond", "Gold",
"None", "Optimized", "Silver"],
"srpId": srp,
@ -400,6 +469,39 @@ class VMAXCommonData(object):
{"symmetrixId": array,
"snapVxCapable": True,
"rdfCapable": True}]}
group_snap_vx = {"generation": 0,
"isLinked": False,
"numUniqueTracks": 0,
"isRestored": False,
"name": group_snapshot_name,
"numStorageGroupVolumes": 1,
"state": ["Established"],
"timeToLiveExpiryDate": "N/A",
"isExpired": False,
"numSharedTracks": 0,
"timestamp": "00:30:50 Fri, 02 Jun 2017 IST +0100",
"numSourceVolumes": 1
}
group_snap_vx_1 = {"generation": 0,
"isLinked": False,
"numUniqueTracks": 0,
"isRestored": False,
"name": group_snapshot_name,
"numStorageGroupVolumes": 1,
"state": ["Copied"],
"timeToLiveExpiryDate": "N/A",
"isExpired": False,
"numSharedTracks": 0,
"timestamp": "00:30:50 Fri, 02 Jun 2017 IST +0100",
"numSourceVolumes": 1,
"linkedStorageGroup":
{"name": target_group_name,
"percentageCopied": 100},
}
grp_snapvx_links = [{"name": target_group_name,
"percentageCopied": 100},
{"name": "another-target",
"percentageCopied": 90}]
rdf_group_list = {"rdfGroupID": [{"rdfgNumber": rdf_group_no,
"label": rdf_group_name}]}
@ -592,12 +694,22 @@ class FakeRequestsSession(object):
return_object = self.data.rdf_group_details
else:
return_object = self.data.rdf_group_list
elif 'storagegroup' in url:
return_object = self._replication_sg(url)
elif 'snapshot' in url:
return_object = self.data.volume_snap_vx
elif 'capabilities' in url:
return_object = self.data.capabilities
return return_object
def _replication_sg(self, url):
return_object = None
if 'generation' in url:
return_object = self.data.group_snap_vx
elif 'storagegroup' in url:
return_object = self.data.sg_details_rep[0]
return return_object
def _system(self, url):
return_object = None
if 'job' in url:
@ -1059,6 +1171,84 @@ class VMAXUtilsTest(test.TestCase):
is_fo3 = self.utils.is_volume_failed_over(None)
self.assertFalse(is_fo3)
def test_update_volume_group_name(self):
group = self.data.test_group_1
ref_group_name = self.data.test_vol_grp_name
vol_grp_name = self.utils.update_volume_group_name(group)
self.assertEqual(ref_group_name, vol_grp_name)
def test_update_volume_group_name_id_only(self):
group = self.data.test_group_without_name
ref_group_name = self.data.test_vol_grp_name_id_only
vol_grp_name = self.utils.update_volume_group_name(group)
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):
group = self.data.test_group_1
array, extraspecs_dict = self.utils.get_volume_group_utils(
group, interval=1, retries=1)
ref_array = self.data.array
self.assertEqual(ref_array, array)
def test_update_extra_specs_list(self):
extra_specs = self.data.extra_specs
volume_type_id = 'abc'
extraspecs_dict = self.utils._update_extra_specs_list(
extra_specs, volume_type_id, interval=1, retries=1)
self.assertEqual(extra_specs, extraspecs_dict['extra_specs'])
def test_update_intervals_and_retries(self):
extra_specs = self.data.extra_specs
ref_interval = 1
extraspecs = self.utils._update_intervals_and_retries(
extra_specs, interval=1, retries=1)
self.assertEqual(ref_interval, extraspecs['interval'])
def test_get_intervals_retries_dict(self):
ref_value = {'interval': 1, 'retries': 1}
ret_dict = self.utils.get_intervals_retries_dict(
interval=1, retries=1)
self.assertEqual(ref_value, ret_dict)
def test_update_volume_model_updates(self):
volume_model_updates = [{'id': '1', 'status': 'available'}]
volumes = [self.data.test_volume]
ref_val = {'id': self.data.test_volume.id,
'status': 'error_deleting'}
ret_val = self.utils.update_volume_model_updates(
volume_model_updates, volumes, 'abc', status='error_deleting')
self.assertEqual(ref_val, ret_val[1])
def test_update_volume_model_updates_empty_update_list(self):
volume_model_updates = []
volumes = [self.data.test_volume]
ref_val = [{'id': self.data.test_volume.id,
'status': 'available'}]
ret_val = self.utils.update_volume_model_updates(
volume_model_updates, volumes, 'abc')
self.assertEqual(ref_val, ret_val)
def test_update_volume_model_updates_empty_vol_list(self):
volume_model_updates = []
volumes = []
ref_val = []
ret_val = self.utils.update_volume_model_updates(
volume_model_updates, volumes, 'abc')
self.assertEqual(ref_val, ret_val)
class VMAXRestTest(test.TestCase):
def setUp(self):
@ -2250,6 +2440,36 @@ class VMAXRestTest(test.TestCase):
failover_payload, resource_name=resource_name,
private='/private')
def test_get_storage_group_rep(self):
array = self.data.array
source_group_name = self.data.storagegroup_name_source
ref_details = self.data.sg_details_rep[0]
volume_group = self.rest.get_storage_group_rep(array,
source_group_name)
self.assertEqual(volume_group, ref_details)
def test_get_volumes_in_storage_group(self):
array = self.data.array
storagegroup_name = self.data.storagegroup_name_source
ref_volumes = [self.data.device_id, self.data.device_id2]
volume_list = self.rest.get_volumes_in_storage_group(
array, storagegroup_name)
self.assertEqual(ref_volumes, volume_list)
def test_create_storagegroup_snap(self):
array = self.data.array
extra_specs = self.data.extra_specs
source_group = self.data.storagegroup_name_source
snap_name = self.data.group_snapshot_name
with mock.patch.object(
self.rest, "create_storagegroup_snap") as mock_create:
self.rest.create_storagegroup_snap(
array, source_group, snap_name, extra_specs)
mock_create.assert_called_once_with(array,
source_group,
snap_name,
extra_specs)
class VMAXProvisionTest(test.TestCase):
def setUp(self):
@ -2267,6 +2487,7 @@ class VMAXProvisionTest(test.TestCase):
self.common = self.driver.common
self.provision = self.common.provision
self.utils = self.common.utils
self.rest = self.common.rest
def test_create_storage_group(self):
array = self.data.array
@ -2573,6 +2794,74 @@ class VMAXProvisionTest(test.TestCase):
array, device_id, rdf_group_name, extra_specs,
split=False)
def test_create_volume_group_success(self):
array = self.data.array
group_name = self.data.storagegroup_name_source
extra_specs = self.data.extra_specs
ref_value = self.data.storagegroup_name_source
storagegroup = self.provision.create_volume_group(array,
group_name,
extra_specs)
self.assertEqual(ref_value, storagegroup)
def test_create_group_replica(self):
array = self.data.array
source_group = self.data.storagegroup_name_source
snap_name = self.data.group_snapshot_name
extra_specs = self.data.extra_specs
with mock.patch.object(
self.provision,
'create_group_replica') as mock_create_replica:
self.provision.create_group_replica(
array, source_group, snap_name, extra_specs)
mock_create_replica.assert_called_once_with(
array, source_group, snap_name, extra_specs)
def test_delete_group_replica(self):
array = self.data.array
snap_name = self.data.group_snapshot_name
source_group_name = self.data.storagegroup_name_source
with mock.patch.object(
self.provision,
'delete_group_replica') as mock_delete_replica:
self.provision.delete_group_replica(array,
snap_name,
source_group_name)
mock_delete_replica.assert_called_once_with(
array, snap_name, source_group_name)
def test_link_and_break_replica(self):
array = self.data.array
source_group_name = self.data.storagegroup_name_source
target_group_name = self.data.target_group_name
snap_name = self.data.group_snapshot_name
extra_specs = self.data.extra_specs
deleteSnapshot = False
with mock.patch.object(
self.provision,
'link_and_break_replica') as mock_link_and_break_replica:
self.provision.link_and_break_replica(
array, source_group_name,
target_group_name, snap_name,
extra_specs, deleteSnapshot)
mock_link_and_break_replica.assert_called_once_with(
array, source_group_name,
target_group_name, snap_name,
extra_specs, deleteSnapshot)
def test_unlink_group(self):
with mock.patch.object(self.rest,
'modify_storagegroup_snap') as mock_mod:
self.provision._unlink_group(
self.data.array, self.data.storagegroup_name_source,
self.data.target_group_name,
self.data.group_snapshot_name, self.data.extra_specs)
mock_mod.assert_called_once_with(
self.data.array, self.data.storagegroup_name_source,
self.data.target_group_name,
self.data.group_snapshot_name, self.data.extra_specs,
unlink=True)
class VMAXCommonTest(test.TestCase):
def setUp(self):
@ -3707,6 +3996,269 @@ class VMAXCommonTest(test.TestCase):
self.data.srp, volume_name, False)
self.assertEqual(ref_return, return_val)
def test_find_volume_group_name_from_id(self):
array = self.data.array
group_id = 'GrpId'
group_name = None
ref_group_name = self.data.storagegroup_name_with_id
with mock.patch.object(
self.rest, 'get_storage_group_list',
return_value=self.data.sg_list_rep):
group_name = self.common._find_volume_group_name_from_id(
array, group_id)
self.assertEqual(ref_group_name, group_name)
def test_find_volume_group_name_from_id_not_found(self):
array = self.data.array
group_id = 'GrpId'
group_name = None
group_name = self.common._find_volume_group_name_from_id(
array, group_id)
self.assertIsNone(group_name)
def test_find_volume_group(self):
group = self.data.test_group_1
array = self.data.array
volume_group = self.common._find_volume_group(array, group)
ref_group = self.data.sg_details_rep[0]
self.assertEqual(ref_group, volume_group)
def test_get_volume_device_ids(self):
array = self.data.array
volumes = [self.data.test_volume]
ref_device_ids = [self.data.device_id]
device_ids = self.common._get_volume_device_ids(volumes, array)
self.assertEqual(ref_device_ids, device_ids)
def test_get_members_of_volume_group(self):
array = self.data.array
group_name = self.data.storagegroup_name_source
ref_volumes = [self.data.device_id, self.data.device_id2]
member_device_ids = self.common._get_members_of_volume_group(
array, group_name)
self.assertEqual(ref_volumes, member_device_ids)
def test_get_members_of_volume_group_empty(self):
array = self.data.array
group_name = self.data.storagegroup_name_source
with mock.patch.object(
self.rest, 'get_volumes_in_storage_group',
return_value=None):
member_device_ids = self.common._get_members_of_volume_group(
array, group_name
)
self.assertIsNone(member_device_ids)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
def test_create_group_replica(self, mock_check):
source_group = self.data.test_group_1
snap_name = self.data.group_snapshot_name
with mock.patch.object(
self.common,
'_create_group_replica') as mock_create_replica:
self.common._create_group_replica(
source_group, snap_name)
mock_create_replica.assert_called_once_with(
source_group, snap_name)
def test_create_group_replica_exception(self):
source_group = self.data.test_group_failed
snap_name = self.data.group_snapshot_name
with mock.patch.object(
volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
self.assertRaises(exception.VolumeBackendAPIException,
self.common._create_group_replica,
source_group,
snap_name)
def test_create_group_snapshot(self):
context = None
group_snapshot = self.data.test_group_snapshot_1
snapshots = []
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
with mock.patch.object(
volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, snapshots_model_update = (
self.common.create_group_snapshot(
context, group_snapshot, snapshots))
self.assertEqual(ref_model_update, model_update)
def test_create_group_snapshot_exception(self):
context = None
group_snapshot = self.data.test_group_snapshot_failed
snapshots = []
with mock.patch.object(
volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
self.assertRaises(exception.VolumeBackendAPIException,
self.common.create_group_snapshot,
context,
group_snapshot,
snapshots)
def test_create_group(self):
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
context = None
group = self.data.test_group_1
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):
context = None
group = self.data.test_group_snapshot_failed
with mock.patch.object(
volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
self.assertRaises(exception.VolumeBackendAPIException,
self.common.create_group,
context,
group)
def test_delete_group_snapshot(self):
group_snapshot = self.data.test_group_snapshot_1
snapshots = []
context = None
ref_model_update = {'status': fields.GroupSnapshotStatus.DELETED}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, snapshots_model_update = (
self.common.delete_group_snapshot(context,
group_snapshot, snapshots))
self.assertEqual(ref_model_update, model_update)
def test_delete_group_snapshot_success(self):
group_snapshot = self.data.test_group_snapshot_1
snapshots = []
ref_model_update = {'status': fields.GroupSnapshotStatus.DELETED}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, snapshots_model_update = (
self.common._delete_group_snapshot(group_snapshot,
snapshots))
self.assertEqual(ref_model_update, model_update)
def test_delete_group_snapshot_failed(self):
group_snapshot = self.data.test_group_snapshot_failed
snapshots = []
ref_model_update = (
{'status': fields.GroupSnapshotStatus.ERROR_DELETING})
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, snapshots_model_update = (
self.common._delete_group_snapshot(group_snapshot,
snapshots))
self.assertEqual(ref_model_update, model_update)
def test_update_group(self):
group = self.data.test_group_1
add_vols = [self.data.test_volume]
remove_vols = []
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, __, __ = self.common.update_group(group,
add_vols,
remove_vols)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
def test_update_group_not_found(self, mock_check):
group = self.data.test_group_1
add_vols = []
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(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
def test_update_group_exception(self, mock_check):
group = self.data.test_group_1
add_vols = []
remove_vols = []
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):
group = self.data.test_group_1
volumes = [self.data.test_volume]
context = None
ref_model_update = {'status': fields.GroupStatus.DELETED}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True),\
mock.patch.object(self.rest, 'get_volumes_in_storage_group',
return_value=[]):
model_update, __ = self.common.delete_group(
context, group, volumes)
self.assertEqual(ref_model_update, model_update)
def test_delete_group_success(self):
group = self.data.test_group_1
volumes = []
ref_model_update = {'status': fields.GroupStatus.DELETED}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True),\
mock.patch.object(self.rest, 'get_volumes_in_storage_group',
return_value=[]):
model_update, __ = self.common._delete_group(group, volumes)
self.assertEqual(ref_model_update, model_update)
def test_delete_group_already_deleted(self):
group = self.data.test_group_failed
ref_model_update = {'status': fields.GroupStatus.DELETED}
volumes = []
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, __ = self.common._delete_group(group, volumes)
self.assertEqual(ref_model_update, model_update)
@mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True)
def test_delete_group_failed(self, mock_check):
group = self.data.test_group_1
volumes = []
ref_model_update = {'status': fields.GroupStatus.ERROR_DELETING}
with mock.patch.object(
self.rest, 'delete_storage_group',
side_effect=exception.VolumeBackendAPIException):
model_update, __ = self.common._delete_group(
group, volumes)
self.assertEqual(ref_model_update, model_update)
def test_create_group_from_src_success(self):
context = None
group = self.data.test_group_1
group_snapshot = self.data.test_group_snapshot_1
snapshots = []
volumes = [self.data.test_volume]
source_group = None
source_vols = []
ref_model_update = {'status': fields.GroupStatus.AVAILABLE}
with mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type',
return_value=True):
model_update, volumes_model_update = (
self.common.create_group_from_src(
context, group, volumes,
group_snapshot, snapshots,
source_group, source_vols))
self.assertEqual(ref_model_update, model_update)
class VMAXFCTest(test.TestCase):
def setUp(self):

View File

@ -22,6 +22,7 @@ from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import strutils
import six
import uuid
from cinder import exception
from cinder.i18n import _
@ -31,7 +32,7 @@ from cinder.volume.drivers.dell_emc.vmax import masking
from cinder.volume.drivers.dell_emc.vmax import provision
from cinder.volume.drivers.dell_emc.vmax import rest
from cinder.volume.drivers.dell_emc.vmax import utils
from cinder.volume import utils as volume_utils
LOG = logging.getLogger(__name__)
@ -195,7 +196,7 @@ class VMAXCommon(object):
all available SLO & Workload combinations
:param array_info: the array information
:returns: finalarrayinfolist
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
try:
array = array_info['SerialNumber']
@ -250,6 +251,14 @@ class VMAXCommon(object):
volume_dict = (self._create_volume(
volume_name, volume_size, extra_specs))
if volume.group_id is not None:
group_name = self._find_volume_group_name_from_id(
extra_specs[utils.ARRAY], volume.group_id)
if group_name is not None:
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
if self.utils.is_replication_enabled(extra_specs):
rep_update = self._replicate_volume(volume, volume_name,
@ -268,7 +277,7 @@ class VMAXCommon(object):
:param volume: volume object
:param snapshot: snapshot object
:returns: model_update
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
LOG.debug("Entering create_volume_from_snapshot.")
model_update = {}
@ -597,7 +606,7 @@ class VMAXCommon(object):
:param volume: the volume Object
:param new_size: the new size to increase the volume to
:returns: dict -- modifiedVolumeDict - the extended volume Object
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
original_vol_size = volume.size
volume_name = volume.name
@ -708,6 +717,7 @@ class VMAXCommon(object):
'location_info': temp_location_info,
'thin_provisioning_support': True,
'thick_provisioning_support': False,
'consistent_group_snapshot_enabled': True,
'max_over_subscription_ratio':
max_oversubscription_ratio,
'reserved_percentage': reserved_percentage,
@ -731,7 +741,7 @@ class VMAXCommon(object):
'consistencygroup_support': False,
'thin_provisioning_support': True,
'thick_provisioning_support': False,
'consistent_group_snapshot_enabled': False,
'consistent_group_snapshot_enabled': True,
'max_over_subscription_ratio':
max_oversubscription_ratio,
'reserved_percentage': reserved_percentage,
@ -854,9 +864,16 @@ class VMAXCommon(object):
device_id = name['keybindings']['DeviceID']
element_name = self.utils.get_volume_element_name(
volume_name)
founddevice_id = self.rest.find_volume_device_id(
array, element_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.find_volume_device_id(
array, target_vol_name)
else:
founddevice_id = self.rest.find_volume_device_id(
array, element_name)
# Allow for an external app to delete the volume.
if device_id and device_id != founddevice_id:
founddevice_id = None
@ -968,7 +985,7 @@ class VMAXCommon(object):
:param config_group_name: the config group name
:returns: string -- configurationFile - name of the configuration file
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
if config_group_name is None:
return CINDER_EMC_CONFIG_FILE
@ -1012,7 +1029,7 @@ class VMAXCommon(object):
:param volume: the volume object
:param volume_type_id: optional override of volume.volume_type_id
:returns: dict -- extra spec dict
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
try:
extra_specs, config_file, qos_specs = (
@ -1134,7 +1151,7 @@ class VMAXCommon(object):
:param is_snapshot: boolean -- Defaults to False
:param from_snapvx: bool -- Defaults to False
:returns: dict -- cloneDict the cloned volume dictionary
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
clone_name = volume.name
snap_name = None
@ -1281,7 +1298,7 @@ class VMAXCommon(object):
:param extra_specs: extra specifications
:returns: int -- return code
:returns: dict -- volume_dict
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
array = extra_specs[utils.ARRAY]
is_valid_slo, is_valid_workload = self.provision.verify_slo_workload(
@ -1431,7 +1448,7 @@ class VMAXCommon(object):
:param device_id: the device id
:param volume_name: the volume name
:param extra_specs: the extra specifications
:raises VolumeBackendAPIException:
:raises: VolumeBackendAPIException:
"""
try:
LOG.debug("Delete Volume: %(name)s. device_id: %(device_id)s.",
@ -1706,7 +1723,7 @@ class VMAXCommon(object):
:param device_id: the device id
:param volume_id: the cinder volume id
:param external_ref: the external reference
:raises ManageExistingInvalidReference, ManageExistingAlreadyManaged:
:raises: ManageExistingInvalidReference, ManageExistingAlreadyManaged:
"""
# Ensure the volume exists on the array
volume_details = self.rest.get_volume(array, device_id)
@ -2630,3 +2647,535 @@ class VMAXCommon(object):
if is_descendant:
is_source_nf_sg = True
return source_nf_sg, source_sg, source_parent_sg, is_source_nf_sg
def create_group(self, context, group):
"""Creates a generic volume group.
:param context: the context
:param group: the group object to be created
:returns: dict -- modelUpdate = {'status': 'available'}
:raises: VolumeBackendAPIException, NotImplementedError
"""
if not volume_utils.is_group_a_cg_snapshot_type(group):
raise NotImplementedError()
model_update = {'status': fields.GroupStatus.AVAILABLE}
LOG.info("Create generic volume group: %(group)s.",
{'group': group.id})
vol_grp_name = self.utils.update_volume_group_name(group)
try:
array, __ = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.create_volume_group(
array, vol_grp_name, interval_retries_dict)
except Exception:
exception_message = (_("Failed to create generic volume group:"
" %(volGrpName)s.")
% {'volGrpName': vol_grp_name})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
return model_update
def delete_group(self, context, group, volumes):
"""Deletes a generic volume group.
:param context: the context
:param group: the group object to be deleted
:param volumes: the list of volumes in the generic group to be deleted
:returns: dict -- modelUpdate
:returns: list -- list of volume model updates
:raises: NotImplementedError
"""
LOG.info("Delete generic volume group: %(group)s.",
{'group': group.id})
if not volume_utils.is_group_a_cg_snapshot_type(group):
raise NotImplementedError()
model_update, volumes_model_update = self._delete_group(
group, volumes)
return model_update, volumes_model_update
def _delete_group(self, group, volumes):
"""Helper function to delete a volume group.
:param group: the group object
:param volumes: the member volume objects
:returns: model_update, volumes_model_update
"""
volumes_model_update = []
array, extraspecs_dict_list = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
vol_grp_name = None
volume_group = self._find_volume_group(
array, group)
if volume_group is None:
LOG.error("Cannot find generic volume group %(volGrpName)s.",
{'volGrpName': group.id})
model_update = {'status': fields.GroupStatus.DELETED}
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update, volumes, group.id, status='deleted')
return model_update, volumes_model_update
if 'name' in volume_group:
vol_grp_name = volume_group['name']
volume_device_ids = self._get_members_of_volume_group(
array, vol_grp_name)
intervals_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
deleted_volume_device_ids = []
try:
# If there are no volumes in sg then delete it
if not volume_device_ids:
self.rest.delete_storage_group(array, vol_grp_name)
model_update = {'status': fields.GroupStatus.DELETED}
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update, volumes, group.id, status='deleted')
return model_update, volumes_model_update
# First remove all the volumes from the SG
self.masking.remove_volumes_from_storage_group(
array, volume_device_ids, vol_grp_name, intervals_retries_dict)
for vol in volumes:
for extraspecs_dict in extraspecs_dict_list:
if vol.volume_type_id in extraspecs_dict['volumeTypeId']:
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
device_id = self._find_device_on_array(vol,
extraspecs)
if device_id in volume_device_ids:
self._remove_vol_and_cleanup_replication(
array, device_id,
vol.name, extraspecs, vol)
self._delete_from_srp(
array, device_id, "group vol", extraspecs)
else:
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
self.rest.delete_storage_group(array, vol_grp_name)
model_update = {'status': fields.GroupStatus.DELETED}
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update, volumes, group.id, status='deleted')
except Exception as e:
LOG.error("Error deleting volume group."
"Error received: %(e)s", {'e': e})
model_update = {'status': fields.GroupStatus.ERROR_DELETING}
# Update the volumes_model_update
volumes_not_deleted = []
for vol in volume_device_ids:
if vol not in deleted_volume_device_ids:
volumes_not_deleted.append(vol)
if not deleted_volume_device_ids:
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update,
deleted_volume_device_ids,
group.id, status='deleted')
if not volumes_not_deleted:
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update,
volumes_not_deleted,
group.id, status='deleted')
# As a best effort try to add back the undeleted volumes to sg
# Dont throw any exception in case of failure
try:
if not volumes_not_deleted:
self.masking.add_volumes_to_storage_group(
array, volumes_not_deleted,
vol_grp_name, intervals_retries_dict)
except Exception as ex:
LOG.error("Error in rollback - %(ex)s. "
"Failed to add back volumes to sg %(sg_name)s",
{'ex': ex, 'sg_name': vol_grp_name})
return model_update, volumes_model_update
def create_group_snapshot(self, context, group_snapshot, snapshots):
"""Creates a generic volume group snapshot.
:param context: the context
:param group_snapshot: the group snapshot to be created
:param snapshots: snapshots
:returns: dict -- modelUpdate
:returns: list -- list of snapshots
:raises: VolumeBackendAPIException, NotImplementedError
"""
grp_id = group_snapshot.group_id
source_group = group_snapshot.get('group')
if not volume_utils.is_group_a_cg_snapshot_type(source_group):
raise NotImplementedError()
snapshots_model_update = []
LOG.info(
"Create snapshot for %(grpId)s "
"group Snapshot ID: %(group_snapshot)s.",
{'group_snapshot': group_snapshot.id,
'grpId': grp_id})
try:
snap_name = self.utils.truncate_string(group_snapshot.id, 19)
self._create_group_replica(source_group,
snap_name)
except Exception as e:
exception_message = (_("Failed to create snapshot for group: "
"%(volGrpName)s. Exception received: %(e)s")
% {'volGrpName': grp_id,
'e': six.text_type(e)})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
for snapshot in snapshots:
snapshots_model_update.append(
{'id': snapshot.id,
'status': fields.SnapshotStatus.AVAILABLE})
model_update = {'status': fields.GroupStatus.AVAILABLE}
return model_update, snapshots_model_update
def _create_group_replica(
self, source_group, snap_name):
"""Create a group replica.
This can be a group snapshot or a cloned volume group.
:param source_group: the group object
:param snap_name: the name of the snapshot
"""
array, __ = (
self.utils.get_volume_group_utils(
source_group, self.interval, self.retries))
vol_grp_name = None
volume_group = (
self._find_volume_group(array, source_group))
if volume_group:
if 'name' in volume_group:
vol_grp_name = volume_group['name']
if vol_grp_name is None:
exception_message = (
_("Cannot find generic volume group %(group_id)s.") %
{'group_id': source_group.id})
raise exception.VolumeBackendAPIException(
data=exception_message)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.create_group_replica(
array, vol_grp_name,
snap_name, interval_retries_dict)
def delete_group_snapshot(self, context, group_snapshot, snapshots):
"""Delete a volume group snapshot.
:param context: the context
:param group_snapshot: the volume group snapshot to be deleted
:param snapshots: the snapshot objects
:returns: model_update, snapshots_model_update
"""
model_update, snapshots_model_update = self._delete_group_snapshot(
group_snapshot, snapshots)
return model_update, snapshots_model_update
def _delete_group_snapshot(self, group_snapshot, snapshots):
"""Helper function to delete a group snapshot.
:param group_snapshot: the group snapshot object
:param snapshots: the snapshot objects
:returns: model_update, snapshots_model_update
:raises: VolumeBackendApiException, NotImplementedError
"""
snapshots_model_update = []
model_update = {}
source_group = group_snapshot.get('group')
grp_id = group_snapshot.group_id
if not volume_utils.is_group_a_cg_snapshot_type(source_group):
raise NotImplementedError()
LOG.info("Delete snapshot grpSnapshotId: %(grpSnapshotId)s"
" for source group %(grpId)s",
{'grpSnapshotId': group_snapshot.id,
'grpId': grp_id})
snap_name = self.utils.truncate_string(group_snapshot.id, 19)
vol_grp_name = None
try:
# Get the array serial
array, __ = (
self.utils.get_volume_group_utils(
source_group, self.interval, self.retries))
# Get the volume group dict for getting the group name
volume_group = (
self._find_volume_group(array, source_group))
if volume_group:
if 'name' in volume_group:
vol_grp_name = volume_group['name']
if vol_grp_name is None:
exception_message = (
_("Cannot find generic volume group %(grp_id)s.") %
{'group_id': source_group.id})
raise exception.VolumeBackendAPIException(
data=exception_message)
# Check if the snapshot exists
if 'snapVXSnapshots' in volume_group:
if snap_name in volume_group['snapVXSnapshots']:
self.provision.delete_group_replica(array,
snap_name,
vol_grp_name)
else:
# Snapshot has been already deleted, return successfully
LOG.error("Cannot find group snapshot %(snapId)s.",
{'snapId': group_snapshot.id})
model_update = {'status': fields.GroupSnapshotStatus.DELETED}
for snapshot in snapshots:
snapshots_model_update.append(
{'id': snapshot.id,
'status': fields.SnapshotStatus.DELETED})
except Exception as e:
LOG.error("Error deleting volume group snapshot."
"Error received: %(e)s", {'e': e})
model_update = {
'status': fields.GroupSnapshotStatus.ERROR_DELETING}
return model_update, snapshots_model_update
def _find_volume_group_name_from_id(self, array, group_id):
"""Finds the volume group name given its id
:param array: the array serial number
:param group_id: the group id
:returns: group_name: Name of the group
"""
group_name = None
sg_list = self.rest.get_storage_group_list(array)
for sg in sg_list:
if group_id in sg:
group_name = sg
return group_name
return group_name
def _find_volume_group(self, array, group):
"""Finds a volume group given the group.
:param array: the array serial number
:param group: the group object
:returns: volume group dictionary
"""
group_name = self.utils.update_volume_group_name(group)
volume_group = self.rest.get_storage_group_rep(array, group_name)
if not volume_group:
LOG.warning("Volume group %(group_id)s cannot be found",
{'group_id': group_name})
return None
return volume_group
def _get_members_of_volume_group(self, array, group_name):
"""Get the members of a volume group.
:param array: the array serial number
:param group_name: the storage group name
:returns: list -- member_device_ids
"""
member_device_ids = self.rest.get_volumes_in_storage_group(
array, group_name)
if not member_device_ids:
LOG.info("No member volumes found in %(group_id)s",
{'group_id': group_name})
return member_device_ids
def update_group(self, group, add_volumes, remove_volumes):
"""Updates LUNs in generic volume group.
:param group: storage configuration service instance
:param add_volumes: the volumes uuids you want to add to the vol grp
:param remove_volumes: the volumes uuids you want to remove from
the CG
:returns: model_update
:raises: VolumeBackendAPIException, NotImplementedError
"""
LOG.info("Update generic volume Group: %(group)s. "
"This adds and/or removes volumes from "
"a generic volume group.",
{'group': group.id})
if not volume_utils.is_group_a_cg_snapshot_type(group):
raise NotImplementedError()
array, __ = self.utils.get_volume_group_utils(
group, self.interval, self.retries)
model_update = {'status': fields.GroupStatus.AVAILABLE}
add_vols = [vol for vol in add_volumes] if add_volumes else []
add_device_ids = self._get_volume_device_ids(add_vols, array)
remove_vols = [vol for vol in remove_volumes] if remove_volumes else []
remove_device_ids = self._get_volume_device_ids(remove_vols, array)
vol_grp_name = None
try:
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)
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
# Add volume(s) to the group
if add_device_ids:
self.masking.add_volumes_to_storage_group(
array, add_device_ids, vol_grp_name, interval_retries_dict)
# Remove volume(s) from the group
if remove_device_ids:
self.masking.remove_volumes_from_storage_group(
array, remove_device_ids,
vol_grp_name, interval_retries_dict)
except exception.GroupNotFound:
raise
except Exception as ex:
exception_message = (_("Failed to update volume group:"
" %(volGrpName)s. Exception: %(ex)s.")
% {'volGrpName': group.id,
'ex': ex})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
return model_update, None, None
def _get_volume_device_ids(self, volumes, array):
"""Get volume device ids from volume.
:param volumes: volume objects
:returns: device_ids
"""
device_ids = []
for volume in volumes:
specs = {utils.ARRAY: array}
device_id = self._find_device_on_array(volume, specs)
if device_id is None:
LOG.error("Volume %(name)s not found on the array.",
{'name': volume['name']})
else:
device_ids.append(device_id)
return device_ids
def create_group_from_src(self, context, group, volumes,
group_snapshot, snapshots, source_group,
source_vols):
"""Creates the volume group from source.
:param context: the context
:param group: the volume group object to be created
:param volumes: volumes in the consistency group
:param group_snapshot: the source volume group snapshot
:param snapshots: snapshots of the source volumes
:param source_group: the source volume group
:param source_vols: the source vols
:returns: model_update, volumes_model_update
model_update is a dictionary of cg status
volumes_model_update is a list of dictionaries of volume
update
:raises: VolumeBackendAPIException, NotImplementedError
"""
if not volume_utils.is_group_a_cg_snapshot_type(group):
raise NotImplementedError()
# Check if we need to create a snapshot
create_snapshot = False
volumes_model_update = []
if group_snapshot:
source_vols_or_snapshots = snapshots
source_id = group_snapshot.id
actual_source_grp = group_snapshot
elif source_group:
source_vols_or_snapshots = source_vols
source_id = source_group.id
actual_source_grp = source_group
create_snapshot = True
else:
exception_message = (_("Must supply either group snapshot or "
"a source group."))
raise exception.VolumeBackendAPIException(
data=exception_message)
LOG.debug("Enter VMAX create_volume group_from_src. Group to be "
"created: %(grpId)s, Source : %(SourceGrpId)s.",
{'grpId': group.id,
'SourceGrpId': source_id})
tgt_name = self.utils.update_volume_group_name(group)
self.create_group(context, group)
model_update = {'status': fields.GroupStatus.AVAILABLE}
snap_name = None
try:
array, extraspecs_dict_list = (
self.utils.get_volume_group_utils(
group, self.interval, self.retries))
vol_grp_name = ""
# Create the target devices
dict_volume_dicts = {}
target_volume_names = {}
for volume, source_vol_or_snapshot in zip(
volumes, source_vols_or_snapshots):
if 'size' in source_vol_or_snapshot:
volume_size = source_vol_or_snapshot['size']
else:
volume_size = source_vol_or_snapshot['volume_size']
for extraspecs_dict in extraspecs_dict_list:
if volume.volume_type_id in (
extraspecs_dict['volumeTypeId']):
extraspecs = extraspecs_dict.get(utils.EXTRA_SPECS)
# Create a random UUID and use it as volume name
target_volume_name = six.text_type(uuid.uuid4())
volume_dict = self.provision.create_volume_from_sg(
array, target_volume_name,
tgt_name, volume_size, extraspecs)
dict_volume_dicts[volume.id] = volume_dict
target_volume_names[volume.id] = target_volume_name
if create_snapshot is True:
# We have to create a snapshot of the source group
snap_name = self.utils.truncate_string(group.id, 19)
self._create_group_replica(actual_source_grp, snap_name)
vol_grp_name = self.utils.update_volume_group_name(
source_group)
else:
# We need to check if the snapshot exists
snap_name = self.utils.truncate_string(source_id, 19)
source_group = actual_source_grp.get('group')
volume_group = self._find_volume_group(array, source_group)
if volume_group is not None:
if 'snapVXSnapshots' in volume_group:
if snap_name in volume_group['snapVXSnapshots']:
LOG.info("Snapshot is present on the array")
if 'name' in volume_group:
vol_grp_name = volume_group['name']
# Link and break the snapshot to the source group
interval_retries_dict = self.utils.get_intervals_retries_dict(
self.interval, self.retries)
self.provision.link_and_break_replica(
array, vol_grp_name, tgt_name, snap_name,
interval_retries_dict, delete_snapshot=create_snapshot)
except Exception:
exception_message = (_("Failed to create vol grp %(volGrpName)s"
" from source %(grpSnapshot)s.")
% {'volGrpName': group.id,
'grpSnapshot': source_id})
LOG.exception(exception_message)
raise exception.VolumeBackendAPIException(data=exception_message)
volumes_model_update = self.utils.update_volume_model_updates(
volumes_model_update, volumes, group.id, model_update['status'])
# Update the provider_location
for volume_model_update in volumes_model_update:
if volume_model_update['id'] in dict_volume_dicts:
volume_model_update.update(
{'provider_location': six.text_type(
dict_volume_dicts[volume_model_update['id']])})
# Update the volumes_model_update with admin_metadata
self.utils.update_admin_metadata(volumes_model_update,
key='targetVolumeName',
values=target_volume_names)
return model_update, volumes_model_update

View File

@ -81,6 +81,7 @@ class VMAXFCDriver(driver.FibreChannelDriver):
- Support for compression on All Flash
- Support for volume replication
- Support for live migration
- Support for Generic Volume Group
"""
VERSION = "3.0.0"
@ -456,3 +457,70 @@ class VMAXFCDriver(driver.FibreChannelDriver):
:returns: secondary_id, volume_update_list, group_update_list
"""
return self.common.failover_host(volumes, secondary_id, groups)
def create_group(self, context, group):
"""Creates a generic volume group.
:param context: the context
:param group: the group object
"""
self.common.create_group(context, group)
def delete_group(self, context, group, volumes):
"""Deletes a generic volume group.
:param context: the context
:param group: the group object
:param volumes: the member volumes
"""
return self.common.delete_group(
context, group, volumes)
def create_group_snapshot(self, context, group_snapshot, snapshots):
"""Creates a group snapshot.
:param context: the context
:param group_snapshot: the grouop snapshot
:param snapshots: snapshots list
"""
return self.common.create_group_snapshot(context,
group_snapshot, snapshots)
def delete_group_snapshot(self, context, group_snapshot, snapshots):
"""Deletes a group snapshot.
:param context: the context
:param group_snapshot: the grouop snapshot
:param snapshots: snapshots list
"""
return self.common.delete_group_snapshot(context,
group_snapshot, snapshots)
def update_group(self, context, group,
add_volumes=None, remove_volumes=None):
"""Updates LUNs in generic volume group.
:param context: the context
:param group: the group object
:param add_volumes: flag for adding volumes
:param remove_volumes: flag for removing volumes
"""
return self.common.update_group(group, add_volumes,
remove_volumes)
def create_group_from_src(
self, context, group, volumes, group_snapshot=None,
snapshots=None, source_group=None, source_vols=None):
"""Creates the volume group from source.
:param context: the context
:param group: the group object to be created
:param volumes: volumes in the group
:param group_snapshot: the source volume group snapshot
:param snapshots: snapshots of the source volumes
:param source_group: the dictionary of a volume group as source.
:param source_vols: a list of volume dictionaries in the source_group.
"""
return self.common.create_group_from_src(
context, group, volumes, group_snapshot, snapshots, source_group,
source_vols)

View File

@ -86,6 +86,7 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
- Support for compression on All Flash
- Support for volume replication
- Support for live migration
- Support for Generic Volume Group
"""
VERSION = "3.0.0"
@ -400,3 +401,70 @@ class VMAXISCSIDriver(driver.ISCSIDriver):
:returns: secondary_id, volume_update_list, group_update_list
"""
return self.common.failover_host(volumes, secondary_id, groups)
def create_group(self, context, group):
"""Creates a generic volume group.
:param context: the context
:param group: the group object
"""
self.common.create_group(context, group)
def delete_group(self, context, group, volumes):
"""Deletes a generic volume group.
:param context: the context
:param group: the group object
:param volumes: the member volumes
"""
return self.common.delete_group(
context, group, volumes)
def create_group_snapshot(self, context, group_snapshot, snapshots):
"""Creates a group snapshot.
:param context: the context
:param group_snapshot: the group snapshot
:param snapshots: snapshots list
"""
return self.common.create_group_snapshot(context,
group_snapshot, snapshots)
def delete_group_snapshot(self, context, group_snapshot, snapshots):
"""Deletes a group snapshot.
:param context: the context
:param group_snapshot: the grouop snapshot
:param snapshots: snapshots list
"""
return self.common.delete_group_snapshot(context,
group_snapshot, snapshots)
def update_group(self, context, group,
add_volumes=None, remove_volumes=None):
"""Updates LUNs in group.
:param context: the context
:param group: the group object
:param add_volumes: flag for adding volumes
:param remove_volumes: flag for removing volumes
"""
return self.common.update_group(group, add_volumes,
remove_volumes)
def create_group_from_src(
self, context, group, volumes, group_snapshot=None,
snapshots=None, source_group=None, source_vols=None):
"""Creates the volume group from source.
:param context: the context
:param group: the consistency group object to be created
:param volumes: volumes in the group
:param group_snapshot: the source volume group snapshot
:param snapshots: snapshots of the source volumes
:param source_group: the dictionary of a volume group as source.
:param source_vols: a list of volume dictionaries in the source_group.
"""
return self.common.create_group_from_src(
context, group, volumes, group_snapshot, snapshots, source_group,
source_vols)

View File

@ -612,6 +612,46 @@ class VMAXMasking(object):
LOG.info("Added volume: %(vol_name)s to storage group %(sg_name)s.",
{'vol_name': volume_name, 'sg_name': storagegroup_name})
def add_volumes_to_storage_group(
self, serial_number, list_device_id, storagegroup_name,
extra_specs):
"""Add a volume to a storage group.
:param serial_number: array serial number
:param list_device_id: list of volume device id
:param storagegroup_name: storage group name
:param extra_specs: extra specifications
"""
if not list_device_id:
LOG.info("add_volumes_to_storage_group: No volumes to add")
return
start_time = time.time()
temp_device_id_list = list_device_id
@coordination.synchronized("emc-sg-{sg_name}")
def do_add_volume_to_sg(sg_name):
# Check if another process has added any volume to the
# sg while this process was waiting for the lock
volume_list = self.rest.get_volumes_in_storage_group(
serial_number, storagegroup_name)
for volume in volume_list:
if volume in temp_device_id_list:
LOG.info("Volume: %(volume_name)s is already part "
"of storage group %(sg_name)s.",
{'volume_name': volume,
'sg_name': storagegroup_name})
# Remove this device id from the list
temp_device_id_list.remove(volume)
self.rest.add_vol_to_sg(serial_number, storagegroup_name,
temp_device_id_list, extra_specs)
do_add_volume_to_sg(storagegroup_name)
LOG.debug("Add volumes to storagegroup took: %(delta)s H:MM:SS.",
{'delta': self.utils.get_time_delta(start_time,
time.time())})
LOG.info("Added volumes to storage group %(sg_name)s.",
{'sg_name': storagegroup_name})
def remove_vol_from_storage_group(
self, serial_number, device_id, storagegroup_name,
volume_name, extra_specs):
@ -643,6 +683,43 @@ class VMAXMasking(object):
raise exception.VolumeBackendAPIException(
data=exception_message)
def remove_volumes_from_storage_group(
self, serial_number, list_of_device_ids,
storagegroup_name, extra_specs):
"""Remove multiple volumes from a storage group.
:param serial_number: the array serial number
:param list_of_device_ids: list of device ids
:param storagegroup_name: the name of the storage group
:param extra_specs: the extra specifications
:raises: VolumeBackendAPIException
"""
start_time = time.time()
@coordination.synchronized("emc-sg-{sg_name}")
def do_remove_volumes_from_storage_group(sg_name):
self.rest.remove_vol_from_sg(
serial_number, storagegroup_name,
list_of_device_ids, extra_specs)
LOG.debug("Remove volumes from storagegroup "
"took: %(delta)s H:MM:SS.",
{'delta': self.utils.get_time_delta(start_time,
time.time())})
volume_list = self.rest.get_volumes_in_storage_group(
serial_number, storagegroup_name)
for device_id in list_of_device_ids:
if device_id in volume_list:
exception_message = (_(
"Failed to remove device "
"with id %(dev_id)s from SG: %(sg_name)s.")
% {'dev_id': device_id, 'sg_name': storagegroup_name})
LOG.error(exception_message)
raise exception.VolumeBackendAPIException(
data=exception_message)
return do_remove_volumes_from_storage_group(storagegroup_name)
def find_initiator_names(self, connector):
"""Check the connector object for initiators(ISCSI) or wwpns(FC).

View File

@ -16,6 +16,7 @@
import time
from oslo_log import log as logging
from oslo_service import loopingcall
from cinder import coordination
from cinder import exception
@ -25,6 +26,8 @@ from cinder.volume.drivers.dell_emc.vmax import utils
LOG = logging.getLogger(__name__)
WRITE_DISABLED = "Write Disabled"
UNLINK_INTERVAL = 15
UNLINK_RETRIES = 30
class VMAXProvision(object):
@ -448,3 +451,125 @@ class VMAXProvision(object):
{'action': action, 'src': device_id})
self.rest.modify_rdf_device_pair(
array, device_id, rdf_group, extra_specs, split=False)
def create_volume_group(self, array, group_name, extra_specs):
"""Create a generic volume group.
:param array: the array serial number
:param group_name: the name of the group
:param extra_specs: the extra specifications
:returns: volume_group
"""
return self.create_storage_group(array, group_name,
None, None, None, extra_specs)
def create_group_replica(
self, array, source_group, snap_name, extra_specs):
"""Create a replica (snapVx) of a volume group.
:param array: the array serial number
:param source_group: the source group name
:param snap_name: the name for the snap shot
:param extra_specs: extra specifications
"""
LOG.debug("Creating Snap Vx snapshot of storage group: %(srcGroup)s.",
{'srcGroup': source_group})
# Create snapshot
self.rest.create_storagegroup_snap(
array, source_group, snap_name, extra_specs)
def delete_group_replica(self, array, snap_name,
source_group_name):
"""Delete the snapshot.
:param array: the array serial number
:param snap_name: the name for the snap shot
:param source_group_name: the source group name
"""
# Delete snapvx snapshot
LOG.debug("Deleting Snap Vx snapshot: source group: %(srcGroup)s "
"snapshot: %(snap_name)s.",
{'srcGroup': source_group_name,
'snap_name': snap_name})
# The check for existence of snapshot has already happened
# So we just need to delete the snapshot
self.rest.delete_storagegroup_snap(array, snap_name, source_group_name)
def link_and_break_replica(self, array, source_group_name,
target_group_name, snap_name, extra_specs,
delete_snapshot=False):
"""Links a group snap and breaks the relationship.
:param array: the array serial
:param source_group_name: the source group name
:param target_group_name: the target group name
:param snap_name: the snapshot name
:param extra_specs: extra specifications
:param delete_snapshot: delete snapshot flag
"""
LOG.debug("Linking Snap Vx snapshot: source group: %(srcGroup)s "
"targetGroup: %(tgtGroup)s.",
{'srcGroup': source_group_name,
'tgtGroup': target_group_name})
# Link the snapshot
self.rest.modify_storagegroup_snap(
array, source_group_name, target_group_name, snap_name,
extra_specs, link=True)
# Unlink the snapshot
LOG.debug("Unlinking Snap Vx snapshot: source group: %(srcGroup)s "
"targetGroup: %(tgtGroup)s.",
{'srcGroup': source_group_name,
'tgtGroup': target_group_name})
self._unlink_group(array, source_group_name,
target_group_name, snap_name, extra_specs)
# Delete the snapshot if necessary
if delete_snapshot:
LOG.debug("Deleting Snap Vx snapshot: source group: %(srcGroup)s "
"snapshot: %(snap_name)s.",
{'srcGroup': source_group_name,
'snap_name': snap_name})
self.rest.delete_storagegroup_snap(array, snap_name,
source_group_name)
def _unlink_group(
self, array, source_group_name, target_group_name, snap_name,
extra_specs):
"""Unlink a target group from it's source group.
:param array: the array serial number
:param source_group_name: the source group name
:param target_group_name: the target device name
:param snap_name: the snap name
:param extra_specs: extra specifications
:returns: return code
"""
def _unlink_grp():
"""Called at an interval until the synchronization is finished.
:raises: loopingcall.LoopingCallDone
"""
retries = kwargs['retries']
try:
kwargs['retries'] = retries + 1
if not kwargs['modify_grp_snap_success']:
self.rest.modify_storagegroup_snap(
array, source_group_name, target_group_name,
snap_name, extra_specs, unlink=True)
kwargs['modify_grp_snap_success'] = True
except exception.VolumeBackendAPIException:
pass
if kwargs['retries'] > UNLINK_RETRIES:
LOG.error("_unlink_grp failed after %(retries)d "
"tries.", {'retries': retries})
raise loopingcall.LoopingCallDone(retvalue=30)
if kwargs['modify_grp_snap_success']:
raise loopingcall.LoopingCallDone()
kwargs = {'retries': 0,
'modify_grp_snap_success': False}
timer = loopingcall.FixedIntervalLoopingCall(_unlink_grp)
rc = timer.start(interval=UNLINK_INTERVAL).wait()
return rc

View File

@ -1626,7 +1626,7 @@ class VMAXRest(object):
kwargs = {'retries': 0,
'wait_for_sync_called': False}
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_sync)
rc = timer.start(interval=extra_specs[utils.INTERVAL]).wait()
rc = timer.start(interval=int(extra_specs[utils.INTERVAL])).wait()
return rc
def _is_sync_complete(self, array, source_device_id, snap_name,
@ -1890,3 +1890,91 @@ class VMAXRest(object):
% {'rdf_num': rdf_group, 'device_id': device_id})
self.delete_resource(array, REPLICATION, 'rdf_group', resource_name,
private="/private", params=params)
def get_storage_group_rep(self, array, storage_group_name):
"""Given a name, return storage group details wrt replication.
:param array: the array serial number
:param storage_group_name: the name of the storage group
:returns: storage group dict or None
"""
return self.get_resource(
array, REPLICATION, 'storagegroup',
resource_name=storage_group_name)
def get_volumes_in_storage_group(self, array, storagegroup_name):
"""Given a volume identifier, find the corresponding device_id.
:param array: the array serial number
:param storagegroup_name: the storage group name
:returns: volume_list
"""
volume_list = None
params = {"storageGroupId": storagegroup_name}
volume_list = self.get_volume_list(array, params)
if not volume_list:
LOG.debug("Cannot find record for storage group %(storageGrpId)s",
{'storageGrpId': storagegroup_name})
return volume_list
def create_storagegroup_snap(self, array, source_group,
snap_name, extra_specs):
"""Create a snapVx snapshot of a storage group.
:param array: the array serial number
:param source_group: the source group name
:param snap_name: the name of the snapshot
:param extra_specs: the extra specifications
"""
payload = {"snapshotName": snap_name}
resource_type = ('storagegroup/%(sg_name)s/snapshot'
% {'sg_name': source_group})
status_code, job = self.create_resource(
array, REPLICATION, resource_type, payload)
self.wait_for_job('Create storage group snapVx', status_code,
job, extra_specs)
def modify_storagegroup_snap(
self, array, source_sg_id, target_sg_id, snap_name,
extra_specs, link=False, unlink=False):
"""Link or unlink a snapVx to or from a target storagegroup.
:param array: the array serial number
:param source_sg_id: the source device id
:param target_sg_id: the target device id
:param snap_name: the snapshot name
:param extra_specs: extra specifications
:param link: Flag to indicate action = Link
:param unlink: Flag to indicate action = Unlink
"""
payload = ''
if link:
payload = {"link": {"linkStorageGroupName": target_sg_id,
"copy": "true"},
"action": "Link"}
elif unlink:
payload = {"unlink": {"unlinkStorageGroupName": target_sg_id},
"action": "Unlink"}
resource_name = ('%(sg_name)s/snapshot/%(snap_id)s/generation/0'
% {'sg_name': source_sg_id, 'snap_id': snap_name})
status_code, job = self.modify_resource(
array, REPLICATION, 'storagegroup', payload,
resource_name=resource_name)
self.wait_for_job('Modify storagegroup snapVx relationship to target',
status_code, job, extra_specs)
def delete_storagegroup_snap(self, array, snap_name, source_sg_id):
"""Delete the snapshot of a storagegroup.
:param array: the array serial number
:param snap_name: the name of the snapshot
:param source_sg_id: the source device id
"""
resource_name = ('%(sg_name)s/snapshot/%(snap_id)s/generation/0'
% {'sg_name': source_sg_id, 'snap_id': snap_name})
return self.delete_resource(
array, REPLICATION, 'storagegroup', resource_name)

View File

@ -19,6 +19,7 @@ import random
import re
from xml.dom import minidom
from cinder.objects.group import Group
from oslo_log import log as logging
from oslo_utils import strutils
import six
@ -501,3 +502,162 @@ class VMAXUtils(object):
fields.ReplicationStatus.FAILED_OVER):
return True
return False
@staticmethod
def update_volume_model_updates(volume_model_updates,
volumes, group_id, status='available'):
"""Update the volume model's status and return it.
:param volume_model_updates: list of volume model update dicts
:param volumes: volumes object api
:param group_id: consistency group id
:param status: string value reflects the status of the member volume
:returns: volume_model_updates - updated volumes
"""
LOG.info(
"Updating status for group: %(id)s.",
{'id': group_id})
if volumes:
for volume in volumes:
volume_model_updates.append({'id': volume.id,
'status': status})
else:
LOG.info("No volume found for group: %(cg)s.",
{'cg': group_id})
return volume_model_updates
@staticmethod
def update_extra_specs(extraspecs):
"""Update extra specs.
:param extraspecs: the additional info
:returns: extraspecs
"""
try:
pool_details = extraspecs['pool_name'].split('+')
extraspecs[SLO] = pool_details[0]
extraspecs[WORKLOAD] = pool_details[1]
extraspecs[SRP] = pool_details[2]
extraspecs[ARRAY] = pool_details[3]
except KeyError:
LOG.error("Error parsing SLO, workload from"
" the provided extra_specs.")
return extraspecs
@staticmethod
def get_intervals_retries_dict(interval, retries):
"""Get the default intervals and retries.
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: default_dict
"""
default_dict = {}
default_dict[INTERVAL] = interval
default_dict[RETRIES] = retries
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):
"""Standard utility for generic volume groups.
:param group: the generic volume group object to be created
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: array, extra specs dict list
:raises: VolumeBackendAPIException
"""
arrays = set()
extraspecs_dict_list = []
# Check if it is a generic volume group instance
if isinstance(group, Group):
for volume_type in group.volume_types:
extraspecs_dict = (
self._update_extra_specs_list(
volume_type.extra_specs,
volume_type.id, interval, retries))
extraspecs_dict_list.append(extraspecs_dict)
arrays.add(extraspecs_dict[EXTRA_SPECS][ARRAY])
else:
msg = (_("Unable to get volume type ids."))
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
if len(arrays) != 1:
if not arrays:
msg = (_("Failed to get an array associated with "
"volume group: %(groupid)s.")
% {'groupid': group.id})
else:
msg = (_("There are multiple arrays "
"associated with volume group: %(groupid)s.")
% {'groupid': group.id})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
array = arrays.pop()
return array, extraspecs_dict_list
def _update_extra_specs_list(self, extraspecs, volumetype_id,
interval, retries):
"""Update the extra specs list.
:param extraspecs: extraspecs
:param volumetype_Id: volume type identifier
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: extraspecs_dict_list
"""
extraspecs_dict = {}
extraspecs = self.update_extra_specs(extraspecs)
extraspecs = self._update_intervals_and_retries(
extraspecs, interval, retries)
extraspecs_dict["volumeTypeId"] = volumetype_id
extraspecs_dict[EXTRA_SPECS] = extraspecs
return extraspecs_dict
def update_volume_group_name(self, group):
"""Format id and name consistency group.
:param group: the generic volume group object
:returns: group_name -- formatted name + id
"""
group_name = ""
if group.name is not None:
group_name = (
self.truncate_string(
group.name, TRUNCATE_27) + "_")
group_name += group.id
return group_name
@staticmethod
def _update_intervals_and_retries(extra_specs, interval, retries):
"""Updates the extraSpecs with intervals and retries values.
:param extra_specs:
:param interval: Interval in seconds between retries
:param retries: Retry count
:returns: Updated extra_specs
"""
extra_specs[INTERVAL] = interval
LOG.debug("The interval is set at: %(intervalInSecs)s.",
{'intervalInSecs': interval})
extra_specs[RETRIES] = retries
LOG.debug("Retries are set at: %(retries)s.",
{'retries': retries})
return extra_specs

View File

@ -0,0 +1,5 @@
---
features:
- |
Add consistent group snapshot support to generic volume groups in
VMAX driver version 3.0.