diff --git a/cinder/tests/unit/fake_hpe_client_exceptions.py b/cinder/tests/unit/fake_hpe_client_exceptions.py index c496e07bf..f753eb3ed 100644 --- a/cinder/tests/unit/fake_hpe_client_exceptions.py +++ b/cinder/tests/unit/fake_hpe_client_exceptions.py @@ -78,8 +78,10 @@ class HTTPConflict(ClientException): message = "Conflict" def __init__(self, error=None): - if error and 'message' in error: - self._error_desc = error['message'] + if error: + super(HTTPConflict, self).__init__(error) + if 'message' in error: + self._error_desc = error['message'] def get_description(self): return self._error_desc diff --git a/cinder/tests/unit/test_hpe3par.py b/cinder/tests/unit/test_hpe3par.py index df72641e9..68b154408 100644 --- a/cinder/tests/unit/test_hpe3par.py +++ b/cinder/tests/unit/test_hpe3par.py @@ -78,6 +78,8 @@ HPE3PAR_CPG_MAP = 'OpenStackCPG:DestOpenStackCPG fakepool:destfakepool' SYNC_MODE = 1 PERIODIC_MODE = 2 SYNC_PERIOD = 900 +# EXISTENT_PATH error code returned from hpe3parclient +EXISTENT_PATH = 73 class Comment(object): @@ -5479,6 +5481,67 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver, test.TestCase): self.assertEqual('fakehost.foo', host['name']) + def test_concurrent_create_host(self): + # tests concurrent requests to create host + # setup_mock_client driver with default configuration + # and return the mock HTTP 3PAR client + mock_client = self.setup_driver() + mock_client.getVolume.return_value = {'userCPG': HPE3PAR_CPG} + mock_client.getCPG.return_value = {} + mock_client.queryHost.side_effect = [ + None, + {'members': [{'name': self.FAKE_HOST}] + }] + mock_client.createHost.side_effect = [ + hpeexceptions.HTTPConflict( + {'code': EXISTENT_PATH, + 'desc': 'host WWN/iSCSI name already used by another host'})] + mock_client.getHost.side_effect = [ + hpeexceptions.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]}, + {'driverVersion': None, + 'firmwareVersion': None, + 'hostSpeed': 0, + 'model': None, + 'portPos': {'cardPort': 1, 'node': 0, + 'slot': 2}, + 'vendor': None, + 'wwn': self.wwn[1]}]}] + + with mock.patch.object(hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client: + mock_create_client.return_value = mock_client + common = self.driver._login() + host = self.driver._create_host( + common, + self.volume, + self.connector) + expected = [ + mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'), + mock.call.getCPG(HPE3PAR_CPG), + mock.call.getHost(self.FAKE_HOST), + mock.call.queryHost(wwns=['123456789012345', + '123456789054321']), + mock.call.createHost( + self.FAKE_HOST, + FCWwns=['123456789012345', '123456789054321'], + optional={'domain': None, 'persona': 2}), + mock.call.queryHost(wwns=['123456789012345', + '123456789054321']), + mock.call.getHost(self.FAKE_HOST)] + + mock_client.assert_has_calls(expected) + + self.assertEqual(self.FAKE_HOST, host['name']) + def test_create_modify_host(self): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client diff --git a/cinder/volume/drivers/hpe/hpe_3par_fc.py b/cinder/volume/drivers/hpe/hpe_3par_fc.py index c6c60bce8..e106d69c5 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_fc.py +++ b/cinder/volume/drivers/hpe/hpe_3par_fc.py @@ -35,9 +35,10 @@ except ImportError: hpeexceptions = None from oslo_log import log as logging +from oslo_utils.excutils import save_and_reraise_exception from cinder import exception -from cinder.i18n import _, _LI, _LW +from cinder.i18n import _, _LI, _LW, _LE from cinder import interface from cinder.volume import driver from cinder.volume.drivers.hpe import hpe_3par_common as hpecommon @@ -46,6 +47,9 @@ from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) +# EXISTENT_PATH error code returned from hpe3parclient +EXISTENT_PATH = 73 + @interface.volumedriver class HPE3PARFCDriver(driver.TransferVD, @@ -105,10 +109,12 @@ class HPE3PARFCDriver(driver.TransferVD, 3.0.7 - Remove metadata that tracks the instance ID. bug #1572665 3.0.8 - NSP feature, creating FC Vlun as match set instead of host sees. bug #1577993 + 3.0.9 - Handling HTTP conflict 409, host WWN/iSCSI name already used + by another host, while creating 3PAR FC Host. bug #1597454 """ - VERSION = "3.0.8" + VERSION = "3.0.9" def __init__(self, *args, **kwargs): super(HPE3PARFCDriver, self).__init__(*args, **kwargs) @@ -435,16 +441,41 @@ class HPE3PARFCDriver(driver.TransferVD, return host_found else: persona_id = int(persona_id) - common.client.createHost(hostname, FCWwns=wwns, - optional={'domain': domain, - 'persona': persona_id}) + try: + common.client.createHost(hostname, FCWwns=wwns, + optional={'domain': domain, + 'persona': persona_id}) + except hpeexceptions.HTTPConflict as path_conflict: + msg = _LE("Create FC host caught HTTP conflict code: %s") + LOG.exception(msg, path_conflict.get_code()) + with save_and_reraise_exception(reraise=False) as ctxt: + if path_conflict.get_code() is EXISTENT_PATH: + # Handle exception : EXISTENT_PATH - host WWN/iSCSI + # name already used by another host + hosts = common.client.queryHost(wwns=wwns) + if hosts and hosts['members'] and ( + 'name' in hosts['members'][0]): + hostname = hosts['members'][0]['name'] + else: + # re rasise last caught exception + ctxt.reraise = True + else: + # re rasise last caught exception + # for other HTTP conflict + ctxt.reraise = True return hostname def _modify_3par_fibrechan_host(self, common, hostname, wwn): mod_request = {'pathOperation': common.client.HOST_EDIT_ADD, 'FCWWNs': wwn} - - common.client.modifyHost(hostname, mod_request) + try: + common.client.modifyHost(hostname, mod_request) + except hpeexceptions.HTTPConflict as path_conflict: + msg = _LE("Modify FC Host %(hostname)s caught " + "HTTP conflict code: %(code)s") + LOG.exception(msg, + {'hostname': hostname, + 'code': path_conflict.get_code()}) def _create_host(self, common, volume, connector): """Creates or modifies existing 3PAR host.""" @@ -464,8 +495,9 @@ class HPE3PARFCDriver(driver.TransferVD, domain, persona_id) host = common._get_3par_host(hostname) - - return self._add_new_wwn_to_host(common, host, connector['wwpns']) + return host + else: + return self._add_new_wwn_to_host(common, host, connector['wwpns']) def _add_new_wwn_to_host(self, common, host, wwns): """Add wwns to a host if one or more don't exist.