From 14dc194af688a18b254aea761beb586214c56224 Mon Sep 17 00:00:00 2001 From: zhangchao010 Date: Sat, 31 Aug 2013 10:24:43 +0800 Subject: [PATCH] Add Fibre Channel drivers for Huawei storage systems This is the third patch, changes as follows: 1.Add Fibre Channel drivers for huawei OceanStor T series and Dorado series arrays. Dorado FC driver inherits codes from FC driver of T. The FC drivers call module ssh_common which has been defined in the preview patch: https://review.openstack.org/#/c/41721/ 2.Add unit test for the changes. Implements: blueprint huawei-fibre-channel-volume-driver Change-Id: Iee20d9746004b57777a7161827b4a23cb10f0859 --- cinder/tests/test_huawei_t_dorado.py | 205 +++++++++++++++- cinder/volume/drivers/huawei/__init__.py | 4 +- cinder/volume/drivers/huawei/huawei_dorado.py | 70 ++++++ cinder/volume/drivers/huawei/huawei_t.py | 222 ++++++++++++++++++ 4 files changed, 498 insertions(+), 3 deletions(-) diff --git a/cinder/tests/test_huawei_t_dorado.py b/cinder/tests/test_huawei_t_dorado.py index 6903b615b..43442e682 100644 --- a/cinder/tests/test_huawei_t_dorado.py +++ b/cinder/tests/test_huawei_t_dorado.py @@ -125,7 +125,8 @@ INITIATOR_SETTING = {'TargetIQN': 'iqn.2006-08.com.huawei:oceanspace:2103037:', 'TargetIQN-form': 'iqn.2006-08.com.huawei:oceanspace:' '2103037::1020001:192.168.100.2', 'Initiator Name': 'iqn.1993-08.debian:01:ec2bff7ac3a3', - 'Initiator TargetIP': '192.168.100.2'} + 'Initiator TargetIP': '192.168.100.2', + 'WWN': ['2011666666666565']} FAKE_VOLUME = {'name': 'Volume-lele34fe-223f-dd33-4423-asdfghjklqwe', 'id': 'lele34fe-223f-dd33-4423-asdfghjklqwe', @@ -147,6 +148,8 @@ FAKE_SNAPSHOT = {'name': 'keke34fe-223f-dd33-4423-asdfghjklqwf', 'provider_location': None} FAKE_CONNECTOR = {'initiator': 'iqn.1993-08.debian:01:ec2bff7ac3a3', + 'wwpns': ['1000000164s45126'], + 'wwnns': ['2000666666666565'], 'host': 'fakehost'} RESPOOL_A_SIM = {'Size': '10240', 'Valid Size': '5120'} @@ -248,8 +251,14 @@ class FakeChannel(): out = self.simu.cli_addhostmap(params) elif cmd == 'delhostmap': out = self.simu.cli_delhostmap(params) + elif cmd == 'showfreeport': + out = self.simu.cli_showfreeport(params) + elif cmd == 'showhostpath': + out = self.simu.cli_showhostpath(params) elif cmd == 'chglun': out = self.simu.cli_chglun(params) + elif cmd == 'showfcmode': + out = self.simu.cli_showfcmode(params) out = self.command[:-1] + out + '\nadmin:/>' return out.replace('\n', '\r\n') @@ -825,6 +834,51 @@ Multipath Type out = 'command operates successfully' return out + def cli_showfreeport(self, params): + out = """/>showfreeport +======================================================================= + Host Free Port Information +----------------------------------------------------------------------- + WWN Or MAC Type Location Connection Status +----------------------------------------------------------------------- + 1000000164s45126 FC Primary Controller Connected +======================================================================= +""" + HOST_PORT_INFO['ID'] = '2' + HOST_PORT_INFO['Name'] = 'FCInitiator001' + HOST_PORT_INFO['Info'] = '1000000164s45126' + HOST_PORT_INFO['Type'] = 'FC' + return out + + def cli_showhostpath(self, params): + host = params[params.index('-host') + 1] + out = """/>showhostpath -host 1 +======================================= + Multi Path Information +--------------------------------------- + Host ID | %s + Controller ID | B + Port Type | FC + Initiator WWN | 1000000164s45126 + Target WWN | %s + Host Port ID | 0 + Link Status | Normal +======================================= +""" % (host, INITIATOR_SETTING['WWN'][0]) + return out + + def cli_showfcmode(self, params): + out = """/>showfcport +========================================================================= + FC Port Topology Mode +------------------------------------------------------------------------- + Controller ID Interface Module ID Port ID WWN Current Mode +------------------------------------------------------------------------- + B 1 P0 %s -- +========================================================================= +-""" % INITIATOR_SETTING['WWN'][0] + return out + def cli_chglun(self, params): if params[params.index('-lun') + 1] == VOLUME_SNAP_ID['vol']: LUN_INFO['Owner Controller'] = 'B' @@ -1393,6 +1447,155 @@ class HuaweiTISCSIDriverTestCase(test.TestCase): self.assertEqual(stats['storage_protocol'], 'iSCSI') +class HuaweiTFCDriverTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(HuaweiTFCDriverTestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(HuaweiTFCDriverTestCase, self).setUp() + + self.tmp_dir = tempfile.mkdtemp() + self.fake_conf_file = self.tmp_dir + '/cinder_huawei_conf.xml' + create_fake_conf_file(self.fake_conf_file) + modify_conf(self.fake_conf_file, 'Storage/Protocol', 'FC') + self.configuration = mox.MockObject(conf.Configuration) + self.configuration.cinder_huawei_conf_file = self.fake_conf_file + self.configuration.append_config_values(mox.IgnoreArg()) + + self.stubs.Set(time, 'sleep', Fake_sleep) + self.stubs.Set(utils, 'SSHPool', FakeSSHPool) + self.stubs.Set(ssh_common.TseriesCommon, '_change_file_mode', + Fake_change_file_mode) + self._init_driver() + + def _init_driver(self): + Curr_test[0] = 'T' + self.driver = HuaweiVolumeDriver(configuration=self.configuration) + self.driver.do_setup(None) + + def tearDown(self): + if os.path.exists(self.fake_conf_file): + os.remove(self.fake_conf_file) + shutil.rmtree(self.tmp_dir) + super(HuaweiTFCDriverTestCase, self).tearDown() + + def test_validate_connector_failed(self): + invalid_connector = {'host': 'testhost'} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.validate_connector, + invalid_connector) + + def test_create_delete_volume(self): + self.driver.create_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], VOLUME_SNAP_ID['vol']) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def test_create_delete_snapshot(self): + self.driver.create_volume(FAKE_VOLUME) + self.driver.create_snapshot(FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], VOLUME_SNAP_ID['snap']) + self.driver.delete_snapshot(FAKE_SNAPSHOT) + self.assertEqual(SNAPSHOT_INFO['ID'], None) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def test_create_cloned_volume(self): + self.driver.create_volume(FAKE_VOLUME) + ret = self.driver.create_cloned_volume(FAKE_CLONED_VOLUME, FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.assertEqual(ret['provider_location'], CLONED_LUN_INFO['ID']) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(LUN_INFO['ID'], None) + + def test_create_snapshot_volume(self): + self.driver.create_volume(FAKE_VOLUME) + self.driver.create_snapshot(FAKE_SNAPSHOT) + ret = self.driver.create_volume_from_snapshot(FAKE_CLONED_VOLUME, + FAKE_SNAPSHOT) + self.assertEqual(CLONED_LUN_INFO['ID'], VOLUME_SNAP_ID['vol_copy']) + self.assertEqual(ret['provider_location'], CLONED_LUN_INFO['ID']) + self.driver.delete_volume(FAKE_CLONED_VOLUME) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(CLONED_LUN_INFO['ID'], None) + self.assertEqual(LUN_INFO['ID'], None) + + def test_initialize_terminitat_connection(self): + self.driver.create_volume(FAKE_VOLUME) + ret = self.driver.initialize_connection(FAKE_VOLUME, FAKE_CONNECTOR) + fc_properties = ret['data'] + self.assertEquals(fc_properties['target_wwn'], + INITIATOR_SETTING['WWN']) + self.assertEqual(MAP_INFO["DEV LUN ID"], LUN_INFO['ID']) + + self.driver.terminate_connection(FAKE_VOLUME, FAKE_CONNECTOR) + self.assertEqual(MAP_INFO["DEV LUN ID"], None) + self.assertEqual(MAP_INFO["Host LUN ID"], None) + self.driver.delete_volume(FAKE_VOLUME) + self.assertEqual(LUN_INFO['ID'], None) + + def _test_get_volume_stats(self): + stats = self.driver.get_volume_stats(True) + fakecapacity = float(POOL_SETTING['Free Capacity']) / 1024 + self.assertEqual(stats['free_capacity_gb'], fakecapacity) + self.assertEqual(stats['storage_protocol'], 'FC') + + +class HuaweiDorado5100FCDriverTestCase(HuaweiTFCDriverTestCase): + def __init__(self, *args, **kwargs): + super(HuaweiDorado5100FCDriverTestCase, self).__init__(*args, **kwargs) + + def setUp(self): + super(HuaweiDorado5100FCDriverTestCase, self).setUp() + + def _init_driver(self): + Curr_test[0] = 'Dorado5100' + modify_conf(self.fake_conf_file, 'Storage/Product', 'Dorado') + self.driver = HuaweiVolumeDriver(configuration=self.configuration) + self.driver.do_setup(None) + + def test_create_cloned_volume(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + + def test_create_snapshot_volume(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + FAKE_CLONED_VOLUME, FAKE_SNAPSHOT) + + +class HuaweiDorado2100G2FCDriverTestCase(HuaweiTFCDriverTestCase): + def __init__(self, *args, **kwargs): + super(HuaweiDorado2100G2FCDriverTestCase, self).__init__(*args, + **kwargs) + + def setUp(self): + super(HuaweiDorado2100G2FCDriverTestCase, self).setUp() + + def _init_driver(self): + Curr_test[0] = 'Dorado2100G2' + modify_conf(self.fake_conf_file, 'Storage/Product', 'Dorado') + self.driver = HuaweiVolumeDriver(configuration=self.configuration) + self.driver.do_setup(None) + + def test_create_cloned_volume(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + FAKE_CLONED_VOLUME, FAKE_VOLUME) + + def test_create_delete_snapshot(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, FAKE_SNAPSHOT) + + def test_create_snapshot_volume(self): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + FAKE_CLONED_VOLUME, FAKE_SNAPSHOT) + + class HuaweiDorado5100ISCSIDriverTestCase(HuaweiTISCSIDriverTestCase): def __init__(self, *args, **kwargs): super(HuaweiDorado5100ISCSIDriverTestCase, self).__init__(*args, diff --git a/cinder/volume/drivers/huawei/__init__.py b/cinder/volume/drivers/huawei/__init__.py index 732db1d75..f1f5777bf 100644 --- a/cinder/volume/drivers/huawei/__init__.py +++ b/cinder/volume/drivers/huawei/__init__.py @@ -48,7 +48,7 @@ class HuaweiVolumeDriver(object): def __init__(self, *args, **kwargs): super(HuaweiVolumeDriver, self).__init__() self._product = {'T': huawei_t, 'Dorado': huawei_dorado} - self._protocol = {'iSCSI': 'ISCSIDriver'} + self._protocol = {'iSCSI': 'ISCSIDriver', 'FC': 'FCDriver'} self.driver = self._instantiate_driver(*args, **kwargs) @@ -85,7 +85,7 @@ class HuaweiVolumeDriver(object): else: msg = (_('"Product" or "Protocol" is illegal. "Product" should ' 'be set to either T or Dorado. "Protocol" should be set ' - 'to iSCSI. Product: %(product)s ' + 'to either iSCSI or FC. Product: %(product)s ' 'Protocol: %(protocol)s') % {'product': str(product), 'protocol': str(protocol)}) diff --git a/cinder/volume/drivers/huawei/huawei_dorado.py b/cinder/volume/drivers/huawei/huawei_dorado.py index 2b1393ef2..469c95252 100644 --- a/cinder/volume/drivers/huawei/huawei_dorado.py +++ b/cinder/volume/drivers/huawei/huawei_dorado.py @@ -19,9 +19,14 @@ Volume Drivers for Huawei OceanStor Dorado series storage arrays. """ +import re + +from cinder.openstack.common import log as logging from cinder.volume.drivers.huawei import huawei_t from cinder.volume.drivers.huawei import ssh_common +LOG = logging.getLogger(__name__) + class HuaweiDoradoISCSIDriver(huawei_t.HuaweiTISCSIDriver): """ISCSI driver class for Huawei OceanStor Dorado storage arrays.""" @@ -36,3 +41,68 @@ class HuaweiDoradoISCSIDriver(huawei_t.HuaweiTISCSIDriver): self.common.do_setup(context) self._assert_cli_out = self.common._assert_cli_out self._assert_cli_operate_out = self.common._assert_cli_operate_out + + +class HuaweiDoradoFCDriver(huawei_t.HuaweiTFCDriver): + """FC driver class for Huawei OceanStor Dorado storage arrays.""" + + def __init__(self, *args, **kwargs): + super(HuaweiDoradoFCDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Instantiate common class.""" + self.common = ssh_common.DoradoCommon(configuration=self.configuration) + + self.common.do_setup(context) + self._assert_cli_out = self.common._assert_cli_out + self._assert_cli_operate_out = self.common._assert_cli_operate_out + + def _get_host_port_details(self, hostid): + cli_cmd = 'showfcmode' + out = self.common._execute_cli(cli_cmd) + + self._assert_cli_out(re.search('FC Port Topology Mode', out), + '_get_tgt_fc_port_wwns', + 'Failed to get FC port WWNs.', + cli_cmd, out) + + return [line.split()[3] for line in out.split('\r\n')[6:-2]] + + def _get_tgt_fc_port_wwns(self, port_details): + return port_details + + def initialize_connection(self, volume, connector): + """Create FC connection between a volume and a host.""" + LOG.debug(_('initialize_connection: volume name: %(vol)s ' + 'host: %(host)s initiator: %(wwn)s') + % {'vol': volume['name'], + 'host': connector['host'], + 'wwn': connector['wwpns']}) + + self.common._update_login_info() + # First, add a host if it is not added before. + host_id = self.common.add_host(connector['host']) + # Then, add free FC ports to the host. + ini_wwns = connector['wwpns'] + free_wwns = self._get_connected_free_wwns() + for wwn in free_wwns: + if wwn in ini_wwns: + self._add_fc_port_to_host(host_id, wwn) + fc_port_details = self._get_host_port_details(host_id) + tgt_wwns = self._get_tgt_fc_port_wwns(fc_port_details) + + LOG.debug(_('initialize_connection: Target FC ports WWNS: %s') + % tgt_wwns) + + # Finally, map the volume to the host. + volume_id = volume['provider_location'] + hostlun_id = self.common.map_volume(host_id, volume_id) + + properties = {} + properties['target_discovered'] = False + properties['target_wwn'] = tgt_wwns + properties['target_lun'] = int(hostlun_id) + properties['volume_id'] = volume['id'] + + return {'driver_volume_type': 'fibre_channel', + 'data': properties} diff --git a/cinder/volume/drivers/huawei/huawei_t.py b/cinder/volume/drivers/huawei/huawei_t.py index ed5dbc3dd..911727bff 100644 --- a/cinder/volume/drivers/huawei/huawei_t.py +++ b/cinder/volume/drivers/huawei/huawei_t.py @@ -359,3 +359,225 @@ class HuaweiTISCSIDriver(driver.ISCSIDriver): self._stats['volume_backend_name'] = (backend_name or self.__class__.__name__) return self._stats + + +class HuaweiTFCDriver(driver.FibreChannelDriver): + """FC driver for Huawei OceanStor T series storage arrays.""" + + VERSION = '1.0.0' + + def __init__(self, *args, **kwargs): + super(HuaweiTFCDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + """Instantiate common class.""" + self.common = ssh_common.TseriesCommon(configuration= + self.configuration) + self.common.do_setup(context) + self._assert_cli_out = self.common._assert_cli_out + self._assert_cli_operate_out = self.common._assert_cli_operate_out + + def check_for_setup_error(self): + """Check something while starting.""" + self.common.check_for_setup_error() + + def create_volume(self, volume): + """Create a new volume.""" + volume_id = self.common.create_volume(volume) + return {'provider_location': volume_id} + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot.""" + volume_id = self.common.create_volume_from_snapshot(volume, snapshot) + return {'provider_location': volume_id} + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the specified volume.""" + volume_id = self.common.create_cloned_volume(volume, src_vref) + return {'provider_location': volume_id} + + def delete_volume(self, volume): + """Delete a volume.""" + self.common.delete_volume(volume) + + def create_export(self, context, volume): + """Export the volume.""" + pass + + def ensure_export(self, context, volume): + """Synchronously recreate an export for a volume.""" + pass + + def remove_export(self, context, volume): + """Remove an export for a volume.""" + pass + + def create_snapshot(self, snapshot): + """Create a snapshot.""" + snapshot_id = self.common.create_snapshot(snapshot) + return {'provider_location': snapshot_id} + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + self.common.delete_snapshot(snapshot) + + def validate_connector(self, connector): + """Check for wwpns in connector.""" + if 'wwpns' not in connector: + err_msg = (_('validate_connector: The FC driver requires the' + 'wwpns in the connector.')) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + + def initialize_connection(self, volume, connector): + """Create FC connection between a volume and a host.""" + LOG.debug(_('initialize_connection: volume name: %(vol)s ' + 'host: %(host)s initiator: %(wwn)s') + % {'vol': volume['name'], + 'host': connector['host'], + 'wwn': connector['wwpns']}) + + self.common._update_login_info() + # First, add a host if it is not added before. + host_id = self.common.add_host(connector['host']) + # Then, add free FC ports to the host. + ini_wwns = connector['wwpns'] + free_wwns = self._get_connected_free_wwns() + for wwn in free_wwns: + if wwn in ini_wwns: + self._add_fc_port_to_host(host_id, wwn) + fc_port_details = self._get_host_port_details(host_id) + tgt_wwns = self._get_tgt_fc_port_wwns(fc_port_details) + + LOG.debug(_('initialize_connection: Target FC ports WWNS: %s') + % tgt_wwns) + + # Finally, map the volume to the host. + volume_id = volume['provider_location'] + hostlun_id = self.common.map_volume(host_id, volume_id) + + # Change LUN ctr for better performance, just for single path. + if len(tgt_wwns) == 1: + lun_details = self.common.get_lun_details(volume_id) + port_ctr = self._get_fc_port_ctr(fc_port_details[0]) + if (lun_details['LunType'] == 'THICK' and + lun_details['OwningController'] != port_ctr): + self.common.change_lun_ctr(volume_id, port_ctr) + + properties = {} + properties['target_discovered'] = False + properties['target_wwn'] = tgt_wwns + properties['target_lun'] = int(hostlun_id) + properties['volume_id'] = volume['id'] + + return {'driver_volume_type': 'fibre_channel', + 'data': properties} + + def _get_connected_free_wwns(self): + """Get free connected FC port WWNs. + + If no new ports connected, return an empty list. + + """ + + cli_cmd = 'showfreeport' + out = self.common._execute_cli(cli_cmd) + wwns = [] + if re.search('Host Free Port Information', out): + for line in out.split('\r\n')[6:-2]: + tmp_line = line.split() + if (tmp_line[1] == 'FC') and (tmp_line[4] == 'Connected'): + wwns.append(tmp_line[0]) + + return wwns + + def _add_fc_port_to_host(self, hostid, wwn, multipathtype=0): + """Add a FC port to host.""" + portname = HOST_PORT_PREFIX + wwn + cli_cmd = ('addhostport -host %(id)s -type 1 ' + '-wwn %(wwn)s -n %(name)s -mtype %(multype)s' + % {'id': hostid, + 'wwn': wwn, + 'name': portname, + 'multype': multipathtype}) + out = self.common._execute_cli(cli_cmd) + + msg = ('Failed to add FC port %(port)s to host %(host)s.' + % {'port': portname, 'host': hostid}) + self._assert_cli_operate_out('_add_fc_port_to_host', msg, cli_cmd, out) + + def _get_host_port_details(self, host_id): + cli_cmd = 'showhostpath -host %s' % host_id + out = self.common._execute_cli(cli_cmd) + + self._assert_cli_out(re.search('Multi Path Information', out), + '_get_host_port_details', + 'Failed to get host port details.', + cli_cmd, out) + + port_details = [] + tmp_details = {} + for line in out.split('\r\n')[4:-2]: + line = line.split('|') + # Cut-point of multipal details, usually is "-------". + if len(line) == 1: + port_details.append(tmp_details) + continue + key = ''.join(line[0].strip().split()) + val = line[1].strip() + tmp_details[key] = val + port_details.append(tmp_details) + return port_details + + def _get_tgt_fc_port_wwns(self, port_details): + wwns = [] + for port in port_details: + wwns.append(port['TargetWWN']) + return wwns + + def _get_fc_port_ctr(self, port_details): + return port_details['ControllerID'] + + def terminate_connection(self, volume, connector, **kwargs): + """Terminate the map.""" + LOG.debug(_('terminate_connection: volume: %(vol)s host: %(host)s ' + 'connector: %(initiator)s') + % {'vol': volume['name'], + 'host': connector['host'], + 'initiator': connector['initiator']}) + + self.common._update_login_info() + host_id = self.common.remove_map(volume['provider_location'], + connector['host']) + # Remove all FC ports and delete the host if + # no volume mapping to it. + if not self.common._get_host_map_info(host_id): + self._remove_fc_ports(host_id, connector) + + def _remove_fc_ports(self, hostid, connector): + """Remove FC ports and delete host.""" + wwns = connector['wwpns'] + port_num = 0 + port_info = self.common._get_host_port_info(hostid) + if port_info: + port_num = len(port_info) + for port in port_info: + if port[2] in wwns: + self.common._delete_hostport(port[0]) + port_num -= 1 + else: + LOG.warn(_('_remove_fc_ports: FC port was not found ' + 'on host %(hostid)s.') % {'hostid': hostid}) + + if port_num == 0: + self.common._delete_host(hostid) + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + self._stats = self.common.get_volume_stats(refresh) + self._stats['storage_protocol'] = 'FC' + self._stats['driver_version'] = self.VERSION + backend_name = self.configuration.safe_get('volume_backend_name') + self._stats['volume_backend_name'] = (backend_name or + self.__class__.__name__) + return self._stats -- 2.45.2