Merge "3PAR: Backend assisted volume migrate"
This commit is contained in:
commit
f2137b37e8
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue