From 8eb35516bda872220c5c044638365c87bcf35f33 Mon Sep 17 00:00:00 2001 From: Jim Branen Date: Fri, 14 Feb 2014 16:02:55 -0800 Subject: [PATCH] HP LeftHand Backend assisted volume migrate 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 --- cinder/tests/test_hplefthand.py | 88 ++++++++++++++++++ .../drivers/san/hp/hp_lefthand_cliq_proxy.py | 13 ++- .../drivers/san/hp/hp_lefthand_iscsi.py | 8 +- .../drivers/san/hp/hp_lefthand_rest_proxy.py | 90 ++++++++++++++++++- 4 files changed, 196 insertions(+), 3 deletions(-) diff --git a/cinder/tests/test_hplefthand.py b/cinder/tests/test_hplefthand.py index 2f757377c..8b6912463 100644 --- a/cinder/tests/test_hplefthand.py +++ b/cinder/tests/test_hplefthand.py @@ -1148,3 +1148,91 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): # 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)) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py index 793bd0ebb..58be55fbd 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py @@ -458,8 +458,19 @@ class HPLeftHandCLIQProxy(SanISCSIDriver): :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) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py index 7c2b78ed6..f60c54d29 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py @@ -47,9 +47,10 @@ class HPLeftHandISCSIDriver(VolumeDriver): 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) @@ -141,3 +142,8 @@ class HPLeftHandISCSIDriver(VolumeDriver): 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) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py index 3465324ac..30468a99b 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py @@ -84,9 +84,10 @@ class HPLeftHandRESTProxy(ISCSIDriver): 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 = {} @@ -96,6 +97,10 @@ class HPLeftHandRESTProxy(ISCSIDriver): 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: @@ -221,6 +226,9 @@ class HPLeftHandRESTProxy(ISCSIDriver): 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) @@ -418,3 +426,83 @@ class HPLeftHandRESTProxy(ISCSIDriver): 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) -- 2.45.2