ScaleIO: Adding CG support to groups
Adding consistency group support capability to generic volume groups in the ScaleIO driver. Change-Id: Iae4dc241137a78db74b8f29b3df540fb3eb89008 Implements: blueprint scaleio-generic-volume-group
This commit is contained in:
parent
641864b662
commit
fcbd762d9d
@ -1,211 +0,0 @@
|
|||||||
# Copyright (c) 2013 - 2016 EMC Corporation.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder.tests.unit.consistencygroup import fake_consistencygroup
|
|
||||||
from cinder.tests.unit import fake_constants as fake
|
|
||||||
from cinder.tests.unit import fake_snapshot
|
|
||||||
from cinder.tests.unit import fake_volume
|
|
||||||
from cinder.tests.unit.volume.drivers.dell_emc import scaleio
|
|
||||||
from cinder.tests.unit.volume.drivers.dell_emc.scaleio import mocks
|
|
||||||
|
|
||||||
|
|
||||||
class TestConsistencyGroups(scaleio.TestScaleIODriver):
|
|
||||||
"""Test cases for ``ScaleIODriver consistency groups support``"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Setup a test case environment.
|
|
||||||
|
|
||||||
Creates a fake volume object and sets up the required API responses.
|
|
||||||
"""
|
|
||||||
super(TestConsistencyGroups, self).setUp()
|
|
||||||
self.ctx = context.RequestContext('fake', 'fake', auth_token=True)
|
|
||||||
self.consistency_group = (
|
|
||||||
fake_consistencygroup.fake_consistencyobject_obj(
|
|
||||||
self.ctx, **{'id': fake.CONSISTENCY_GROUP_ID}))
|
|
||||||
fake_volume1 = fake_volume.fake_volume_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.VOLUME_ID, 'provider_id': fake.PROVIDER_ID})
|
|
||||||
fake_volume2 = fake_volume.fake_volume_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.VOLUME2_ID, 'provider_id': fake.PROVIDER2_ID})
|
|
||||||
fake_volume3 = fake_volume.fake_volume_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.VOLUME3_ID, 'provider_id': fake.PROVIDER3_ID})
|
|
||||||
fake_volume4 = fake_volume.fake_volume_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.VOLUME4_ID, 'provider_id': fake.PROVIDER4_ID})
|
|
||||||
self.volumes = [fake_volume1, fake_volume2]
|
|
||||||
self.volumes2 = [fake_volume3, fake_volume4]
|
|
||||||
fake_snapshot1 = fake_snapshot.fake_snapshot_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.SNAPSHOT_ID, 'volume_id': fake.VOLUME_ID,
|
|
||||||
'volume': fake_volume1})
|
|
||||||
fake_snapshot2 = fake_snapshot.fake_snapshot_obj(
|
|
||||||
self.ctx,
|
|
||||||
**{'id': fake.SNAPSHOT2_ID, 'volume_id': fake.VOLUME2_ID, 'volume':
|
|
||||||
fake_volume2})
|
|
||||||
self.snapshots = [fake_snapshot1, fake_snapshot2]
|
|
||||||
self.snapshot_reply = json.dumps({
|
|
||||||
'volumeIdList': ['sid1', 'sid2'],
|
|
||||||
'snapshotGroupId': 'sgid1'})
|
|
||||||
self.HTTPS_MOCK_RESPONSES = {
|
|
||||||
self.RESPONSE_MODE.Valid: {
|
|
||||||
'instances/Volume::{}/action/removeVolume'.format(
|
|
||||||
fake_volume1['provider_id']
|
|
||||||
): fake_volume1['provider_id'],
|
|
||||||
'instances/Volume::{}/action/removeVolume'.format(
|
|
||||||
fake_volume2['provider_id']
|
|
||||||
): fake_volume2['provider_id'],
|
|
||||||
'instances/Volume::{}/action/removeMappedSdc'.format(
|
|
||||||
fake_volume1['provider_id']
|
|
||||||
): fake_volume1['provider_id'],
|
|
||||||
'instances/Volume::{}/action/removeMappedSdc'.format(
|
|
||||||
fake_volume2['provider_id']
|
|
||||||
): fake_volume2['provider_id'],
|
|
||||||
'instances/System/action/snapshotVolumes':
|
|
||||||
self.snapshot_reply,
|
|
||||||
},
|
|
||||||
self.RESPONSE_MODE.BadStatus: {
|
|
||||||
'instances/Volume::{}/action/removeVolume'.format(
|
|
||||||
fake_volume1['provider_id']
|
|
||||||
): mocks.MockHTTPSResponse(
|
|
||||||
{
|
|
||||||
'errorCode': 401,
|
|
||||||
'message': 'BadStatus Volume Test',
|
|
||||||
}, 401
|
|
||||||
),
|
|
||||||
'instances/Volume::{}/action/removeVolume'.format(
|
|
||||||
fake_volume2['provider_id']
|
|
||||||
): mocks.MockHTTPSResponse(
|
|
||||||
{
|
|
||||||
'errorCode': 401,
|
|
||||||
'message': 'BadStatus Volume Test',
|
|
||||||
}, 401
|
|
||||||
),
|
|
||||||
'instances/System/action/snapshotVolumes':
|
|
||||||
self.BAD_STATUS_RESPONSE
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _fake_cgsnapshot(self):
|
|
||||||
cgsnap = {'id': 'cgsid', 'name': 'testsnap',
|
|
||||||
'consistencygroup_id': fake.CONSISTENCY_GROUP_ID,
|
|
||||||
'status': 'available'}
|
|
||||||
return cgsnap
|
|
||||||
|
|
||||||
def test_create_consistencygroup(self):
|
|
||||||
result = self.driver.create_consistencygroup(self.ctx,
|
|
||||||
self.consistency_group)
|
|
||||||
self.assertEqual('available', result['status'])
|
|
||||||
|
|
||||||
def test_delete_consistencygroup_valid(self):
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
|
||||||
self.driver.configuration.set_override(
|
|
||||||
'sio_unmap_volume_before_deletion',
|
|
||||||
override=True)
|
|
||||||
result_model_update, result_volumes_update = (
|
|
||||||
self.driver.delete_consistencygroup(self.ctx,
|
|
||||||
self.consistency_group,
|
|
||||||
self.volumes))
|
|
||||||
self.assertTrue(all(volume['status'] == 'deleted' for volume in
|
|
||||||
result_volumes_update))
|
|
||||||
self.assertEqual('deleted', result_model_update['status'])
|
|
||||||
|
|
||||||
def test_delete_consistency_group_fail(self):
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
|
|
||||||
result_model_update, result_volumes_update = (
|
|
||||||
self.driver.delete_consistencygroup(self.ctx,
|
|
||||||
self.consistency_group,
|
|
||||||
self.volumes))
|
|
||||||
self.assertTrue(any(volume['status'] == 'error_deleting' for volume in
|
|
||||||
result_volumes_update))
|
|
||||||
self.assertIn(result_model_update['status'],
|
|
||||||
['error_deleting', 'error'])
|
|
||||||
|
|
||||||
def test_create_consistencygroup_from_cg(self):
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
|
||||||
result_model_update, result_volumes_model_update = (
|
|
||||||
self.driver.create_consistencygroup_from_src(
|
|
||||||
self.ctx, self.consistency_group, self.volumes2,
|
|
||||||
source_cg=self.consistency_group, source_vols=self.volumes))
|
|
||||||
self.assertEqual('available', result_model_update['status'])
|
|
||||||
get_pid = lambda snapshot: snapshot['provider_id']
|
|
||||||
volume_provider_list = list(map(get_pid, result_volumes_model_update))
|
|
||||||
self.assertListEqual(volume_provider_list, ['sid1', 'sid2'])
|
|
||||||
|
|
||||||
def test_create_consistencygroup_from_cgs(self):
|
|
||||||
self.snapshots[0]['provider_id'] = fake.PROVIDER_ID
|
|
||||||
self.snapshots[1]['provider_id'] = fake.PROVIDER2_ID
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
|
||||||
result_model_update, result_volumes_model_update = (
|
|
||||||
self.driver.create_consistencygroup_from_src(
|
|
||||||
self.ctx, self.consistency_group, self.volumes2,
|
|
||||||
cgsnapshot=self._fake_cgsnapshot(),
|
|
||||||
snapshots=self.snapshots))
|
|
||||||
self.assertEqual('available', result_model_update['status'])
|
|
||||||
get_pid = lambda snapshot: snapshot['provider_id']
|
|
||||||
volume_provider_list = list(map(get_pid, result_volumes_model_update))
|
|
||||||
self.assertListEqual(['sid1', 'sid2'], volume_provider_list)
|
|
||||||
|
|
||||||
@mock.patch('cinder.objects.snapshot')
|
|
||||||
@mock.patch('cinder.objects.snapshot')
|
|
||||||
def test_create_cgsnapshots(self, snapshot1, snapshot2):
|
|
||||||
type(snapshot1).volume = mock.PropertyMock(
|
|
||||||
return_value=self.volumes[0])
|
|
||||||
type(snapshot2).volume = mock.PropertyMock(
|
|
||||||
return_value=self.volumes[1])
|
|
||||||
snapshots = [snapshot1, snapshot2]
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
|
||||||
result_model_update, result_snapshot_model_update = (
|
|
||||||
self.driver.create_cgsnapshot(
|
|
||||||
self.ctx,
|
|
||||||
self._fake_cgsnapshot(),
|
|
||||||
snapshots
|
|
||||||
))
|
|
||||||
self.assertEqual('available', result_model_update['status'])
|
|
||||||
self.assertTrue(all(snapshot['status'] == 'available' for snapshot in
|
|
||||||
result_snapshot_model_update))
|
|
||||||
get_pid = lambda snapshot: snapshot['provider_id']
|
|
||||||
snapshot_provider_list = list(map(get_pid,
|
|
||||||
result_snapshot_model_update))
|
|
||||||
self.assertListEqual(['sid1', 'sid2'], snapshot_provider_list)
|
|
||||||
|
|
||||||
@mock.patch('cinder.objects.snapshot')
|
|
||||||
@mock.patch('cinder.objects.snapshot')
|
|
||||||
def test_delete_cgsnapshots(self, snapshot1, snapshot2):
|
|
||||||
type(snapshot1).volume = mock.PropertyMock(
|
|
||||||
return_value=self.volumes[0])
|
|
||||||
type(snapshot2).volume = mock.PropertyMock(
|
|
||||||
return_value=self.volumes[1])
|
|
||||||
type(snapshot1).provider_id = mock.PropertyMock(
|
|
||||||
return_value=fake.PROVIDER_ID)
|
|
||||||
type(snapshot2).provider_id = mock.PropertyMock(
|
|
||||||
return_value=fake.PROVIDER2_ID)
|
|
||||||
snapshots = [snapshot1, snapshot2]
|
|
||||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
|
||||||
result_model_update, result_snapshot_model_update = (
|
|
||||||
self.driver.delete_cgsnapshot(
|
|
||||||
self.ctx,
|
|
||||||
self._fake_cgsnapshot(),
|
|
||||||
snapshots
|
|
||||||
))
|
|
||||||
self.assertEqual('deleted', result_model_update['status'])
|
|
||||||
self.assertTrue(all(snapshot['status'] == 'deleted' for snapshot in
|
|
||||||
result_snapshot_model_update))
|
|
279
cinder/tests/unit/volume/drivers/dell_emc/scaleio/test_groups.py
Normal file
279
cinder/tests/unit/volume/drivers/dell_emc/scaleio/test_groups.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
# Copyright (C) 2017 Dell Inc. or its subsidiaries.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder.objects import fields
|
||||||
|
|
||||||
|
from cinder.tests.unit import fake_constants as fake
|
||||||
|
from cinder.tests.unit import fake_group
|
||||||
|
from cinder.tests.unit import fake_snapshot
|
||||||
|
from cinder.tests.unit import fake_volume
|
||||||
|
from cinder.tests.unit.volume.drivers.dell_emc import scaleio
|
||||||
|
from cinder.tests.unit.volume.drivers.dell_emc.scaleio import mocks
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroups(scaleio.TestScaleIODriver):
|
||||||
|
"""Test cases for ``ScaleIODriver groups support``"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup a test case environment.
|
||||||
|
|
||||||
|
Creates a fake volume object and sets up the required API responses.
|
||||||
|
"""
|
||||||
|
super(TestGroups, self).setUp()
|
||||||
|
self.ctx = context.RequestContext('fake', 'fake', auth_token=True)
|
||||||
|
self.fake_grp_snap = {'id': 'group_snap_id',
|
||||||
|
'name': 'test_group_snapshot',
|
||||||
|
'group_id': fake.GROUP_ID,
|
||||||
|
'status': fields.GroupSnapshotStatus.AVAILABLE
|
||||||
|
}
|
||||||
|
self.group = (
|
||||||
|
fake_group.fake_group_obj(
|
||||||
|
self.ctx, **{'id': fake.GROUP_ID}))
|
||||||
|
fake_volume1 = fake_volume.fake_volume_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.VOLUME_ID, 'provider_id': fake.PROVIDER_ID})
|
||||||
|
fake_volume2 = fake_volume.fake_volume_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.VOLUME2_ID, 'provider_id': fake.PROVIDER2_ID})
|
||||||
|
fake_volume3 = fake_volume.fake_volume_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.VOLUME3_ID, 'provider_id': fake.PROVIDER3_ID})
|
||||||
|
fake_volume4 = fake_volume.fake_volume_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.VOLUME4_ID, 'provider_id': fake.PROVIDER4_ID})
|
||||||
|
self.volumes = [fake_volume1, fake_volume2]
|
||||||
|
self.volumes2 = [fake_volume3, fake_volume4]
|
||||||
|
fake_snapshot1 = fake_snapshot.fake_snapshot_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.SNAPSHOT_ID, 'volume_id': fake.VOLUME_ID,
|
||||||
|
'volume': fake_volume1})
|
||||||
|
fake_snapshot2 = fake_snapshot.fake_snapshot_obj(
|
||||||
|
self.ctx,
|
||||||
|
**{'id': fake.SNAPSHOT2_ID, 'volume_id': fake.VOLUME2_ID, 'volume':
|
||||||
|
fake_volume2})
|
||||||
|
self.snapshots = [fake_snapshot1, fake_snapshot2]
|
||||||
|
self.snapshot_reply = json.dumps({
|
||||||
|
'volumeIdList': ['sid1', 'sid2'],
|
||||||
|
'snapshotGroupId': 'sgid1'})
|
||||||
|
self.HTTPS_MOCK_RESPONSES = {
|
||||||
|
self.RESPONSE_MODE.Valid: {
|
||||||
|
'instances/Volume::{}/action/removeVolume'.format(
|
||||||
|
fake_volume1['provider_id']
|
||||||
|
): fake_volume1['provider_id'],
|
||||||
|
'instances/Volume::{}/action/removeVolume'.format(
|
||||||
|
fake_volume2['provider_id']
|
||||||
|
): fake_volume2['provider_id'],
|
||||||
|
'instances/Volume::{}/action/removeMappedSdc'.format(
|
||||||
|
fake_volume1['provider_id']
|
||||||
|
): fake_volume1['provider_id'],
|
||||||
|
'instances/Volume::{}/action/removeMappedSdc'.format(
|
||||||
|
fake_volume2['provider_id']
|
||||||
|
): fake_volume2['provider_id'],
|
||||||
|
'instances/System/action/snapshotVolumes':
|
||||||
|
self.snapshot_reply,
|
||||||
|
},
|
||||||
|
self.RESPONSE_MODE.BadStatus: {
|
||||||
|
'instances/Volume::{}/action/removeVolume'.format(
|
||||||
|
fake_volume1['provider_id']
|
||||||
|
): mocks.MockHTTPSResponse(
|
||||||
|
{
|
||||||
|
'errorCode': 401,
|
||||||
|
'message': 'BadStatus Volume Test',
|
||||||
|
}, 401
|
||||||
|
),
|
||||||
|
'instances/Volume::{}/action/removeVolume'.format(
|
||||||
|
fake_volume2['provider_id']
|
||||||
|
): mocks.MockHTTPSResponse(
|
||||||
|
{
|
||||||
|
'errorCode': 401,
|
||||||
|
'message': 'BadStatus Volume Test',
|
||||||
|
}, 401
|
||||||
|
),
|
||||||
|
'instances/System/action/snapshotVolumes':
|
||||||
|
self.BAD_STATUS_RESPONSE
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_create_group(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test group create.
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns status of 'available'
|
||||||
|
"""
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.create_group, self.ctx, self.group)
|
||||||
|
|
||||||
|
model_update = self.driver.create_group(self.ctx, self.group)
|
||||||
|
self.assertEqual(fields.GroupStatus.AVAILABLE,
|
||||||
|
model_update['status'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_delete_group(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test group deletion.
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns status of 'deleted'
|
||||||
|
"""
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.delete_group,
|
||||||
|
self.ctx, self.group, self.volumes)
|
||||||
|
|
||||||
|
model_update = self.driver.delete_group(self.ctx,
|
||||||
|
self.group,
|
||||||
|
self.volumes)
|
||||||
|
self.assertEqual(fields.GroupStatus.DELETED,
|
||||||
|
model_update[0]['status'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_update_group(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test updating a group
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns 'None' for each of the updates
|
||||||
|
"""
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.update_group, self.ctx, self.group)
|
||||||
|
|
||||||
|
mod_up, add_up, remove_up = self.driver.update_group(self.ctx,
|
||||||
|
self.group)
|
||||||
|
self.assertIsNone(mod_up)
|
||||||
|
self.assertIsNone(add_up)
|
||||||
|
self.assertIsNone(remove_up)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_create_group_from_src_group(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test creating group from source group
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns list of volumes in 'available' state
|
||||||
|
"""
|
||||||
|
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||||
|
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.create_group_from_src,
|
||||||
|
self.ctx, self.group, self.volumes,
|
||||||
|
source_group=self.group, source_vols=self.volumes)
|
||||||
|
|
||||||
|
result_model_update, result_volumes_model_update = (
|
||||||
|
self.driver.create_group_from_src(
|
||||||
|
self.ctx, self.group, self.volumes,
|
||||||
|
source_group=self.group, source_vols=self.volumes))
|
||||||
|
self.assertEqual(fields.GroupStatus.AVAILABLE,
|
||||||
|
result_model_update['status'])
|
||||||
|
get_pid = lambda snapshot: snapshot['provider_id']
|
||||||
|
volume_provider_list = list(map(get_pid, result_volumes_model_update))
|
||||||
|
self.assertListEqual(volume_provider_list, ['sid1', 'sid2'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_create_group_from_src_snapshot(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test creating group from snapshot
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.create_group_from_src,
|
||||||
|
self.ctx, self.group, self.volumes,
|
||||||
|
group_snapshot=self.fake_grp_snap,
|
||||||
|
snapshots=self.snapshots)
|
||||||
|
|
||||||
|
result_model_update, result_volumes_model_update = (
|
||||||
|
self.driver.create_group_from_src(
|
||||||
|
self.ctx, self.group, self.volumes,
|
||||||
|
group_snapshot=self.fake_grp_snap,
|
||||||
|
snapshots=self.snapshots))
|
||||||
|
self.assertEqual(fields.GroupStatus.AVAILABLE,
|
||||||
|
result_model_update['status'])
|
||||||
|
get_pid = lambda snapshot: snapshot['provider_id']
|
||||||
|
volume_provider_list = list(map(get_pid, result_volumes_model_update))
|
||||||
|
self.assertListEqual(volume_provider_list, ['sid1', 'sid2'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_delete_group_snapshot(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test deleting group snapshot
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns model updates
|
||||||
|
"""
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||||
|
|
||||||
|
self.snapshots[0].volume = self.volumes[0]
|
||||||
|
self.snapshots[1].volume = self.volumes[1]
|
||||||
|
self.snapshots[0].provider_id = fake.PROVIDER_ID
|
||||||
|
self.snapshots[1].provider_id = fake.PROVIDER2_ID
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.delete_group_snapshot,
|
||||||
|
self.ctx,
|
||||||
|
self.group,
|
||||||
|
self.snapshots)
|
||||||
|
|
||||||
|
result_model_update, result_snapshot_model_update = (
|
||||||
|
self.driver.delete_group_snapshot(
|
||||||
|
self.ctx,
|
||||||
|
self.group,
|
||||||
|
self.snapshots
|
||||||
|
))
|
||||||
|
self.assertEqual(fields.GroupSnapshotStatus.DELETED,
|
||||||
|
result_model_update['status'])
|
||||||
|
self.assertTrue(all(snapshot['status'] == 'deleted' for snapshot in
|
||||||
|
result_snapshot_model_update))
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type')
|
||||||
|
def test_create_group_snapshot(self, is_group_a_cg_snapshot_type):
|
||||||
|
"""Test creating group snapshot
|
||||||
|
|
||||||
|
should throw NotImplementedError, is_group_a_cg_snapshot_type=False
|
||||||
|
otherwise returns model updates
|
||||||
|
"""
|
||||||
|
is_group_a_cg_snapshot_type.side_effect = [False, True]
|
||||||
|
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError,
|
||||||
|
self.driver.create_group_snapshot,
|
||||||
|
self.ctx,
|
||||||
|
self.group,
|
||||||
|
self.snapshots)
|
||||||
|
|
||||||
|
result_model_update, result_snapshot_model_update = (
|
||||||
|
self.driver.create_group_snapshot(
|
||||||
|
self.ctx,
|
||||||
|
self.group,
|
||||||
|
self.snapshots
|
||||||
|
))
|
||||||
|
self.assertEqual(fields.GroupSnapshotStatus.AVAILABLE,
|
||||||
|
result_model_update['status'])
|
||||||
|
self.assertTrue(all(snapshot['status'] == 'available' for snapshot in
|
||||||
|
result_snapshot_model_update))
|
||||||
|
get_pid = lambda snapshot: snapshot['provider_id']
|
||||||
|
snapshot_provider_list = list(map(get_pid,
|
||||||
|
result_snapshot_model_update))
|
||||||
|
|
||||||
|
self.assertListEqual(['sid1', 'sid2'], snapshot_provider_list)
|
@ -36,9 +36,12 @@ from cinder.i18n import _
|
|||||||
from cinder.image import image_utils
|
from cinder.image import image_utils
|
||||||
from cinder import interface
|
from cinder import interface
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
|
from cinder.objects import fields
|
||||||
from cinder.volume import driver
|
from cinder.volume import driver
|
||||||
from cinder.volume.drivers.san import san
|
from cinder.volume.drivers.san import san
|
||||||
from cinder.volume import qos_specs
|
from cinder.volume import qos_specs
|
||||||
|
from cinder.volume import utils as volume_utils
|
||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -114,9 +117,10 @@ SIO_MAX_OVERSUBSCRIPTION_RATIO = 10.0
|
|||||||
class ScaleIODriver(driver.VolumeDriver):
|
class ScaleIODriver(driver.VolumeDriver):
|
||||||
"""Dell EMC ScaleIO Driver."""
|
"""Dell EMC ScaleIO Driver."""
|
||||||
|
|
||||||
VERSION = "2.0.1"
|
VERSION = "2.0.2"
|
||||||
# Major changes
|
# Major changes
|
||||||
# 2.0.1: Added support for SIO 1.3x in addition to 2.0.x
|
# 2.0.1: Added support for SIO 1.3x in addition to 2.0.x
|
||||||
|
# 2.0.2: Added consistency group support to generic volume groups
|
||||||
|
|
||||||
# ThirdPartySystems wiki
|
# ThirdPartySystems wiki
|
||||||
CI_WIKI_NAME = "EMC_ScaleIO_CI"
|
CI_WIKI_NAME = "EMC_ScaleIO_CI"
|
||||||
@ -826,7 +830,7 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
stats['storage_protocol'] = 'scaleio'
|
stats['storage_protocol'] = 'scaleio'
|
||||||
stats['reserved_percentage'] = 0
|
stats['reserved_percentage'] = 0
|
||||||
stats['QoS_support'] = True
|
stats['QoS_support'] = True
|
||||||
stats['consistencygroup_support'] = True
|
stats['consistent_group_snapshot_enabled'] = True
|
||||||
stats['thick_provisioning_support'] = True
|
stats['thick_provisioning_support'] = True
|
||||||
stats['thin_provisioning_support'] = True
|
stats['thin_provisioning_support'] = True
|
||||||
pools = []
|
pools = []
|
||||||
@ -951,7 +955,7 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
'total_capacity_gb': total_capacity_gb,
|
'total_capacity_gb': total_capacity_gb,
|
||||||
'free_capacity_gb': free_capacity_gb,
|
'free_capacity_gb': free_capacity_gb,
|
||||||
'QoS_support': True,
|
'QoS_support': True,
|
||||||
'consistencygroup_support': True,
|
'consistent_group_snapshot_enabled': True,
|
||||||
'reserved_percentage': 0,
|
'reserved_percentage': 0,
|
||||||
'thin_provisioning_support': True,
|
'thin_provisioning_support': True,
|
||||||
'thick_provisioning_support': True,
|
'thick_provisioning_support': True,
|
||||||
@ -1251,24 +1255,44 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_consistencygroup(self, context, group):
|
def create_group(self, context, group):
|
||||||
"""Creates a consistency group.
|
"""Creates a group.
|
||||||
|
|
||||||
|
:param context: the context of the caller.
|
||||||
|
:param group: the group object.
|
||||||
|
:returns: model_update
|
||||||
|
|
||||||
ScaleIO won't create CG until cg-snapshot creation,
|
ScaleIO won't create CG until cg-snapshot creation,
|
||||||
db will maintain the volumes and CG relationship.
|
db will maintain the volumes and CG relationship.
|
||||||
"""
|
"""
|
||||||
LOG.info("Creating Consistency Group")
|
|
||||||
model_update = {'status': 'available'}
|
# let generic volume group support handle non-cgsnapshots
|
||||||
|
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
LOG.info("Creating Group")
|
||||||
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
return model_update
|
return model_update
|
||||||
|
|
||||||
def delete_consistencygroup(self, context, group, volumes):
|
def delete_group(self, context, group, volumes):
|
||||||
"""Deletes a consistency group.
|
"""Deletes a group.
|
||||||
|
|
||||||
|
:param context: the context of the caller.
|
||||||
|
:param group: the group object.
|
||||||
|
:param volumes: a list of volume objects in the group.
|
||||||
|
:returns: model_update, volumes_model_update
|
||||||
|
|
||||||
ScaleIO will delete the volumes of the CG.
|
ScaleIO will delete the volumes of the CG.
|
||||||
"""
|
"""
|
||||||
LOG.info("Deleting Consistency Group")
|
|
||||||
model_update = {'status': 'deleted'}
|
# let generic volume group support handle non-cgsnapshots
|
||||||
error_statuses = ['error', 'error_deleting']
|
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
LOG.info("Deleting Group")
|
||||||
|
model_update = {'status': fields.GroupStatus.DELETED}
|
||||||
|
error_statuses = [fields.GroupStatus.ERROR,
|
||||||
|
fields.GroupStatus.ERROR_DELETING]
|
||||||
volumes_model_update = []
|
volumes_model_update = []
|
||||||
for volume in volumes:
|
for volume in volumes:
|
||||||
try:
|
try:
|
||||||
@ -1282,13 +1306,24 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
volumes_model_update.append(update_item)
|
volumes_model_update.append(update_item)
|
||||||
if model_update['status'] not in error_statuses:
|
if model_update['status'] not in error_statuses:
|
||||||
model_update['status'] = 'error_deleting'
|
model_update['status'] = 'error_deleting'
|
||||||
LOG.error("Failed to delete the volume %(vol)s of CG. "
|
LOG.error("Failed to delete the volume %(vol)s of group. "
|
||||||
"Exception: %(exception)s.",
|
"Exception: %(exception)s.",
|
||||||
{'vol': volume['name'], 'exception': err})
|
{'vol': volume['name'], 'exception': err})
|
||||||
return model_update, volumes_model_update
|
return model_update, volumes_model_update
|
||||||
|
|
||||||
def create_cgsnapshot(self, context, cgsnapshot, snapshots):
|
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
"""Creates a cgsnapshot."""
|
"""Creates a group snapshot.
|
||||||
|
|
||||||
|
:param context: the context of the caller.
|
||||||
|
:param group_snapshot: the GroupSnapshot object to be created.
|
||||||
|
:param snapshots: a list of Snapshot objects in the group_snapshot.
|
||||||
|
:returns: model_update, snapshots_model_update
|
||||||
|
"""
|
||||||
|
|
||||||
|
# let generic volume group support handle non-cgsnapshots
|
||||||
|
if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
get_scaleio_snapshot_params = lambda snapshot: {
|
get_scaleio_snapshot_params = lambda snapshot: {
|
||||||
'volumeId': snapshot.volume['provider_id'],
|
'volumeId': snapshot.volume['provider_id'],
|
||||||
'snapshotName': self._id_to_base64(snapshot['id'])}
|
'snapshotName': self._id_to_base64(snapshot['id'])}
|
||||||
@ -1304,46 +1339,74 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
snapshot_model_update = []
|
snapshot_model_update = []
|
||||||
for snapshot, scaleio_id in zip(snapshots, response['volumeIdList']):
|
for snapshot, scaleio_id in zip(snapshots, response['volumeIdList']):
|
||||||
update_item = {'id': snapshot['id'],
|
update_item = {'id': snapshot['id'],
|
||||||
'status': 'available',
|
'status': fields.SnapshotStatus.AVAILABLE,
|
||||||
'provider_id': scaleio_id}
|
'provider_id': scaleio_id}
|
||||||
snapshot_model_update.append(update_item)
|
snapshot_model_update.append(update_item)
|
||||||
model_update = {'status': 'available'}
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
return model_update, snapshot_model_update
|
return model_update, snapshot_model_update
|
||||||
|
|
||||||
def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
|
def delete_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
"""Deletes a cgsnapshot."""
|
"""Deletes a snapshot.
|
||||||
error_statuses = ['error', 'error_deleting']
|
|
||||||
model_update = {'status': cgsnapshot['status']}
|
:param context: the context of the caller.
|
||||||
|
:param group_snapshot: the GroupSnapshot object to be deleted.
|
||||||
|
:param snapshots: a list of snapshot objects in the group_snapshot.
|
||||||
|
:returns: model_update, snapshots_model_update
|
||||||
|
"""
|
||||||
|
|
||||||
|
# let generic volume group support handle non-cgsnapshots
|
||||||
|
if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
error_statuses = [fields.SnapshotStatus.ERROR,
|
||||||
|
fields.SnapshotStatus.ERROR_DELETING]
|
||||||
|
model_update = {'status': group_snapshot['status']}
|
||||||
snapshot_model_update = []
|
snapshot_model_update = []
|
||||||
for snapshot in snapshots:
|
for snapshot in snapshots:
|
||||||
try:
|
try:
|
||||||
self._delete_volume(snapshot.provider_id)
|
self._delete_volume(snapshot.provider_id)
|
||||||
update_item = {'id': snapshot['id'],
|
update_item = {'id': snapshot['id'],
|
||||||
'status': 'deleted'}
|
'status': fields.SnapshotStatus.DELETED}
|
||||||
snapshot_model_update.append(update_item)
|
snapshot_model_update.append(update_item)
|
||||||
except exception.VolumeBackendAPIException as err:
|
except exception.VolumeBackendAPIException as err:
|
||||||
update_item = {'id': snapshot['id'],
|
update_item = {'id': snapshot['id'],
|
||||||
'status': 'error_deleting'}
|
'status': fields.SnapshotStatus.ERROR_DELETING}
|
||||||
snapshot_model_update.append(update_item)
|
snapshot_model_update.append(update_item)
|
||||||
if model_update['status'] not in error_statuses:
|
if model_update['status'] not in error_statuses:
|
||||||
model_update['status'] = 'error_deleting'
|
model_update['status'] = (
|
||||||
|
fields.SnapshotStatus.ERROR_DELETING)
|
||||||
LOG.error("Failed to delete the snapshot %(snap)s "
|
LOG.error("Failed to delete the snapshot %(snap)s "
|
||||||
"of cgsnapshot: %(cgsnapshot_id)s. "
|
"of snapshot: %(snapshot_id)s. "
|
||||||
"Exception: %(exception)s.",
|
"Exception: %(exception)s.",
|
||||||
{'snap': snapshot['name'],
|
{'snap': snapshot['name'],
|
||||||
'exception': err,
|
'exception': err,
|
||||||
'cgsnapshot_id': cgsnapshot.id})
|
'snapshot_id': group_snapshot.id})
|
||||||
model_update['status'] = 'deleted'
|
model_update['status'] = fields.GroupSnapshotStatus.DELETED
|
||||||
return model_update, snapshot_model_update
|
return model_update, snapshot_model_update
|
||||||
|
|
||||||
def create_consistencygroup_from_src(self, context, group, volumes,
|
def create_group_from_src(self, context, group, volumes,
|
||||||
cgsnapshot=None, snapshots=None,
|
group_snapshot=None, snapshots=None,
|
||||||
source_cg=None, source_vols=None):
|
source_group=None, source_vols=None):
|
||||||
"""Creates a consistency group from a source."""
|
"""Creates a group from source.
|
||||||
|
|
||||||
|
:param context: the context of the caller.
|
||||||
|
:param group: the Group object to be created.
|
||||||
|
:param volumes: a list of Volume objects in the group.
|
||||||
|
:param group_snapshot: the GroupSnapshot object as source.
|
||||||
|
:param snapshots: a list of snapshot objects in group_snapshot.
|
||||||
|
:param source_group: the Group object as source.
|
||||||
|
:param source_vols: a list of volume objects in the source_group.
|
||||||
|
:returns: model_update, volumes_model_update
|
||||||
|
"""
|
||||||
|
|
||||||
|
# let generic volume group support handle non-cgsnapshots
|
||||||
|
if not volume_utils.is_group_a_cg_snapshot_type(group):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
get_scaleio_snapshot_params = lambda src_volume, trg_volume: {
|
get_scaleio_snapshot_params = lambda src_volume, trg_volume: {
|
||||||
'volumeId': src_volume['provider_id'],
|
'volumeId': src_volume['provider_id'],
|
||||||
'snapshotName': self._id_to_base64(trg_volume['id'])}
|
'snapshotName': self._id_to_base64(trg_volume['id'])}
|
||||||
if cgsnapshot and snapshots:
|
if group_snapshot and snapshots:
|
||||||
snapshot_defs = map(get_scaleio_snapshot_params,
|
snapshot_defs = map(get_scaleio_snapshot_params,
|
||||||
snapshots,
|
snapshots,
|
||||||
volumes)
|
volumes)
|
||||||
@ -1365,17 +1428,29 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
'status': 'available',
|
'status': 'available',
|
||||||
'provider_id': scaleio_id}
|
'provider_id': scaleio_id}
|
||||||
volumes_model_update.append(update_item)
|
volumes_model_update.append(update_item)
|
||||||
model_update = {'status': 'available'}
|
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||||
return model_update, volumes_model_update
|
return model_update, volumes_model_update
|
||||||
|
|
||||||
def update_consistencygroup(self, context, group,
|
def update_group(self, context, group,
|
||||||
add_volumes=None, remove_volumes=None):
|
add_volumes=None, remove_volumes=None):
|
||||||
"""Update a consistency group.
|
"""Update a group.
|
||||||
|
|
||||||
|
:param context: the context of the caller.
|
||||||
|
:param group: the group object.
|
||||||
|
:param add_volumes: a list of volume objects to be added.
|
||||||
|
:param remove_volumes: a list of volume objects to be removed.
|
||||||
|
:returns: model_update, add_volumes_update, remove_volumes_update
|
||||||
|
|
||||||
ScaleIO does not handle volume grouping.
|
ScaleIO does not handle volume grouping.
|
||||||
Cinder maintains volumes and CG relationship.
|
Cinder maintains volumes and CG relationship.
|
||||||
"""
|
"""
|
||||||
return None, None, None
|
|
||||||
|
if volume_utils.is_group_a_cg_snapshot_type(group):
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# we'll rely on the generic group implementation if it is not a
|
||||||
|
# consistency group request.
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _snapshot_volume_group(self, snapshot_defs):
|
def _snapshot_volume_group(self, snapshot_defs):
|
||||||
LOG.info("ScaleIO snapshot group of volumes")
|
LOG.info("ScaleIO snapshot group of volumes")
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added consistency group support to generic volume groups in ScaleIO Driver.
|
Loading…
Reference in New Issue
Block a user