From fc00a865fe095e3d4d51726c890c7cedb1fb701a Mon Sep 17 00:00:00 2001 From: "Walter A. Boring IV" Date: Fri, 11 Mar 2016 02:28:08 -0800 Subject: [PATCH] 3PAR fix create_cloned_volume for larger size This patch fixes the 3PAR driver's create_cloned_volume call in the case where a larger size for the volume was specified. When the new volume size is the same as the source volume, we still use an 'online' copy, which is 3PAR speak for background copy. When the new volume size is larger than the source volume, we have to do an offline copy, which entails creating a fresh volume with the requested size, and then copying the bits from the old volume to the new. This copy happens on the 3PAR itself and can take some time to complete. This patch also updates the minimum client version to 4.2.0 Change-Id: Ie2da441b32017f38e743189060209084e1f20974 Closes-bug: #1554740 --- cinder/tests/unit/fake_hpe_3par_client.py | 2 +- cinder/tests/unit/test_hpe3par.py | 53 ++++++++- cinder/volume/drivers/hpe/hpe_3par_common.py | 116 +++++++++++++------ 3 files changed, 132 insertions(+), 39 deletions(-) diff --git a/cinder/tests/unit/fake_hpe_3par_client.py b/cinder/tests/unit/fake_hpe_3par_client.py index 56c5dd76d0f..b4567efd5f4 100644 --- a/cinder/tests/unit/fake_hpe_3par_client.py +++ b/cinder/tests/unit/fake_hpe_3par_client.py @@ -22,7 +22,7 @@ import mock from cinder.tests.unit import fake_hpe_client_exceptions as hpeexceptions hpe3par = mock.Mock() -hpe3par.version = "4.1.0" +hpe3par.version = "4.2.0" hpe3par.exceptions = hpeexceptions sys.modules['hpe3parclient'] = hpe3par diff --git a/cinder/tests/unit/test_hpe3par.py b/cinder/tests/unit/test_hpe3par.py index 463ce3ee203..3370684fe16 100644 --- a/cinder/tests/unit/test_hpe3par.py +++ b/cinder/tests/unit/test_hpe3par.py @@ -1740,7 +1740,8 @@ class HPE3PARBaseDriver(object): HPE3PAR_CPG2), 'source_volid': HPE3PARBaseDriver.VOLUME_ID} src_vref = {'id': HPE3PARBaseDriver.VOLUME_ID, - 'name': HPE3PARBaseDriver.VOLUME_NAME} + 'name': HPE3PARBaseDriver.VOLUME_NAME, + 'size': 2} model_update = self.driver.create_cloned_volume(volume, src_vref) self.assertIsNone(model_update) @@ -1765,6 +1766,53 @@ class HPE3PARBaseDriver(object): expected + self.standard_logout) + def test_create_cloned_volume_offline_copy(self): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + mock_client = self.setup_driver() + mock_client.getVolume.return_value = {'name': mock.ANY} + task_id = 1 + mock_client.copyVolume.return_value = {'taskid': task_id} + mock_client.getTask.return_value = {'status': 1} + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + + volume = {'name': HPE3PARBaseDriver.VOLUME_NAME, + 'id': HPE3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 5, + 'host': volume_utils.append_host(self.FAKE_HOST, + HPE3PAR_CPG2), + 'source_volid': HPE3PARBaseDriver.VOLUME_ID} + src_vref = {'id': HPE3PARBaseDriver.VOLUME_ID, + 'name': HPE3PARBaseDriver.VOLUME_NAME, + 'size': 2} + model_update = self.driver.create_cloned_volume(volume, src_vref) + self.assertIsNone(model_update) + + common = hpecommon.HPE3PARCommon(None) + vol_name = common._get_3par_vol_name(volume['id']) + src_vol_name = common._get_3par_vol_name(src_vref['id']) + optional = {'priority': 1} + comment = mock.ANY + + expected = [ + mock.call.createVolume(vol_name, 'fakepool', + 5120, comment), + mock.call.copyVolume( + src_vol_name, + vol_name, + None, + optional=optional), + mock.call.getTask(task_id), + ] + + mock_client.assert_has_calls( + self.standard_login + + expected + + self.standard_logout) + @mock.patch.object(volume_types, 'get_volume_type') def test_create_cloned_qos_volume(self, _mock_volume_types): _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_2 @@ -1776,7 +1824,8 @@ class HPE3PARBaseDriver(object): mock_create_client.return_value = mock_client src_vref = {'id': HPE3PARBaseDriver.CLONE_ID, - 'name': HPE3PARBaseDriver.VOLUME_NAME} + 'name': HPE3PARBaseDriver.VOLUME_NAME, + 'size': 2} volume = self.volume_qos.copy() host = "TEST_HOST" pool = "TEST_POOL" diff --git a/cinder/volume/drivers/hpe/hpe_3par_common.py b/cinder/volume/drivers/hpe/hpe_3par_common.py index 92a3f67e7f3..889cab5b97d 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_common.py +++ b/cinder/volume/drivers/hpe/hpe_3par_common.py @@ -71,7 +71,7 @@ from taskflow.patterns import linear_flow LOG = logging.getLogger(__name__) -MIN_CLIENT_VERSION = '4.1.0' +MIN_CLIENT_VERSION = '4.2.0' DEDUP_API_VERSION = 30201120 FLASH_CACHE_API_VERSION = 30201200 SRSTATLD_API_VERSION = 30201200 @@ -230,10 +230,11 @@ class HPE3PARCommon(object): 3.0.15 - Update replication to version 2.1 3.0.16 - Use same LUN ID for each VLUN path #1551994 3.0.17 - Don't fail on clearing 3PAR object volume key. bug #1546392 + 3.0.18 - create_cloned_volume account for larger size. bug #1554740 """ - VERSION = "3.0.17" + VERSION = "3.0.18" stats = {} @@ -1971,28 +1972,62 @@ class HPE3PARCommon(object): def create_cloned_volume(self, volume, src_vref): try: vol_name = self._get_3par_vol_name(volume['id']) - # create a temporary snapshot - snapshot = self._create_temp_snapshot(src_vref) + src_vol_name = self._get_3par_vol_name(src_vref['id']) - type_info = self.get_volume_settings_from_type(volume) + # if the sizes of the 2 volumes are the same + # we can do an online copy, which is a background process + # on the 3PAR that makes the volume instantly available. + # We can't resize a volume, while it's being copied. + if volume['size'] == src_vref['size']: + LOG.debug("Creating a clone of same size, using online copy.") + # create a temporary snapshot + snapshot = self._create_temp_snapshot(src_vref) - # make the 3PAR copy the contents. - # can't delete the original until the copy is done. - cpg = type_info['cpg'] - self._copy_volume(snapshot['name'], vol_name, cpg=cpg, - snap_cpg=type_info['snap_cpg'], - tpvv=type_info['tpvv'], - tdvv=type_info['tdvv']) + type_info = self.get_volume_settings_from_type(volume) + cpg = type_info['cpg'] - # v2 replication check - replication_flag = False - if self._volume_of_replicated_type(volume) and ( - self._do_volume_replication_setup(volume)): - replication_flag = True + # make the 3PAR copy the contents. + # can't delete the original until the copy is done. + self._copy_volume(snapshot['name'], vol_name, cpg=cpg, + snap_cpg=type_info['snap_cpg'], + tpvv=type_info['tpvv'], + tdvv=type_info['tdvv']) - return self._get_model_update(volume['host'], cpg, - replication=replication_flag, - provider_location=self.client.id) + # v2 replication check + replication_flag = False + if self._volume_of_replicated_type(volume) and ( + self._do_volume_replication_setup(volume)): + replication_flag = True + + return self._get_model_update(volume['host'], cpg, + replication=replication_flag, + provider_location=self.client.id) + else: + # The size of the new volume is different, so we have to + # copy the volume and wait. Do the resize after the copy + # is complete. + LOG.debug("Clone a volume with a different target size. " + "Using non-online copy.") + + # we first have to create the destination volume + model_update = self.create_volume(volume) + + optional = {'priority': 1} + body = self.client.copyVolume(src_vol_name, vol_name, None, + optional=optional) + task_id = body['taskid'] + + task_status = self._wait_for_task_completion(task_id) + if task_status['status'] is not self.client.TASK_DONE: + dbg = {'status': task_status, 'id': volume['id']} + msg = _('Copy volume task failed: create_cloned_volume ' + 'id=%(id)s, status=%(status)s.') % dbg + raise exception.CinderException(msg) + else: + LOG.debug('Copy volume completed: create_cloned_volume: ' + 'id=%s.', volume['id']) + + return model_update except hpeexceptions.HTTPForbidden: raise exception.NotAuthorized() @@ -2384,6 +2419,28 @@ class HPE3PARCommon(object): return {'_name_id': name_id, 'provider_location': provider_location} + def _wait_for_task_completion(self, task_id): + """This waits for a 3PAR background task complete or fail. + + This looks for a task to get out of the 'active' state. + """ + # Wait for the physical copy task to complete + def _wait_for_task(task_id): + status = self.client.getTask(task_id) + LOG.debug("3PAR Task id %(id)s status = %(status)s", + {'id': task_id, + 'status': status['status']}) + if status['status'] is not self.client.TASK_ACTIVE: + self._task_status = status + raise loopingcall.LoopingCallDone() + + self._task_status = None + timer = loopingcall.FixedIntervalLoopingCall( + _wait_for_task, task_id) + timer.start(interval=1).wait() + + return self._task_status + def _convert_to_base_volume(self, volume, new_cpg=None): try: type_info = self.get_volume_settings_from_type(volume) @@ -2405,23 +2462,10 @@ class HPE3PARCommon(object): LOG.debug('Copy volume scheduled: convert_to_base_volume: ' 'id=%s.', volume['id']) - # Wait for the physical copy task to complete - def _wait_for_task(task_id): - status = self.client.getTask(task_id) - LOG.debug("3PAR Task id %(id)s status = %(status)s", - {'id': task_id, - 'status': status['status']}) - if status['status'] is not self.client.TASK_ACTIVE: - self._task_status = status - raise loopingcall.LoopingCallDone() + task_status = self._wait_for_task_completion(task_id) - self._task_status = None - timer = loopingcall.FixedIntervalLoopingCall( - _wait_for_task, task_id) - timer.start(interval=1).wait() - - if self._task_status['status'] is not self.client.TASK_DONE: - dbg = {'status': self._task_status, 'id': volume['id']} + if task_status['status'] is not self.client.TASK_DONE: + dbg = {'status': task_status, 'id': volume['id']} msg = _('Copy volume task failed: convert_to_base_volume: ' 'id=%(id)s, status=%(status)s.') % dbg raise exception.CinderException(msg)