From a2ddf888041161eba427b9c47fc810d0edad889b Mon Sep 17 00:00:00 2001 From: Liu Xinguo <295988511@qq.com> Date: Tue, 14 Jul 2015 11:25:46 +0800 Subject: [PATCH] Add volume migration support for Huawei driver 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 | 264 ++++++++++++++--- cinder/volume/drivers/huawei/constants.py | 5 + cinder/volume/drivers/huawei/huawei_driver.py | 273 ++++++++++++++++-- cinder/volume/drivers/huawei/huawei_utils.py | 60 +++- cinder/volume/drivers/huawei/rest_client.py | 72 ++++- 5 files changed, 589 insertions(+), 85 deletions(-) diff --git a/cinder/tests/unit/test_huawei_drivers.py b/cinder/tests/unit/test_huawei_drivers.py index 1bbdf0ad6..c327a2854 100644 --- a/cinder/tests/unit/test_huawei_drivers.py +++ b/cinder/tests/unit/test_huawei_drivers.py @@ -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': ' true', + 'smartcache': ' true', + 'smartpartition': ' true', + 'thin_provisioning': ' true', + 'thick_provisioning': ' 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': ' true', + 'smartcache': ' true', + 'thin_provisioning': ' true', + 'thick_provisioning': ' 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) diff --git a/cinder/volume/drivers/huawei/constants.py b/cinder/volume/drivers/huawei/constants.py index e8f6060cc..3def14a58 100644 --- a/cinder/volume/drivers/huawei/constants.py +++ b/cinder/volume/drivers/huawei/constants.py @@ -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 diff --git a/cinder/volume/drivers/huawei/huawei_driver.py b/cinder/volume/drivers/huawei/huawei_driver.py index 30d2fbb9d..3e1f94580 100644 --- a/cinder/volume/drivers/huawei/huawei_driver.py +++ b/cinder/volume/drivers/huawei/huawei_driver.py @@ -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 diff --git a/cinder/volume/drivers/huawei/huawei_utils.py b/cinder/volume/drivers/huawei/huawei_utils.py index 2cee9022b..e2de141d1 100644 --- a/cinder/volume/drivers/huawei/huawei_utils.py +++ b/cinder/volume/drivers/huawei/huawei_utils.py @@ -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] == ''): + LOG.error(_LE("Capabilities value must be specified as " + "' True' or ' 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 diff --git a/cinder/volume/drivers/huawei/rest_client.py b/cinder/volume/drivers/huawei/rest_client.py index a7e560173..ff7bd2dcd 100644 --- a/cinder/volume/drivers/huawei/rest_client.py +++ b/cinder/volume/drivers/huawei/rest_client.py @@ -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) -- 2.45.2