]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add volume migration support for Huawei driver
authorLiu Xinguo <295988511@qq.com>
Tue, 14 Jul 2015 03:25:46 +0000 (11:25 +0800)
committerWilson Liu <liuxinguo@huawei.com>
Mon, 17 Aug 2015 13:03:12 +0000 (21:03 +0800)
If the target host and the original volume are on the same array,
then Huawei driver will do the migration now instead of having
it done by the general migration process.

Change-Id: Ife8fca765402e95778079fb0166b3abf9684d134
Implements: blueprint huawei-storage-volume-migration-support

cinder/tests/unit/test_huawei_drivers.py
cinder/volume/drivers/huawei/constants.py
cinder/volume/drivers/huawei/huawei_driver.py
cinder/volume/drivers/huawei/huawei_utils.py
cinder/volume/drivers/huawei/rest_client.py

index 1bbdf0ad6ecfdbb9fc7c3e53786397e7d0c2c649..c327a2854a24c52e8c2c6e801e0cd2551a39676c 100644 (file)
@@ -75,6 +75,28 @@ test_snap = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635',
              'provider_location': '11',
              }
 
+test_host = {'host': 'ubuntu001@backend001#OpenStack_Pool',
+             'capabilities': {'smartcache': True,
+                              'location_info': '210235G7J20000000000',
+                              'QoS_support': True,
+                              'pool_name': 'OpenStack_Pool',
+                              'timestamp': '2015-07-13T11:41:00.513549',
+                              'smartpartition': True,
+                              'allocated_capacity_gb': 0,
+                              'volume_backend_name': 'Huawei18000FCDriver',
+                              'free_capacity_gb': 20.0,
+                              'driver_version': '1.1.0',
+                              'total_capacity_gb': 20.0,
+                              'smarttier': True,
+                              'hypermetro': True,
+                              'reserved_percentage': 0,
+                              'vendor_name': None,
+                              'thick_provisioning': True,
+                              'thin_provisioning': True,
+                              'storage_protocol': 'FC',
+                              }
+             }
+
 FakeConnector = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3',
                  'wwpns': ['10000090fa0d6754'],
                  'wwnns': ['10000090fa0d6755'],
@@ -162,12 +184,23 @@ FAKE_LUN_DELETE_SUCCESS_RESPONSE = """
     },
     "data": {
         "ID": "11",
-        "IOCLASSID": "11",
+        "IOCLASSID": "",
         "NAME": "5mFHcBv4RkCcD+JyrWc0SA",
         "RUNNINGSTATUS": "2",
         "HEALTHSTATUS": "1",
         "RUNNINGSTATUS": "27",
-        "LUNLIST": ""
+        "ALLOCTYPE": "0",
+        "CAPACITY": "2097152",
+        "WRITEPOLICY": "1",
+        "MIRRORPOLICY": "0",
+        "PREFETCHPOLICY": "1",
+        "PREFETCHVALUE": "20",
+        "DATATRANSFERPOLICY": "1",
+        "READCACHEPOLICY": "2",
+        "WRITECACHEPOLICY": "5",
+        "OWNINGCONTROLLER": "0B",
+        "SMARTCACHEPARTITIONID": "",
+        "CACHEPARTITIONID": ""
     }
 }
 """
@@ -663,45 +696,6 @@ FAKE_PORT_GROUP_RESPONSE = """
 }
 """
 
-FAKE_ISCSI_INITIATOR_RESPONSE = """
-{
-    "error":{
-        "code": 0
-    },
-    "data":[{
-        "CHAPNAME": "mm-user",
-        "HEALTHSTATUS": "1",
-        "ID": "iqn.1993-08.org.debian:01:9073aba6c6f",
-        "ISFREE": "true",
-        "MULTIPATHTYPE": "1",
-        "NAME": "",
-        "OPERATIONSYSTEM": "255",
-        "RUNNINGSTATUS": "28",
-        "TYPE": 222,
-        "USECHAP": "true"
-    }]
-}
-"""
-
-FAKE_ISCSI_INITIATOR_RESPONSE = """
-{
-    "error":{
-        "code":0
-    },
-    "data":[{
-        "CHAPNAME":"mm-user",
-        "HEALTHSTATUS":"1",
-        "ID":"iqn.1993-08.org.debian:01:9073aba6c6f",
-        "ISFREE":"true",
-        "MULTIPATHTYPE":"1",
-        "NAME":"",
-        "OPERATIONSYSTEM":"255",
-        "RUNNINGSTATUS":"28",
-        "TYPE":222,
-        "USECHAP":"true"
-    }]
-}
-"""
 
 FAKE_ERROR_INFO_RESPONSE = """
 {
@@ -735,6 +729,26 @@ FAKE_SYSTEM_VERSION_RESPONSE = """
 }
 """
 
+FAKE_GET_LUN_MIGRATION_RESPONSE = """
+{
+    "data":[{"ENDTIME":"1436816174",
+             "ID":"9",
+             "PARENTID":"11",
+             "PARENTNAME":"xmRBHMlVRruql5vwthpPXQ",
+             "PROCESS":"-1",
+             "RUNNINGSTATUS":"76",
+             "SPEED":"2",
+             "STARTTIME":"1436816111",
+             "TARGETLUNID":"1",
+             "TARGETLUNNAME":"4924891454902893639",
+             "TYPE":253,
+             "WORKMODE":"0"
+             }],
+    "error":{"code":0,
+             "description":"0"}
+}
+"""
+
 FAKE_QOS_INFO_RESPONSE = """
 {
     "error":{
@@ -753,6 +767,15 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/xx/sessions'] = (
 MAP_COMMAND_TO_FAKE_RESPONSE['sessions'] = (
     FAKE_LOGIN_OUT_STORAGE_RESPONSE)
 
+MAP_COMMAND_TO_FAKE_RESPONSE['LUN_MIGRATION/POST'] = (
+    FAKE_COMMON_SUCCESS_RESPONSE)
+
+MAP_COMMAND_TO_FAKE_RESPONSE['LUN_MIGRATION?range=[0-100]/GET'] = (
+    FAKE_GET_LUN_MIGRATION_RESPONSE)
+
+MAP_COMMAND_TO_FAKE_RESPONSE['LUN_MIGRATION/11/DELETE'] = (
+    FAKE_COMMON_SUCCESS_RESPONSE)
+
 # mock storage info map
 MAP_COMMAND_TO_FAKE_RESPONSE['storagepool'] = (
     FAKE_STORAGE_POOL_RESPONSE)
@@ -811,7 +834,7 @@ MAP_COMMAND_TO_FAKE_RESPONSE['lungroup/associate?TYPE=256&ASSOCIATEOBJTYPE=11'
 
 MAP_COMMAND_TO_FAKE_RESPONSE['lungroup/associate?TYPE=256&ASSOCIATEOBJTYPE=11'
                              '&ASSOCIATEOBJID=1/GET'] = (
-    FAKE_LUN_ASSOCIATE_RESPONSE)
+    FAKE_COMMON_SUCCESS_RESPONSE)
 
 MAP_COMMAND_TO_FAKE_RESPONSE['lungroup/associate?ID=11&ASSOCIATEOBJTYPE=11'
                              '&ASSOCIATEOBJID=11/DELETE'] = (
@@ -878,7 +901,7 @@ MAP_COMMAND_TO_FAKE_RESPONSE['iscsi_initiator?range=[0-256]/GET'] = (
     FAKE_COMMON_SUCCESS_RESPONSE)
 
 MAP_COMMAND_TO_FAKE_RESPONSE['iscsi_initiator/'] = (
-    FAKE_COMMON_SUCCESS_RESPONSE)
+    FAKE_ISCSI_INITIATOR_RESPONSE)
 
 MAP_COMMAND_TO_FAKE_RESPONSE['iscsi_initiator/POST'] = (
     FAKE_ISCSI_INITIATOR_RESPONSE)
@@ -1099,7 +1122,7 @@ class Huawei18000ISCSIDriverTestCase(test.TestCase):
         self.driver.restclient.login()
         lun_info = self.driver.create_volume_from_snapshot(test_volume,
                                                            test_volume)
-        self.assertEqual('1', lun_info['provider_location'])
+        self.assertEqual('1', lun_info['ID'])
 
     def test_initialize_connection_success(self):
         self.driver.restclient.login()
@@ -1447,7 +1470,7 @@ class Huawei18000FCDriverTestCase(test.TestCase):
         self.driver.restclient.login()
         lun_info = self.driver.create_volume_from_snapshot(test_volume,
                                                            test_volume)
-        self.assertEqual('1', lun_info['provider_location'])
+        self.assertEqual('1', lun_info['ID'])
 
     def test_initialize_connection_success(self):
         self.driver.restclient.login()
@@ -1556,6 +1579,146 @@ class Huawei18000FCDriverTestCase(test.TestCase):
                                                      self.configuration)
         self.assertEqual('0', host_os)
 
+    def test_migrate_volume_success(self):
+        self.driver.restclient.login()
+
+        # Migrate volume without new type.
+        model_update = None
+        moved = False
+        empty_dict = {}
+        # Migrate volume without new type.
+        moved, model_update = self.driver.migrate_volume(None,
+                                                         test_volume,
+                                                         test_host,
+                                                         None)
+        self.assertTrue(moved)
+        self.assertEqual(empty_dict, model_update)
+
+        # Migrate volume with new type.
+        moved = False
+        empty_dict = {}
+        new_type = {'extra_specs':
+                    {'smarttier': '<is> true',
+                     'smartcache': '<is> true',
+                     'smartpartition': '<is> true',
+                     'thin_provisioning': '<is> true',
+                     'thick_provisioning': '<is> False',
+                     'policy': '2',
+                     'smartcache:cachename': 'cache-test',
+                     'smartpartition:partitionname': 'partition-test'}}
+        moved, model_update = self.driver.migrate_volume(None,
+                                                         test_volume,
+                                                         test_host,
+                                                         new_type)
+        self.assertTrue(moved)
+        self.assertEqual(empty_dict, model_update)
+
+    def test_migrate_volume_fail(self):
+        self.driver.restclient.login()
+        self.driver.restclient.test_fail = True
+
+        # Migrate volume without new type.
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.migrate_volume, None,
+                          test_volume, test_host, None)
+
+        # Migrate volume with new type.
+        new_type = {'extra_specs':
+                    {'smarttier': '<is> true',
+                     'smartcache': '<is> true',
+                     'thin_provisioning': '<is> true',
+                     'thick_provisioning': '<is> False',
+                     'policy': '2',
+                     'smartcache:cachename': 'cache-test',
+                     'partitionname': 'partition-test'}}
+        self.driver.restclient.test_fail = True
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.migrate_volume, None,
+                          test_volume, test_host, new_type)
+
+    def test_check_migration_valid(self):
+        self.driver.restclient.login()
+        is_valid = self.driver._check_migration_valid(test_host,
+                                                      test_volume)
+        self.assertTrue(is_valid)
+        # No pool_name in capabilities.
+        invalid_host1 = {'host': 'ubuntu001@backend002#OpenStack_Pool',
+                         'capabilities':
+                             {'location_info': '210235G7J20000000000',
+                              'allocated_capacity_gb': 0,
+                              'volume_backend_name': 'Huawei18000FCDriver',
+                              'storage_protocol': 'FC'}}
+        is_valid = self.driver._check_migration_valid(invalid_host1,
+                                                      test_volume)
+        self.assertFalse(is_valid)
+        # location_info in capabilities is not matched.
+        invalid_host2 = {'host': 'ubuntu001@backend002#OpenStack_Pool',
+                         'capabilities':
+                             {'location_info': '210235G7J20000000001',
+                              'allocated_capacity_gb': 0,
+                              'pool_name': 'OpenStack_Pool',
+                              'volume_backend_name': 'Huawei18000FCDriver',
+                              'storage_protocol': 'FC'}}
+        is_valid = self.driver._check_migration_valid(invalid_host2,
+                                                      test_volume)
+        self.assertFalse(is_valid)
+        # storage_protocol is not match current protocol and volume status is
+        # 'in-use'.
+        volume_in_use = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635',
+                         'size': 2,
+                         'volume_name': 'vol1',
+                         'id': '21ec7341-9256-497b-97d9-ef48edcf0635',
+                         'volume_id': '21ec7341-9256-497b-97d9-ef48edcf0635',
+                         'volume_attachment': 'in-use',
+                         'provider_location': '11'}
+        invalid_host2 = {'host': 'ubuntu001@backend002#OpenStack_Pool',
+                         'capabilities':
+                             {'location_info': '210235G7J20000000001',
+                              'allocated_capacity_gb': 0,
+                              'pool_name': 'OpenStack_Pool',
+                              'volume_backend_name': 'Huawei18000FCDriver',
+                              'storage_protocol': 'iSCSI'}}
+        is_valid = self.driver._check_migration_valid(invalid_host2,
+                                                      volume_in_use)
+        self.assertFalse(is_valid)
+        # pool_name is empty.
+        invalid_host3 = {'host': 'ubuntu001@backend002#OpenStack_Pool',
+                         'capabilities':
+                             {'location_info': '210235G7J20000000001',
+                              'allocated_capacity_gb': 0,
+                              'pool_name': '',
+                              'volume_backend_name': 'Huawei18000FCDriver',
+                              'storage_protocol': 'iSCSI'}}
+        is_valid = self.driver._check_migration_valid(invalid_host3,
+                                                      test_volume)
+        self.assertFalse(is_valid)
+
+    @mock.patch.object(rest_client.RestClient, 'rename_lun')
+    def test_update_migrated_volume_success(self, mock_rename_lun):
+        self.driver.restclient.login()
+        original_volume = {'id': '21ec7341-9256-497b-97d9-ef48edcf0635'}
+        current_volume = {'id': '21ec7341-9256-497b-97d9-ef48edcf0636'}
+        model_update = self.driver.update_migrated_volume(None,
+                                                          original_volume,
+                                                          current_volume,
+                                                          'available')
+        self.assertEqual({'_name_id': None}, model_update)
+
+    @mock.patch.object(rest_client.RestClient, 'rename_lun')
+    def test_update_migrated_volume_fail(self, mock_rename_lun):
+        self.driver.restclient.login()
+        mock_rename_lun.side_effect = exception.VolumeBackendAPIException(
+            data='Error occurred.')
+        original_volume = {'id': '21ec7341-9256-497b-97d9-ef48edcf0635'}
+        current_volume = {'id': '21ec7341-9256-497b-97d9-ef48edcf0636',
+                          '_name_id': '21ec7341-9256-497b-97d9-ef48edcf0637'}
+        model_update = self.driver.update_migrated_volume(None,
+                                                          original_volume,
+                                                          current_volume,
+                                                          'available')
+        self.assertEqual({'_name_id': '21ec7341-9256-497b-97d9-ef48edcf0637'},
+                         model_update)
+
     def create_fake_conf_file(self):
         """Create a fake Config file
 
@@ -1587,6 +1750,12 @@ class Huawei18000FCDriverTestCase(test.TestCase):
         userpassword_text = doc.createTextNode('Admin@storage')
         userpassword.appendChild(userpassword_text)
         storage.appendChild(userpassword)
+
+        protocol = doc.createElement('Protocol')
+        protocol_text = doc.createTextNode('FC')
+        protocol.appendChild(protocol_text)
+        storage.appendChild(protocol)
+
         url = doc.createElement('RestURL')
         url_text = doc.createTextNode('http://100.115.10.69:8082/'
                                       'deviceManager/rest/')
@@ -1605,6 +1774,11 @@ class Huawei18000FCDriverTestCase(test.TestCase):
         storagepool.appendChild(pool_text)
         lun.appendChild(storagepool)
 
+        lun_type = doc.createElement('LUNType')
+        lun_type_text = doc.createTextNode('Thick')
+        lun_type.appendChild(lun_type_text)
+        lun.appendChild(lun_type)
+
         timeout = doc.createElement('Timeout')
         timeout_text = doc.createTextNode('43200')
         timeout.appendChild(timeout_text)
index e8f6060cc22d6516ba63f95710a9d427ad497139..3def14a581ca84661a31630df41defb972273d96 100644 (file)
@@ -30,6 +30,11 @@ ARRAY_VERSION = 'V300R003C00'
 CAPACITY_UNIT = 1024.0 / 1024.0 / 2
 DEFAULT_WAIT_TIMEOUT = 3600 * 24 * 30
 DEFAULT_WAIT_INTERVAL = 5
+
+MIGRATION_WAIT_INTERVAL = 5
+MIGRATION_FAULT = '74'
+MIGRATION_COMPLETE = '76'
+
 ERROR_CONNECT_TO_SERVER = -403
 ERROR_UNAUTHORIZED_TO_SERVER = -401
 SOCKET_TIME_OUT = 720
index 30d2fbb9d90fdf7fd0e7908d0c16008020dea069..3e1f94580c8e0e24277c68b8ccb6ffcbd77ce2d1 100644 (file)
@@ -22,7 +22,7 @@ from oslo_utils import excutils
 from oslo_utils import units
 
 from cinder import exception
-from cinder.i18n import _, _LI, _LW
+from cinder.i18n import _, _LE, _LI, _LW
 from cinder import utils
 from cinder.volume import driver
 from cinder.volume.drivers.huawei import constants
@@ -30,6 +30,7 @@ from cinder.volume.drivers.huawei import huawei_utils
 from cinder.volume.drivers.huawei import rest_client
 from cinder.volume.drivers.huawei import smartx
 from cinder.volume import utils as volume_utils
+from cinder.volume import volume_types
 from cinder.zonemanager import utils as fczm_utils
 
 LOG = logging.getLogger(__name__)
@@ -44,7 +45,7 @@ CONF = cfg.CONF
 CONF.register_opts(huawei_opt)
 
 
-class HuaweiBaseDriver(driver.VolumeDriver):
+class HuaweiBaseDriver(driver.MigrateVD, driver.BaseVD):
 
     def __init__(self, *args, **kwargs):
         super(HuaweiBaseDriver, self).__init__(*args, **kwargs)
@@ -104,14 +105,22 @@ class HuaweiBaseDriver(driver.VolumeDriver):
         # Create LUN on the array.
         lun_info = self.restclient.create_volume(lun_param)
         lun_id = lun_info['ID']
-        qos = huawei_utils.get_volume_qos(volume)
-        if qos:
-            smart_qos = smartx.SmartQos(self.restclient)
-            smart_qos.create_qos(qos, lun_id)
-        smartpartition = smartx.SmartPartition(self.restclient)
-        smartpartition.add(opts, lun_id)
-        smartcache = smartx.SmartCache(self.restclient)
-        smartcache.add(opts, lun_id)
+
+        try:
+            qos = huawei_utils.get_volume_qos(volume)
+            if qos:
+                smart_qos = smartx.SmartQos(self.restclient)
+                smart_qos.create_qos(qos, lun_id)
+            smartpartition = smartx.SmartPartition(self.restclient)
+            smartpartition.add(opts, lun_id)
+
+            smartcache = smartx.SmartCache(self.restclient)
+            smartcache.add(opts, lun_id)
+        except Exception as err:
+            if lun_id:
+                self._delete_lun_with_check(lun_id)
+            raise exception.InvalidInput(
+                reason=_('Create volume error. Because %s.') % err)
 
         return {'provider_location': lun_info['ID'],
                 'ID': lun_id,
@@ -154,6 +163,231 @@ class HuaweiBaseDriver(driver.VolumeDriver):
                                                 lun_list,
                                                 qos_id)
 
+    def _delete_lun_with_check(self, dst_id):
+        LOG.info(_LI("Try to delete lun %s if it exists."), dst_id)
+        if self.restclient.check_lun_exist(dst_id):
+            qos_id = self.restclient.get_qosid_by_lunid(dst_id)
+            if qos_id:
+                qos = smartx.SmartQos(self.restclient)
+                qos.delete_qos(qos_id)
+            self.restclient.delete_lun(dst_id)
+
+    def _is_lun_migration_complete(self, src_id, dst_id):
+        result = self.restclient.get_lun_migration_task()
+        found_migration_task = False
+        if 'data' in result:
+            for item in result['data']:
+                if (src_id == item['PARENTID']
+                        and dst_id == item['TARGETLUNID']):
+                    found_migration_task = True
+                    if constants.MIGRATION_COMPLETE == item['RUNNINGSTATUS']:
+                        return True
+                    if constants.MIGRATION_FAULT == item['RUNNINGSTATUS']:
+                        err_msg = (_('Lun migration error.'))
+                        LOG.error(err_msg)
+                        raise exception.VolumeBackendAPIException(data=err_msg)
+        if not found_migration_task:
+            err_msg = (_("Cannot find migration task."))
+            LOG.error(err_msg)
+            raise exception.VolumeBackendAPIException(data=err_msg)
+
+        return False
+
+    def _is_lun_migration_exist(self, src_id, dst_id):
+        try:
+            result = self.restclient.get_lun_migration_task()
+        except Exception:
+            LOG.error(_LE("Get LUN migration error."))
+            return False
+        if 'data' in result:
+            for item in result['data']:
+                if (src_id == item['PARENTID']
+                        and dst_id == item['TARGETLUNID']):
+                    return True
+        return False
+
+    def _migrate_lun(self, src_id, dst_id):
+        try:
+            self.restclient.create_lun_migration(src_id, dst_id)
+
+            def _is_lun_migration_complete():
+                return self._is_lun_migration_complete(src_id, dst_id)
+
+            wait_interval = constants.MIGRATION_WAIT_INTERVAL
+            huawei_utils.wait_for_condition(self.xml_file_path,
+                                            _is_lun_migration_complete,
+                                            wait_interval)
+        # Clean up if migration failed.
+        except Exception as ex:
+            raise exception.VolumeBackendAPIException(data=ex)
+        finally:
+            if self._is_lun_migration_exist(src_id, dst_id):
+                self.restclient.delete_lun_migration(src_id, dst_id)
+            self._delete_lun_with_check(dst_id)
+
+        LOG.debug("Migrate lun %s successfully.", src_id)
+        return True
+
+    def _wait_volume_ready(self, lun_id):
+        event_type = 'LUNReadyWaitInterval'
+        wait_interval = huawei_utils.get_wait_interval(self.xml_file_path,
+                                                       event_type)
+
+        def _volume_ready():
+            result = self.restclient.get_lun_info(lun_id)
+            if (result['HEALTHSTATUS'] == constants.STATUS_HEALTH
+               and result['RUNNINGSTATUS'] == constants.STATUS_VOLUME_READY):
+                return True
+            return False
+
+        huawei_utils.wait_for_condition(self.xml_file_path,
+                                        _volume_ready,
+                                        wait_interval,
+                                        wait_interval * 10)
+
+    def _get_original_status(self, volume):
+        if not volume['volume_attachment']:
+            return 'available'
+        else:
+            return 'in-use'
+
+    def update_migrated_volume(self, ctxt, volume, new_volume,
+                               original_volume_status):
+        original_name = huawei_utils.encode_name(volume['id'])
+        current_name = huawei_utils.encode_name(new_volume['id'])
+
+        lun_id = self.restclient.get_volume_by_name(current_name)
+        try:
+            self.restclient.rename_lun(lun_id, original_name)
+        except exception.VolumeBackendAPIException:
+            LOG.error(_LE('Unable to rename lun %s on array.'), current_name)
+            return {'_name_id': new_volume['_name_id'] or new_volume['id']}
+
+        LOG.debug("Rename lun from %(current_name)s to %(original_name)s "
+                  "successfully.",
+                  {'current_name': current_name,
+                   'original_name': original_name})
+
+        model_update = {'_name_id': None}
+
+        return model_update
+
+    def migrate_volume(self, ctxt, volume, host, new_type=None):
+        """Migrate a volume within the same array."""
+        return self._migrate_volume(volume, host, new_type)
+
+    def _check_migration_valid(self, host, volume):
+        if 'pool_name' not in host['capabilities']:
+            return False
+
+        target_device = host['capabilities']['location_info']
+
+        # Source and destination should be on same array.
+        if target_device != self.restclient.device_id:
+            return False
+
+        # Same protocol should be used if volume is in-use.
+        protocol = huawei_utils.get_protocol(self.xml_file_path)
+        if (host['capabilities']['storage_protocol'] != protocol
+                and self._get_original_status(volume) == 'in-use'):
+            return False
+
+        pool_name = host['capabilities']['pool_name']
+        if len(pool_name) == 0:
+            return False
+
+        return True
+
+    def _migrate_volume(self, volume, host, new_type=None):
+        if not self._check_migration_valid(host, volume):
+            return (False, None)
+
+        type_id = volume['volume_type_id']
+
+        volume_type = None
+        if type_id:
+            volume_type = volume_types.get_volume_type(None, type_id)
+
+        pool_name = host['capabilities']['pool_name']
+        pools = self.restclient.find_all_pools()
+        pool_info = self.restclient.find_pool_info(pool_name, pools)
+        src_volume_name = huawei_utils.encode_name(volume['id'])
+        dst_volume_name = six.text_type(hash(src_volume_name))
+        src_id = volume.get('provider_location', None)
+
+        src_lun_params = self.restclient.get_lun_info(src_id)
+
+        opts = None
+        qos = None
+        if new_type:
+            # If new type exists, use new type.
+            opts = huawei_utils._get_extra_spec_value(
+                new_type['extra_specs'])
+            opts = smartx.SmartX().get_smartx_specs_opts(opts)
+            if 'LUNType' not in opts:
+                opts['LUNType'] = huawei_utils.find_luntype_in_xml(
+                    self.xml_file_path)
+
+            qos = huawei_utils.get_qos_by_volume_type(new_type)
+        elif volume_type:
+            qos = huawei_utils.get_qos_by_volume_type(volume_type)
+
+        if not opts:
+            opts = huawei_utils.get_volume_params(volume)
+            opts = smartx.SmartX().get_smartx_specs_opts(opts)
+
+        lun_info = self._create_lun_with_extra_feature(pool_info,
+                                                       dst_volume_name,
+                                                       src_lun_params,
+                                                       opts)
+        lun_id = lun_info['ID']
+
+        if qos:
+            LOG.info(_LI('QoS: %s.'), qos)
+            SmartQos = smartx.SmartQos(self.restclient)
+            SmartQos.create_qos(qos, lun_id)
+        if opts:
+            smartpartition = smartx.SmartPartition(self.restclient)
+            smartpartition.add(opts, lun_id)
+            smartcache = smartx.SmartCache(self.restclient)
+            smartcache.add(opts, lun_id)
+
+        dst_id = lun_info['ID']
+        self._wait_volume_ready(dst_id)
+        moved = self._migrate_lun(src_id, dst_id)
+
+        return moved, {}
+
+    def _create_lun_with_extra_feature(self, pool_info,
+                                       lun_name,
+                                       lun_params,
+                                       spec_opts):
+        LOG.info(_LI('Create a new lun %s for migration.'), lun_name)
+
+        # Prepare lun parameters.
+        lunparam = {"TYPE": '11',
+                    "NAME": lun_name,
+                    "PARENTTYPE": '216',
+                    "PARENTID": pool_info['ID'],
+                    "ALLOCTYPE": lun_params['ALLOCTYPE'],
+                    "CAPACITY": lun_params['CAPACITY'],
+                    "WRITEPOLICY": lun_params['WRITEPOLICY'],
+                    "MIRRORPOLICY": lun_params['MIRRORPOLICY'],
+                    "PREFETCHPOLICY": lun_params['PREFETCHPOLICY'],
+                    "PREFETCHVALUE": lun_params['PREFETCHVALUE'],
+                    "DATATRANSFERPOLICY": '0',
+                    "READCACHEPOLICY": lun_params['READCACHEPOLICY'],
+                    "WRITECACHEPOLICY": lun_params['WRITECACHEPOLICY'],
+                    "OWNINGCONTROLLER": lun_params['OWNINGCONTROLLER'],
+                    }
+        if 'LUNType' in spec_opts:
+            lunparam['ALLOCTYPE'] = spec_opts['LUNType']
+        if spec_opts['policy']:
+            lunparam['DATATRANSFERPOLICY'] = spec_opts['policy']
+
+        lun_info = self.restclient.create_volume(lunparam)
+        return lun_info
+
     def create_volume_from_snapshot(self, volume, snapshot):
         """Create a volume from a snapshot.
 
