]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
HP LeftHand Backend assisted volume migrate
authorJim Branen <james.branen@hp.com>
Sat, 15 Feb 2014 00:02:55 +0000 (16:02 -0800)
committerJim Branen <james.branen@hp.com>
Wed, 19 Feb 2014 20:12:42 +0000 (12:12 -0800)
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
cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py
cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py
cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py

index 2f757377c51de4ca639a4f1305344bbd422896ec..8b69124632cf1e0f29581164a63ff585d1a6d178 100644 (file)
@@ -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))
index 793bd0ebbe7ad2a6bd1416e1d6ea41f2fa7e821a..58be55fbd5c6a839cbc0f91abe8ea76487b1819e 100644 (file)
@@ -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)
index 7c2b78ed60bf8745cde81574d4c775dc89861539..f60c54d29809af9bc81e63208cc68ca832adbdc5 100644 (file)
@@ -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)
index 3465324ac6d54710e1db1381d4b582c25dad4c91..30468a99b45fc4ffa24f17b90ff013b0c1b02d53 100644 (file)
@@ -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)