]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
NetApp: Fix volume extend with E-Series
authorMichael Price <michael.price@netapp.com>
Tue, 25 Aug 2015 22:36:44 +0000 (17:36 -0500)
committerMichael Price <michael.price@netapp.com>
Wed, 16 Sep 2015 20:15:35 +0000 (20:15 +0000)
Thin provisioned volumes were being converted to thick provisioned
volumes by extend_volume. When thin provisioning support was added,
the extend_volume method was not updated to use the correct API
for performing the capacity expansion operation.

This patch changes the volume extension strategy when a volume is
thin provisioned to use the correct API. With a thick provisioned
volume, a clone will be created with the new capacity, and the old
volume removed, but a thin provisioned volume's capacity can simply
be expanded directly, because that is merely the capacity that the user
sees, as opposed to the backend provisioned capacity.

Closes-Bug: 1493874
Change-Id: Iec25f29381d5fba3409d1de83bf6816ae1a9ac22

cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py
cinder/volume/drivers/netapp/eseries/client.py
cinder/volume/drivers/netapp/eseries/library.py

index 95a05e9b901abb126a8958a192b2abeda1612309..686166c0cbd1f811e882caf1e599ba0ebe176021 100644 (file)
@@ -52,6 +52,7 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
         fake_response = mock.Mock()
         fake_response.status_code = 200
         self.my_client.invoke_service = mock.Mock(return_value=fake_response)
+        self.my_client.api_version = '01.52.9000.1'
 
     @ddt.data(200, 201, 203, 204)
     def test_eval_response_success(self, status_code):
@@ -631,6 +632,37 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
                                                        )
         self.assertDictMatch(expected_volume, updated_volume)
 
+    def test_extend_volume(self):
+        new_capacity = 10
+        fake_volume = copy.deepcopy(eseries_fake.VOLUME)
+        self.my_client.features = mock.Mock()
+        self.my_client.features.SSC_API_V2 = na_utils.FeatureState(
+            supported=True)
+        self.my_client._invoke = mock.Mock(return_value=fake_volume)
+
+        expanded_volume = self.my_client.expand_volume(fake_volume['id'],
+                                                       new_capacity)
+
+        url = self.my_client.RESOURCE_PATHS.get('ssc_volume')
+        body = {'newSize': new_capacity, 'sizeUnit': 'gb'}
+        self.my_client._invoke.assert_called_once_with('POST', url, body,
+                                                       **{'object-id':
+                                                          fake_volume['id']})
+        self.assertEqual(fake_volume, expanded_volume)
+
+    def test_extend_volume_unsupported(self):
+        new_capacity = 10
+        min_version = 1
+        fake_volume = copy.deepcopy(eseries_fake.VOLUME)
+        self.my_client.features = mock.Mock()
+        self.my_client.features.SSC_API_V2 = na_utils.FeatureState(
+            supported=False, minimum_version=min_version)
+        self.my_client._invoke = mock.Mock(return_value=fake_volume)
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.my_client.expand_volume, fake_volume['id'],
+                          new_capacity)
+
     @ddt.data(True, False)
     def test_delete_volume(self, ssc_api_enabled):
         fake_volume = copy.deepcopy(eseries_fake.VOLUME)
index 3657af40e021b1cdb6d3a9ff3caddfc32c3da427..eb0f9a48392235f9bb135ae53f90d0670ec688ed 100644 (file)
@@ -1051,6 +1051,67 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
         # Ensure the volume we created is not cleaned up
         self.assertEqual(0, self.library._client.delete_volume.call_count)
 
+    def test_extend_volume(self):
+        fake_volume = copy.deepcopy(get_fake_volume())
+        volume = copy.deepcopy(eseries_fake.VOLUME)
+        new_capacity = 10
+        volume['objectType'] = 'volume'
+        self.library.create_cloned_volume = mock.Mock()
+        self.library._get_volume = mock.Mock(return_value=volume)
+        self.library._client.update_volume = mock.Mock()
+
+        self.library.extend_volume(fake_volume, new_capacity)
+
+        self.library.create_cloned_volume.assert_called_with(mock.ANY,
+                                                             fake_volume)
+
+    def test_extend_volume_thin(self):
+        fake_volume = copy.deepcopy(get_fake_volume())
+        volume = copy.deepcopy(eseries_fake.VOLUME)
+        new_capacity = 10
+        volume['objectType'] = 'thinVolume'
+        self.library._client.expand_volume = mock.Mock(return_value=volume)
+        self.library._get_volume = mock.Mock(return_value=volume)
+
+        self.library.extend_volume(fake_volume, new_capacity)
+
+        self.library._client.expand_volume.assert_called_with(volume['id'],
+                                                              new_capacity)
+
+    def test_extend_volume_stage_2_failure(self):
+        fake_volume = copy.deepcopy(get_fake_volume())
+        volume = copy.deepcopy(eseries_fake.VOLUME)
+        new_capacity = 10
+        volume['objectType'] = 'volume'
+        self.library.create_cloned_volume = mock.Mock()
+        self.library._client.delete_volume = mock.Mock()
+        # Create results for multiple calls to _get_volume and _update_volume
+        get_volume_results = [volume, {'id': 'newId', 'label': 'newVolume'}]
+        self.library._get_volume = mock.Mock(side_effect=get_volume_results)
+        update_volume_results = [volume, exception.NetAppDriverException,
+                                 volume]
+        self.library._client.update_volume = mock.Mock(
+            side_effect=update_volume_results)
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.extend_volume, fake_volume,
+                          new_capacity)
+        self.assertTrue(self.library._client.delete_volume.called)
+
+    def test_extend_volume_stage_1_failure(self):
+        fake_volume = copy.deepcopy(get_fake_volume())
+        volume = copy.deepcopy(eseries_fake.VOLUME)
+        new_capacity = 10
+        volume['objectType'] = 'volume'
+        self.library.create_cloned_volume = mock.Mock()
+        self.library._get_volume = mock.Mock(return_value=volume)
+        self.library._client.update_volume = mock.Mock(
+            side_effect=exception.NetAppDriverException)
+
+        self.assertRaises(exception.NetAppDriverException,
+                          self.library.extend_volume, fake_volume,
+                          new_capacity)
+
     def test_delete_non_existing_volume(self):
         volume2 = get_fake_volume()
         # Change to a nonexistent id.