@@ -193,9 +427,9 @@ class HuaweiBaseDriver(driver.VolumeDriver):
         def _volume_ready():
             result = self.restclient.get_lun_info(tgt_lun_id)
 
-            if result['HEALTHSTATUS'] == constants.STATUS_HEALTH:
-                if result['RUNNINGSTATUS'] == constants.STATUS_VOLUME_READY:
-                    return True
+            if (result['HEALTHSTATUS'] == constants.STATUS_HEALTH
+               and result['RUNNINGSTATUS'] == constants.STATUS_VOLUME_READY):
+                return True
             return False
 
         huawei_utils.wait_for_condition(self.xml_file_path,
@@ -206,13 +440,15 @@ class HuaweiBaseDriver(driver.VolumeDriver):
         self._copy_volume(volume, luncopy_name,
                           snapshot_id, tgt_lun_id)
 
-        return {'provider_location': lun_info['ID'],
+        return {'ID': lun_info['ID'],
                 'lun_info': lun_info}
 
     def create_cloned_volume(self, volume, src_vref):
         """Clone a new volume from an existing volume."""
         # Form the snapshot structure.
-        snapshot = {'id': uuid.uuid4().__str__(), 'volume_id': src_vref['id']}
+        snapshot = {'id': uuid.uuid4().__str__(),
+                    'volume_id': src_vref['id'],
+                    'volume': src_vref}
 
         # Create snapshot.
         self.create_snapshot(snapshot)
@@ -563,9 +799,6 @@ class HuaweiBaseDriver(driver.VolumeDriver):
 
         return info
 
-    def migrate_volume(self, context, volume, host):
-        return (False, None)
-
     def create_export(self, context, volume, connector):
         """Export a volume."""
         pass
@@ -633,6 +866,7 @@ class Huawei18000ISCSIDriver(HuaweiBaseDriver, driver.ISCSIDriver):
                 Multiple pools support
                 ISCSI multipath support
                 SmartX support
+                Volume migration support
     """
 
     VERSION = "1.1.1"
@@ -670,6 +904,7 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
         1.1.1 - Code refactor
                 Multiple pools support
                 SmartX support
+                Volume migration support
     """
 
     VERSION = "1.1.1"
@@ -684,7 +919,7 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver):
         data['volume_backend_name'] = backend_name or self.__class__.__name__
         data['storage_protocol'] = 'FC'
         data['driver_version'] = self.VERSION
-        data['verdor_name'] = 'Huawei'
+        data['vendor_name'] = 'Huawei'
         return data
 
     @fczm_utils.AddFCZone
index 2cee9022bff78103d5436c405275fc067801a532..e2de141d178e75ba3648e753118b6643751a3237 100644 (file)
@@ -108,6 +108,18 @@ def _get_opts_from_specs(opts_capability, opts_value, specs):
         if key:
             key = key.lower()
 
+        if ((not scope or scope == 'capabilities') and
+           key in opts_capability):
+            words = value.split()
+
+            if not (words and len(words) == 2 and words[0] == '<is>'):
+                LOG.error(_LE("Capabilities value must be specified as "
+                              "'<is> True' or '<is> true'."))
+            else:
+                del words[0]
+                value = words[0]
+                opts[key] = value.lower()
+
         if (scope in opts_capability) and (key in opts_value):
             if (scope in opts_associate) and (opts_associate[scope] == key):
                 opts[key] = value
@@ -126,7 +138,6 @@ def _get_smartx_specs_params(lunsetinfo, smartx_opts):
 
 
 def get_lun_params(xml_file_path, smartx_opts):
-    lunsetinfo = {}
     lunsetinfo = get_lun_conf_params(xml_file_path)
     lunsetinfo = _get_smartx_specs_params(lunsetinfo, smartx_opts)
     return lunsetinfo
@@ -262,16 +273,18 @@ def get_lun_conf_params(xml_file_path):
             lunsetinfo['LUNType'] = luntype.strip()
             if luntype.strip() == 'Thick':
                 lunsetinfo['LUNType'] = 0
-            if luntype.strip() == 'Thin':
+            elif luntype.strip() == 'Thin':
                 lunsetinfo['LUNType'] = 1
 
-        elif luntype is not '' and luntype is not None:
+        else:
             err_msg = (_(
-                'Config file is wrong. LUNType must be "Thin"'
-                ' or "Thick". LUNType: %(fetchtype)s.')
+                "LUNType config is wrong. LUNType must be 'Thin'"
+                " or 'Thick'. LUNType: %(fetchtype)s.")
                 % {'fetchtype': luntype})
             LOG.error(err_msg)
             raise exception.VolumeBackendAPIException(data=err_msg)
