From: Xing Yang Date: Sun, 22 Nov 2015 03:19:12 +0000 (-0500) Subject: Manage/unmanage volume in ScaleIO driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=eac894c09f31801430e61e89f3dbad127125ed6b;p=openstack-build%2Fcinder-build.git Manage/unmanage volume in ScaleIO driver Add support for manage/unmanage volume in the ScaleIO driver. Also fixed an error code for volume not found. DocImpact Implements: blueprint scaleio-manage-existing Closes-Bug: #1545023 Change-Id: I14ad94905aaa7ea2bef7c75011a40c5d057e1cc0 --- diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py b/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py index 172e457f6..1e2c9e9b5 100644 --- a/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py @@ -59,9 +59,6 @@ class ScaleIODriver(scaleio.ScaleIODriver): def reenable_replication(self, context, volume): pass - def manage_existing(self, volume, existing_ref): - pass - def promote_replica(self, context, volume): pass @@ -78,9 +75,6 @@ class ScaleIODriver(scaleio.ScaleIODriver): def create_consistencygroup(self, context, group): pass - def manage_existing_get_size(self, volume, existing_ref): - pass - def unmanage(self, volume): pass diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py index 6d49f718b..106bd54a0 100644 --- a/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py @@ -84,7 +84,7 @@ class TestDeleteSnapShot(scaleio.TestScaleIODriver): def test_delete_invalid_snapshot_force_delete(self): self.driver.configuration.set_override('sio_force_delete', override=True) - self.set_https_response_mode(self.RESPONSE_MODE.Invalid) + self.set_https_response_mode(self.RESPONSE_MODE.Valid) self.driver.delete_snapshot(self.snapshot) def test_delete_invalid_snapshot(self): diff --git a/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing.py b/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing.py new file mode 100644 index 000000000..5479741c7 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing.py @@ -0,0 +1,126 @@ +# 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 cinder import context +from cinder import exception +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 +from mock import patch +from six.moves import urllib + + +class TestManageExisting(scaleio.TestScaleIODriver): + """Test cases for ``ScaleIODriver.manage_existing()``""" + + def setUp(self): + """Setup a test case environment. + + Creates a fake volume object and sets up the required API responses. + """ + super(TestManageExisting, self).setUp() + ctx = context.RequestContext('fake', 'fake', auth_token=True) + self.volume = fake_volume.fake_volume_obj( + ctx, **{'provider_id': 'pid_1'}) + self.volume_attached = fake_volume.fake_volume_obj( + ctx, **{'provider_id': 'pid_2'}) + self.volume_no_provider_id = fake_volume.fake_volume_obj(ctx) + self.volume_name_2x_enc = urllib.parse.quote( + urllib.parse.quote(self.driver._id_to_base64(self.volume.id)) + ) + + self.HTTPS_MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + 'instances/Volume::' + self.volume['provider_id']: + mocks.MockHTTPSResponse({ + 'id': 'pid_1', + 'sizeInKb': 8388608, + 'mappedSdcInfo': None + }, 200) + }, + self.RESPONSE_MODE.BadStatus: { + 'instances/Volume::' + self.volume['provider_id']: + mocks.MockHTTPSResponse({ + 'errorCode': 401, + 'message': 'BadStatus Volume Test', + }, 401), + 'instances/Volume::' + self.volume_attached['provider_id']: + mocks.MockHTTPSResponse({ + 'id': 'pid_2', + 'sizeInKb': 8388608, + 'mappedSdcInfo': 'Mapped' + }, 200) + } + } + + def test_no_source_id(self): + existing_ref = {'source-name': 'scaleioVolName'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, self.volume, + existing_ref) + + def test_no_type_id(self): + self.volume['volume_type_id'] = None + existing_ref = {'source-id': 'pid_1'} + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, self.volume, + existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_volume_not_found(self, _mock_volume_type): + self.volume['volume_type_id'] = 'ScaleIO' + existing_ref = {'source-id': 'pid_1'} + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, self.volume, + existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_volume_attached(self, _mock_volume_type): + self.volume_attached['volume_type_id'] = 'ScaleIO' + existing_ref = {'source-id': 'pid_2'} + self.set_https_response_mode(self.RESPONSE_MODE.BadStatus) + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, self.volume_attached, + existing_ref) + + @patch.object( + volume_types, + 'get_volume_type', + return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}}) + def test_manage_get_size_calc(self, _mock_volume_type): + self.volume['volume_type_id'] = 'ScaleIO' + existing_ref = {'source-id': 'pid_1'} + self.set_https_response_mode(self.RESPONSE_MODE.Valid) + result = self.driver.manage_existing_get_size(self.volume, + 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_valid(self, _mock_volume_type): + self.volume['volume_type_id'] = 'ScaleIO' + existing_ref = {'source-id': 'pid_1'} + result = self.driver.manage_existing(self.volume, existing_ref) + self.assertEqual('pid_1', result['provider_id']) diff --git a/cinder/volume/drivers/emc/scaleio.py b/cinder/volume/drivers/emc/scaleio.py index fb96405c3..76bf97b00 100644 --- a/cinder/volume/drivers/emc/scaleio.py +++ b/cinder/volume/drivers/emc/scaleio.py @@ -86,7 +86,7 @@ QOS_BANDWIDTH_LIMIT = 'maxBWS' BLOCK_SIZE = 8 OK_STATUS_CODE = 200 -VOLUME_NOT_FOUND_ERROR = 78 +VOLUME_NOT_FOUND_ERROR = 79 VOLUME_NOT_MAPPED_ERROR = 84 VOLUME_ALREADY_MAPPED_ERROR = 81 @@ -126,8 +126,7 @@ class ScaleIODriver(driver.VolumeDriver): if self.configuration.sio_storage_pools: self.storage_pools = [ e.strip() for e in - self.configuration.sio_storage_pools.split(',') - ] + self.configuration.sio_storage_pools.split(',')] self.storage_pool_name = self.configuration.sio_storage_pool_name self.storage_pool_id = self.configuration.sio_storage_pool_id @@ -240,11 +239,11 @@ class ScaleIODriver(driver.VolumeDriver): extraspecs_limit = storage_type.get(extraspecs_key) if extraspecs_limit is not None: if qos_limit is not None: - LOG.warning(_LW("QoS specs are overriding extra_specs.")) + LOG.warning(_LW("QoS specs are overriding extraspecs")) else: - LOG.info(_LI("Using extra_specs for defining QoS specs " - "will be deprecated in the N release " - "of OpenStack. Please use QoS specs.")) + LOG.info(_LI("Using extraspecs for defining QoS specs " + "will be deprecated in the next " + "version of OpenStack, please use QoS specs")) return qos_limit if qos_limit is not None else extraspecs_limit def _id_to_base64(self, id): @@ -261,9 +260,8 @@ class ScaleIODriver(driver.VolumeDriver): encoded_name = base64.b64encode(encoded_name) if six.PY3: encoded_name = encoded_name.decode('ascii') - LOG.debug( - "Converted id %(id)s to scaleio name %(name)s.", - {'id': id, 'name': encoded_name}) + LOG.debug("Converted id %(id)s to scaleio name %(name)s.", + {'id': id, 'name': encoded_name}) return encoded_name def create_volume(self, volume): @@ -430,8 +428,7 @@ class ScaleIODriver(driver.VolumeDriver): if not round_volume_capacity: exception_msg = (_( "Cannot create volume of size %s: " - "not multiple of 8GB.") % - size) + "not multiple of 8GB.") % size) LOG.error(exception_msg) raise exception.VolumeBackendAPIException(data=exception_msg) @@ -651,18 +648,15 @@ class ScaleIODriver(driver.VolumeDriver): if r.status_code != OK_STATUS_CODE: response = r.json() error_code = response['errorCode'] - if error_code == 78: - force_delete = self.configuration.sio_force_delete - if force_delete: - LOG.warning(_LW( - "Ignoring error in delete volume %s:" - " volume not found " - "due to force delete settings."), vol_id) - else: - msg = (_("Error deleting volume %s: volume not found.") % - vol_id) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) + if error_code == VOLUME_NOT_FOUND_ERROR: + LOG.warning(_LW( + "Ignoring error in delete volume %s:" + " Volume not found."), vol_id) + elif vol_id is None: + LOG.warning(_LW( + "Volume does not have provider_id thus does not " + "map to a ScaleIO volume. " + "Allowing deletion to proceed.")) else: msg = (_("Error deleting volume %(vol)s: %(err)s.") % {'vol': vol_id, @@ -1019,6 +1013,80 @@ class ScaleIODriver(driver.VolumeDriver): "%(new_name)s."), {'vol': vol_id, 'new_name': new_name}) + 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") + 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) + return int(response['sizeInKb'] / units.Mi) + + def _execute_scaleio_get_request(self, request): + r = requests.get( + request, + auth=( + self.server_username, + self.server_token), + verify=self._get_verify_cert()) + r = self._check_response(r, request) + response = r.json() + return r, response + + def _create_scaleio_get_volume_request(self, volume, existing_ref): + """Throws an exception if the input is invalid for manage existing. + + if the input is valid - return a request. + """ + type_id = volume.get('volume_type_id') + if 'source-id' not in existing_ref: + reason = _("Reference must contain source-id.") + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, + reason=reason + ) + if type_id is None: + reason = _("Volume must have a volume type") + raise exception.ManageExistingVolumeTypeMismatch( + existing_ref=existing_ref, + reason=reason + ) + vol_id = existing_ref['source-id'] + req_vars = {'server_ip': self.server_ip, + 'server_port': self.server_port, + 'id': vol_id} + request = ("https://%(server_ip)s:%(server_port)s" + "/api/instances/Volume::%(id)s" % req_vars) + LOG.info(_LI("ScaleIO get volume by id request: %s."), request) + return request + + def _manage_existing_check_legal_response(self, response, existing_ref): + if response.status_code != OK_STATUS_CODE: + reason = (_("Error managing volume: %s.") % response.json()[ + 'message']) + raise exception.ManageExistingInvalidReference( + existing_ref=existing_ref, + reason=reason + ) + def ensure_export(self, context, volume): """Driver entry point to get the export info for an existing volume.""" pass diff --git a/releasenotes/notes/scaleio-manage-existing-32217f6d1c295193.yaml b/releasenotes/notes/scaleio-manage-existing-32217f6d1c295193.yaml new file mode 100644 index 000000000..ca9ded754 --- /dev/null +++ b/releasenotes/notes/scaleio-manage-existing-32217f6d1c295193.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds support for manage/unmanage volume in the ScaleIO driver.