'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,
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(),
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()
]
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,
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,
'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):
'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'}
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'])
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'),
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):
import re
import uuid
+import six
+
from cinder.openstack.common import importutils
hp3parclient = importutils.try_import("hp3parclient")
if hp3parclient:
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 = {}
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:
'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
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
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:
{'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.
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)
"""
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']
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)
'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)
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,