]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Manage/unmanage volume in ScaleIO driver
authorXing Yang <xing.yang@emc.com>
Sun, 22 Nov 2015 03:19:12 +0000 (22:19 -0500)
committerXing Yang <xing.yang@emc.com>
Fri, 12 Feb 2016 15:16:58 +0000 (10:16 -0500)
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

cinder/tests/unit/volume/drivers/emc/scaleio/mocks.py
cinder/tests/unit/volume/drivers/emc/scaleio/test_delete_snapshot.py
cinder/tests/unit/volume/drivers/emc/scaleio/test_manage_existing.py [new file with mode: 0644]
cinder/volume/drivers/emc/scaleio.py
releasenotes/notes/scaleio-manage-existing-32217f6d1c295193.yaml [new file with mode: 0644]

index 172e457f667b61f8e4f031f81b574c5793ea4ff5..1e2c9e9b5dae9f65f6befffb58297ff922394f16 100644 (file)
@@ -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
 
index 6d49f718bcde1dd250098281240b85fec43d0ce9..106bd54a06c1b78758bd7e0272caa2d0c2c28218 100644 (file)
@@ -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 (file)
index 0000000..5479741
--- /dev/null
@@ -0,0 +1,126 @@
+# Copyright (c) 2016 EMC Corporation.\r
+# All Rights Reserved.\r
+#\r
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+#    not use this file except in compliance with the License. You may obtain\r
+#    a copy of the License at\r
+#\r
+#         http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+#    Unless required by applicable law or agreed to in writing, software\r
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+#    License for the specific language governing permissions and limitations\r
+#    under the License.\r
+\r
+from cinder import context\r
+from cinder import exception\r
+from cinder.tests.unit import fake_volume\r
+from cinder.tests.unit.volume.drivers.emc import scaleio\r
+from cinder.tests.unit.volume.drivers.emc.scaleio import mocks\r
+from cinder.volume import volume_types\r
+from mock import patch\r
+from six.moves import urllib\r
+\r
+\r
+class TestManageExisting(scaleio.TestScaleIODriver):\r
+    """Test cases for ``ScaleIODriver.manage_existing()``"""\r
+\r
+    def setUp(self):\r
+        """Setup a test case environment.\r
+\r
+        Creates a fake volume object and sets up the required API responses.\r
+        """\r
+        super(TestManageExisting, self).setUp()\r
+        ctx = context.RequestContext('fake', 'fake', auth_token=True)\r
+        self.volume = fake_volume.fake_volume_obj(\r
+            ctx, **{'provider_id': 'pid_1'})\r
+        self.volume_attached = fake_volume.fake_volume_obj(\r
+            ctx, **{'provider_id': 'pid_2'})\r
+        self.volume_no_provider_id = fake_volume.fake_volume_obj(ctx)\r
+        self.volume_name_2x_enc = urllib.parse.quote(\r
+            urllib.parse.quote(self.driver._id_to_base64(self.volume.id))\r
+        )\r
+\r
+        self.HTTPS_MOCK_RESPONSES = {\r
+            self.RESPONSE_MODE.Valid: {\r
+                'instances/Volume::' + self.volume['provider_id']:\r
+                    mocks.MockHTTPSResponse({\r
+                        'id': 'pid_1',\r
+                        'sizeInKb': 8388608,\r
+                        'mappedSdcInfo': None\r
+                    }, 200)\r
+            },\r
+            self.RESPONSE_MODE.BadStatus: {\r
+                'instances/Volume::' + self.volume['provider_id']:\r
+                    mocks.MockHTTPSResponse({\r
+                        'errorCode': 401,\r
+                        'message': 'BadStatus Volume Test',\r
+                    }, 401),\r
+                'instances/Volume::' + self.volume_attached['provider_id']:\r
+                    mocks.MockHTTPSResponse({\r
+                        'id': 'pid_2',\r
+                        'sizeInKb': 8388608,\r
+                        'mappedSdcInfo': 'Mapped'\r
+                    }, 200)\r
+            }\r
+        }\r
+\r
+    def test_no_source_id(self):\r
+        existing_ref = {'source-name': 'scaleioVolName'}\r
+        self.assertRaises(exception.ManageExistingInvalidReference,\r
+                          self.driver.manage_existing, self.volume,\r
+                          existing_ref)\r
+\r
+    def test_no_type_id(self):\r
+        self.volume['volume_type_id'] = None\r
+        existing_ref = {'source-id': 'pid_1'}\r
+        self.assertRaises(exception.ManageExistingVolumeTypeMismatch,\r
+                          self.driver.manage_existing, self.volume,\r
+                          existing_ref)\r
+\r
+    @patch.object(\r
+        volume_types,\r
+        'get_volume_type',\r
+        return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}})\r
+    def test_volume_not_found(self, _mock_volume_type):\r
+        self.volume['volume_type_id'] = 'ScaleIO'\r
+        existing_ref = {'source-id': 'pid_1'}\r
+        self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)\r
+        self.assertRaises(exception.ManageExistingInvalidReference,\r
+                          self.driver.manage_existing, self.volume,\r
+                          existing_ref)\r
+\r
+    @patch.object(\r
+        volume_types,\r
+        'get_volume_type',\r
+        return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}})\r
+    def test_volume_attached(self, _mock_volume_type):\r
+        self.volume_attached['volume_type_id'] = 'ScaleIO'\r
+        existing_ref = {'source-id': 'pid_2'}\r
+        self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)\r
+        self.assertRaises(exception.ManageExistingInvalidReference,\r
+                          self.driver.manage_existing, self.volume_attached,\r
+                          existing_ref)\r
+\r
+    @patch.object(\r
+        volume_types,\r
+        'get_volume_type',\r
+        return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}})\r
+    def test_manage_get_size_calc(self, _mock_volume_type):\r
+        self.volume['volume_type_id'] = 'ScaleIO'\r
+        existing_ref = {'source-id': 'pid_1'}\r
+        self.set_https_response_mode(self.RESPONSE_MODE.Valid)\r
+        result = self.driver.manage_existing_get_size(self.volume,\r
+                                                      existing_ref)\r
+        self.assertEqual(8, result)\r
+\r
+    @patch.object(\r
+        volume_types,\r
+        'get_volume_type',\r
+        return_value={'extra_specs': {'volume_backend_name': 'ScaleIO'}})\r
+    def test_manage_existing_valid(self, _mock_volume_type):\r
+        self.volume['volume_type_id'] = 'ScaleIO'\r
+        existing_ref = {'source-id': 'pid_1'}\r
+        result = self.driver.manage_existing(self.volume, existing_ref)\r
+        self.assertEqual('pid_1', result['provider_id'])\r
index fb96405c34763561a55b25d0c44995fdb0208500..76bf97b00eb41dda586413e5ebb48288b281222d 100644 (file)
@@ -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': <id of ScaleIO volume>}
+        """
+        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 (file)
index 0000000..ca9ded7
--- /dev/null
@@ -0,0 +1,3 @@
+---
+features:
+  - Adds support for manage/unmanage volume in the ScaleIO driver.