This patch implements volume migrate using native LeftHand APIs.
Limitations:
1. Same LeftHand backend
2. Volume cannot be attached
3. Volumes with snapshots cannot be migrated
4. Source and Destination clusters must be in the same management group
5. Volume re-type not supported
Change-Id: I503d5a10ee59db14130c676a5c3a07abf9a2b7af
Implements: blueprint native-lefthand-volume-migrate
# validate call chain
mock_client.assert_has_calls(expected)
+
+ def test_migrate_no_location(self):
+ # setup drive with default configuration
+ # and return the mock HTTP LeftHand client
+ mock_client = self.setup_driver()
+
+ host = {'host': self.serverName, 'capabilities': {}}
+ (migrated, update) = self.driver.migrate_volume(
+ None,
+ self.volume,
+ host)
+ self.assertFalse(migrated)
+
+ # only startup code is called
+ mock_client.assert_has_calls(self.driver_startup_call_stack)
+ # and nothing else
+ self.assertEqual(
+ len(self.driver_startup_call_stack),
+ len(mock_client.method_calls))
+
+ def test_migrate_incorrect_vip(self):
+ # setup drive with default configuration
+ # and return the mock HTTP LeftHand client
+ mock_client = self.setup_driver()
+ mock_client.getClusterByName.return_value = {
+ "virtualIPAddresses": [{
+ "ipV4Address": "10.10.10.10",
+ "ipV4NetMask": "255.255.240.0"}]}
+
+ mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+ location = (self.driver.proxy.DRIVER_LOCATION % {
+ 'cluster': 'New_CloudCluster',
+ 'vip': '10.10.10.111'})
+
+ host = {
+ 'host': self.serverName,
+ 'capabilities': {'location_info': location}}
+ (migrated, update) = self.driver.migrate_volume(
+ None,
+ self.volume,
+ host)
+ self.assertFalse(migrated)
+
+ expected = self.driver_startup_call_stack + [
+ mock.call.getClusterByName('New_CloudCluster')]
+
+ mock_client.assert_has_calls(expected)
+ # and nothing else
+ self.assertEqual(
+ len(expected),
+ len(mock_client.method_calls))
+
+ def test_migrate_with_location(self):
+ # setup drive with default configuration
+ # and return the mock HTTP LeftHand client
+ mock_client = self.setup_driver()
+ mock_client.getClusterByName.return_value = {
+ "virtualIPAddresses": [{
+ "ipV4Address": "10.10.10.111",
+ "ipV4NetMask": "255.255.240.0"}]}
+
+ mock_client.getVolumeByName.return_value = {'id': self.volume_id,
+ 'iscsiSessions': None}
+
+ location = (self.driver.proxy.DRIVER_LOCATION % {
+ 'cluster': 'New_CloudCluster',
+ 'vip': '10.10.10.111'})
+
+ host = {
+ 'host': self.serverName,
+ 'capabilities': {'location_info': location}}
+ (migrated, update) = self.driver.migrate_volume(
+ None,
+ self.volume,
+ host)
+ self.assertTrue(migrated)
+
+ expected = self.driver_startup_call_stack + [
+ mock.call.getClusterByName('New_CloudCluster'),
+ mock.call.getVolumeByName('fakevolume'),
+ mock.call.modifyVolume(1, {'clusterName': 'New_CloudCluster'})]
+
+ mock_client.assert_has_calls(expected)
+ # and nothing else
+ self.assertEqual(
+ len(expected),
+ len(mock_client.method_calls))
:param volume: A dictionary describing the volume to migrate
:param new_type: A dictionary describing the volume type to convert to
:param diff: A dictionary with the difference between the two types
+ """
+ return False
+
+ def migrate_volume(self, ctxt, volume, host):
+ """Migrate the volume to the specified host.
+
+ Returns a boolean indicating whether the migration occurred, as well as
+ model_update.
+
+ :param ctxt: Context
+ :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.
"""
- return False
+ return (False, None)
Version history:
1.0.0 - Initial driver
1.0.1 - Added support for retype
+ 1.0.2 - Added support for volume migrate
"""
- VERSION = "1.0.1"
+ VERSION = "1.0.2"
def __init__(self, *args, **kwargs):
super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs)
def retype(self, context, volume, new_type, diff, host):
"""Convert the volume to be of the new type."""
return self.proxy.retype(context, volume, new_type, diff, host)
+
+ @utils.synchronized('lefthand', external=True)
+ def migrate_volume(self, ctxt, volume, host):
+ """Migrate directly if source and dest are managed by same storage."""
+ return self.proxy.migrate_volume(ctxt, volume, host)
Version history:
1.0.0 - Initial REST iSCSI proxy
1.0.1 - Added support for retype
+ 1.0.2 - Added support for volume migrate
"""
- VERSION = "1.0.1"
+ VERSION = "1.0.2"
device_stats = {}
if not self.configuration.hplefthand_api_url:
raise exception.NotFound(_("HPLeftHand url not found"))
+ # blank is the only invalid character for cluster names
+ # so we need to use it as a separator
+ self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'
+
def do_setup(self, context):
"""Set up LeftHand client."""
try:
data['reserved_percentage'] = 0
data['storage_protocol'] = 'iSCSI'
data['vendor_name'] = 'Hewlett-Packard'
+ data['location_info'] = (self.DRIVER_LOCATION % {
+ 'cluster': self.configuration.hplefthand_clustername,
+ 'vip': self.cluster_vip})
cluster_info = self.client.getCluster(self.cluster_id)
LOG.warning("%s" % str(ex))
return False
+
+ def migrate_volume(self, ctxt, volume, host):
+ """Migrate the volume to the specified host.
+
+ Backend assisted volume migration will occur if and only if;
+
+ 1. Same LeftHand backend
+ 2. Volume cannot be attached
+ 3. Volumes with snapshots cannot be migrated
+ 4. Source and Destination clusters must be in the same management group
+
+ Volume re-type is not supported.
+
+ Returns a boolean indicating whether the migration occurred, as well as
+ model_update.
+
+ :param ctxt: Context
+ :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.
+ """
+ LOG.debug(_('enter: migrate_volume: id=%(id)s, host=%(host)s, '
+ 'cluster=%(cluster)s') % {
+ 'id': volume['id'],
+ 'host': host,
+ 'cluster': self.configuration.hplefthand_clustername})
+
+ false_ret = (False, None)
+ if 'location_info' not in host['capabilities']:
+ return false_ret
+
+ host_location = host['capabilities']['location_info']
+ (driver, cluster, vip) = host_location.split(' ')
+ try:
+ # get the cluster info, if it exists and compare
+ cluster_info = self.client.getClusterByName(cluster)
+ LOG.debug(_('Clister info: %s') % cluster_info)
+ virtual_ips = cluster_info['virtualIPAddresses']
+
+ if driver != self.__class__.__name__:
+ LOG.info(_("Can not provide backend assisted migration for "
+ "volume:%s because volume is from a different "
+ "backend.") % volume['name'])
+ return false_ret
+ if vip != virtual_ips[0]['ipV4Address']:
+ LOG.info(_("Can not provide backend assisted migration for "
+ "volume:%s because cluster exists in different "
+ "management group.") % volume['name'])
+ return false_ret
+
+ except hpexceptions.HTTPNotFound:
+ LOG.info(_("Can not provide backend assisted migration for "
+ "volume:%s because cluster exists in different "
+ "management group.") % volume['name'])
+ return false_ret
+
+ try:
+ options = {'clusterName': cluster}
+ volume_info = self.client.getVolumeByName(volume['name'])
+ LOG.debug(_('Volume info: %s') % volume_info)
+
+ # can't migrate if server is attached
+ if volume_info['iscsiSessions'] is not None:
+ LOG.info(_("Can not provide backend assisted migration "
+ "for volume:%s because the volume has been "
+ "exported.") % volume['name'])
+ return false_ret
+
+ self.client.modifyVolume(volume_info['id'], options)
+ except hpexceptions.HTTPNotFound:
+ LOG.info(_("Can not provide backend assisted migration for "
+ "volume:%s because volume does not exist in this "
+ "management group.") % volume['name'])
+ return false_ret
+ except hpexceptions.HTTPServerError as ex:
+ LOG.error(str(ex))
+ return false_ret
+
+ return (True, None)