+    else:
+        lunsetinfo['LUNType'] = 0
 
     stripunitsize = root.findtext('LUN/StripUnitSize')
     if stripunitsize is not None:
@@ -310,6 +323,27 @@ def get_lun_conf_params(xml_file_path):
     return lunsetinfo
 
 
+def find_luntype_in_xml(xml_file_path):
+    root = parse_xml_file(xml_file_path)
+    luntype = root.findtext('LUN/LUNType')
+    if luntype:
+        if luntype.strip() in ['Thick', 'Thin']:
+            if luntype.strip() == 'Thick':
+                luntype = 0
+            elif luntype.strip() == 'Thin':
+                luntype = 1
+        else:
+            err_msg = (_(
+                "LUNType config is wrong. LUNType must be 'Thin'"
+                " or 'Thick'. LUNType: %(fetchtype)s.")
+                % {'fetchtype': luntype})
+            LOG.error(err_msg)
+            raise exception.VolumeBackendAPIException(data=err_msg)
+    else:
+        luntype = 0
+    return luntype
+
+
 def encode_name(name):
     uuid_str = name.replace("-", "")
     vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
@@ -421,8 +455,8 @@ def get_iscsi_conf(xml_file_path):
     """Get iSCSI info from config file."""
     iscsiinfo = {}
     root = parse_xml_file(xml_file_path)
