From 1861ed5836eb9475fe4d5cd41203b670c4e71626 Mon Sep 17 00:00:00 2001 From: Matan Sabag Date: Mon, 16 May 2016 04:18:23 -0700 Subject: [PATCH] Manage/unmanage snapshot in ScaleIO driver Add support for manage/unmanage snapshot in the ScaleIO driver. Change-Id: I1b6ff49294977bf086213355c240640117338dab DocImpact: Implements: blueprint scaleio-manage-existing-snapshot --- .../scaleio/test_manage_existing_snapshot.py | 154 ++++++++++++++++++ cinder/volume/drivers/emc/scaleio.py | 64 ++++++-- ...ge-existing-snapshot-5bbd1818654c0776.yaml | 3 + 3 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing_snapshot.py create mode 100644 releasenotes/notes/scaleio-manage-existing-snapshot-5bbd1818654c0776.yaml diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing_snapshot.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing_snapshot.py new file mode 100644 index 00000000000..33521bcebe0 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing_snapshot.py @@ -0,0 +1,154 @@ +# Copyright (c) 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. +from mock import patch + +from cinder import context +from cinder import exception +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.emc import scaleio +from cinder.tests.unit.volume.drivers.emc.scaleio import mocks +from cinder.volume import volume_types + + +class TestManageExistingSnapshot(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.manage_existing_snapshot()``""" + + def setUp(self): + """Setup a test case environment. + + Creates a fake volume object and sets up the required API responses. + """ + super(TestManageExistingSnapshot, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + self.volume = fake_volume.fake_volume_obj( + ctx, **{'provider_id': fake.PROVIDER_ID}) + self.snapshot = fake_snapshot.fake_snapshot_obj( + ctx, **{'provider_id': fake.PROVIDER2_ID}) + self.snapshot2 = fake_snapshot.fake_snapshot_obj( + ctx, **{'provider_id': fake.PROVIDER3_ID}) + self.snapshot.volume = self.snapshot2.volume = self.volume + self.snapshot['volume_type_id'] = fake.VOLUME_TYPE_ID + self.snapshot2['volume_type_id'] = fake.VOLUME_TYPE_ID + self.snapshot_attached = fake_snapshot.fake_snapshot_obj( + ctx, **{'provider_id': fake.PROVIDER3_ID}) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'instances/Volume::' + self.volume['provider_id']: + mocks.MockHTTPSResponse({ + 'id': fake.PROVIDER_ID, + 'sizeInKb': 8388608, + 'mappedSdcInfo': None, + 'ancestorVolumeId': None + }, 200), + 'instances/Volume::' + self.snapshot['provider_id']: + mocks.MockHTTPSResponse({ + 'id': fake.PROVIDER2_ID, + 'sizeInKb': 8388608, + 'mappedSdcInfo': None, + 'ancestorVolumeId': fake.PROVIDER_ID + }, 200), + 'instances/Volume::' + self.snapshot2['provider_id']: + mocks.MockHTTPSResponse({ + 'id': fake.PROVIDER3_ID, + 'sizeInKb': 8388608, + 'mappedSdcInfo': None, + 'ancestorVolumeId': fake.PROVIDER2_ID + }, 200) + }, + self.RESPONSE_MODE.BadStatus: { + 'instances/Volume::' + self.snapshot['provider_id']: + mocks.MockHTTPSResponse({ + 'errorCode': 401, + 'message': 'BadStatus Volume Test', + }, 401), + 'instances/Volume::' + self.snapshot2['provider_id']: + mocks.MockHTTPSResponse({ + 'id': fake.PROVIDER3_ID, + 'sizeInKb': 8388608, + 'ancestorVolumeId': fake.PROVIDER2_ID + }, 200), + 'instances/Volume::' + self.snapshot_attached['provider_id']: + mocks.MockHTTPSResponse({ + 'id': fake.PROVIDER3_ID, + 'sizeInKb': 8388608, + 'mappedSdcInfo': 'Mapped', + 'ancestorVolumeId': fake.PROVIDER_ID + }, 200) + } + } + + def test_no_source_id(self): + existing_ref = {'source-name': 'scaleioSnapName'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, self.snapshot, + existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_snapshot_not_found(self, _mock_volume_type): + existing_ref = {'source-id': fake.PROVIDER2_ID} + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, self.snapshot, + existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_snapshot_attached(self, _mock_volume_type): + self.snapshot_attached['volume_type_id'] = fake.VOLUME_TYPE_ID + existing_ref = {'source-id': fake.PROVIDER2_ID} + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + self.snapshot_attached, existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_different_ancestor(self, _mock_volume_type): + existing_ref = {'source-id': fake.PROVIDER3_ID} + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_snapshot, + self.snapshot2, existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_manage_snapshot_get_size_calc(self, _mock_volume_type): + existing_ref = {'source-id': fake.PROVIDER2_ID} + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + result = self.driver.manage_existing_snapshot_get_size( + self.snapshot, existing_ref) + self.assertEqual(8, result) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_manage_existing_snapshot_valid(self, _mock_volume_type): + existing_ref = {'source-id': fake.PROVIDER2_ID} + result = self.driver.manage_existing_snapshot( + self.snapshot, existing_ref) + self.assertEqual(fake.PROVIDER2_ID, result['provider_id']) diff --git a/cinder/volume/drivers/emc/scaleio.py b/cinder/volume/drivers/emc/scaleio.py index e11b809a1d2..57ee3de77c7 100644 --- a/cinder/volume/drivers/emc/scaleio.py +++ b/cinder/volume/drivers/emc/scaleio.py @@ -1022,31 +1022,60 @@ class ScaleIODriver(driver.VolumeDriver): "%(new_name)s."), {'vol': vol_id, 'new_name': new_name}) + def _query_scaleio_volume(self, volume, existing_ref): + request = self._create_scaleio_get_volume_request(volume, existing_ref) + r, response = self._execute_scaleio_get_request(request) + LOG.info(_LI("Get Volume response: %(res)s"), + {'res': response}) + self._manage_existing_check_legal_response(r, existing_ref) + return response + def manage_existing(self, volume, existing_ref): """Manage an existing ScaleIO volume. existing_ref is a dictionary of the form: {'source-id': } """ - request = self._create_scaleio_get_volume_request(volume, existing_ref) - r, response = self._execute_scaleio_get_request(request) - LOG.info(_LI("Get Volume response: %s"), response) - self._manage_existing_check_legal_response(r, existing_ref) - if response['mappedSdcInfo'] is not None: - reason = _("manage_existing cannot manage a volume " - "connected to hosts. Please disconnect this volume " - "from existing hosts before importing") + response = self._query_scaleio_volume(volume, existing_ref) + return {'provider_id': response['id']} + + def manage_existing_get_size(self, volume, existing_ref): + return self._get_volume_size(volume, existing_ref) + + def manage_existing_snapshot(self, snapshot, existing_ref): + """Manage an existing ScaleIO snapshot. + + :param existing_ref: dictionary of the form: + {'source-id': } + """ + response = self._query_scaleio_volume(snapshot, existing_ref) + not_real_parent = (response.get('orig_parent_overriden') or + response.get('is_source_deleted')) + if not_real_parent: + reason = (_("The snapshot's parent is not the original parent due " + "to deletion or revert action, therefore " + "this snapshot cannot be managed.")) + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, + reason=reason + ) + ancestor_id = response['ancestorVolumeId'] + volume_id = snapshot.volume.provider_id + if ancestor_id != volume_id: + reason = (_("The snapshot's parent in ScaleIO is %(ancestor)s " + "and not %(volume)s.") % + {'ancestor': ancestor_id, 'volume': volume_id}) raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason ) return {'provider_id': response['id']} - def manage_existing_get_size(self, volume, existing_ref): - request = self._create_scaleio_get_volume_request(volume, existing_ref) - r, response = self._execute_scaleio_get_request(request) - LOG.info(_LI("Get Volume response: %s"), response) - self._manage_existing_check_legal_response(r, existing_ref) + def manage_existing_snapshot_get_size(self, snapshot, existing_ref): + return self._get_volume_size(snapshot, existing_ref) + + def _get_volume_size(self, volume, existing_ref): + response = self._query_scaleio_volume(volume, existing_ref) return int(response['sizeInKb'] / units.Mi) def _execute_scaleio_get_request(self, request): @@ -1096,6 +1125,15 @@ class ScaleIODriver(driver.VolumeDriver): reason=reason ) + if response.json()['mappedSdcInfo'] is not None: + reason = _("manage_existing cannot manage a volume " + "connected to hosts. Please disconnect this volume " + "from existing hosts before importing.") + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, + reason=reason + ) + def create_consistencygroup(self, context, group): """Creates a consistency group. diff --git a/releasenotes/notes/scaleio-manage-existing-snapshot-5bbd1818654c0776.yaml b/releasenotes/notes/scaleio-manage-existing-snapshot-5bbd1818654c0776.yaml new file mode 100644 index 00000000000..7808000bb2e --- /dev/null +++ b/releasenotes/notes/scaleio-manage-existing-snapshot-5bbd1818654c0776.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support for manage/unmanage snapshot in the ScaleIO driver.