Unity: Add consistent group support

Users could create a group type supporting consistent groups with
specification `'consistent_group_snapshot_enabled': <is> True`, then any
groups created of that group type are consistent groups, otherwise they
are generic groups.

The supported operations are:

- Create/delete consistent groups
- Add volumes to and remove volumes from consistent groups
- Create/delete consistent group snapshots
- Create consistent groups from snapshots
- Clone consistent groups

This change also does some refactor and puts extra capabilities report
together in `utils.py`, including the existing
`thin_provisioning_support`, `thick_provisioning_support` and the newly
one added for cg named `consistent_group_snapshot_enabled`.

Implements: blueprint unity-consistent-group-support

Change-Id: I0ef2ec959f892acb79d8d08a31d9a8ad47c4350f
This commit is contained in:
Ryan Liang 2018-05-30 16:19:12 +08:00
parent fbd7c0377d
commit 661a4f1212
11 changed files with 883 additions and 28 deletions

View File

@ -92,3 +92,7 @@ class UnityThinCloneNotAllowedError(StoropsException):
class SystemAPINotSupported(StoropsException): class SystemAPINotSupported(StoropsException):
pass pass
class UnityConsistencyGroupNameInUseError(StoropsException):
pass

View File

@ -16,6 +16,7 @@
import contextlib import contextlib
import functools import functools
import ddt
import mock import mock
from oslo_utils import units from oslo_utils import units
@ -375,7 +376,10 @@ class IdMatcher(object):
# #
######################## ########################
@ddt.ddt
@mock.patch.object(adapter, 'storops_ex', new=ex) @mock.patch.object(adapter, 'storops_ex', new=ex)
@mock.patch.object(adapter.vol_utils, 'is_group_a_cg_snapshot_type',
new=lambda x: True)
class CommonAdapterTest(test.TestCase): class CommonAdapterTest(test.TestCase):
def setUp(self): def setUp(self):
super(CommonAdapterTest, self).setUp() super(CommonAdapterTest, self).setUp()
@ -389,7 +393,8 @@ class CommonAdapterTest(test.TestCase):
@patch_for_unity_adapter @patch_for_unity_adapter
def test_create_volume(self): def test_create_volume(self):
volume = MockOSResource(name='lun_3', size=5, host='unity#pool1') volume = MockOSResource(name='lun_3', size=5, host='unity#pool1',
group=None)
ret = self.adapter.create_volume(volume) ret = self.adapter.create_volume(volume)
expected = get_lun_pl('lun_3') expected = get_lun_pl('lun_3')
self.assertEqual(expected, ret['provider_location']) self.assertEqual(expected, ret['provider_location'])
@ -397,7 +402,7 @@ class CommonAdapterTest(test.TestCase):
@patch_for_unity_adapter @patch_for_unity_adapter
def test_create_volume_thick(self): def test_create_volume_thick(self):
volume = MockOSResource(name='lun_3', size=5, host='unity#pool1', volume = MockOSResource(name='lun_3', size=5, host='unity#pool1',
volume_type_id='thick') group=None, volume_type_id='thick')
ret = self.adapter.create_volume(volume) ret = self.adapter.create_volume(volume)
expected = get_lun_pl('lun_3_thick') expected = get_lun_pl('lun_3_thick')
@ -408,7 +413,7 @@ class CommonAdapterTest(test.TestCase):
volume_type = MockOSResource( volume_type = MockOSResource(
extra_specs={'compression_support': '<is> True'}) extra_specs={'compression_support': '<is> True'})
volume = MockOSResource(name='lun_3', size=5, host='unity#pool1', volume = MockOSResource(name='lun_3', size=5, host='unity#pool1',
volume_type=volume_type) group=None, volume_type=volume_type)
ret = self.adapter.create_volume(volume) ret = self.adapter.create_volume(volume)
expected = get_lun_pl('lun_3') expected = get_lun_pl('lun_3')
self.assertEqual(expected, ret['provider_location']) self.assertEqual(expected, ret['provider_location'])
@ -454,6 +459,7 @@ class CommonAdapterTest(test.TestCase):
self.assertTrue(stats['thick_provisioning_support']) self.assertTrue(stats['thick_provisioning_support'])
self.assertTrue(stats['thin_provisioning_support']) self.assertTrue(stats['thin_provisioning_support'])
self.assertTrue(stats['compression_support']) self.assertTrue(stats['compression_support'])
self.assertTrue(stats['consistent_group_snapshot_enabled'])
def test_update_volume_stats(self): def test_update_volume_stats(self):
stats = self.adapter.update_volume_stats() stats = self.adapter.update_volume_stats()
@ -461,6 +467,7 @@ class CommonAdapterTest(test.TestCase):
self.assertEqual('unknown', stats['storage_protocol']) self.assertEqual('unknown', stats['storage_protocol'])
self.assertTrue(stats['thin_provisioning_support']) self.assertTrue(stats['thin_provisioning_support'])
self.assertTrue(stats['thick_provisioning_support']) self.assertTrue(stats['thick_provisioning_support'])
self.assertTrue(stats['consistent_group_snapshot_enabled'])
self.assertEqual(1, len(stats['pools'])) self.assertEqual(1, len(stats['pools']))
def test_serial_number(self): def test_serial_number(self):
@ -678,6 +685,7 @@ class CommonAdapterTest(test.TestCase):
volume = MockOSResource(name=lun_id, id=lun_id, host='unity#pool1', volume = MockOSResource(name=lun_id, id=lun_id, host='unity#pool1',
provider_location=get_lun_pl(lun_id)) provider_location=get_lun_pl(lun_id))
src_snap = test_client.MockResource(name=src_snap_id, _id=src_snap_id) src_snap = test_client.MockResource(name=src_snap_id, _id=src_snap_id)
src_snap.size = 5 * units.Gi
src_snap.storage_resource = test_client.MockResource(name=src_lun_id, src_snap.storage_resource = test_client.MockResource(name=src_lun_id,
_id=src_lun_id) _id=src_lun_id)
with patch_copy_volume() as copy_volume: with patch_copy_volume() as copy_volume:
@ -841,6 +849,289 @@ class CommonAdapterTest(test.TestCase):
ret = self.adapter.migrate_volume(volume, host) ret = self.adapter.migrate_volume(volume, host)
self.assertEqual((False, None), ret) self.assertEqual((False, None), ret)
@ddt.unpack
@ddt.data((('group-1', 'group-1_name', 'group-1_description'),
('group-1', 'group-1_description')),
(('group-2', 'group-2_name', None), ('group-2', 'group-2_name')),
(('group-3', 'group-3_name', ''), ('group-3', 'group-3_name')))
def test_create_group(self, inputs, expected):
cg_id, cg_name, cg_description = inputs
cg = MockOSResource(id=cg_id, name=cg_name, description=cg_description)
with mock.patch.object(self.adapter.client, 'create_cg',
create=True) as mocked:
model_update = self.adapter.create_group(cg)
self.assertEqual('available', model_update['status'])
mocked.assert_called_once_with(expected[0],
description=expected[1])
def test_delete_group(self):
cg = MockOSResource(id='group-1')
with mock.patch.object(self.adapter.client, 'delete_cg',
create=True) as mocked:
ret = self.adapter.delete_group(cg)
self.assertIsNone(ret[0])
self.assertIsNone(ret[1])
mocked.assert_called_once_with('group-1')
def test_update_group(self):
cg = MockOSResource(id='group-1')
add_volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-1', 'sv_1'),
('volume-2', 'sv_2'))]
remove_volumes = [MockOSResource(
id='volume-3', provider_location=get_lun_pl('sv_3'))]
with mock.patch.object(self.adapter.client, 'update_cg',
create=True) as mocked:
ret = self.adapter.update_group(cg, add_volumes, remove_volumes)
self.assertEqual('available', ret[0]['status'])
self.assertIsNone(ret[1])
self.assertIsNone(ret[2])
mocked.assert_called_once_with('group-1', {'sv_1', 'sv_2'},
{'sv_3'})
def test_update_group_add_volumes_none(self):
cg = MockOSResource(id='group-1')
remove_volumes = [MockOSResource(
id='volume-3', provider_location=get_lun_pl('sv_3'))]
with mock.patch.object(self.adapter.client, 'update_cg',
create=True) as mocked:
ret = self.adapter.update_group(cg, None, remove_volumes)
self.assertEqual('available', ret[0]['status'])
self.assertIsNone(ret[1])
self.assertIsNone(ret[2])
mocked.assert_called_once_with('group-1', set(), {'sv_3'})
def test_update_group_remove_volumes_none(self):
cg = MockOSResource(id='group-1')
add_volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-1', 'sv_1'),
('volume-2', 'sv_2'))]
with mock.patch.object(self.adapter.client, 'update_cg',
create=True) as mocked:
ret = self.adapter.update_group(cg, add_volumes, None)
self.assertEqual('available', ret[0]['status'])
self.assertIsNone(ret[1])
self.assertIsNone(ret[2])
mocked.assert_called_once_with('group-1', {'sv_1', 'sv_2'}, set())
def test_update_group_add_remove_volumes_none(self):
cg = MockOSResource(id='group-1')
with mock.patch.object(self.adapter.client, 'update_cg',
create=True) as mocked:
ret = self.adapter.update_group(cg, None, None)
self.assertEqual('available', ret[0]['status'])
self.assertIsNone(ret[1])
self.assertIsNone(ret[2])
mocked.assert_called_once_with('group-1', set(), set())
@patch_for_unity_adapter
def test_copy_luns_in_group(self):
cg = MockOSResource(id='group-1')
volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-3', 'sv_3'),
('volume-4', 'sv_4'))]
src_cg_snap = test_client.MockResource(_id='id_src_cg_snap')
src_volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-1', 'sv_1'),
('volume-2', 'sv_2'))]
copied_luns = [test_client.MockResource(_id=lun_id)
for lun_id in ('sv_3', 'sv_4')]
def _prepare_lun_snaps(lun_id):
lun_snap = test_client.MockResource(_id='snap_{}'.format(lun_id))
lun_snap.lun = test_client.MockResource(_id=lun_id)
return lun_snap
lun_snaps = list(map(_prepare_lun_snaps, ('sv_1', 'sv_2')))
with mock.patch.object(self.adapter.client, 'filter_snaps_in_cg_snap',
create=True) as mocked_filter, \
mock.patch.object(self.adapter.client, 'create_cg',
create=True) as mocked_create_cg, \
patch_dd_copy(None) as mocked_dd:
mocked_filter.return_value = lun_snaps
mocked_dd.side_effect = copied_luns
ret = self.adapter.copy_luns_in_group(cg, volumes, src_cg_snap,
src_volumes)
mocked_filter.assert_called_once_with('id_src_cg_snap')
dd_args = zip([adapter.VolumeParams(self.adapter, vol)
for vol in volumes],
lun_snaps)
mocked_dd.assert_has_calls([mock.call(*args) for args in dd_args])
mocked_create_cg.assert_called_once_with('group-1',
lun_add=copied_luns)
self.assertEqual('available', ret[0]['status'])
self.assertEqual(2, len(ret[1]))
for vol_id in ('volume-3', 'volume-4'):
self.assertIn({'id': vol_id, 'status': 'available'}, ret[1])
def test_create_group_from_snap(self):
cg = MockOSResource(id='group-2')
volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-3', 'sv_3'),
('volume-4', 'sv_4'))]
cg_snap = MockOSResource(id='snap-group-1')
vol_1 = MockOSResource(id='volume-1')
vol_2 = MockOSResource(id='volume-2')
vol_snaps = [MockOSResource(id='snap-volume-1', volume=vol_1),
MockOSResource(id='snap-volume-2', volume=vol_2)]
src_cg_snap = test_client.MockResource(_id='id_src_cg_snap')
with mock.patch.object(self.adapter.client, 'get_snap',
create=True, return_value=src_cg_snap), \
mock.patch.object(self.adapter, 'copy_luns_in_group',
create=True) as mocked_copy:
mocked_copy.return_value = ({'status': 'available'},
[{'id': 'volume-3',
'status': 'available'},
{'id': 'volume-4',
'status': 'available'}])
ret = self.adapter.create_group_from_snap(cg, volumes, cg_snap,
vol_snaps)
mocked_copy.assert_called_once_with(cg, volumes, src_cg_snap,
[vol_1, vol_2])
self.assertEqual('available', ret[0]['status'])
self.assertEqual(2, len(ret[1]))
for vol_id in ('volume-3', 'volume-4'):
self.assertIn({'id': vol_id, 'status': 'available'}, ret[1])
def test_create_group_from_snap_none_snapshots(self):
cg = MockOSResource(id='group-2')
volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-3', 'sv_3'),
('volume-4', 'sv_4'))]
cg_snap = MockOSResource(id='snap-group-1')
src_cg_snap = test_client.MockResource(_id='id_src_cg_snap')
with mock.patch.object(self.adapter.client, 'get_snap',
create=True, return_value=src_cg_snap), \
mock.patch.object(self.adapter, 'copy_luns_in_group',
create=True) as mocked_copy:
mocked_copy.return_value = ({'status': 'available'},
[{'id': 'volume-3',
'status': 'available'},
{'id': 'volume-4',
'status': 'available'}])
ret = self.adapter.create_group_from_snap(cg, volumes, cg_snap,
None)
mocked_copy.assert_called_once_with(cg, volumes, src_cg_snap, [])
self.assertEqual('available', ret[0]['status'])
self.assertEqual(2, len(ret[1]))
for vol_id in ('volume-3', 'volume-4'):
self.assertIn({'id': vol_id, 'status': 'available'}, ret[1])
def test_create_cloned_group(self):
cg = MockOSResource(id='group-2')
volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-3', 'sv_3'),
('volume-4', 'sv_4'))]
src_cg = MockOSResource(id='group-1')
vol_1 = MockOSResource(id='volume-1')
vol_2 = MockOSResource(id='volume-2')
src_vols = [vol_1, vol_2]
src_cg_snap = test_client.MockResource(_id='id_src_cg_snap')
with mock.patch.object(self.adapter.client, 'create_cg_snap',
create=True,
return_value=src_cg_snap) as mocked_create, \
mock.patch.object(self.adapter, 'copy_luns_in_group',
create=True) as mocked_copy:
mocked_create.__name__ = 'create_cg_snap'
mocked_copy.return_value = ({'status': 'available'},
[{'id': 'volume-3',
'status': 'available'},
{'id': 'volume-4',
'status': 'available'}])
ret = self.adapter.create_cloned_group(cg, volumes, src_cg,
src_vols)
mocked_create.assert_called_once_with('group-1',
'snap_clone_group_group-1')
mocked_copy.assert_called_once_with(cg, volumes, src_cg_snap,
[vol_1, vol_2])
self.assertEqual('available', ret[0]['status'])
self.assertEqual(2, len(ret[1]))
for vol_id in ('volume-3', 'volume-4'):
self.assertIn({'id': vol_id, 'status': 'available'}, ret[1])
def test_create_cloned_group_none_source_vols(self):
cg = MockOSResource(id='group-2')
volumes = [MockOSResource(id=vol_id,
provider_location=get_lun_pl(lun_id))
for vol_id, lun_id in (('volume-3', 'sv_3'),
('volume-4', 'sv_4'))]
src_cg = MockOSResource(id='group-1')
src_cg_snap = test_client.MockResource(_id='id_src_cg_snap')
with mock.patch.object(self.adapter.client, 'create_cg_snap',
create=True,
return_value=src_cg_snap) as mocked_create, \
mock.patch.object(self.adapter, 'copy_luns_in_group',
create=True) as mocked_copy:
mocked_create.__name__ = 'create_cg_snap'
mocked_copy.return_value = ({'status': 'available'},
[{'id': 'volume-3',
'status': 'available'},
{'id': 'volume-4',
'status': 'available'}])
ret = self.adapter.create_cloned_group(cg, volumes, src_cg,
None)
mocked_create.assert_called_once_with('group-1',
'snap_clone_group_group-1')
mocked_copy.assert_called_once_with(cg, volumes, src_cg_snap, [])
self.assertEqual('available', ret[0]['status'])
self.assertEqual(2, len(ret[1]))
for vol_id in ('volume-3', 'volume-4'):
self.assertIn({'id': vol_id, 'status': 'available'}, ret[1])
def test_create_group_snapshot(self):
cg_snap = MockOSResource(id='snap-group-1', group_id='group-1')
vol_1 = MockOSResource(id='volume-1')
vol_2 = MockOSResource(id='volume-2')
vol_snaps = [MockOSResource(id='snap-volume-1', volume=vol_1),
MockOSResource(id='snap-volume-2', volume=vol_2)]
with mock.patch.object(self.adapter.client, 'create_cg_snap',
create=True) as mocked_create:
mocked_create.return_value = ({'status': 'available'},
[{'id': 'snap-volume-1',
'status': 'available'},
{'id': 'snap-volume-2',
'status': 'available'}])
ret = self.adapter.create_group_snapshot(cg_snap, vol_snaps)
mocked_create.assert_called_once_with('group-1',
snap_name='snap-group-1')
self.assertEqual({'status': 'available'}, ret[0])
self.assertEqual(2, len(ret[1]))
for snap_id in ('snap-volume-1', 'snap-volume-2'):
self.assertIn({'id': snap_id, 'status': 'available'}, ret[1])
def test_delete_group_snapshot(self):
group_snap = MockOSResource(id='snap-group-1')
cg_snap = test_client.MockResource(_id='snap_cg_1')
with mock.patch.object(self.adapter.client, 'get_snap',
create=True,
return_value=cg_snap) as mocked_get, \
mock.patch.object(self.adapter.client, 'delete_snap',
create=True) as mocked_delete:
ret = self.adapter.delete_group_snapshot(group_snap)
mocked_get.assert_called_once_with('snap-group-1')
mocked_delete.assert_called_once_with(cg_snap)
self.assertEqual((None, None), ret)
class FCAdapterTest(test.TestCase): class FCAdapterTest(test.TestCase):
def setUp(self): def setUp(self):

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
import unittest import unittest
import ddt
from mock import mock from mock import mock
from oslo_utils import units from oslo_utils import units
@ -22,6 +23,7 @@ from cinder.tests.unit.volume.drivers.dell_emc.unity \
import fake_exception as ex import fake_exception as ex
from cinder.volume.drivers.dell_emc.unity import client from cinder.volume.drivers.dell_emc.unity import client
######################## ########################
# #
# Start of Mocks # Start of Mocks
@ -50,6 +52,9 @@ class MockResource(object):
self.host_cache = [] self.host_cache = []
self.is_thin = None self.is_thin = None
self.is_all_flash = True self.is_all_flash = True
self.description = None
self.luns = None
self.lun = None
@property @property
def id(self): def id(self):
@ -220,6 +225,14 @@ class MockResourceList(object):
def name(self): def name(self):
return map(lambda i: i.name, self.resources) return map(lambda i: i.name, self.resources)
@property
def list(self):
return self.resources
@list.setter
def list(self, value):
self.resources = []
def __iter__(self): def __iter__(self):
return self.resources.__iter__() return self.resources.__iter__()
@ -327,6 +340,7 @@ def get_client():
# Start of Tests # Start of Tests
# #
######################## ########################
@ddt.ddt
@mock.patch.object(client, 'storops_ex', new=ex) @mock.patch.object(client, 'storops_ex', new=ex)
class ClientTest(unittest.TestCase): class ClientTest(unittest.TestCase):
def setUp(self): def setUp(self):
@ -591,3 +605,146 @@ class ClientTest(unittest.TestCase):
self.client.host_cache['empty-host-in-cache'] = host self.client.host_cache['empty-host-in-cache'] = host
self.client.delete_host_wo_lock(host) self.client.delete_host_wo_lock(host)
self.assertNotIn(host.name, self.client.host_cache) self.assertNotIn(host.name, self.client.host_cache)
@ddt.data(('cg_1', 'cg_1_description', [MockResource(_id='sv_1')]),
('cg_2', None, None),
('cg_3', None, [MockResource(_id='sv_2')]),
('cg_4', 'cg_4_description', None))
@ddt.unpack
def test_create_cg(self, cg_name, cg_description, lun_add):
created_cg = MockResource(_id='cg_1')
with mock.patch.object(self.client.system, 'create_cg',
create=True, return_value=created_cg
) as mocked_create:
ret = self.client.create_cg(cg_name, description=cg_description,
lun_add=lun_add)
mocked_create.assert_called_once_with(cg_name,
description=cg_description,
lun_add=lun_add)
self.assertEqual(created_cg, ret)
def test_create_cg_existing_name(self):
existing_cg = MockResource(_id='cg_1')
with mock.patch.object(
self.client.system, 'create_cg',
side_effect=ex.UnityConsistencyGroupNameInUseError,
create=True) as mocked_create, \
mock.patch.object(self.client.system, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.create_cg('existing_name')
mocked_create.assert_called_once_with('existing_name',
description=None,
lun_add=None)
mocked_get.assert_called_once_with(name='existing_name')
self.assertEqual(existing_cg, ret)
def test_get_cg(self):
existing_cg = MockResource(_id='cg_1')
with mock.patch.object(self.client.system, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.get_cg('existing_name')
mocked_get.assert_called_once_with(name='existing_name')
self.assertEqual(existing_cg, ret)
def test_get_cg_not_found(self):
with mock.patch.object(self.client.system, 'get_cg',
create=True,
side_effect=ex.UnityResourceNotFoundError
) as mocked_get:
ret = self.client.get_cg('not_found_name')
mocked_get.assert_called_once_with(name='not_found_name')
self.assertIsNone(ret)
def test_delete_cg(self):
existing_cg = MockResource(_id='cg_1')
with mock.patch.object(existing_cg, 'delete', create=True
) as mocked_delete, \
mock.patch.object(self.client, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.delete_cg('cg_1_name')
mocked_get.assert_called_once_with('cg_1_name')
mocked_delete.assert_called_once()
self.assertIsNone(ret)
def test_update_cg(self):
existing_cg = MockResource(_id='cg_1')
lun_1 = MockResource(_id='sv_1')
lun_2 = MockResource(_id='sv_2')
lun_3 = MockResource(_id='sv_3')
def _mocked_get_lun(lun_id):
if lun_id == 'sv_1':
return lun_1
if lun_id == 'sv_2':
return lun_2
if lun_id == 'sv_3':
return lun_3
with mock.patch.object(existing_cg, 'update_lun', create=True
) as mocked_update, \
mock.patch.object(self.client, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get, \
mock.patch.object(self.client, 'get_lun',
side_effect=_mocked_get_lun):
ret = self.client.update_cg('cg_1_name', ['sv_1', 'sv_2'],
['sv_3'])
mocked_get.assert_called_once_with('cg_1_name')
mocked_update.assert_called_once_with(add_luns=[lun_1, lun_2],
remove_luns=[lun_3])
self.assertIsNone(ret)
def test_update_cg_empty_lun_ids(self):
existing_cg = MockResource(_id='cg_1')
with mock.patch.object(existing_cg, 'update_lun', create=True
) as mocked_update, \
mock.patch.object(self.client, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.update_cg('cg_1_name', set(), set())
mocked_get.assert_called_once_with('cg_1_name')
mocked_update.assert_called_once_with(add_luns=[], remove_luns=[])
self.assertIsNone(ret)
def test_create_cg_group(self):
existing_cg = MockResource(_id='cg_1')
created_snap = MockResource(_id='snap_cg_1', name='snap_name_cg_1')
with mock.patch.object(existing_cg, 'create_snap', create=True,
return_value=created_snap) as mocked_create, \
mock.patch.object(self.client, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.create_cg_snap('cg_1_name',
snap_name='snap_name_cg_1')
mocked_get.assert_called_once_with('cg_1_name')
mocked_create.assert_called_once_with(name='snap_name_cg_1',
is_auto_delete=False)
self.assertEqual(created_snap, ret)
def test_create_cg_group_none_name(self):
existing_cg = MockResource(_id='cg_1')
created_snap = MockResource(_id='snap_cg_1')
with mock.patch.object(existing_cg, 'create_snap', create=True,
return_value=created_snap) as mocked_create, \
mock.patch.object(self.client, 'get_cg',
create=True,
return_value=existing_cg) as mocked_get:
ret = self.client.create_cg_snap('cg_1_name')
mocked_get.assert_called_once_with('cg_1_name')
mocked_create.assert_called_once_with(name=None,
is_auto_delete=False)
self.assertEqual(created_snap, ret)
def test_filter_snaps_in_cg_snap(self):
snaps = [MockResource(_id='snap_{}'.format(n)) for n in (1, 2)]
snap_list = mock.MagicMock()
snap_list.list = snaps
with mock.patch.object(self.client.system, 'get_snap',
create=True,
return_value=snap_list) as mocked_get:
ret = self.client.filter_snaps_in_cg_snap('snap_cg_1')
mocked_get.assert_called_once_with(snap_group='snap_cg_1')
self.assertEqual(snaps, ret)

View File

@ -13,8 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import functools
import unittest import unittest
import mock
from cinder.tests.unit.volume.drivers.dell_emc.unity \ from cinder.tests.unit.volume.drivers.dell_emc.unity \
import fake_exception as ex import fake_exception as ex
from cinder.tests.unit.volume.drivers.dell_emc.unity import test_adapter from cinder.tests.unit.volume.drivers.dell_emc.unity import test_adapter
@ -104,6 +107,34 @@ class MockAdapter(object):
def migrate_volume(volume, host): def migrate_volume(volume, host):
return True, {} return True, {}
@staticmethod
def create_group(group):
return group
@staticmethod
def delete_group(group):
return group
@staticmethod
def update_group(group, add_volumes, remove_volumes):
return group, add_volumes, remove_volumes
@staticmethod
def create_group_from_snap(group, volumes, group_snapshot, snapshots):
return group, volumes, group_snapshot, snapshots
@staticmethod
def create_cloned_group(group, volumes, source_group, source_vols):
return group, volumes, source_group, source_vols
@staticmethod
def create_group_snapshot(group_snapshot, snapshots):
return group_snapshot, snapshots
@staticmethod
def delete_group_snapshot(group_snapshot):
return group_snapshot
######################## ########################
# #
@ -112,16 +143,41 @@ class MockAdapter(object):
######################## ########################
patch_check_cg = mock.patch(
'cinder.volume.utils.is_group_a_cg_snapshot_type',
side_effect=lambda g: not g.id.endswith('_generic'))
class UnityDriverTest(unittest.TestCase): class UnityDriverTest(unittest.TestCase):
@staticmethod @staticmethod
def get_volume(): def get_volume():
return test_adapter.MockOSResource(provider_location='id^lun_43', return test_adapter.MockOSResource(provider_location='id^lun_43',
id='id_43') id='id_43')
@staticmethod
def get_generic_group():
return test_adapter.MockOSResource(name='group_name_generic',
id='group_id_generic')
@staticmethod
def get_cg():
return test_adapter.MockOSResource(name='group_name_cg',
id='group_id_cg')
@classmethod @classmethod
def get_snapshot(cls): def get_snapshot(cls):
return test_adapter.MockOSResource(volume=cls.get_volume()) return test_adapter.MockOSResource(volume=cls.get_volume())
@classmethod
def get_generic_group_snapshot(cls):
return test_adapter.MockOSResource(group=cls.get_generic_group(),
id='group_snapshot_id_generic')
@classmethod
def get_cg_group_snapshot(cls):
return test_adapter.MockOSResource(group=cls.get_cg(),
id='group_snapshot_id_cg')
@staticmethod @staticmethod
def get_context(): def get_context():
return None return None
@ -279,3 +335,90 @@ class UnityDriverTest(unittest.TestCase):
volume = self.get_volume() volume = self.get_volume()
r = self.driver.revert_to_snapshot(None, volume, snapshot) r = self.driver.revert_to_snapshot(None, volume, snapshot)
self.assertTrue(r) self.assertTrue(r)
@patch_check_cg
def test_operate_generic_group_not_implemented(self, _):
group = self.get_generic_group()
context = self.get_context()
for func in (self.driver.create_group, self.driver.update_group):
self.assertRaises(NotImplementedError,
functools.partial(func, context, group))
volumes = [self.get_volume()]
for func in (self.driver.delete_group,
self.driver.create_group_from_src):
self.assertRaises(NotImplementedError,
functools.partial(func, context, group, volumes))
group_snap = self.get_generic_group_snapshot()
volume_snaps = [self.get_snapshot()]
for func in (self.driver.create_group_snapshot,
self.driver.delete_group_snapshot):
self.assertRaises(NotImplementedError,
functools.partial(func, context, group_snap,
volume_snaps))
@patch_check_cg
def test_create_group_cg(self, _):
cg = self.get_cg()
ret = self.driver.create_group(self.get_context(), cg)
self.assertEqual(ret, cg)
@patch_check_cg
def test_delete_group_cg(self, _):
cg = self.get_cg()
volumes = [self.get_volume()]
ret = self.driver.delete_group(self.get_context(), cg, volumes)
self.assertEqual(ret, cg)
@patch_check_cg
def test_update_group_cg(self, _):
cg = self.get_cg()
volumes = [self.get_volume()]
ret = self.driver.update_group(self.get_context(), cg,
add_volumes=volumes)
self.assertEqual(ret[0], cg)
self.assertListEqual(ret[1], volumes)
self.assertIsNone(ret[2])
@patch_check_cg
def test_create_group_from_src_group(self, _):
cg = self.get_cg()
volumes = [self.get_volume()]
source_group = cg
ret = self.driver.create_group_from_src(self.get_context(), cg,
volumes,
source_group=source_group)
self.assertEqual(ret[0], cg)
self.assertListEqual(ret[1], volumes)
self.assertEqual(ret[2], source_group)
self.assertIsNone(ret[3])
@patch_check_cg
def test_create_group_from_src_group_snapshot(self, _):
cg = self.get_cg()
volumes = [self.get_volume()]
cg_snap = self.get_cg_group_snapshot()
ret = self.driver.create_group_from_src(self.get_context(), cg,
volumes,
group_snapshot=cg_snap)
self.assertEqual(ret[0], cg)
self.assertListEqual(ret[1], volumes)
self.assertEqual(ret[2], cg_snap)
self.assertIsNone(ret[3])
@patch_check_cg
def test_create_group_snapshot_cg(self, _):
cg_snap = self.get_cg_group_snapshot()
ret = self.driver.create_group_snapshot(self.get_context(), cg_snap,
None)
self.assertEqual(ret[0], cg_snap)
self.assertIsNone(ret[1])
@patch_check_cg
def test_delete_group_snapshot_cg(self, _):
cg_snap = self.get_cg_group_snapshot()
ret = self.driver.delete_group_snapshot(self.get_context(), cg_snap,
None)
self.assertEqual(ret, cg_snap)

View File

@ -26,6 +26,7 @@ from oslo_utils import importutils
from cinder import exception from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder.objects import fields
from cinder import utils as cinder_utils from cinder import utils as cinder_utils
from cinder.volume.drivers.dell_emc.unity import client from cinder.volume.drivers.dell_emc.unity import client
from cinder.volume.drivers.dell_emc.unity import utils from cinder.volume.drivers.dell_emc.unity import utils
@ -59,6 +60,7 @@ class VolumeParams(object):
self._io_limit_policy = None self._io_limit_policy = None
self._is_thick = None self._is_thick = None
self._is_compressed = None self._is_compressed = None
self._is_in_cg = None
@property @property
def volume_id(self): def volume_id(self):
@ -133,13 +135,29 @@ class VolumeParams(object):
def is_compressed(self, value): def is_compressed(self, value):
self._is_compressed = value self._is_compressed = value
@property
def is_in_cg(self):
if self._is_in_cg is None:
self._is_in_cg = (self._volume.group and
vol_utils.is_group_a_cg_snapshot_type(
self._volume.group))
return self._is_in_cg
@property
def cg_id(self):
if self.is_in_cg:
return self._volume.group_id
return None
def __eq__(self, other): def __eq__(self, other):
return (self.volume_id == other.volume_id and return (self.volume_id == other.volume_id and
self.name == other.name and self.name == other.name and
self.size == other.size and self.size == other.size and
self.io_limit_policy == other.io_limit_policy and self.io_limit_policy == other.io_limit_policy and
self.is_thick == other.is_thick and self.is_thick == other.is_thick and
self.is_compressed == other.is_compressed) self.is_compressed == other.is_compressed and
self.is_in_cg == other.is_in_cg and
self.cg_id == other.cg_id)
class CommonAdapter(object): class CommonAdapter(object):
@ -302,22 +320,30 @@ class CommonAdapter(object):
'pool': params.pool, 'pool': params.pool,
'io_limit_policy': params.io_limit_policy, 'io_limit_policy': params.io_limit_policy,
'is_thick': params.is_thick, 'is_thick': params.is_thick,
'is_compressed': params.is_compressed 'is_compressed': params.is_compressed,
'cg_id': params.cg_id
} }
LOG.info('Create Volume: %(name)s, size: %(size)s, description: ' LOG.info('Create Volume: %(name)s, size: %(size)s, description: '
'%(description)s, pool: %(pool)s, io limit policy: ' '%(description)s, pool: %(pool)s, io limit policy: '
'%(io_limit_policy)s, thick: %(is_thick)s, ' '%(io_limit_policy)s, thick: %(is_thick)s, '
'%(is_compressed)s.', log_params) 'compressed: %(is_compressed)s, cg_group: %(cg_id)s.',
log_params)
return self.makeup_model( lun = self.client.create_lun(
self.client.create_lun(name=params.name, name=params.name,
size=params.size, size=params.size,
pool=params.pool, pool=params.pool,
description=params.description, description=params.description,
io_limit_policy=params.io_limit_policy, io_limit_policy=params.io_limit_policy,
is_thin=False if params.is_thick else None, is_thin=False if params.is_thick else None,
is_compressed=params.is_compressed)) is_compressed=params.is_compressed)
if params.cg_id:
LOG.debug('Adding lun %(lun)s to cg %(cg)s.',
{'lun': lun.get_id(), 'cg': params.cg_id})
self.client.update_cg(params.cg_id, [lun.get_id()], ())
return self.makeup_model(lun)
def delete_volume(self, volume): def delete_volume(self, volume):
lun_id = self.get_lun_id(volume) lun_id = self.get_lun_id(volume)
@ -442,12 +468,11 @@ class CommonAdapter(object):
lun_id=lun_id, lun_id=lun_id,
version=self.version) version=self.version)
@utils.append_capabilities
def update_volume_stats(self): def update_volume_stats(self):
return { return {
'volume_backend_name': self.volume_backend_name, 'volume_backend_name': self.volume_backend_name,
'storage_protocol': self.protocol, 'storage_protocol': self.protocol,
'thin_provisioning_support': True,
'thick_provisioning_support': True,
'pools': self.get_pools_stats(), 'pools': self.get_pools_stats(),
} }
@ -459,6 +484,7 @@ class CommonAdapter(object):
def pools(self): def pools(self):
return self.storage_pools_map.values() return self.storage_pools_map.values()
@utils.append_capabilities
def _get_pool_stats(self, pool): def _get_pool_stats(self, pool):
return { return {
'pool_name': pool.name, 'pool_name': pool.name,
@ -470,8 +496,6 @@ class CommonAdapter(object):
'location_info': ('%(pool_name)s|%(array_serial)s' % 'location_info': ('%(pool_name)s|%(array_serial)s' %
{'pool_name': pool.name, {'pool_name': pool.name,
'array_serial': self.serial_number}), 'array_serial': self.serial_number}),
'thin_provisioning_support': True,
'thick_provisioning_support': True,
'compression_support': pool.is_all_flash, 'compression_support': pool.is_all_flash,
'max_over_subscription_ratio': ( 'max_over_subscription_ratio': (
self.max_over_subscription_ratio), self.max_over_subscription_ratio),
@ -643,9 +667,7 @@ class CommonAdapter(object):
if src_lun is None: if src_lun is None:
# If size is not specified, need to get the size from LUN # If size is not specified, need to get the size from LUN
# of snapshot. # of snapshot.
lun = self.client.get_lun( size_in_m = utils.byte_to_mib(src_snap.size)
lun_id=src_snap.storage_resource.get_id())
size_in_m = utils.byte_to_mib(lun.size_total)
else: else:
size_in_m = utils.byte_to_mib(src_lun.size_total) size_in_m = utils.byte_to_mib(src_lun.size_total)
vol_utils.copy_volume( vol_utils.copy_volume(
@ -814,6 +836,95 @@ class CommonAdapter(object):
'host-assisted migration.') 'host-assisted migration.')
return False, None return False, None
def create_group(self, group):
"""Creates a generic group.
:param group: group information
"""
cg_name = group.id
description = group.description if group.description else group.name
LOG.info('Create group: %(name)s, description: %(description)s',
{'name': cg_name, 'description': description})
self.client.create_cg(cg_name, description=description)
return {'status': fields.GroupStatus.AVAILABLE}
def delete_group(self, group):
"""Deletes the generic group.
:param group: the group to delete
"""
# Deleting cg will also delete all the luns in it.
self.client.delete_cg(group.id)
return None, None
def update_group(self, group, add_volumes, remove_volumes):
add_lun_ids = (set(map(self.get_lun_id, add_volumes)) if add_volumes
else set())
remove_lun_ids = (set(map(self.get_lun_id, remove_volumes))
if remove_volumes else set())
self.client.update_cg(group.id, add_lun_ids, remove_lun_ids)
return {'status': fields.GroupStatus.AVAILABLE}, None, None
def copy_luns_in_group(self, group, volumes, src_cg_snap, src_volumes):
# Use dd to copy data here. The reason why not using thinclone is:
# 1. Cannot use cg thinclone due to the tight couple between source
# group and cloned one.
# 2. Cannot use lun thinclone due to clone lun in cg is not supported.
lun_snaps = self.client.filter_snaps_in_cg_snap(src_cg_snap.id)
# Make sure the `lun_snaps` is as order of `src_volumes`
src_lun_ids = [self.get_lun_id(volume) for volume in src_volumes]
lun_snaps.sort(key=lambda snap: src_lun_ids.index(snap.lun.id))
dest_luns = [self._dd_copy(VolumeParams(self, dest_volume), lun_snap)
for dest_volume, lun_snap in zip(volumes, lun_snaps)]
self.client.create_cg(group.id, lun_add=dest_luns)
return ({'status': fields.GroupStatus.AVAILABLE},
[{'id': dest_volume.id, 'status': fields.GroupStatus.AVAILABLE}
for dest_volume in volumes])
def create_group_from_snap(self, group, volumes,
group_snapshot, snapshots):
src_cg_snap = self.client.get_snap(group_snapshot.id)
src_vols = ([snap.volume for snap in snapshots] if snapshots else [])
return self.copy_luns_in_group(group, volumes, src_cg_snap, src_vols)
def create_cloned_group(self, group, volumes, source_group, source_vols):
src_group_snap_name = 'snap_clone_group_{}'.format(source_group.id)
create_snap_func = functools.partial(self.client.create_cg_snap,
source_group.id,
src_group_snap_name)
with utils.assure_cleanup(create_snap_func,
self.client.delete_snap,
True) as src_cg_snap:
LOG.debug('Internal group snapshot for clone is created, '
'name: %(name)s, id: %(id)s.',
{'name': src_group_snap_name,
'id': src_cg_snap.get_id()})
source_vols = source_vols if source_vols else []
return self.copy_luns_in_group(group, volumes, src_cg_snap,
source_vols)
def create_group_snapshot(self, group_snapshot, snapshots):
self.client.create_cg_snap(group_snapshot.group_id,
snap_name=group_snapshot.id)
model_update = {'status': fields.GroupStatus.AVAILABLE}
snapshots_model_update = [{'id': snapshot.id,
'status': fields.SnapshotStatus.AVAILABLE}
for snapshot in snapshots]
return model_update, snapshots_model_update
def delete_group_snapshot(self, group_snapshot):
cg_snap = self.client.get_snap(group_snapshot.id)
self.client.delete_snap(cg_snap)
return None, None
class ISCSIAdapter(CommonAdapter): class ISCSIAdapter(CommonAdapter):
protocol = PROTOCOL_ISCSI protocol = PROTOCOL_ISCSI

View File

@ -58,7 +58,7 @@ class UnityClient(object):
def create_lun(self, name, size, pool, description=None, def create_lun(self, name, size, pool, description=None,
io_limit_policy=None, is_thin=None, io_limit_policy=None, is_thin=None,
is_compressed=None): is_compressed=None, cg_name=None):
"""Creates LUN on the Unity system. """Creates LUN on the Unity system.
:param name: lun name :param name: lun name
@ -68,6 +68,7 @@ class UnityClient(object):
:param io_limit_policy: io limit on the LUN :param io_limit_policy: io limit on the LUN
:param is_thin: if False, a thick LUN will be created :param is_thin: if False, a thick LUN will be created
:param is_compressed: is compressed LUN enabled :param is_compressed: is compressed LUN enabled
:param cg_name: the name of cg to join if any
:return: UnityLun object :return: UnityLun object
""" """
try: try:
@ -312,9 +313,9 @@ class UnityClient(object):
# so use filter instead of shadow_copy here. # so use filter instead of shadow_copy here.
wwns.update(p.wwn.upper() wwns.update(p.wwn.upper()
for p in filter( for p in filter(
lambda fcp: (allowed_ports is None or lambda fcp: (allowed_ports is None or
fcp.get_id() in allowed_ports), fcp.get_id() in allowed_ports),
paths.fc_port)) paths.fc_port))
else: else:
ports = self.get_fc_ports() ports = self.get_fc_ports()
ports = ports.shadow_copy(port_ids=allowed_ports) ports = ports.shadow_copy(port_ids=allowed_ports)
@ -349,3 +350,41 @@ class UnityClient(object):
def restore_snapshot(self, snap_name): def restore_snapshot(self, snap_name):
snap = self.get_snap(snap_name) snap = self.get_snap(snap_name)
return snap.restore(delete_backup=True) return snap.restore(delete_backup=True)
def create_cg(self, name, description=None, lun_add=None):
try:
cg = self.system.create_cg(name, description=description,
lun_add=lun_add)
except storops_ex.UnityConsistencyGroupNameInUseError:
LOG.debug('CG %s already exists. Return the existing one.', name)
cg = self.system.get_cg(name=name)
return cg
def get_cg(self, name):
try:
cg = self.system.get_cg(name=name)
except storops_ex.UnityResourceNotFoundError:
LOG.info('CG %s not found.', name)
return None
else:
return cg
def delete_cg(self, name):
cg = self.get_cg(name)
if cg:
cg.delete() # Deleting cg will also delete the luns in it
def update_cg(self, name, add_lun_ids, remove_lun_ids):
cg = self.get_cg(name)
cg.update_lun(add_luns=[self.get_lun(lun_id=lun_id)
for lun_id in add_lun_ids],
remove_luns=[self.get_lun(lun_id=lun_id)
for lun_id in remove_lun_ids])
def create_cg_snap(self, cg_name, snap_name=None):
cg = self.get_cg(cg_name)
# Creating snap of cg will create corresponding snaps of luns in it
return cg.create_snap(name=snap_name, is_auto_delete=False)
def filter_snaps_in_cg_snap(self, cg_snap_id):
return self.system.get_snap(snap_group=cg_snap_id).list

View File

@ -18,11 +18,14 @@
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import six
from cinder import interface from cinder import interface
from cinder.volume import configuration from cinder.volume import configuration
from cinder.volume import driver from cinder.volume import driver
from cinder.volume.drivers.dell_emc.unity import adapter from cinder.volume.drivers.dell_emc.unity import adapter
from cinder.volume.drivers.san.san import san_opts from cinder.volume.drivers.san.san import san_opts
from cinder.volume import utils
from cinder.zonemanager import utils as zm_utils from cinder.zonemanager import utils as zm_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -33,7 +36,7 @@ UNITY_OPTS = [
cfg.ListOpt('unity_storage_pool_names', cfg.ListOpt('unity_storage_pool_names',
default=[], default=[],
help='A comma-separated list of storage pool names to be ' help='A comma-separated list of storage pool names to be '
'used.'), 'used.'),
cfg.ListOpt('unity_io_ports', cfg.ListOpt('unity_io_ports',
default=[], default=[],
help='A comma-separated list of iSCSI or FC ports to be used. ' help='A comma-separated list of iSCSI or FC ports to be used. '
@ -46,6 +49,20 @@ UNITY_OPTS = [
CONF.register_opts(UNITY_OPTS, group=configuration.SHARED_CONF_GROUP) CONF.register_opts(UNITY_OPTS, group=configuration.SHARED_CONF_GROUP)
def skip_if_not_cg(func):
@six.wraps(func)
def inner(self, *args, **kwargs):
# Only used to decorating the second argument is `group`
if utils.is_group_a_cg_snapshot_type(args[1]):
return func(self, *args, **kwargs)
LOG.debug('Group is not a consistency group. Unity driver does '
'nothing.')
# This exception will let cinder handle it as a generic group
raise NotImplementedError()
return inner
@interface.volumedriver @interface.volumedriver
class UnityDriver(driver.ManageableVD, class UnityDriver(driver.ManageableVD,
driver.ManageableSnapshotsVD, driver.ManageableSnapshotsVD,
@ -60,9 +77,10 @@ class UnityDriver(driver.ManageableVD,
4.0.0 - Support remove empty host 4.0.0 - Support remove empty host
4.2.0 - Support compressed volume 4.2.0 - Support compressed volume
5.0.0 - Support storage assisted volume migration 5.0.0 - Support storage assisted volume migration
6.0.0 - Support generic group and consistent group
""" """
VERSION = '05.00.00' VERSION = '06.00.00'
VENDOR = 'Dell EMC' VENDOR = 'Dell EMC'
# ThirdPartySystems wiki page # ThirdPartySystems wiki page
CI_WIKI_NAME = "EMC_UNITY_CI" CI_WIKI_NAME = "EMC_UNITY_CI"
@ -252,3 +270,43 @@ class UnityDriver(driver.ManageableVD,
def revert_to_snapshot(self, context, volume, snapshot): def revert_to_snapshot(self, context, volume, snapshot):
"""Reverts a volume to a snapshot.""" """Reverts a volume to a snapshot."""
return self.adapter.restore_snapshot(volume, snapshot) return self.adapter.restore_snapshot(volume, snapshot)
@skip_if_not_cg
def create_group(self, context, group):
"""Creates a consistency group."""
return self.adapter.create_group(group)
@skip_if_not_cg
def delete_group(self, context, group, volumes):
"""Deletes a consistency group."""
return self.adapter.delete_group(group)
@skip_if_not_cg
def update_group(self, context, group, add_volumes=None,
remove_volumes=None):
"""Updates a consistency group, i.e. add/remove luns to/from it."""
# TODO(Ryan L) update other information (like description) of group
return self.adapter.update_group(group, add_volumes, remove_volumes)
@skip_if_not_cg
def create_group_from_src(self, context, group, volumes,
group_snapshot=None, snapshots=None,
source_group=None, source_vols=None):
"""Creates a consistency group from another group or group snapshot."""
if group_snapshot:
return self.adapter.create_group_from_snap(group, volumes,
group_snapshot,
snapshots)
elif source_group:
return self.adapter.create_cloned_group(group, volumes,
source_group, source_vols)
@skip_if_not_cg
def create_group_snapshot(self, context, group_snapshot, snapshots):
"""Creates a snapshot of consistency group."""
return self.adapter.create_group_snapshot(group_snapshot, snapshots)
@skip_if_not_cg
def delete_group_snapshot(self, context, group_snapshot, snapshots):
"""Deletes a snapshot of consistency group."""
return self.adapter.delete_group_snapshot(group_snapshot)

View File

@ -319,6 +319,22 @@ def lock_if(condition, lock_name):
return functools.partial return functools.partial
def append_capabilities(func):
capabilities = {
'thin_provisioning_support': True,
'thick_provisioning_support': True,
'consistent_group_snapshot_enabled': True
}
@six.wraps(func)
def _inner(*args, **kwargs):
output = func(*args, **kwargs)
output.update(capabilities)
return output
return _inner
def is_multiattach_to_host(volume_attachment, host_name): def is_multiattach_to_host(volume_attachment, host_name):
# When multiattach is enabled, a volume could be attached to two or more # When multiattach is enabled, a volume could be attached to two or more
# instances which are hosted on one nova host. # instances which are hosted on one nova host.

View File

@ -35,6 +35,11 @@ Supported operations
- Efficient non-disruptive volume backup. - Efficient non-disruptive volume backup.
- Revert a volume to a snapshot. - Revert a volume to a snapshot.
- Create thick volumes. - Create thick volumes.
- Create and delete consistent groups.
- Add/remove volumes to/from a consistent group.
- Create and delete consistent group snapshots.
- Clone a consistent group.
- Create a consistent group from a snapshot.
- Attach a volume to multiple servers simultaneously (multiattach). - Attach a volume to multiple servers simultaneously (multiattach).
Driver configuration Driver configuration
@ -382,6 +387,26 @@ attached hosts.
For more detail, please refer to For more detail, please refer to
https://developer.openstack.org/api-ref/block-storage/v2/?expanded=force-detach-volume-detail#force-detach-volume https://developer.openstack.org/api-ref/block-storage/v2/?expanded=force-detach-volume-detail#force-detach-volume
Consistent group support
~~~~~~~~~~~~~~~~~~~~~~~~
For a group to support consistent group snapshot, the group specs in the
corresponding group type should have the following entry:
.. code-block:: ini
{'consistent_group_snapshot_enabled': <is> True}
Similarly, for a volume to be in a group that supports consistent group
snapshots, the volume type extra specs would also have the following entry:
.. code-block:: ini
{'consistent_group_snapshot_enabled': <is> True}
Refer to https://docs.openstack.org/cinder/latest/admin/blockstorage-groups.html
for command lines detail.
Troubleshooting Troubleshooting
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~

View File

@ -554,7 +554,7 @@ driver.datera=missing
driver.dell_emc_powermax=complete driver.dell_emc_powermax=complete
driver.dell_emc_ps=missing driver.dell_emc_ps=missing
driver.dell_emc_sc=complete driver.dell_emc_sc=complete
driver.dell_emc_unity=missing driver.dell_emc_unity=complete
driver.dell_emc_vmax_af=complete driver.dell_emc_vmax_af=complete
driver.dell_emc_vmax_3=complete driver.dell_emc_vmax_3=complete
driver.dell_emc_vnx=complete driver.dell_emc_vnx=complete

View File

@ -0,0 +1,11 @@
---
features:
- |
Dell EMC Unity driver: Add consistent group support. Users could create a
group type supporting consistent groups with specification
`'consistent_group_snapshot_enabled': <is> True`, then any groups created
of that group type are consistent groups, otherwise they are generic
groups. The supported operations are: create/delete consistent groups, add
volumes to and remove volumes from consistent groups, create/delete
consistent group snapshots, create consistent groups from snapshots, clone
consistent groups.