-    TargetIP = root.findtext('iSCSI/DefaultTargetIP').strip()
-    iscsiinfo['DefaultTargetIP'] = TargetIP
+    target_ip = root.findtext('iSCSI/DefaultTargetIP').strip()
+    iscsiinfo['DefaultTargetIP'] = target_ip
     initiator_list = []
 
     for dic in root.findall('iSCSI/Initiator'):
@@ -481,3 +515,15 @@ def get_volume_size(volume):
         volume_size = int(volume['size']) * units.Gi / 512
 
     return volume_size
+
+
+def get_protocol(xml_file_path):
+    """Get protocol from huawei conf file."""
+    root = parse_xml_file(xml_file_path)
+    protocol = root.findtext('Storage/Protocol')
+    if not protocol:
+        err_msg = (_('Get protocol from huawei conf file error.'))
+        LOG.error(err_msg)
+        raise exception.InvalidInput(reason=err_msg)
+
+    return protocol
index a7e56017364e19d4d90c025c2e50131696a0e86d..ff7bd2dcd57afd062a1a4eb61f340cb377dba3d3 100644 (file)
@@ -74,7 +74,7 @@ class RestClient(object):
             LOG.error(_LE('Bad response from server: %(url)s.'
                           ' Error: %(err)s'), {'url': url, 'err': err})
             json_msg = ('{"error":{"code": %s,"description": "Connect to '
-                        'server error."}}'), constants.ERROR_CONNECT_TO_SERVER
+                        'server error."}}') % constants.ERROR_CONNECT_TO_SERVER
             res_json = json.loads(json_msg)
             return res_json
 
