From ec7f21ff8b8c15734ab845ee10187a1005e979fd Mon Sep 17 00:00:00 2001 From: Ramy Asselin Date: Wed, 12 Feb 2014 16:23:15 -0800 Subject: [PATCH] 3PAR: Backend assisted volume migrate This patch implements volume migrate using native 3PAR APIs. Prerequisites: 1. Same 3PAR backend 2. Volume cannot be attached 3. Source and Dest CPGS are in the same domain Steps: 1. Create a temporary volume in the destination with a copy of the source 2. Wait for the copy to complete 3. Delete the source 4. Rename the temporary to the same name as the original Change-Id: Ia42503d41bf8afd908cd3ffb0288f0d353f313f1 Implements: blueprint native-3par-migrate-volume --- cinder/tests/test_hp3par.py | 156 +++++++++++++++++- .../volume/drivers/san/hp/hp_3par_common.py | 144 +++++++++++++++- cinder/volume/drivers/san/hp/hp_3par_fc.py | 11 +- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 11 +- 4 files changed, 314 insertions(+), 8 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 30e2ea88820..36d2ae6241a 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -184,6 +184,7 @@ class HP3PARBaseDriver(object): PORT_STATE_READY=client.HP3ParClient.PORT_STATE_READY, PORT_PROTO_ISCSI=client.HP3ParClient.PORT_PROTO_ISCSI, PORT_PROTO_FC=client.HP3ParClient.PORT_PROTO_FC, + TASK_DONE=client.HP3ParClient.TASK_DONE, HOST_EDIT_ADD=client.HP3ParClient.HOST_EDIT_ADD) def setup_mock_client(self, _m_client, driver, conf=None, m_conf=None): @@ -275,6 +276,8 @@ class HP3PARBaseDriver(object): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client mock_client = self.setup_driver() + mock_client.copyVolume.return_value = {'taskid': 1} + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', @@ -297,6 +300,151 @@ class HP3PARBaseDriver(object): mock_client.assert_has_calls(expected) + def test_migrate_volume(self): + + conf = { + 'getPorts.return_value': { + 'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]}, + 'getStorageSystemInfo.return_value': { + 'serialNumber': '1234'}, + 'getTask.return_value': { + 'status': 1}, + 'getCPG.return_value': {}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': {} + } + + mock_client = self.setup_driver(mock_conf=conf) + + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'status': 'available', + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + + volume_name_3par = self.driver.common._encode_name(volume['id']) + + loc_info = 'HP3PARDriver:1234:CPG-FC1' + host = {'host': 'stack@3parfc1', + 'capabilities': {'location_info': loc_info}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + self.assertIsNotNone(result) + self.assertEqual((True, None), result) + + osv_matcher = 'osv-' + volume_name_3par + omv_matcher = 'omv-' + volume_name_3par + + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.getStorageSystemInfo(), + mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG('CPG-FC1'), + mock.call.copyVolume(osv_matcher, omv_matcher, mock.ANY, mock.ANY), + mock.call.getTask(mock.ANY), + mock.call.getVolume(osv_matcher), + mock.call.deleteVolume(osv_matcher), + mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}), + mock.call.logout() + ] + + mock_client.assert_has_calls(expected) + + def test_migrate_volume_diff_host(self): + conf = { + 'getPorts.return_value': { + 'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]}, + 'getStorageSystemInfo.return_value': { + 'serialNumber': 'different'}, + } + + mock_client = self.setup_driver(mock_conf=conf) + + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'status': 'available', + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + + loc_info = 'HP3PARDriver:1234:CPG-FC1' + host = {'host': 'stack@3parfc1', + 'capabilities': {'location_info': loc_info}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + self.assertIsNotNone(result) + self.assertEqual((False, None), result) + + def test_migrate_volume_diff_domain(self): + conf = { + 'getPorts.return_value': { + 'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]}, + 'getStorageSystemInfo.return_value': { + 'serialNumber': '1234'}, + 'getTask.return_value': { + 'status': 1}, + 'getCPG.side_effect': + lambda x: {'OpenStackCPG': {'domain': 'OpenStack'}}.get(x, {}) + } + + mock_client = self.setup_driver(mock_conf=conf) + + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'status': 'available', + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + + loc_info = 'HP3PARDriver:1234:CPG-FC1' + host = {'host': 'stack@3parfc1', + 'capabilities': {'location_info': loc_info}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + self.assertIsNotNone(result) + self.assertEqual((False, None), result) + + def test_migrate_volume_attached(self): + conf = { + 'getPorts.return_value': { + 'members': self.FAKE_FC_PORTS + [self.FAKE_ISCSI_PORT]}, + 'getStorageSystemInfo.return_value': { + 'serialNumber': '1234'}, + 'getTask.return_value': { + 'status': 1} + } + + mock_client = self.setup_driver(mock_conf=conf) + + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'status': 'in-use', + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + + volume_name_3par = self.driver.common._encode_name(volume['id']) + + mock_client.getVLUNs.return_value = { + 'members': [{'volumeName': 'osv-' + volume_name_3par}]} + + loc_info = 'HP3PARDriver:1234:CPG-FC1' + host = {'host': 'stack@3parfc1', + 'capabilities': {'location_info': loc_info}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + self.assertIsNotNone(result) + self.assertEqual((False, None), result) + def test_attach_volume(self): # setup_mock_client drive with default configuration @@ -733,7 +881,8 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): # and return the mock HTTP 3PAR client mock_client = self.setup_driver() mock_client.getCPG.return_value = self.cpgs[0] - + mock_client.getStorageSystemInfo.return_value = {'serialNumber': + '1234'} stats = self.driver.get_volume_stats(True) self.assertEqual(stats['storage_protocol'], 'FC') self.assertEqual(stats['total_capacity_gb'], 'infinite') @@ -742,6 +891,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getStorageSystemInfo(), mock.call.logout()] mock_client.assert_has_calls(expected) @@ -1021,7 +1171,8 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): # and return the mock HTTP 3PAR client mock_client = self.setup_driver() mock_client.getCPG.return_value = self.cpgs[0] - + mock_client.getStorageSystemInfo.return_value = {'serialNumber': + '1234'} stats = self.driver.get_volume_stats(True) self.assertEqual(stats['storage_protocol'], 'iSCSI') self.assertEqual(stats['total_capacity_gb'], 'infinite') @@ -1030,6 +1181,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getStorageSystemInfo(), mock.call.logout()] mock_client.assert_has_calls(expected) diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 435d5146ee2..cad3a390c6d 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -39,6 +39,7 @@ import base64 import json import pprint import re +import time import uuid import hp3parclient @@ -114,10 +115,11 @@ class HP3PARCommon(object): 1.3.0 - Removed all SSH code. We rely on the hp3parclient now. 2.0.0 - Update hp3parclient API uses 3.0.x 2.0.1 - Updated to use qos_specs, added new qos settings and personas + 2.0.2 - Add back-end assisted volume migrate """ - VERSION = "2.0.1" + VERSION = "2.0.2" stats = {} @@ -410,6 +412,11 @@ class HP3PARCommon(object): LOG.error(err) raise exception.InvalidInput(reason=err) + info = self.client.getStorageSystemInfo() + stats['location_info'] = ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' % + {'sys_id': info['serialNumber'], + 'dest_cpg': self.config.safe_get( + 'hp3par_cpg')}) self.stats = stats def create_vlun(self, volume, host, nsp=None): @@ -718,17 +725,27 @@ class HP3PARCommon(object): raise ex except Exception as ex: LOG.error(str(ex)) - raise exception.CinderException(ex.get_description()) + raise exception.CinderException(str(ex)) + + def _wait_for_task(self, task_id, poll_interval_sec=1): + while True: + status = self.client.getTask(task_id) + if status['status'] is not self.client.TASK_ACTIVE: + return status + time.sleep(poll_interval_sec) def _copy_volume(self, src_name, dest_name, cpg=None, snap_cpg=None, tpvv=True): # Virtual volume sets are not supported with the -online option - LOG.debug('Creating clone of a volume %s' % src_name) + LOG.debug(_('Creating clone of a volume %(src)s to %(dest)s.') % + {'src': src_name, 'dest': dest_name}) + optional = {'tpvv': tpvv, 'online': True} if snap_cpg is not None: optional['snapCPG'] = snap_cpg - self.client.copyVolume(src_name, dest_name, cpg, optional) + body = self.client.copyVolume(src_name, dest_name, cpg, optional) + return body['taskid'] def get_next_word(self, s, search_string): """Return the next word. @@ -818,6 +835,9 @@ class HP3PARCommon(object): except hpexceptions.HTTPForbidden as ex: LOG.error(str(ex)) raise exception.NotAuthorized(ex.get_description()) + except hpexceptions.HTTPConflict as ex: + LOG.error(str(ex)) + raise exception.VolumeIsBusy(ex.get_description()) except Exception as ex: LOG.error(str(ex)) raise exception.CinderException(ex) @@ -982,6 +1002,122 @@ class HP3PARCommon(object): with excutils.save_and_reraise_exception(): LOG.error(_("Error detaching volume %s") % volume) + def migrate_volume(self, volume, host): + """Migrate directly if source and dest are managed by same storage. + + :param volume: A dictionary describing the volume to migrate + :param host: A dictionary describing the host to migrate to, where + host['host'] is its name, and host['capabilities'] is a + dictionary of its reported capabilities. + :returns (False, None) if the driver does not support migration, + (True, None) if sucessful + + """ + + dbg = {'id': volume['id'], 'host': host['host']} + LOG.debug(_('enter: migrate_volume: id=%(id)s, host=%(host)s.') % dbg) + + try: + false_ret = (False, None) + + # Make sure volume is not attached + if volume['status'] != 'available': + LOG.debug(_('Volume is attached: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + return false_ret + + if 'location_info' not in host['capabilities']: + return false_ret + + info = host['capabilities']['location_info'] + try: + (dest_type, dest_id, dest_cpg) = info.split(':') + except ValueError: + return false_ret + + sys_info = self.client.getStorageSystemInfo() + if not (dest_type == 'HP3PARDriver' and + dest_id == sys_info['serialNumber']): + LOG.debug(_('Dest does not match: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + return false_ret + + type_info = self.get_volume_settings_from_type(volume) + + if dest_cpg == type_info['cpg']: + LOG.debug(_('CPGs are the same: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + return false_ret + + # Check to make sure CPGs are in the same domain + src_domain = self.get_domain(type_info['cpg']) + dst_domain = self.get_domain(dest_cpg) + if src_domain != dst_domain: + LOG.debug(_('CPGs in different domains: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + return false_ret + + # Change the name such that it is unique since 3PAR + # names must be unique across all CPGs + volume_name = self._get_3par_vol_name(volume['id']) + temp_vol_name = volume_name.replace("osv-", "omv-") + + # Create a physical copy of the volume + task_id = self._copy_volume(volume_name, temp_vol_name, + dest_cpg, dest_cpg, type_info['tpvv']) + + LOG.debug(_('Copy volume scheduled: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + + # Wait for the physical copy task to complete + status = self._wait_for_task(task_id) + if status['status'] is not self.client.TASK_DONE: + dbg['status'] = status + msg = _('Copy volume task failed: migrate_volume: ' + 'id=%(id)s, host=%(host)s, status=%(status)s.') % dbg + raise exception.CinderException(msg) + else: + LOG.debug(_('Copy volume completed: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + + comment = self._get_3par_vol_comment(volume_name) + if comment: + self.client.modifyVolume(temp_vol_name, {'comment': comment}) + LOG.debug(_('Migrated volume rename completed: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + + # Delete source volume after the copy is complete + self.client.deleteVolume(volume_name) + LOG.debug(_('Delete src volume completed: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + + # Rename the new volume to the original name + self.client.modifyVolume(temp_vol_name, {'newName': volume_name}) + + # TODO(Ramy) When volume retype is available, + # use that to change the type + LOG.info(_('Completed: migrate_volume: ' + 'id=%(id)s, host=%(host)s.') % dbg) + except hpexceptions.HTTPConflict: + msg = _("Volume (%s) already exists on array.") % volume_name + LOG.error(msg) + raise exception.Duplicate(msg) + except hpexceptions.HTTPBadRequest as ex: + LOG.error(str(ex)) + raise exception.Invalid(ex.get_description()) + except exception.InvalidInput as ex: + LOG.error(str(ex)) + raise ex + except exception.CinderException as ex: + LOG.error(str(ex)) + raise ex + except Exception as ex: + LOG.error(str(ex)) + raise exception.CinderException(ex) + + LOG.debug(_('leave: migrate_volume: id=%(id)s, host=%(host)s.') % dbg) + return (True, None) + def delete_snapshot(self, snapshot): LOG.debug("Delete Snapshot id %s %s" % (snapshot['id'], pprint.pformat(snapshot))) diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index b0ac4d066f0..f1e77127560 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -56,10 +56,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): 1.2.4 - Added metadata during attach/detach bug #1258033. 1.3.0 - Removed all SSH code. We rely on the hp3parclient now. 2.0.0 - Update hp3parclient API uses 3.0.x + 2.0.2 - Add back-end assisted volume migrate """ - VERSION = "2.0.0" + VERSION = "2.0.2" def __init__(self, *args, **kwargs): super(HP3PARFCDriver, self).__init__(*args, **kwargs) @@ -334,3 +335,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): @utils.synchronized('3par', external=True) def detach_volume(self, context, volume): self.common.detach_volume(volume) + + @utils.synchronized('3par', external=True) + def migrate_volume(self, context, volume, host): + self.common.client_login() + try: + return self.common.migrate_volume(volume, host) + finally: + self.common.client_logout() diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 498c987ff06..01d851b7a5b 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -60,10 +60,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): This update now requires 3.1.2 MU3 firmware 1.3.0 - Removed all SSH code. We rely on the hp3parclient now. 2.0.0 - Update hp3parclient API uses 3.0.x + 2.0.2 - Add back-end assisted volume migrate """ - VERSION = "2.0.0" + VERSION = "2.0.2" def __init__(self, *args, **kwargs): super(HP3PARISCSIDriver, self).__init__(*args, **kwargs) @@ -446,3 +447,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): @utils.synchronized('3par', external=True) def detach_volume(self, context, volume): self.common.detach_volume(volume) + + @utils.synchronized('3par', external=True) + def migrate_volume(self, context, volume, host): + self.common.client_login() + try: + return self.common.migrate_volume(volume, host) + finally: + self.common.client_logout()