]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
3PAR: Backend assisted volume migrate
authorRamy Asselin <ramy.asselin@hp.com>
Thu, 13 Feb 2014 00:23:15 +0000 (16:23 -0800)
committerGerrit Code Review <review@openstack.org>
Fri, 21 Feb 2014 16:58:42 +0000 (16:58 +0000)
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

cinder/tests/test_hp3par.py
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_fc.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

index 30e2ea88820fd589302cb9ba4cd958616aaf1600..36d2ae6241a604a67d63089d0bca48e3547f39dc 100644 (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)
index 435d5146ee2f0d908bece49b84dad7c4cd04bb16..cad3a390c6d47fffb3398ab23fbefdfb3b0b5f98 100644 (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)))
index b0ac4d066f0dd2b8642da23f15ab80e104e8cfc0..f1e7712756040bbcec2a3d4d43a16aaad7366458 100644 (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()
index 498c987ff06c8febde394803f3647568a6b0f09d..01d851b7a5bcc2300867bef692ec7335a1ca515d 100644 (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()