@@ -107,6 +107,7 @@ class RestClient(object):
                 raise exception.VolumeBackendAPIException(data=msg)
 
             device_id = result['data']['deviceid']
+            self.device_id = device_id
             self.url = item_url + device_id
             self.headers['iBaseToken'] = result['data']['iBaseToken']
             return device_id
@@ -240,7 +241,7 @@ class RestClient(object):
         return result['data']
 
     def check_snapshot_exist(self, snapshot_id):
-        url = self.url + "/snapshot/" + snapshot_id
+        url = self.url + "/snapshot/%s" % snapshot_id
         data = json.dumps({"TYPE": "27",
                            "ID": snapshot_id})
         result = self.call(url, data, "GET")
@@ -448,8 +449,8 @@ class RestClient(object):
             if hostgroup_id is None:
                 err_msg = (_(
                     'Failed to create hostgroup: %(name)s. '
-                    'Check if it exists on the array.'),
-                    {'name': hostgroup_name})
+                    'Check if it exists on the array.')
+                    {'name': hostgroup_name})
                 LOG.error(err_msg)
                 raise exception.VolumeBackendAPIException(data=err_msg)
 
@@ -591,8 +592,6 @@ class RestClient(object):
 
         if 'data' in result:
             return result['data']['ID']
-        else:
-            return
 
     def _is_host_associate_to_hostgroup(self, hostgroup_id, host_id):
         """Check whether the host is associated to the hostgroup."""
