From d1c889da1e1db9f748022d82c401aa2b1bf42393 Mon Sep 17 00:00:00 2001 From: Mark Sturdevant Date: Mon, 1 Sep 2014 14:33:12 -0700 Subject: [PATCH] 3PAR migrate without losing type settings Leverage retype to fix migrate. It was losing type settings. Closes-Bug: 1356608 Change-Id: Ic4d5233533abf65e4784b0e89f5793a6cd1ce521 --- cinder/tests/test_hp3par.py | 214 ++++++++++++++++-- .../volume/drivers/san/hp/hp_3par_common.py | 137 ++++++----- cinder/volume/drivers/san/hp/hp_3par_fc.py | 11 +- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 11 +- 4 files changed, 272 insertions(+), 101 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index a547bed3b..78f2d9470 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -981,14 +981,19 @@ class HP3PARBaseDriver(object): 'status': 1}, 'getCPG.return_value': {}, 'copyVolume.return_value': {'taskid': 1}, - 'getVolume.return_value': {} + 'getVolume.return_value': self.RETYPE_VOLUME_INFO_1 } mock_client = self.setup_driver(mock_conf=conf) + mock_client.getVolume.return_value = self.MANAGE_VOLUME_INFO + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', + 'volume_type_id': None, 'size': 2, 'status': 'available', 'host': HP3PARBaseDriver.FAKE_HOST, @@ -997,7 +1002,7 @@ class HP3PARBaseDriver(object): volume_name_3par = self.driver.common._encode_name(volume['id']) loc_info = 'HP3PARDriver:1234:CPG-FC1' - host = {'host': 'stack@3parfc1', + host = {'host': 'stack@3parfc1#CPG-FC1', 'capabilities': {'location_info': loc_info}} result = self.driver.migrate_volume(context.get_admin_context(), @@ -1006,18 +1011,89 @@ class HP3PARBaseDriver(object): 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.modifyVolume( + osv_matcher, + {'comment': '{"qos": {}, "display_name": "Foo Volume"}', + 'snapCPG': 'CPG-FC1'}), + mock.call.modifyVolume(osv_matcher, + {'action': 6, + 'userCPG': 'CPG-FC1', + 'conversionOperation': 1, + 'tuneOperation': 1}), + mock.call.getTask(mock.ANY), + mock.call.logout() + ] + + mock_client.assert_has_calls(expected) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_migrate_volume_with_type(self, _mock_volume_types): + _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_2 + + conf = { + 'getStorageSystemInfo.return_value': { + 'serialNumber': '1234'}, + 'getTask.return_value': { + 'status': 1}, + 'getCPG.return_value': {}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': self.RETYPE_VOLUME_INFO_1 + } + + mock_client = self.setup_driver(mock_conf=conf) + + mock_client.getVolume.return_value = self.MANAGE_VOLUME_INFO + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE + + display_name = 'Foo Volume' + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': display_name, + "volume_type_id": self.RETYPE_VOLUME_TYPE_2['id'], + '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#CPG-FC1', + 'capabilities': {'location_info': loc_info}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + self.assertIsNotNone(result) + expected_host = volume_utils.append_host( + "stack@3parfc1", + self.RETYPE_VOLUME_TYPE_2['extra_specs']['cpg']) + self.assertEqual((True, {'host': expected_host}), result) + + osv_matcher = 'osv-' + volume_name_3par + + expected_comment = { + "display_name": display_name, + "volume_type_id": self.RETYPE_VOLUME_TYPE_2['id'], + "volume_type_name": self.RETYPE_VOLUME_TYPE_2['name'], + "vvs": self.RETYPE_VOLUME_TYPE_2['extra_specs']['vvs'] + } + expected = [ + mock.call.modifyVolume( + osv_matcher, + {'comment': self.CommentMatcher(self.assertEqual, + expected_comment), + 'snapCPG': self.RETYPE_VOLUME_TYPE_2 + ['extra_specs']['snap_cpg']}), + mock.call.modifyVolume( + osv_matcher, + {'action': 6, + 'userCPG': self.RETYPE_VOLUME_TYPE_2['extra_specs']['cpg'], + 'conversionOperation': 1, + 'tuneOperation': 1}), 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() ] @@ -1034,6 +1110,7 @@ class HP3PARBaseDriver(object): volume = {'name': HP3PARBaseDriver.VOLUME_NAME, 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', + 'volume_type_id': None, 'size': 2, 'status': 'available', 'host': HP3PARBaseDriver.FAKE_HOST, @@ -1048,40 +1125,71 @@ class HP3PARBaseDriver(object): self.assertIsNotNone(result) self.assertEqual((False, None), result) - def test_migrate_volume_diff_domain(self): + @mock.patch.object(volume_types, 'get_volume_type') + def test_migrate_volume_diff_domain(self, _mock_volume_types): + _mock_volume_types.return_value = self.volume_type + conf = { 'getStorageSystemInfo.return_value': { 'serialNumber': '1234'}, 'getTask.return_value': { 'status': 1}, - 'getCPG.side_effect': - lambda x: {'OpenStackCPG': {'domain': 'OpenStack'}}.get(x, {}) + 'getCPG.return_value': {}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': self.RETYPE_VOLUME_INFO_1 } - self.setup_driver(mock_conf=conf) + mock_client = self.setup_driver(mock_conf=conf) + + mock_client.getVolume.return_value = self.MANAGE_VOLUME_INFO + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE volume = {'name': HP3PARBaseDriver.VOLUME_NAME, 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', + 'volume_type_id': None, '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', + host = {'host': 'stack@3parfc1#CPG-FC1', 'capabilities': {'location_info': loc_info}} result = self.driver.migrate_volume(context.get_admin_context(), volume, host) self.assertIsNotNone(result) - self.assertEqual((False, None), result) + self.assertEqual((True, None), result) - def test_migrate_volume_attached(self): + osv_matcher = 'osv-' + volume_name_3par - mock_client = self.setup_driver() + expected = [ + mock.call.modifyVolume( + osv_matcher, + {'comment': '{"qos": {}, "display_name": "Foo Volume"}', + 'snapCPG': 'CPG-FC1'}), + mock.call.modifyVolume(osv_matcher, + {'action': 6, + 'userCPG': 'CPG-FC1', + 'conversionOperation': 1, + 'tuneOperation': 1}), + mock.call.getTask(mock.ANY), + mock.call.logout() + ] + + mock_client.assert_has_calls(expected) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_migrate_volume_attached(self, _mock_volume_types): + _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_1 + mock_client = self.setup_driver(mock_conf=self.RETYPE_CONF) volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'volume_type_id': None, 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', 'size': 2, @@ -1090,18 +1198,70 @@ class HP3PARBaseDriver(object): 'source_volid': HP3PARBaseDriver.VOLUME_ID} volume_name_3par = self.driver.common._encode_name(volume['id']) + osv_matcher = 'osv-' + volume_name_3par - mock_client.getVLUNs.return_value = { - 'members': [{'volumeName': 'osv-' + volume_name_3par}]} + loc_info = 'HP3PARDriver:1234567:CPG-FC1' + + protocol = "FC" + if self.properties['driver_volume_type'] == "iscsi": + protocol = "iSCSI" - loc_info = 'HP3PARDriver:1234:CPG-FC1' host = {'host': 'stack@3parfc1', - 'capabilities': {'location_info': loc_info}} + 'capabilities': {'location_info': loc_info, + 'storage_protocol': protocol}} + + result = self.driver.migrate_volume(context.get_admin_context(), + volume, host) + + new_comment = {"qos": {}, + "retype_test": "test comment"} + expected = [ + mock.call.modifyVolume(osv_matcher, + {'comment': self.CommentMatcher( + self.assertEqual, new_comment), + 'snapCPG': 'OpenStackCPGSnap'}), + mock.call.modifyVolume(osv_matcher, + {'action': 6, + 'userCPG': 'OpenStackCPG', + 'conversionOperation': 1, + 'tuneOperation': 1}), + mock.call.getTask(1), + mock.call.logout() + ] + mock_client.assert_has_calls(expected) + + self.assertIsNotNone(result) + self.assertEqual((True, {'host': 'stack@3parfc1#OpenStackCPG'}), + result) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_migrate_volume_attached_diff_protocol(self, _mock_volume_types): + _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_1 + mock_client = self.setup_driver(mock_conf=self.RETYPE_CONF) + + protocol = "OTHER" + + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'volume_type_id': None, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'status': 'in-use', + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + + loc_info = 'HP3PARDriver:1234567:CPG-FC1' + host = {'host': 'stack@3parfc1', + 'capabilities': {'location_info': loc_info, + 'storage_protocol': protocol}} result = self.driver.migrate_volume(context.get_admin_context(), volume, host) + self.assertIsNotNone(result) self.assertEqual((False, None), result) + expected = [] + mock_client.assert_has_calls(expected) def test_attach_volume(self): @@ -1962,6 +2122,7 @@ class HP3PARBaseDriver(object): 'volume_type': self.volume_type}} volume = {'display_name': None, + 'host': 'stack1@3pariscsi#POOL1', 'volume_type': 'gold', 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', 'id': '007dbfce-7579-40bc-8f90-a20b3902283e'} @@ -1971,7 +2132,8 @@ class HP3PARBaseDriver(object): mock_client.getTask.return_value = self.STATUS_DONE mock_client.getCPG.side_effect = [ {'domain': 'domain1'}, - {'domain': 'domain2'} + {'domain': 'domain2'}, + {'domain': 'domain3'}, ] unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id']) @@ -1989,6 +2151,7 @@ class HP3PARBaseDriver(object): mock.call.getVolume(unm_matcher), mock.call.modifyVolume( unm_matcher, {'newName': osv_matcher, 'comment': mock.ANY}), + mock.call.getCPG('POOL1'), mock.call.getVolume(osv_matcher), mock.call.getCPG('testUserCpg0'), mock.call.getCPG('OpenStackCPG'), @@ -3499,8 +3662,9 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): self.setup_driver() volume = {'host': 'test-host@3pariscsi#pool_foo', 'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'} + pool = volume_utils.extract_host(volume['host'], 'pool') model = self.driver.common.get_volume_settings_from_type_id('gold-id', - volume) + pool) self.assertEqual(model['cpg'], 'pool_foo') def test_get_model_update(self): diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 1f0157883..f4c75f651 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -42,6 +42,8 @@ import pprint import re import uuid +import six + from cinder.openstack.common import importutils hp3parclient = importutils.try_import("hp3parclient") if hp3parclient: @@ -153,10 +155,11 @@ class HP3PARCommon(object): 2.0.22 - HP 3PAR drivers should not claim to have 'infinite' space 2.0.23 - Increase the hostname size from 23 to 31 Bug #1371242 2.0.24 - Add pools (hp3par_cpg now accepts a list of CPGs) + 2.0.25 - Migrate without losing type settings bug #1356608 """ - VERSION = "2.0.24" + VERSION = "2.0.25" stats = {} @@ -943,33 +946,23 @@ class HP3PARCommon(object): qos = self._get_qos_by_volume_type(volume_type) return hp3par_keys, qos, volume_type, vvs_name - def get_volume_settings_from_type_id(self, type_id, volume): + def get_volume_settings_from_type_id(self, type_id, pool): """Get 3PAR volume settings given a type_id. Combines type info and config settings to return a dictionary describing the 3PAR volume settings. Does some validation (CPG). - Uses volume['host'] to determine default cpg (when not specified in - volume type specs). + Uses pool as the default cpg (when not specified in volume type specs). - :param type_id: + :param type_id: id of type to get settings for + :param pool: CPG to use if type does not have one set :return: dict """ hp3par_keys, qos, volume_type, vvs_name = self.get_type_info(type_id) - # Default to 1st configured CPG unless we can extract pool from host. - default_cpg = self.config.hp3par_cpg[0] - try: - pool = volume_utils.extract_host(volume['host'], 'pool') - if pool: - default_cpg = pool - LOG.debug("Default CPG from volume['host'] is (%s)" % - default_cpg) - else: - LOG.debug("Default CPG from volume['host'] not found") - except Exception as ex: - LOG.debug("Default CPG from volume['host'] not found due to (%s)" % - ex) + # Default to pool extracted from host. + # If that doesn't work use the 1st CPG in the config as the default. + default_cpg = pool or self.config.hp3par_cpg[0] cpg = self._get_key_value(hp3par_keys, 'cpg', default_cpg) if cpg not in self.config.hp3par_cpg: @@ -1011,7 +1004,7 @@ class HP3PARCommon(object): 'vvs_name': vvs_name, 'qos': qos, 'tpvv': tpvv, 'volume_type': volume_type} - def get_volume_settings_from_type(self, volume): + def get_volume_settings_from_type(self, volume, host=None): """Get 3PAR volume settings given a volume. Combines type info and config settings to return a dictionary @@ -1019,13 +1012,19 @@ class HP3PARCommon(object): persona). :param volume: + :param host: Optional host to use for default pool. :return: dict """ type_id = volume.get('volume_type_id', None) - volume_settings = self.get_volume_settings_from_type_id(type_id, - volume) + pool = None + if host: + pool = volume_utils.extract_host(host['host'], 'pool') + else: + pool = volume_utils.extract_host(volume['host'], 'pool') + + volume_settings = self.get_volume_settings_from_type_id(type_id, pool) # check for valid persona even if we don't use it until # attach time, this will give the end user notice that the @@ -1430,58 +1429,31 @@ class HP3PARCommon(object): 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 successful + (True, model_update) if successful """ - dbg = {'id': volume['id'], 'host': host['host']} + dbg = {'id': volume['id'], + 'host': host['host'], + 'status': volume['status']} LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s.' % dbg) - 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) + ret = False, None - 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 + if volume['status'] in ['available', 'in-use']: + volume_type = None + if volume['volume_type_id']: + volume_type = self._get_volume_type(volume['volume_type_id']) - self._convert_to_base_volume(volume, new_cpg=dest_cpg) + try: + ret = self.retype(volume, volume_type, None, host) + except Exception as e: + LOG.info(_('3PAR driver cannot perform migration. ' + 'Retype exception: %s') % six.text_type(e)) - # TODO(Ramy) When volume retype is available, - # use that to change the type LOG.debug('leave: migrate_volume: id=%(id)s, host=%(host)s.' % dbg) - return (True, None) + LOG.debug('migrate_volume result: %s, %s' % ret) + return ret def _convert_to_base_volume(self, volume, new_cpg=None): try: @@ -1709,7 +1681,7 @@ class HP3PARCommon(object): {'status': status, 'volume_name': volume_name}) raise exception.VolumeBackendAPIException(msg) - def _retype_pre_checks(self, host, new_persona, + def _retype_pre_checks(self, volume, host, new_persona, old_cpg, new_cpg, new_snap_cpg): """Test retype parameters before making retype changes. @@ -1759,7 +1731,7 @@ class HP3PARCommon(object): action = "volume:retype" - self._retype_pre_checks(host, new_persona, + self._retype_pre_checks(volume, host, new_persona, old_cpg, new_cpg, new_snap_cpg) @@ -1803,10 +1775,18 @@ class HP3PARCommon(object): """ volume_id = volume['id'] volume_name = self._get_3par_vol_name(volume_id) - new_type_name = new_type['name'] - new_type_id = new_type['id'] + new_type_name = None + new_type_id = None + if new_type: + new_type_name = new_type['name'] + new_type_id = new_type['id'] + pool = None + if host: + pool = volume_utils.extract_host(host['host'], 'pool') + else: + pool = volume_utils.extract_host(volume['host'], 'pool') new_volume_settings = self.get_volume_settings_from_type_id( - new_type_id, volume) + new_type_id, pool) new_cpg = new_volume_settings['cpg'] new_snap_cpg = new_volume_settings['snap_cpg'] new_tpvv = new_volume_settings['tpvv'] @@ -1854,8 +1834,8 @@ class HP3PARCommon(object): volume-type is not used here. This method uses None. :param new_type: A dictionary describing the volume type to convert to """ - none_type_settings = self.get_volume_settings_from_type_id( - None, volume) + pool = volume_utils.extract_host(volume['host'], 'pool') + none_type_settings = self.get_volume_settings_from_type_id(None, pool) return self._retype_from_old_to_new(volume, new_type, none_type_settings, None) @@ -1878,7 +1858,7 @@ class HP3PARCommon(object): 'new_type': new_type, 'diff': diff, 'host': host}) - old_volume_settings = self.get_volume_settings_from_type(volume) + old_volume_settings = self.get_volume_settings_from_type(volume, host) return self._retype_from_old_to_new(volume, new_type, old_volume_settings, host) @@ -1944,8 +1924,17 @@ class ModifyVolumeTask(flow_utils.CinderTask): comment_dict['qos'] = new_qos else: comment_dict['qos'] = {} - comment_dict['volume_type_name'] = new_type_name - comment_dict['volume_type_id'] = new_type_id + + if new_type_name: + comment_dict['volume_type_name'] = new_type_name + else: + comment_dict.pop('volume_type_name', None) + + if new_type_id: + comment_dict['volume_type_id'] = new_type_id + else: + comment_dict.pop('volume_type_id', None) + return comment_dict def execute(self, common, volume_name, old_snap_cpg, new_snap_cpg, diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index 38101065c..1c5adadac 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -71,10 +71,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): is present. bug #1360001 2.0.8 - Fixing missing login/logout around attach/detach bug #1367429 2.0.9 - Add support for pools with model update + 2.0.10 - Migrate without losing type settings bug #1356608 """ - VERSION = "2.0.9" + VERSION = "2.0.10" def __init__(self, *args, **kwargs): super(HP3PARFCDriver, self).__init__(*args, **kwargs) @@ -460,6 +461,14 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): @utils.synchronized('3par', external=True) def migrate_volume(self, context, volume, host): + + if volume['status'] == 'in-use': + protocol = host['capabilities']['storage_protocol'] + if protocol != 'FC': + LOG.debug("3PAR FC driver cannot migrate in-use volume " + "to a host with storage_protocol=%s." % protocol) + return False, None + self.common.client_login() try: return self.common.migrate_volume(volume, host) diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 3500d9e71..b2e7ae2eb 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -75,10 +75,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): and hp3parclient 3.1.0. 2.0.6 - Fixing missing login/logout around attach/detach bug #1367429 2.0.7 - Add support for pools with model update + 2.0.8 - Migrate without losing type settings bug #1356608 """ - VERSION = "2.0.7" + VERSION = "2.0.8" def __init__(self, *args, **kwargs): super(HP3PARISCSIDriver, self).__init__(*args, **kwargs) @@ -667,6 +668,14 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): @utils.synchronized('3par', external=True) def migrate_volume(self, context, volume, host): + + if volume['status'] == 'in-use': + protocol = host['capabilities']['storage_protocol'] + if protocol != 'iSCSI': + LOG.debug("3PAR ISCSI driver cannot migrate in-use volume " + "to a host with storage_protocol=%s." % protocol) + return False, None + self.common.client_login() try: return self.common.migrate_volume(volume, host) -- 2.45.2