Merge "3PAR: Backend assisted volume migrate"

This commit is contained in:
Jenkins 2014-02-24 19:00:58 +00:00 committed by Gerrit Code Review
commit f2137b37e8
4 changed files with 314 additions and 8 deletions

View File

@ -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)

View File

@ -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)))

View File

@ -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()

View File

@ -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()