@@ -895,8 +894,13 @@ class RestClient(object):
         result = self.call(url, None, "GET")
         self._assert_rest_result(result, _('Find lun group from mapping view '
                                  'error.'))
+        lungroup_id = None
+        if 'data' in result:
+            # One map can have only one lungroup.
+            for item in result['data']:
+                lungroup_id = item['ID']
 
-        return self._get_id_from_result(result, view_id, 'ID')
+        return lungroup_id
 
     def start_luncopy(self, luncopy_id):
         """Start a LUNcopy."""
@@ -1019,8 +1023,6 @@ class RestClient(object):
             iqn = iqn_prefix + ':' + iqn_suffix + ':' + iscsi_ip
             LOG.info(_LI('_get_tgt_iqn: iSCSI target iqn is: %s.'), iqn)
             return iqn
-        else:
-            return
 
     def get_fc_target_wwpns(self, wwn):
         url = (self.url +
@@ -1056,6 +1058,7 @@ class RestClient(object):
             capacity = self._get_capacity(pool_name, result)
             pool = {}
             pool.update(dict(
+                location_info=self.device_id,
                 pool_name=pool_name,
                 total_capacity_gb=capacity['total_capacity'],
                 free_capacity_gb=capacity['free_capacity'],
@@ -1160,6 +1163,7 @@ class RestClient(object):
             target_iqn = self._get_tgt_iqn_from_rest(ip)
             if not target_iqn:
                 target_iqn = self._get_tgt_iqn(ip)
+            if target_iqn:
                 target_iqns.append(target_iqn)
 
         return (target_iqns, target_ips, portgroup_id)
@@ -1173,7 +1177,10 @@ class RestClient(object):
             LOG.warning(_LW("Can't find target iqn from rest."))
             return target_iqn
 
-        target_iqn = self._get_id_from_result(result, target_ip, 'ID')
+        if 'data' in result:
+            for item in result['data']:
+                if target_ip in item['ID']:
+                    target_iqn = item['ID']
 
         if not target_iqn:
             LOG.warning(_LW("Can't find target iqn from rest."))
@@ -1299,7 +1306,13 @@ class RestClient(object):
         result = self.call(url, None, "GET")
         self._assert_rest_result(result, _('Get lungroup id by lun id error.'))
 
-        return self._get_id_from_result(result, lun_id, 'ID')
+        lun_group_id = None
+        # Lun only in one lungroup.
+        if 'data' in result:
+            for item in result['data']:
+                lun_group_id = item['ID']
+
+        return lun_group_id
 
     def get_lun_info(self, lun_id):
         url = self.url + "/lun/" + lun_id
@@ -1325,6 +1338,32 @@ class RestClient(object):
 
         return result['data']
 
+    def create_lun_migration(self, src_id, dst_id, speed=2):
+        url = self.url + "/LUN_MIGRATION"
+        data = json.dumps({"TYPE": '253',
+                           "PARENTID": src_id,
+                           "TARGETLUNID": dst_id,
+                           "SPEED": speed,
+                           "WORKMODE": 0})
+
+        result = self.call(url, data, "POST")
+        msg = _('Create lun migration error.')
+        self._assert_rest_result(result, msg)
+        self._assert_data_in_result(result, msg)
+
+    def get_lun_migration_task(self):
+        url = self.url + '/LUN_MIGRATION?range=[0-100]'
+        result = self.call(url, None, "GET")
+        self._assert_rest_result(result, _('Get lun migration task error.'))
+        return result
+
+    def delete_lun_migration(self, src_id, dst_id):
+        url = self.url + '/LUN_MIGRATION/' + src_id
+        result = self.call(url, None, "DELETE")
+        msg = _('Delete lun migration error.')
+        self._assert_rest_result(result, msg)
+        self._assert_data_in_result(result, msg)
+
     def get_partition_id_by_name(self, name):
         url = self.url + "/cachepartition"
         result = self.call(url, None, "GET")
@@ -1337,8 +1376,6 @@ class RestClient(object):
                 if name == item['NAME']:
                     return item['ID']
 
-        return
-
     def get_partition_info_by_id(self, partition_id):
 
         url = self.url + '/cachepartition/' + partition_id
@@ -1368,7 +1405,6 @@ class RestClient(object):
             for item in result['data']:
                 if name == item['NAME']:
                     return item['ID']
-        return
 
     def find_available_qos(self, qos):
         """"Find available QoS on the array."""
@@ -1450,3 +1486,11 @@ class RestClient(object):
                            "ID": initiator})
         result = self.call(url, data, "PUT")
         self._assert_rest_result(result, _('Remove iscsi from host error.'))
+
+    def rename_lun(self, lun_id, new_name):
+        url = self.url + "/lun/" + lun_id
+        data = json.dumps({"NAME": new_name})
+        result = self.call(url, data, "PUT")
+        msg = _('Rename lun on array error.')
+        self._assert_rest_result(result, msg)
+        self._assert_data_in_result(result, msg)