index e045d36cf8a8ec82b65d8977db2c36bd6a39e725..7b96f02ad11108573cdf0377f477f022a21561ad 100644 (file)
@@ -350,6 +350,21 @@ class RestClient(object):
         data = {'name': label}
         return self._invoke('POST', path, data, **{'object-id': object_id})
 
+    def expand_volume(self, object_id, new_capacity, capacity_unit='gb'):
+        """Increase the capacity of a volume"""
+        if not self.features.SSC_API_V2:
+            msg = _("E-series proxy API version %(current_version)s does "
+                    "not support this expansion API. The proxy"
+                    " version must be at at least %(min_version)s")
+            min_version = self.features.SSC_API_V2.minimum_version
+            msg_args = {'current_version': self.api_version,
+                        'min_version': min_version}
+            raise exception.NetAppDriverException(msg % msg_args)
+
+        path = self.RESOURCE_PATHS.get('ssc_volume')
+        data = {'newSize': new_capacity, 'sizeUnit': capacity_unit}
+        return self._invoke('POST', path, data, **{'object-id': object_id})
+
     def get_volume_mappings(self):
         """Creates volume mapping on array."""
         path = "/storage-systems/{system-id}/volume-mappings"
index 8ed9765f5ace2900fec5a8eba0fb34f42dab0ec5..1f690915164f05a06d989032cf35207e83aa7328 100644 (file)
@@ -1144,28 +1144,37 @@ class NetAppESeriesLibrary(object):
                             "%s."), size_gb)
         return avl_pools
 
+    def _is_thin_provisioned(self, volume):
+        """Determine if a volume is thin provisioned"""
+        return volume.get('objectType') == 'thinVolume' or volume.get(
+            'thinProvisioned', False)
+
     def extend_volume(self, volume, new_size):
         """Extend an existing volume to the new size."""
-        stage_1, stage_2 = 0, 0
         src_vol = self._get_volume(volume['name_id'])
-        src_label = src_vol['label']
-        stage_label = 'tmp-%s' % utils.convert_uuid_to_es_fmt(uuid.uuid4())
-        extend_vol = {'id': uuid.uuid4(), 'size': new_size}
-        self.create_cloned_volume(extend_vol, volume)
-        new_vol = self._get_volume(extend_vol['id'])
-        try:
-            stage_1 = self._client.update_volume(src_vol['id'], stage_label)
-            stage_2 = self._client.update_volume(new_vol['id'], src_label)
-            new_vol = stage_2
-            LOG.info(_LI('Extended volume with label %s.'), src_label)
-        except exception.NetAppDriverException:
-            if stage_1 == 0:
-                with excutils.save_and_reraise_exception():
-                    self._client.delete_volume(new_vol['id'])
-            if stage_2 == 0:
-                with excutils.save_and_reraise_exception():
-                    self._client.update_volume(src_vol['id'], src_label)
-                    self._client.delete_volume(new_vol['id'])
+        if self._is_thin_provisioned(src_vol):
+            self._client.expand_volume(src_vol['id'], new_size)
+        else:
+            stage_1, stage_2 = 0, 0
+            src_label = src_vol['label']
+            stage_label = 'tmp-%s' % utils.convert_uuid_to_es_fmt(uuid.uuid4())
+            extend_vol = {'id': uuid.uuid4(), 'size': new_size}
+            self.create_cloned_volume(extend_vol, volume)
+            new_vol = self._get_volume(extend_vol['id'])
+            try:
+                stage_1 = self._client.update_volume(src_vol['id'],
+                                                     stage_label)
+                stage_2 = self._client.update_volume(new_vol['id'], src_label)
+                new_vol = stage_2
+                LOG.info(_LI('Extended volume with label %s.'), src_label)
+            except exception.NetAppDriverException:
+                if stage_1 == 0:
+                    with excutils.save_and_reraise_exception():
+                        self._client.delete_volume(new_vol['id'])
+                elif stage_2 == 0:
+                    with excutils.save_and_reraise_exception():
+                        self._client.update_volume(src_vol['id'], src_label)
+                        self._client.delete_volume(new_vol['id'])
 
     def _garbage_collect_tmp_vols(self):
         """Removes tmp vols with no snapshots."""