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:
parent
fbd7c0377d
commit
661a4f1212
@ -92,3 +92,7 @@ class UnityThinCloneNotAllowedError(StoropsException):
|
|||||||
|
|
||||||
class SystemAPINotSupported(StoropsException):
|
class SystemAPINotSupported(StoropsException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnityConsistencyGroupNameInUseError(StoropsException):
|
||||||
|
pass
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
11
releasenotes/notes/support-cg-2b55da0bd9f69c7d.yaml
Normal file
11
releasenotes/notes/support-cg-2b55da0bd9f69c7d.yaml
Normal 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.
|
Loading…
x
Reference in New Issue
Block a user