From e4d36161c966a1648bcabcdcc41fb7c648c1bf61 Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Thu, 21 Aug 2014 15:57:06 -0700 Subject: [PATCH] Fixing 3PAR excessive FC port usage Updating the 3PAR FC driver so that it can detect if there is only a single FC path available. When a single FC path is detected only a single VLUN will be created instead of one for every available NSP on the host. This will prevent a host from using extra FC ports that are not needed. If multiple FC paths are available all the ports will still be used. Closes-Bug: 1360001 Change-Id: I4b2bdf12289dd255dd959675991fa9e2e0fcbd1e --- cinder/tests/test_hp3par.py | 143 ++++++++++++++++++++- cinder/volume/drivers/san/hp/hp_3par_fc.py | 64 +++++++-- cinder/zonemanager/utils.py | 14 ++ 3 files changed, 206 insertions(+), 15 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 8a159ea88..a5b172279 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -1954,18 +1954,97 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): mock.call.getHost(self.FAKE_HOST), mock.ANY, mock.call.getHost(self.FAKE_HOST), + mock.call.getPorts(), mock.call.createVLUN( self.VOLUME_3PAR_NAME, auto=True, hostname=self.FAKE_HOST), mock.call.getHostVLUNs(self.FAKE_HOST), - mock.call.getPorts(), mock.call.logout()] mock_client.assert_has_calls(expected) self.assertDictMatch(result, self.properties) + @mock.patch('cinder.zonemanager.utils.create_lookup_service') + def test_initialize_connection_with_lookup_single_nsp(self, mock_lookup): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + class fake_lookup_object: + def get_device_mapping_from_network(self, connector, target_wwns): + fake_map = { + 'FAB_1': { + 'target_port_wwn_list': ['0987654321234'], + 'initiator_port_wwn_list': ['123456789012345'] + } + } + return fake_map + mock_lookup.return_value = fake_lookup_object() + mock_client = self.setup_driver() + mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG} + mock_client.getCPG.return_value = {} + mock_client.getHost.side_effect = [ + hpexceptions.HTTPNotFound('fake'), + {'name': self.FAKE_HOST, + 'FCPaths': [{'driverVersion': None, + 'firmwareVersion': None, + 'hostSpeed': 0, + 'model': None, + 'portPos': {'cardPort': 1, 'node': 1, + 'slot': 2}, + 'vendor': None, + 'wwn': self.wwn[0]}]}] + mock_client.findHost.return_value = self.FAKE_HOST + mock_client.getHostVLUNs.return_value = [ + {'active': True, + 'volumeName': self.VOLUME_3PAR_NAME, + 'lun': 90, 'type': 0}] + location = ("%(volume_name)s,%(lun_id)s,%(host)s,%(nsp)s" % + {'volume_name': self.VOLUME_3PAR_NAME, + 'lun_id': 90, + 'host': self.FAKE_HOST, + 'nsp': 'something'}) + mock_client.createVLUN.return_value = location + + connector = {'ip': '10.0.0.2', + 'initiator': 'iqn.1993-08.org.debian:01:222', + 'wwpns': [self.wwn[0]], + 'wwnns': ["223456789012345"], + 'host': self.FAKE_HOST} + + expected_properties = { + 'driver_volume_type': 'fibre_channel', + 'data': { + 'target_lun': 90, + 'target_wwn': ['0987654321234'], + 'target_discovered': True, + 'initiator_target_map': {'123456789012345': + ['0987654321234'] + }}} + + result = self.driver.initialize_connection(self.volume, connector) + + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.getVolume(self.VOLUME_3PAR_NAME), + mock.call.getCPG(HP3PAR_CPG), + mock.call.getHost(self.FAKE_HOST), + mock.ANY, + mock.call.getHost(self.FAKE_HOST), + mock.call.getPorts(), + mock.call.getPorts(), + mock.call.createVLUN( + self.VOLUME_3PAR_NAME, + auto=True, + hostname=self.FAKE_HOST, + portPos={'node': 7, 'slot': 1, 'cardPort': 1}), + mock.call.getHostVLUNs(self.FAKE_HOST), + mock.call.logout()] + + mock_client.assert_has_calls(expected) + + self.assertDictMatch(result, expected_properties) + def test_terminate_connection(self): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client @@ -2017,6 +2096,68 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): self.connector) mock_client.assert_has_calls(expected) + @mock.patch('cinder.zonemanager.utils.create_lookup_service') + def test_terminate_connection_with_lookup(self, mock_lookup): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + class fake_lookup_object: + def get_device_mapping_from_network(self, connector, target_wwns): + fake_map = { + 'FAB_1': { + 'target_port_wwn_list': ['0987654321234'], + 'initiator_port_wwn_list': ['123456789012345'] + } + } + return fake_map + mock_lookup.return_value = fake_lookup_object() + mock_client = self.setup_driver() + + effects = [ + [{'active': True, 'volumeName': self.VOLUME_3PAR_NAME, + 'lun': None, 'type': 0}], + hpexceptions.HTTPNotFound] + + mock_client.getHostVLUNs.side_effect = effects + + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.getHostVLUNs(self.FAKE_HOST), + mock.call.deleteVLUN( + self.VOLUME_3PAR_NAME, + None, + self.FAKE_HOST), + mock.call.deleteHost(self.FAKE_HOST), + mock.call.getHostVLUNs(self.FAKE_HOST), + mock.call.getPorts(), + mock.call.logout()] + + conn_info = self.driver.terminate_connection(self.volume, + self.connector) + mock_client.assert_has_calls(expected) + self.assertIn('data', conn_info) + self.assertIn('initiator_target_map', conn_info['data']) + mock_client.reset_mock() + + mock_client.getHostVLUNs.side_effect = effects + + # mock some deleteHost exceptions that are handled + delete_with_vlun = hpexceptions.HTTPConflict( + error={'message': "has exported VLUN"}) + delete_with_hostset = hpexceptions.HTTPConflict( + error={'message': "host is a member of a set"}) + mock_client.deleteHost = mock.Mock( + side_effect=[delete_with_vlun, delete_with_hostset]) + + conn_info = self.driver.terminate_connection(self.volume, + self.connector) + mock_client.assert_has_calls(expected) + mock_client.reset_mock() + mock_client.getHostVLUNs.side_effect = effects + + conn_info = self.driver.terminate_connection(self.volume, + self.connector) + mock_client.assert_has_calls(expected) + def test_terminate_connection_more_vols(self): mock_client = self.setup_driver() # mock more than one vlun on the host (don't even try to remove host) diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index 6bc6e746a..936722a17 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -66,16 +66,19 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): 2.0.4 - Added support for managing/unmanaging of volumes 2.0.5 - Only remove FC Zone on last volume detach 2.0.6 - Added support for volume retype + 2.0.7 - Only one FC port is used when a single FC path + is present. bug #1360001 """ - VERSION = "2.0.6" + VERSION = "2.0.7" def __init__(self, *args, **kwargs): super(HP3PARFCDriver, self).__init__(*args, **kwargs) self.common = None self.configuration.append_config_values(hpcommon.hp3par_opts) self.configuration.append_config_values(san.san_opts) + self.lookup_service = fczm_utils.create_lookup_service() def _init_common(self): return hpcommon.HP3PARCommon(self.configuration) @@ -210,11 +213,20 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): # we have to make sure we have a host host = self._create_host(volume, connector) - # now that we have a host, create the VLUN - vlun = self.common.create_vlun(volume, host) + target_wwns, init_targ_map, numPaths = \ + self._build_initiator_target_map(connector) - target_wwns, init_targ_map = self._build_initiator_target_map( - connector) + # now that we have a host, create the VLUN + if self.lookup_service is not None and numPaths == 1: + nsp = None + active_fc_port_list = self.common.get_active_fc_target_ports() + for port in active_fc_port_list: + if port['portWWN'].lower() == target_wwns[0].lower(): + nsp = port['nsp'] + break + vlun = self.common.create_vlun(volume, host, nsp) + else: + vlun = self.common.create_vlun(volume, host) info = {'driver_volume_type': 'fibre_channel', 'data': {'target_lun': vlun['lun'], @@ -244,8 +256,9 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): # No more exports for this host. LOG.info(_("Need to remove FC Zone, building initiator " "target map")) - target_wwns, init_targ_map = self._build_initiator_target_map( - connector) + + target_wwns, init_targ_map, numPaths = \ + self._build_initiator_target_map(connector) info['data'] = {'target_wwn': target_wwns, 'initiator_target_map': init_targ_map} @@ -258,18 +271,41 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): """Build the target_wwns and the initiator target map.""" fc_ports = self.common.get_active_fc_target_ports() + all_target_wwns = [] target_wwns = [] + init_targ_map = {} + numPaths = 0 for port in fc_ports: - target_wwns.append(port['portWWN']) - - initiator_wwns = connector['wwpns'] + all_target_wwns.append(port['portWWN']) + + if self.lookup_service is not None: + # use FC san lookup to determine which NSPs to use + # for the new VLUN. + dev_map = self.lookup_service.get_device_mapping_from_network( + connector['wwpns'], + all_target_wwns) + + for fabric_name in dev_map: + fabric = dev_map[fabric_name] + target_wwns += fabric['target_port_wwn_list'] + for initiator in fabric['initiator_port_wwn_list']: + if initiator not in init_targ_map: + init_targ_map[initiator] = [] + init_targ_map[initiator] += fabric['target_port_wwn_list'] + init_targ_map[initiator] = list(set( + init_targ_map[initiator])) + for target in init_targ_map[initiator]: + numPaths += 1 + target_wwns = list(set(target_wwns)) + else: + initiator_wwns = connector['wwpns'] + target_wwns = all_target_wwns - init_targ_map = {} - for initiator in initiator_wwns: - init_targ_map[initiator] = target_wwns + for initiator in initiator_wwns: + init_targ_map[initiator] = target_wwns - return target_wwns, init_targ_map + return target_wwns, init_targ_map, numPaths def _create_3par_fibrechan_host(self, hostname, wwns, domain, persona_id): """Create a 3PAR host. diff --git a/cinder/zonemanager/utils.py b/cinder/zonemanager/utils.py index f9941592c..13c91e3f8 100644 --- a/cinder/zonemanager/utils.py +++ b/cinder/zonemanager/utils.py @@ -23,6 +23,7 @@ from cinder.i18n import _ from cinder.openstack.common import log from cinder.volume.configuration import Configuration from cinder.volume import manager +from cinder.zonemanager import fc_san_lookup_service from cinder.zonemanager import fc_zone_manager LOG = log.getLogger(__name__) @@ -47,6 +48,19 @@ def create_zone_manager(): return None +def create_lookup_service(): + config = Configuration(manager.volume_manager_opts) + LOG.debug("zoning mode %s" % config.safe_get('zoning_mode')) + if config.safe_get('zoning_mode') == 'fabric': + LOG.debug("FC Lookup Service enabled.") + lookup = fc_san_lookup_service.FCSanLookupService(configuration=config) + LOG.info(_("Using FC lookup service %s") % lookup.lookup_service) + return lookup + else: + LOG.debug("FC Lookup Service not enabled in cinder.conf.") + return None + + def AddFCZone(initialize_connection): """Decorator to add a FC Zone.""" def decorator(self, *args, **kwargs): -- 2.45.2