'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',
'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'}
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')
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'
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,
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)
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)})
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."""
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}
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