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
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):
# 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',
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
# 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')
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)
# 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')
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)
import json
import pprint
import re
+import time
import uuid
import hp3parclient
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 = {}
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):
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.
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)
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)))
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)
@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()
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)
@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()