diff --git a/cinder/tests/unit/test_huawei_drivers.py b/cinder/tests/unit/test_huawei_drivers.py index e79426891..886fff92d 100644 --- a/cinder/tests/unit/test_huawei_drivers.py +++ b/cinder/tests/unit/test_huawei_drivers.py @@ -30,11 +30,24 @@ from cinder.volume.drivers.huawei import constants from cinder.volume.drivers.huawei import fc_zone_helper from cinder.volume.drivers.huawei import huawei_driver from cinder.volume.drivers.huawei import huawei_utils +from cinder.volume.drivers.huawei import hypermetro from cinder.volume.drivers.huawei import rest_client from cinder.volume.drivers.huawei import smartx LOG = logging.getLogger(__name__) +hypermetro_devices = """{ + "remote_device": { + "RestURL": "http://100.115.10.69:8082/deviceManager/rest", + "UserName": "admin", + "UserPassword": "Admin@storage1", + "StoragePool": "StoragePool001", + "domain_name": "hypermetro-domain", + "remote_target_ip": "111.111.101.241" + } +} +""" + test_volume = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', 'size': 2, 'volume_name': 'vol1', @@ -59,6 +72,32 @@ fake_smartx_value = {'smarttier': 'true', 'partitionname': 'partition-test', } +fake_hypermetro_opts = {'hypermetro': 'true', + 'smarttier': False, + 'smartcache': False, + 'smartpartition': False, + 'thin_provisioning_support': False, + 'thick_provisioning_support': False, + } + +hyper_volume = {'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', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'vol1', + 'display_description': 'test volume', + 'volume_type_id': None, + 'host': 'ubuntu@huawei#OpenStack_Pool', + 'provider_location': '11', + 'volume_metadata': [{'key': 'hypermetro_id', + 'value': '1'}, + {'key': 'remote_lun_id', + 'value': '11'}], + } + test_snap = {'name': 'volume-21ec7341-9256-497b-97d9-ef48edcf0635', 'size': 1, 'volume_name': 'vol1', @@ -118,6 +157,24 @@ test_new_type = { 'description': None, } +hypermetro_devices = """ +{ + "remote_device": { + "RestURL": "http://100.115.10.69:8082/deviceManager/rest", + "UserName":"admin", + "UserPassword":"Admin@storage2", + "StoragePool":"StoragePool001", + "domain_name":"hypermetro_test"} +} +""" + +FAKE_FIND_POOL_RESPONSE = {'CAPACITY': '985661440', + 'ID': '0', + 'TOTALCAPACITY': '985661440'} + +FAKE_CREATE_VOLUME_RESPONSE = {"ID": "1", + "NAME": "5mFHcBv4RkCcD+JyrWc0SA"} + FakeConnector = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3', 'wwpns': ['10000090fa0d6754'], 'wwnns': ['10000090fa0d6755'], @@ -265,7 +322,7 @@ FAKE_QUERY_ALL_LUN_RESPONSE = """ "code": 0 }, "data": [{ - "ID": "11", + "ID": "1", "NAME": "IexzQZJWSXuX2e9I7c8GNQ" }] } @@ -920,6 +977,20 @@ FAKE_GET_FC_INI_RESPONSE = """ } """ +FAKE_HYPERMETRODOMAIN_RESPONSE = """ +{ + "error":{ + "code": 0 + }, + "data":{ + "PRODUCTVERSION": "V100R001C10", + "ID": "11", + "NAME": "hypermetro_test", + "RUNNINGSTATUS": "42" + } +} +""" + FAKE_QOS_INFO_RESPONSE = """ { "error":{ @@ -944,8 +1015,42 @@ FAKE_GET_FC_PORT_RESPONSE = """ } """ +FAKE_SMARTCACHEPARTITION_RESPONSE = """ +{ + "error":{ + "code":0 + }, + "data":{ + "ID":"11", + "NAME":"cache-name" + } +} +""" + +FAKE_CONNECT_FC_RESPONCE = { + "driver_volume_type": 'fibre_channel', + "data": { + "target_wwn": ["10000090fa0d6754"], + "target_lun": "1", + "volume_id": "21ec7341-9256-497b-97d9-ef48edcf0635" + } +} + +FAKE_METRO_INFO_RESPONCE = { + "error": { + "code": 0 + }, + "data": { + "PRODUCTVERSION": "V100R001C10", + "ID": "11", + "NAME": "hypermetro_test", + "RUNNINGSTATUS": "42" + } +} + # mock login info map MAP_COMMAND_TO_FAKE_RESPONSE = {} + MAP_COMMAND_TO_FAKE_RESPONSE['/xx/sessions'] = ( FAKE_GET_LOGIN_STORAGE_RESPONSE) @@ -1243,6 +1348,12 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/fc_port/GET'] = ( MAP_COMMAND_TO_FAKE_RESPONSE['/fc_initiator/GET'] = ( FAKE_GET_FC_PORT_RESPONSE) +MAP_COMMAND_TO_FAKE_RESPONSE['fc_initiator?range=[0-100]/GET'] = ( + FAKE_GET_FC_PORT_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/fc_initiator?PARENTTYPE=21&PARENTID=1/GET'] = ( + FAKE_GET_FC_PORT_RESPONSE) + MAP_COMMAND_TO_FAKE_RESPONSE['/lun/associate/cachepartition/POST'] = ( FAKE_SYSTEM_VERSION_RESPONSE) @@ -1252,6 +1363,33 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/fc_initiator?range=[0-100]&PARENTID=1/GET'] = ( MAP_COMMAND_TO_FAKE_RESPONSE['/fc_initiator?PARENTTYPE=21&PARENTID=1/GET'] = ( FAKE_GET_FC_PORT_RESPONSE) +MAP_COMMAND_TO_FAKE_RESPONSE['/system/'] = ( + FAKE_SYSTEM_VERSION_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/SMARTCACHEPARTITION/0/GET'] = ( + FAKE_SMARTCACHEPARTITION_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/SMARTCACHEPARTITION/REMOVE_ASSOCIATE/PUT'] = ( + FAKE_COMMON_SUCCESS_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/cachepartition/0/GET'] = ( + FAKE_SMARTCACHEPARTITION_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroDomain?range=[0-100]/GET'] = ( + FAKE_HYPERMETRODOMAIN_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroPair/POST'] = ( + FAKE_HYPERMETRODOMAIN_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroPair/11/GET'] = ( + FAKE_HYPERMETRODOMAIN_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroPair/disable_hcpair/PUT'] = ( + FAKE_COMMON_SUCCESS_RESPONSE) + +MAP_COMMAND_TO_FAKE_RESPONSE['/HyperMetroPair/11/DELETE'] = ( + FAKE_COMMON_SUCCESS_RESPONSE) + def Fake_sleep(time): pass @@ -1373,6 +1511,7 @@ class Huawei18000ISCSIDriverTestCase(test.TestCase): self.configuration = mock.Mock(spec=conf.Configuration) self.configuration.cinder_huawei_conf_file = self.fake_conf_file self.xml_file_path = self.configuration.cinder_huawei_conf_file + self.configuration.hypermetro_devices = hypermetro_devices self.stubs.Set(time, 'sleep', Fake_sleep) driver = Fake18000ISCSIStorage(configuration=self.configuration) self.driver = driver @@ -1722,26 +1861,133 @@ class Huawei18000ISCSIDriverTestCase(test.TestCase): (qos_id, lun_list) = self.driver.restclient.find_available_qos(qos) self.assertEqual(("11", u'["0", "1", "2"]'), (qos_id, lun_list)) - @mock.patch.object(rest_client.RestClient, 'get_volume_by_name', + @mock.patch.object(huawei_utils, 'get_volume_params', + return_value=fake_hypermetro_opts) + @mock.patch.object(rest_client.RestClient, 'login_with_ip', + return_value='123456789') + @mock.patch.object(rest_client.RestClient, 'find_all_pools', + return_value=FAKE_STORAGE_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'find_pool_info', + return_value=FAKE_FIND_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'create_volume', + return_value=FAKE_CREATE_VOLUME_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'get_hyper_domain_id', return_value='11') - @mock.patch.object(rest_client.RestClient, 'get_lun_info', - return_value={'ID': '11'}) - def test_create_volume_exist(self, mock_lun_info, mock_volume_info): + @mock.patch.object(hypermetro.HuaweiHyperMetro, '_wait_volume_ready', + return_value=True) + @mock.patch.object(hypermetro.HuaweiHyperMetro, + '_create_hypermetro_pair', + return_value={"ID": '11', + "NAME": 'hypermetro-pair'}) + @mock.patch.object(rest_client.RestClient, 'logout', + return_value=None) + def test_create_hypermetro_success(self, mock_logout, + mock_hyper_pair_info, + mock_volume_ready, + mock_hyper_domain, + mock_create_volume, + mock_pool_info, + mock_all_pool_info, + mock_login_return, + mock_hypermetro_opts): self.driver.restclient.login() - lun_param = {'NAME': 'IexzQZJWSXuX2e9I7c8GNQ'} + metadata = {"hypermetro_id": '11', + "remote_lun_id": '1'} + lun_info = self.driver.create_volume(hyper_volume) + mock_logout.assert_called_with() + self.assertEqual(metadata, lun_info['metadata']) - fack_error_volume_exist = {"error": {"code": 1077948993}} - with mock.patch.object(rest_client.RestClient, 'call', - return_value=fack_error_volume_exist): - lun_info = self.driver.restclient.create_volume(lun_param) - self.assertEqual('11', lun_info['ID']) + @mock.patch.object(huawei_utils, 'get_volume_params', + return_value=fake_hypermetro_opts) + @mock.patch.object(rest_client.RestClient, 'login_with_ip', + return_value='123456789') + @mock.patch.object(rest_client.RestClient, 'find_all_pools', + return_value=FAKE_STORAGE_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'find_pool_info', + return_value=FAKE_FIND_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'create_volume', + return_value=FAKE_CREATE_VOLUME_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'get_hyper_domain_id', + return_value='11') + @mock.patch.object(hypermetro.HuaweiHyperMetro, '_wait_volume_ready', + return_value=True) + @mock.patch.object(hypermetro.HuaweiHyperMetro, + '_create_hypermetro_pair') + @mock.patch.object(rest_client.RestClient, 'delete_lun', + return_value=None) + @mock.patch.object(rest_client.RestClient, 'logout', + return_value=None) + def test_create_hypermetro_fail(self, mock_logout, + mock_delete_lun, + mock_hyper_pair_info, + mock_volume_ready, + mock_hyper_domain, + mock_create_volume, + mock_pool_info, + mock_all_pool_info, + mock_login_return, + mock_hypermetro_opts): + self.driver.restclient.login() + mock_hyper_pair_info.side_effect = exception.VolumeBackendAPIException( + data='Create hypermetro error.') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, hyper_volume) + mock_delete_lun.assert_called_with('1') + mock_logout.assert_called_with() - fack_error_volume_exist = {"error": {"code": 123456789}} - with mock.patch.object(rest_client.RestClient, 'call', - return_value=fack_error_volume_exist): - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.restclient.create_volume, - lun_param) + @mock.patch.object(rest_client.RestClient, 'login_with_ip', + return_value='123456789') + @mock.patch.object(rest_client.RestClient, 'check_lun_exist', + return_value=True) + @mock.patch.object(rest_client.RestClient, 'check_hypermetro_exist', + return_value=True) + @mock.patch.object(rest_client.RestClient, 'get_hypermetro_by_id', + return_value=FAKE_METRO_INFO_RESPONCE) + @mock.patch.object(rest_client.RestClient, 'delete_hypermetro', + return_value=FAKE_COMMON_SUCCESS_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'delete_lun', + return_value=None) + @mock.patch.object(rest_client.RestClient, 'logout', + return_value=None) + def test_delete_hypermetro_success(self, mock_logout, + mock_delete_lun, + mock_delete_hypermetro, + mock_metro_info, + mock_check_hyermetro, + mock_lun_exit, + mock_login_info): + self.driver.restclient.login() + result = self.driver.delete_volume(hyper_volume) + mock_logout.assert_called_with() + self.assertTrue(result) + + @mock.patch.object(rest_client.RestClient, 'login_with_ip', + return_value='123456789') + @mock.patch.object(rest_client.RestClient, 'check_lun_exist', + return_value=True) + @mock.patch.object(rest_client.RestClient, 'check_hypermetro_exist', + return_value=True) + @mock.patch.object(rest_client.RestClient, 'get_hypermetro_by_id', + return_value=FAKE_METRO_INFO_RESPONCE) + @mock.patch.object(rest_client.RestClient, 'delete_hypermetro') + @mock.patch.object(rest_client.RestClient, 'delete_lun', + return_value=None) + @mock.patch.object(rest_client.RestClient, 'logout', + return_value=None) + def test_delete_hypermetro_fail(self, mock_logout, + mock_delete_lun, + mock_delete_hypermetro, + mock_metro_info, + mock_check_hyermetro, + mock_lun_exit, + mock_login_info): + self.driver.restclient.login() + mock_delete_hypermetro.side_effect = ( + exception.VolumeBackendAPIException(data='Delete hypermetro ' + 'error.')) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_volume, hyper_volume) + mock_delete_lun.assert_called_with('11') def create_fake_conf_file(self): """Create a fake Config file. @@ -1848,6 +2094,7 @@ class Huawei18000FCDriverTestCase(test.TestCase): self.configuration = mock.Mock(spec=conf.Configuration) self.configuration.cinder_huawei_conf_file = self.fake_conf_file self.xml_file_path = self.configuration.cinder_huawei_conf_file + self.configuration.hypermetro_devices = hypermetro_devices self.stubs.Set(time, 'sleep', Fake_sleep) driver = Fake18000FCStorage(configuration=self.configuration) self.driver = driver @@ -2243,6 +2490,41 @@ class Huawei18000FCDriverTestCase(test.TestCase): None) self.assertEqual(expected_pool_capacity, pool_capacity) + @mock.patch.object(huawei_utils, 'get_volume_params', + return_value=fake_hypermetro_opts) + @mock.patch.object(rest_client.RestClient, 'login_with_ip', + return_value='123456789') + @mock.patch.object(rest_client.RestClient, 'find_all_pools', + return_value=FAKE_STORAGE_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'find_pool_info', + return_value=FAKE_FIND_POOL_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'create_volume', + return_value=FAKE_CREATE_VOLUME_RESPONSE) + @mock.patch.object(rest_client.RestClient, 'get_hyper_domain_id', + return_value='11') + @mock.patch.object(hypermetro.HuaweiHyperMetro, '_wait_volume_ready', + return_value=True) + @mock.patch.object(hypermetro.HuaweiHyperMetro, + '_create_hypermetro_pair', + return_value={"ID": '11', + "NAME": 'hypermetro-pair'}) + @mock.patch.object(rest_client.RestClient, 'logout', + return_value=None) + def test_create_hypermetro_success(self, mock_hypermetro_opts, + mock_login_return, + mock_all_pool_info, + mock_pool_info, + mock_create_volume, + mock_hyper_domain, + mock_volume_ready, + mock_pair_info, + mock_logout): + self.driver.restclient.login() + metadata = {"hypermetro_id": '11', + "remote_lun_id": '1'} + lun_info = self.driver.create_volume(hyper_volume) + self.assertEqual(metadata, lun_info['metadata']) + def create_fake_conf_file(self): """Create a fake Config file diff --git a/cinder/volume/drivers/huawei/constants.py b/cinder/volume/drivers/huawei/constants.py index 47f59a566..764c0f734 100644 --- a/cinder/volume/drivers/huawei/constants.py +++ b/cinder/volume/drivers/huawei/constants.py @@ -42,6 +42,10 @@ ERROR_UNAUTHORIZED_TO_SERVER = -401 SOCKET_TIMEOUT = 52 ERROR_VOLUME_ALREADY_EXIST = 1077948993 LOGIN_SOCKET_TIMEOUT = 4 +ERROR_VOLUME_NOT_EXIST = 1077939726 +RELOGIN_ERROR_PASS = [ERROR_VOLUME_NOT_EXIST] +HYPERMETRO_RUNNSTATUS_STOP = 41 +HYPERMETRO_RUNNSTATUS_NORMAL = 1 THICK_LUNTYPE = 0 THIN_LUNTYPE = 1 @@ -62,3 +66,4 @@ HUAWEI_VALID_KEYS = ['maxIOPS', 'minIOPS', 'minBandWidth', QOS_KEYS = ['MAXIOPS', 'MINIOPS', 'MINBANDWidth', 'MAXBANDWidth', 'LATENCY', 'IOTYPE'] MAX_LUN_NUM_IN_QOS = 64 +HYPERMETRO_CLASS = "cinder.volume.drivers.huawei.hypermetro.HuaweiHyperMetro" diff --git a/cinder/volume/drivers/huawei/huawei_driver.py b/cinder/volume/drivers/huawei/huawei_driver.py index 90b269420..3e1a1a2e5 100644 --- a/cinder/volume/drivers/huawei/huawei_driver.py +++ b/cinder/volume/drivers/huawei/huawei_driver.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import six import uuid @@ -28,6 +29,7 @@ from cinder.volume import driver from cinder.volume.drivers.huawei import constants from cinder.volume.drivers.huawei import fc_zone_helper from cinder.volume.drivers.huawei import huawei_utils +from cinder.volume.drivers.huawei import hypermetro from cinder.volume.drivers.huawei import rest_client from cinder.volume.drivers.huawei import smartx from cinder.volume import utils as volume_utils @@ -39,8 +41,11 @@ LOG = logging.getLogger(__name__) huawei_opts = [ cfg.StrOpt('cinder_huawei_conf_file', default='/etc/cinder/cinder_huawei_conf.xml', - help='The configuration file for the Cinder Huawei ' - 'driver.')] + help='The configuration file for the Cinder Huawei driver.'), + cfg.StrOpt('hypermetro_devices', + default=None, + help='The remote device hypermetro will use.'), +] CONF = cfg.CONF CONF.register_opts(huawei_opts) @@ -57,6 +62,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.configuration.append_config_values(huawei_opts) self.xml_file_path = self.configuration.cinder_huawei_conf_file + self.hypermetro_devices = self.configuration.hypermetro_devices def do_setup(self, context): """Instantiate common class and login storage system.""" @@ -127,9 +133,31 @@ class HuaweiBaseDriver(driver.VolumeDriver): raise exception.InvalidInput( reason=_('Create volume error. Because %s.') % err) - return {'provider_location': lun_info['ID'], + # Update the metadata. + LOG.info(_LI('Create volume option: %s.'), opts) + metadata = huawei_utils.get_volume_metadata(volume) + if opts.get('hypermetro'): + hyperm = hypermetro.HuaweiHyperMetro(self.restclient, None, + self.configuration) + try: + metro_id, remote_lun_id = hyperm.create_hypermetro(lun_id, + lun_param) + except exception.VolumeBackendAPIException as err: + LOG.exception(_LE('Create hypermetro error: %s.'), err) + self._delete_lun_with_check(lun_id) + raise + + LOG.info(_LI("Hypermetro id: %(metro_id)s. " + "Remote lun id: %(remote_lun_id)s."), + {'metro_id': metro_id, + 'remote_lun_id': remote_lun_id}) + + metadata.update({'hypermetro_id': metro_id, + 'remote_lun_id': remote_lun_id}) + + return {'provider_location': lun_id, 'ID': lun_id, - 'lun_info': lun_info} + 'metadata': metadata} @utils.synchronized('huawei', external=True) def delete_volume(self, volume): @@ -150,6 +178,17 @@ class HuaweiBaseDriver(driver.VolumeDriver): if qos_id: self.remove_qos_lun(lun_id, qos_id) + metadata = huawei_utils.get_volume_metadata(volume) + if 'hypermetro_id' in metadata: + hyperm = hypermetro.HuaweiHyperMetro(self.restclient, None, + self.configuration) + try: + hyperm.delete_hypermetro(volume) + except exception.VolumeBackendAPIException as err: + LOG.exception(_LE('Delete hypermetro error: %s.'), err) + self.restclient.delete_lun(lun_id) + raise + self.restclient.delete_lun(lun_id) else: LOG.warning(_LW("Can't find lun %s on the array."), lun_id) @@ -188,11 +227,11 @@ class HuaweiBaseDriver(driver.VolumeDriver): if constants.MIGRATION_COMPLETE == item['RUNNINGSTATUS']: return True if constants.MIGRATION_FAULT == item['RUNNINGSTATUS']: - err_msg = (_('Lun migration error.')) + 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.")) + err_msg = _("Cannot find migration task.") LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) @@ -1010,6 +1049,7 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): Volume migration support Volume retype support FC zone enhancement + Volume hypermetro support """ VERSION = "1.1.1" @@ -1090,23 +1130,82 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): # Add host into hostgroup. hostgroup_id = self.restclient.add_host_into_hostgroup(host_id) - self.restclient.do_mapping(lun_id, hostgroup_id, host_id) + map_info = self.restclient.do_mapping(lun_id, + hostgroup_id, + host_id) host_lun_id = self.restclient.find_host_lun_id(host_id, lun_id) # Return FC properties. - info = {'driver_volume_type': 'fibre_channel', - 'data': {'target_lun': int(host_lun_id), - 'target_discovered': True, - 'target_wwn': tgt_port_wwns, - 'volume_id': volume['id'], - 'initiator_target_map': init_targ_map}, } + fc_info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': int(host_lun_id), + 'target_discovered': True, + 'target_wwn': tgt_port_wwns, + 'volume_id': volume['id'], + 'initiator_target_map': init_targ_map, + 'map_info': map_info}, } - LOG.info(_LI("initialize_connection, return data is: %s."), - info) + loc_tgt_wwn = fc_info['data']['target_wwn'] + local_ini_tgt_map = fc_info['data']['initiator_target_map'] - return info + # Deal with hypermetro connection. + metadata = huawei_utils.get_volume_metadata(volume) + LOG.info(_LI("initialize_connection, metadata is: %s."), metadata) + if 'hypermetro_id' in metadata: + hyperm = hypermetro.HuaweiHyperMetro(self.restclient, None, + self.configuration) + rmt_fc_info = hyperm.connect_volume_fc(volume, connector) + + rmt_tgt_wwn = rmt_fc_info['data']['target_wwn'] + rmt_ini_tgt_map = rmt_fc_info['data']['initiator_target_map'] + fc_info['data']['target_wwn'] = (loc_tgt_wwn + rmt_tgt_wwn) + wwns = connector['wwpns'] + for wwn in wwns: + if (wwn in local_ini_tgt_map + and wwn in rmt_ini_tgt_map): + fc_info['data']['initiator_target_map'][wwn].extend( + rmt_ini_tgt_map[wwn]) + + elif (wwn not in local_ini_tgt_map + and wwn in rmt_ini_tgt_map): + fc_info['data']['initiator_target_map'][wwn] = ( + rmt_ini_tgt_map[wwn]) + # else, do nothing + + loc_map_info = fc_info['data']['map_info'] + rmt_map_info = rmt_fc_info['data']['map_info'] + same_host_id = self._get_same_hostid(loc_map_info, + rmt_map_info) + + self.restclient.change_hostlun_id(loc_map_info, same_host_id) + hyperm.rmt_client.change_hostlun_id(rmt_map_info, same_host_id) + + fc_info['data']['target_lun'] = same_host_id + hyperm.rmt_client.logout() + + LOG.info(_LI("Return FC info is: %s."), fc_info) + return fc_info + + def _get_same_hostid(self, loc_fc_info, rmt_fc_info): + loc_aval_luns = loc_fc_info['aval_luns'] + loc_aval_luns = json.loads(loc_aval_luns) + + rmt_aval_luns = rmt_fc_info['aval_luns'] + rmt_aval_luns = json.loads(rmt_aval_luns) + same_host_id = None + + for i in range(1, 512): + if i in rmt_aval_luns and i in loc_aval_luns: + same_host_id = i + break + + LOG.info(_LI("The same hostid is: %s."), same_host_id) + if not same_host_id: + msg = _("Can't find the same host id from arrays.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + return same_host_id - @utils.synchronized('huawei', external=True) @fczm_utils.RemoveFCZone def terminate_connection(self, volume, connector, **kwargs): """Delete map between a volume and a host.""" @@ -1150,8 +1249,8 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): if lungroup_id: left_lunnum = self.restclient.get_lunnum_from_lungroup(lungroup_id) if int(left_lunnum) > 0: - info = {'driver_volume_type': 'fibre_channel', - 'data': {}} + fc_info = {'driver_volume_type': 'fibre_channel', + 'data': {}} else: if not self.fcsan_lookup_service: self.fcsan_lookup_service = fczm_utils.create_lookup_service() @@ -1195,10 +1294,19 @@ class Huawei18000FCDriver(HuaweiBaseDriver, driver.FibreChannelDriver): if view_id: self.restclient.delete_mapping_view(view_id) - info = {'driver_volume_type': 'fibre_channel', - 'data': {'target_wwn': tgt_port_wwns, - 'initiator_target_map': init_targ_map}} - LOG.info(_LI("terminate_connection, return data is: %s."), - info) + fc_info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_wwn': tgt_port_wwns, + 'initiator_target_map': init_targ_map}} - return info + # Deal with hypermetro connection. + metadata = huawei_utils.get_volume_metadata(volume) + LOG.info(_LI("Detach Volume, metadata is: %s."), metadata) + if 'hypermetro_id' in metadata: + hyperm = hypermetro.HuaweiHyperMetro(self.restclient, None, + self.configuration) + hyperm.disconnect_volume_fc(volume, connector) + + LOG.info(_LI("terminate_connection, return data is: %s."), + fc_info) + + return fc_info diff --git a/cinder/volume/drivers/huawei/huawei_utils.py b/cinder/volume/drivers/huawei/huawei_utils.py index bb55b369c..0da3efa5d 100644 --- a/cinder/volume/drivers/huawei/huawei_utils.py +++ b/cinder/volume/drivers/huawei/huawei_utils.py @@ -14,6 +14,7 @@ # under the License. import base64 +import json import six import time import uuid @@ -40,6 +41,7 @@ opts_capability = { 'smartpartition': False, 'thin_provisioning_support': False, 'thick_provisioning_support': False, + 'hypermetro': False, } @@ -539,3 +541,29 @@ def get_pools(xml_file_path): LOG.error(msg) raise exception.InvalidInput(msg) return pool_names + + +def get_remote_device_info(valid_hypermetro_devices): + remote_device_info = {} + try: + if valid_hypermetro_devices: + remote_device_info = json.loads(valid_hypermetro_devices) + else: + return + + except ValueError as err: + msg = _("Get remote device info error. %s.") % err + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if len(remote_device_info) == 1: + for device_key, device_value in remote_device_info.items(): + return remote_device_info.get(device_key) + + +def get_volume_metadata(volume): + if 'volume_metadata' in volume: + metadata = volume.get('volume_metadata') + return {item['key']: item['value'] for item in metadata} + + return {} diff --git a/cinder/volume/drivers/huawei/hypermetro.py b/cinder/volume/drivers/huawei/hypermetro.py new file mode 100644 index 000000000..22eea83af --- /dev/null +++ b/cinder/volume/drivers/huawei/hypermetro.py @@ -0,0 +1,321 @@ +# Copyright (c) 2015 Huawei Technologies Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import six + +from oslo_log import log as logging + +from cinder import exception +from cinder.i18n import _, _LI, _LW +from cinder.volume.drivers.huawei import constants +from cinder.volume.drivers.huawei import huawei_utils +from cinder.volume.drivers.huawei import rest_client + +LOG = logging.getLogger(__name__) + + +class HuaweiHyperMetro(object): + + def __init__(self, client, rmt_client, configuration): + self.client = client + self.rmt_client = rmt_client + self.configuration = configuration + self.xml_file_path = self.configuration.cinder_huawei_conf_file + + def create_hypermetro(self, local_lun_id, lun_param): + """Create hypermetro.""" + metro_devices = self.configuration.hypermetro_devices + device_info = huawei_utils.get_remote_device_info(metro_devices) + self.rmt_client = rest_client.RestClient(self.configuration) + self.rmt_client.login_with_ip(device_info) + + try: + # Get the remote pool info. + config_pool = device_info['StoragePool'] + remote_pool = self.rmt_client.find_all_pools() + pool = self.rmt_client.find_pool_info(config_pool, + remote_pool) + # Create remote lun + lun_param['PARENTID'] = pool['ID'] + remotelun_info = self.rmt_client.create_volume(lun_param) + remote_lun_id = remotelun_info['ID'] + + # Get hypermetro domain + try: + domain_name = device_info['domain_name'] + domain_id = self.rmt_client.get_hyper_domain_id(domain_name) + self._wait_volume_ready(remote_lun_id) + hypermetro = self._create_hypermetro_pair(domain_id, + local_lun_id, + remote_lun_id) + + return hypermetro['ID'], remote_lun_id + except Exception as err: + self.rmt_client.delete_lun(remote_lun_id) + msg = _('Create hypermetro error. %s.') % err + raise exception.VolumeBackendAPIException(data=msg) + except exception.VolumeBackendAPIException: + raise + except Exception as err: + msg = _("Create remote LUN error. %s.") % err + LOG.exception(msg) + raise exception.VolumeBackendAPIException(data=msg) + finally: + self.rmt_client.logout() + + def delete_hypermetro(self, volume): + """Delete hypermetro.""" + metadata = huawei_utils.get_volume_metadata(volume) + metro_id = metadata['hypermetro_id'] + remote_lun_id = metadata['remote_lun_id'] + + if metro_id: + exst_flag = self.client.check_hypermetro_exist(metro_id) + if exst_flag: + metro_info = self.client.get_hypermetro_by_id(metro_id) + metro_status = int(metro_info['data']['RUNNINGSTATUS']) + + LOG.debug("Hypermetro status is: %s.", metro_status) + if constants.HYPERMETRO_RUNNSTATUS_STOP != metro_status: + self.client.stop_hypermetro(metro_id) + + # Delete hypermetro + self.client.delete_hypermetro(metro_id) + + # Delete remote lun. + if remote_lun_id: + metro_devices = self.configuration.hypermetro_devices + device_info = huawei_utils.get_remote_device_info(metro_devices) + self.rmt_client = rest_client.RestClient(self.configuration) + self.rmt_client.login_with_ip(device_info) + + try: + if self.rmt_client.check_lun_exist(remote_lun_id): + self.rmt_client.delete_lun(remote_lun_id) + except Exception as err: + msg = _("Delete remote lun err. %s.") % err + LOG.exception(msg) + raise exception.VolumeBackendAPIException(data=msg) + finally: + self.rmt_client.logout() + + def _create_hypermetro_pair(self, domain_id, lun_id, remote_lun_id): + """Create a HyperMetroPair.""" + hcp_param = {"DOMAINID": domain_id, + "HCRESOURCETYPE": '1', + "ISFIRSTSYNC": False, + "LOCALOBJID": lun_id, + "RECONVERYPOLICY": '1', + "REMOTEOBJID": remote_lun_id, + "SPEED": '2'} + + return self.client.create_hypermetro(hcp_param) + + def connect_volume_fc(self, volume, connector): + """Create map between a volume and a host for FC.""" + self.xml_file_path = self.configuration.cinder_huawei_conf_file + metro_devices = self.configuration.hypermetro_devices + device_info = huawei_utils.get_remote_device_info(metro_devices) + self.rmt_client = rest_client.RestClient(self.configuration) + self.rmt_client.login_with_ip(device_info) + + try: + wwns = connector['wwpns'] + volume_name = huawei_utils.encode_name(volume['id']) + + LOG.info(_LI( + 'initialize_connection_fc, initiator: %(wwpns)s,' + ' volume name: %(volume)s.'), + {'wwpns': wwns, + 'volume': volume_name}) + + metadata = huawei_utils.get_volume_metadata(volume) + lun_id = metadata['remote_lun_id'] + + if lun_id is None: + lun_id = self.rmt_client.get_volume_by_name(volume_name) + if lun_id is None: + msg = _("Can't get volume id. Volume name: %s.") % volume_name + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + host_name_before_hash = None + host_name = connector['host'] + if host_name and (len(host_name) > constants.MAX_HOSTNAME_LENGTH): + host_name_before_hash = host_name + host_name = six.text_type(hash(host_name)) + + # Create hostgroup if not exist. + host_id = self.rmt_client.add_host_with_check( + host_name, host_name_before_hash) + + online_wwns_in_host = ( + self.rmt_client.get_host_online_fc_initiators(host_id)) + online_free_wwns = self.rmt_client.get_online_free_wwns() + for wwn in wwns: + if (wwn not in online_wwns_in_host + and wwn not in online_free_wwns): + wwns_in_host = ( + self.rmt_client.get_host_fc_initiators(host_id)) + iqns_in_host = ( + self.rmt_client.get_host_iscsi_initiators(host_id)) + if not wwns_in_host and not iqns_in_host: + self.rmt_client.remove_host(host_id) + + msg = _('Can not add FC port to host.') + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + for wwn in wwns: + if wwn in online_free_wwns: + self.rmt_client.add_fc_port_to_host(host_id, wwn) + + (tgt_port_wwns, init_targ_map) = ( + self.rmt_client.get_init_targ_map(wwns)) + + # Add host into hostgroup. + hostgroup_id = self.rmt_client.add_host_into_hostgroup(host_id) + map_info = self.rmt_client.do_mapping(lun_id, + hostgroup_id, + host_id) + host_lun_id = self.rmt_client.find_host_lun_id(host_id, lun_id) + except exception.VolumeBackendAPIException: + raise + except Exception as err: + msg = _("Connect volume fc: connect volume error. %s.") % err + LOG.exception(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Return FC properties. + fc_info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': int(host_lun_id), + 'target_discovered': True, + 'target_wwn': tgt_port_wwns, + 'volume_id': volume['id'], + 'initiator_target_map': init_targ_map, + 'map_info': map_info}, + } + + LOG.info(_LI('Remote return FC info is: %s.'), fc_info) + + return fc_info + + def disconnect_volume_fc(self, volume, connector): + """Delete map between a volume and a host for FC.""" + # Login remote storage device. + self.xml_file_path = self.configuration.cinder_huawei_conf_file + metro_devices = self.configuration.hypermetro_devices + device_info = huawei_utils.get_remote_device_info(metro_devices) + self.rmt_client = rest_client.RestClient(self.configuration) + self.rmt_client.login_with_ip(device_info) + + try: + wwns = connector['wwpns'] + volume_name = huawei_utils.encode_name(volume['id']) + metadata = huawei_utils.get_volume_metadata(volume) + lun_id = metadata['remote_lun_id'] + host_name = connector['host'] + left_lunnum = -1 + lungroup_id = None + view_id = None + + LOG.info(_LI('terminate_connection_fc: volume name: %(volume)s, ' + 'wwpns: %(wwns)s, ' + 'lun_id: %(lunid)s.'), + {'volume': volume_name, + 'wwns': wwns, + 'lunid': lun_id},) + + if host_name and (len(host_name) > constants.MAX_HOSTNAME_LENGTH): + host_name = six.text_type(hash(host_name)) + + hostid = self.rmt_client.find_host(host_name) + if hostid: + mapping_view_name = constants.MAPPING_VIEW_PREFIX + hostid + view_id = self.rmt_client.find_mapping_view( + mapping_view_name) + if view_id: + lungroup_id = self.rmt_client.find_lungroup_from_map( + view_id) + + if lun_id and self.rmt_client.check_lun_exist(lun_id): + if lungroup_id: + lungroup_ids = self.rmt_client.get_lungroupids_by_lunid( + lun_id) + if lungroup_id in lungroup_ids: + self.rmt_client.remove_lun_from_lungroup( + lungroup_id, lun_id) + else: + LOG.warning(_LW("Lun is not in lungroup. " + "Lun id: %(lun_id)s, " + "lungroup id: %(lungroup_id)s"), + {"lun_id": lun_id, + "lungroup_id": lungroup_id}) + + (tgt_port_wwns, init_targ_map) = ( + self.rmt_client.get_init_targ_map(wwns)) + + hostid = self.rmt_client.find_host(host_name) + if hostid: + mapping_view_name = constants.MAPPING_VIEW_PREFIX + hostid + view_id = self.rmt_client.find_mapping_view( + mapping_view_name) + if view_id: + lungroup_id = self.rmt_client.find_lungroup_from_map( + view_id) + if lungroup_id: + left_lunnum = self.rmt_client.get_lunnum_from_lungroup( + lungroup_id) + + except Exception as err: + msg = _("Remote detatch volume error. %s.") % err + LOG.exception(msg) + raise exception.VolumeBackendAPIException(data=msg) + finally: + self.rmt_client.logout() + + if int(left_lunnum) > 0: + info = {'driver_volume_type': 'fibre_channel', + 'data': {}} + else: + info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_wwn': tgt_port_wwns, + 'initiator_target_map': init_targ_map}, } + + return info + + 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.rmt_client.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 retype(self, volume, new_type): + return False + + def get_hypermetro_stats(self, hypermetro_id): + pass diff --git a/cinder/volume/drivers/huawei/rest_client.py b/cinder/volume/drivers/huawei/rest_client.py index c956e1c07..4541c3bc5 100644 --- a/cinder/volume/drivers/huawei/rest_client.py +++ b/cinder/volume/drivers/huawei/rest_client.py @@ -14,6 +14,7 @@ # under the License. import json +import six import socket import time @@ -38,6 +39,7 @@ class RestClient(object): self.configuration = configuration self.xml_file_path = configuration.cinder_huawei_conf_file self.productversion = None + self.init_http_head() def init_http_head(self): self.cookie = http_cookiejar.CookieJar() @@ -149,8 +151,47 @@ class RestClient(object): {'old_url': old_url, 'new_url': self.url}) result = self.do_call(url, data, method) + if result['error']['code'] in constants.RELOGIN_ERROR_PASS: + result['error']['code'] = 0 return result + def login_with_ip(self, login_info): + """Login 18000 array with the specific URL.""" + urlstr = login_info['RestURL'] + url_list = urlstr.split(";") + for item_url in url_list: + url = item_url + "xx/sessions" + data = json.dumps({"username": login_info['UserName'], + "password": login_info['UserPassword'], + "scope": '0'}) + result = self.call(url, data) + + if result['error']['code'] == constants.ERROR_CONNECT_TO_SERVER: + continue + + if (result['error']['code'] != 0) or ('data' not in result): + msg = (_("Login error, reason is: %s.") % result) + LOG.error(msg) + 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 + + msg = _("Login error: Can not connect to server.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def logout(self): + """Logout the session.""" + url = "/sessions" + if self.url: + result = self.call(url, None, "DELETE") + self._assert_rest_result(result, _('Logout session error.')) + def _assert_rest_result(self, result, err_str): if result['error']['code'] != 0: msg = (_('%(err)s\nresult: %(res)s.') % {'err': err_str, @@ -383,6 +424,7 @@ class RestClient(object): mapping_view_name = constants.MAPPING_VIEW_PREFIX + host_id lungroup_id = self._find_lungroup(lungroup_name) view_id = self.find_mapping_view(mapping_view_name) + map_info = {} LOG.info(_LI( 'do_mapping, lun_group: %(lun_group)s, ' @@ -418,6 +460,13 @@ class RestClient(object): self._associate_portgroup_to_view(view_id, tgtportgroup_id) + version = self.find_array_version() + if version >= constants.ARRAY_VERSION: + aval_luns = self.find_view_by_id(view_id) + map_info["lun_id"] = lun_id + map_info["view_id"] = view_id + map_info["aval_luns"] = aval_luns + except Exception: with excutils.save_and_reraise_exception(): LOG.error(_LE( @@ -425,6 +474,8 @@ class RestClient(object): 'view. Remove lun from lungroup now.')) self.remove_lun_from_lungroup(lungroup_id, lun_id) + return map_info + def ensure_initiator_added(self, xml_file_path, initiator_name, host_id): added = self._initiator_is_added_to_array(initiator_name) if not added: @@ -1114,6 +1165,7 @@ class RestClient(object): smarttier=True, smartcache=True, smartpartition=True, + hypermetro=True, )) data['pools'].append(pool) return data @@ -1200,6 +1252,9 @@ class RestClient(object): LOG.error(msg) raise exception.InvalidInput(reason=msg) + # Deal with the remote tgt ip. + if 'remote_target_ip' in connector: + target_ips.append(connector['remote_target_ip']) LOG.info(_LI('Get the default ip: %s.'), target_ips) for ip in target_ips: target_iqn = self._get_tgt_iqn_from_rest(ip) @@ -1678,3 +1733,112 @@ class RestClient(object): constants.FC_PORT_CONNECTED): port_list_from_contr.append(item['WWN']) return port_list_from_contr + + def get_hyper_domain_id(self, domain_name): + url = "/HyperMetroDomain?range=[0-100]" + result = self.call(url, None, "GET") + domain_id = None + if "data" in result: + for item in result['data']: + if domain_name == item['NAME']: + domain_id = item['ID'] + break + + msg = _('get_hyper_domain_id error.') + self._assert_rest_result(result, msg) + return domain_id + + def create_hypermetro(self, hcp_param): + url = "/HyperMetroPair" + data = json.dumps(hcp_param) + result = self.call(url, data, "POST") + + msg = _('create_hypermetro_pair error.') + self._assert_rest_result(result, msg) + self._assert_data_in_result(result, msg) + return result['data'] + + def delete_hypermetro(self, metro_id): + url = "/HyperMetroPair/" + metro_id + result = self.call(url, None, "DELETE") + + msg = _('delete_hypermetro error.') + self._assert_rest_result(result, msg) + + def sync_hypermetro(self, metro_id): + url = "/HyperMetroPair/synchronize_hcpair" + + data = json.dumps({"ID": metro_id, + "TYPE": "15361"}) + result = self.call(url, data, "PUT") + + msg = _('sync_hypermetro error.') + self._assert_rest_result(result, msg) + + def stop_hypermetro(self, metro_id): + url = '/HyperMetroPair/disable_hcpair' + + data = json.dumps({"ID": metro_id, + "TYPE": "15361"}) + result = self.call(url, data, "PUT") + + msg = _('stop_hypermetro error.') + self._assert_rest_result(result, msg) + + def get_hypermetro_by_id(self, metro_id): + url = "/HyperMetroPair/" + metro_id + result = self.call(url, None, "GET") + + msg = _('get_hypermetro_by_id error.') + self._assert_rest_result(result, msg) + return result + + def check_hypermetro_exist(self, metro_id): + url = "/HyperMetroPair/" + metro_id + result = self.call(url, None, "GET") + error_code = result['error']['code'] + + if (error_code == constants.ERROR_CONNECT_TO_SERVER + or error_code == constants.ERROR_UNAUTHORIZED_TO_SERVER): + LOG.error(_LE("Can not open the recent url, login again.")) + self.login() + result = self.call(url, None, "GET") + + error_code = result['error']['code'] + if (error_code == constants.ERROR_CONNECT_TO_SERVER + or error_code == constants.ERROR_UNAUTHORIZED_TO_SERVER): + msg = _("check_hypermetro_exist error.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if error_code != 0: + return False + + return True + + def change_hostlun_id(self, map_info, hostlun_id): + url = "/mappingview" + view_id = six.text_type(map_info['view_id']) + lun_id = six.text_type(map_info['lun_id']) + hostlun_id = six.text_type(hostlun_id) + data = json.dumps({"TYPE": 245, + "ID": view_id, + "ASSOCIATEOBJTYPE": 11, + "ASSOCIATEOBJID": lun_id, + "ASSOCIATEMETADATA": [{"LUNID": lun_id, + "hostLUNId": hostlun_id}] + }) + + result = self.call(url, data, "PUT") + + msg = 'change hostlun id error.' + self._assert_rest_result(result, msg) + + def find_view_by_id(self, view_id): + url = "/MAPPINGVIEW/" + view_id + result = self.call(url, None, "GET") + + msg = _('Change hostlun id error.') + self._assert_rest_result(result, msg) + if 'data' in result: + return result["data"]["AVAILABLEHOSTLUNIDLIST"]