From 9b6dc3dc58e0095dcf59bfc129cc7e75f066b20f Mon Sep 17 00:00:00 2001 From: Jim Branen Date: Fri, 24 May 2013 09:30:10 -0700 Subject: [PATCH] Fixes 3PAR Host already exists error. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixed “Host already exists” error by, * Screen scraping error message and extracting the host name used by the 3PAR backend. * Cache the host name used by the 3PAR backend for subsequent methods using host name. * After a restart, and 3PAR host name it no longer available, retrieve 3PAR host name using wwn/iqn, if a terminate_connection fails. Fixes bug 1182134 Change-Id: Ia08a311af168ae19dbe7c1405db9c606fd878e53 --- cinder/tests/test_hp3par.py | 65 ++++++++++++++++--- .../volume/drivers/san/hp/hp_3par_common.py | 60 ++++++++++++++++- cinder/volume/drivers/san/hp/hp_3par_fc.py | 36 +++++----- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 34 +++++----- 4 files changed, 152 insertions(+), 43 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 865192900..1fa0b3efe 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -496,6 +496,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): 'target_lun': 186, 'target_portal': '1.1.1.2:1234'}, 'driver_volume_type': 'fibre_channel'} + return hostname def test_create_volume(self): self.flags(lock_path=self.tempdir) @@ -602,14 +603,17 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): create_host_cmd = ('createhost -persona 1 -domain (\'OpenStack\',) ' 'fakehost 123456789012345 123456789054321') - create_host_ret = pack(CLI_CR + 'already used by host fakehost.foo ') + create_host_ret = pack(CLI_CR + + 'already used by host fakehost.foo (19)') _run_ssh(create_host_cmd, False).AndReturn([create_host_ret, '']) + + show_3par_cmd = 'showhost -verbose fakehost.foo' + _run_ssh(show_3par_cmd, False).AndReturn([pack(FC_SHOWHOST_RET), '']) self.mox.ReplayAll() - self.assertRaises(exception.Duplicate3PARHost, - self.driver._create_host, - self.volume, - self.connector) + host = self.driver._create_host(self.volume, self.connector) + + self.assertEquals(host['name'], 'fakehost.foo') def test_create_modify_host(self): self.flags(lock_path=self.tempdir) @@ -714,6 +718,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): 'id': 11, 'name': hostname} self._hosts[hostname] = host + return hostname def test_create_volume(self): self.flags(lock_path=self.tempdir) @@ -830,12 +835,14 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): 'fakehost iqn.1993-08.org.debian:01:222') in_use_ret = pack('\r\nalready used by host fakehost.foo ') _run_ssh(create_host_cmd, False).AndReturn([in_use_ret, '']) + + show_3par_cmd = 'showhost -verbose fakehost.foo' + _run_ssh(show_3par_cmd, False).AndReturn([pack(ISCSI_3PAR_RET), '']) self.mox.ReplayAll() - self.assertRaises(exception.Duplicate3PARHost, - self.driver._create_host, - self.volume, - self.connector) + host = self.driver._create_host(self.volume, self.connector) + + self.assertEquals(host['name'], 'fakehost.foo') def test_create_modify_host(self): self.flags(lock_path=self.tempdir) @@ -934,6 +941,27 @@ FC_HOST_RET = ( 'Contact : --\r\n' 'Comment : -- \r\n\r\n\r\n') +FC_SHOWHOST_RET = ( + 'Id,Name,Persona,-WWN/iSCSI_Name-,Port,IP_addr\r\n' + '75,fakehost.foo,Generic,50014380242B8B4C,0:2:1,n/a\r\n' + '75,fakehost.foo,Generic,50014380242B8B4E,---,n/a\r\n' + '75,fakehost.foo,Generic,1000843497F90711,0:2:1,n/a \r\n' + '75,fakehost.foo,Generic,1000843497F90715,1:2:1,n/a\r\n' + '\r\n' + 'Id,Name,-Initiator_CHAP_Name-,-Target_CHAP_Name-\r\n' + '75,fakehost.foo,--,--\r\n' + '\r\n' + '---------- Host fakehost.foo ----------\r\n' + 'Name : fakehost.foo\r\n' + 'Domain : FAKE_TEST\r\n' + 'Id : 75\r\n' + 'Location : --\r\n' + 'IP Address : --\r\n' + 'OS : --\r\n' + 'Model : --\r\n' + 'Contact : --\r\n' + 'Comment : -- \r\n\r\n\r\n') + NO_FC_HOST_RET = ( 'Id,Name,Persona,-WWN/iSCSI_Name-,Port,IP_addr\r\n' '\r\n' @@ -1042,3 +1070,22 @@ ISCSI_PORT_RET = ( '1:8:1,ready,10.10.220.253,255.255.224.0,0.0.0.0,181,1500,10Gbps,' '0,0.0.0.0,3205\r\n' '1:8:2,loss_sync,0.0.0.0,0.0.0.0,0.0.0.0,182,1500,n/a,0,0.0.0.0,3205\r\n') + +ISCSI_3PAR_RET = ( + 'Id,Name,Persona,-WWN/iSCSI_Name-,Port,IP_addr\r\n' + '75,fakehost.foo,Generic,iqn.1993-08.org.debian:01:222,---,' + '10.10.222.12\r\n' + '\r\n' + 'Id,Name,-Initiator_CHAP_Name-,-Target_CHAP_Name-\r\n' + '75,fakehost.foo,--,--\r\n' + '\r\n' + '---------- Host fakehost.foo ----------\r\n' + 'Name : fakehost.foo\r\n' + 'Domain : FAKE_TEST\r\n' + 'Id : 75\r\n' + 'Location : --\r\n' + 'IP Address : --\r\n' + 'OS : --\r\n' + 'Model : --\r\n' + 'Contact : --\r\n' + 'Comment : -- \r\n\r\n\r\n') diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 946890499..abfb74135 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -41,6 +41,7 @@ import json import paramiko import pprint from random import randint +import re import time import uuid @@ -111,6 +112,7 @@ class HP3PARCommon(): def __init__(self, config): self.sshpool = None self.config = config + self.hosts_naming_dict = dict() def check_flags(self, options, required_flags): for flag in required_flags: @@ -459,9 +461,7 @@ exit self._create_3par_vlun(volume_name, host['name']) return client.getVLUN(volume_name) - def delete_vlun(self, volume, connector, client): - hostname = self._safe_hostname(connector['host']) - + def delete_vlun(self, volume, hostname, client): volume_name = self._get_3par_vol_name(volume['id']) vlun = client.getVLUN(volume_name) client.deleteVLUN(volume_name, vlun['lun'], hostname) @@ -595,6 +595,16 @@ exit return status + def get_next_word(self, s, search_string): + """Return the next word. + + Search 's' for 'search_string', if found + return the word preceding 'search_string' + from 's'. + """ + word = re.search(search_string.strip(' ') + ' ([^ ]*)', s) + return word.groups()[0].strip(' ') + @utils.synchronized('3parclone', external=True) def create_cloned_volume(self, volume, src_vref, client): @@ -740,3 +750,47 @@ exit raise exception.NotAuthorized() except hpexceptions.HTTPNotFound as ex: LOG.error(str(ex)) + + def _get_3par_hostname_from_wwn_iqn(self, wwns_iqn): + out = self._cli_run('showhost -d', None) + # wwns_iqn may be a list of strings or a single + # string. So, if necessary, create a list to loop. + if not isinstance(wwns_iqn, list): + wwn_iqn_list = [wwns_iqn] + else: + wwn_iqn_list = wwns_iqn + + for wwn_iqn in wwn_iqn_list: + for showhost in out: + if (wwn_iqn.upper() in showhost.upper()): + return showhost.split(',')[1] + + def terminate_connection(self, volume, hostname, wwn_iqn, client): + """ Driver entry point to unattach a volume from an instance.""" + try: + # does 3par know this host by a different name? + if hostname in self.hosts_naming_dict: + hostname = self.hosts_naming_dict.get(hostname) + self.delete_vlun(volume, hostname, client) + return + except hpexceptions.HTTPNotFound as e: + if 'host does not exist' in e.get_description(): + # use the wwn to see if we can find the hostname + hostname = self._get_3par_hostname_from_wwn_iqn(wwn_iqn) + # no 3par host, re-throw + if (hostname == None): + raise + else: + # not a 'host does not exist' HTTPNotFound exception, re-throw + raise + + #try again with name retrieved from 3par + self.delete_vlun(volume, hostname, client) + + def parse_create_host_error(self, hostname, out): + search_str = "already used by host " + if search_str in out[1]: + #host exists, return name used by 3par + hostname_3par = self.get_next_word(out[1], search_str) + self.hosts_naming_dict[hostname] = hostname_3par + return hostname_3par diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index 7af686268..0479e083a 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -202,21 +202,26 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) @utils.synchronized('3par-attach', external=True) def terminate_connection(self, volume, connector, force): - """ - Driver entry point to unattach a volume from an instance. - """ - self.common.delete_vlun(volume, connector, self.client) - pass + """Driver entry point to unattach a volume from an instance.""" + self.common.terminate_connection(volume, + connector['host'], + connector['wwpns'], + self.client) def _create_3par_fibrechan_host(self, hostname, wwn, domain, persona_id): + """Create a 3PAR host. + + Create a 3PAR host, if there is already a host on the 3par using + the same wwn but with a different hostname, return the hostname + used by 3PAR. + """ out = self.common._cli_run('createhost -persona %s -domain %s %s %s' % (persona_id, domain, hostname, " ".join(wwn)), None) if out and len(out) > 1: - if "already used by host" in out[1]: - err = out[1].strip() - info = _("The hostname must be called '%s'") % hostname - raise exception.Duplicate3PARHost(err=err, info=info) + return self.common.parse_create_host_error(hostname, out) + + return hostname def _modify_3par_fibrechan_host(self, hostname, wwn): # when using -add, you can not send the persona or domain options @@ -224,10 +229,7 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) % (hostname, " ".join(wwn)), None) def _create_host(self, volume, connector): - """ - This is a 3PAR host entry for exporting volumes - via active VLUNs. - """ + """Creates or modifies existing 3PAR host.""" host = None hostname = self.common._safe_hostname(connector['host']) try: @@ -239,9 +241,11 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) # get persona from the volume type extra specs persona_id = self.common.get_persona_type(volume) # host doesn't exist, we have to create it - self._create_3par_fibrechan_host(hostname, connector['wwpns'], - self.configuration.hp3par_domain, - persona_id) + hostname = self._create_3par_fibrechan_host(hostname, + connector['wwpns'], + self.configuration. + hp3par_domain, + persona_id) host = self.common._get_3par_host(hostname) return host diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index f1b1b8355..bf14c8791 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -207,10 +207,11 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) @utils.synchronized('3par-attach', external=True) def terminate_connection(self, volume, connector, force): - """ - Driver entry point to unattach a volume from an instance. - """ - self.common.delete_vlun(volume, connector, self.client) + """Driver entry point to unattach a volume from an instance.""" + self.common.terminate_connection(volume, + connector['host'], + connector['initiator'], + self.client) def _iscsi_discover_target_iqn(self, remote_ip): result = self.common._cli_run('showport -ids', None) @@ -228,14 +229,18 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) return iqn def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain, persona_id): + """Create a 3PAR host. + + Create a 3PAR host, if there is already a host on the 3par using + the same iqn but with a different hostname, return the hostname + used by 3PAR. + """ cmd = 'createhost -iscsi -persona %s -domain %s %s %s' % \ (persona_id, domain, hostname, iscsi_iqn) out = self.common._cli_run(cmd, None) if out and len(out) > 1: - if "already used by host" in out[1]: - err = out[1].strip() - info = _("The hostname must be called '%s'") % hostname - raise exception.Duplicate3PARHost(err=err, info=info) + return self.common.parse_create_host_error(hostname, out) + return hostname def _modify_3par_iscsi_host(self, hostname, iscsi_iqn): # when using -add, you can not send the persona or domain options @@ -243,10 +248,7 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) % (hostname, iscsi_iqn), None) def _create_host(self, volume, connector): - """ - This is a 3PAR host entry for exporting volumes - via active VLUNs. - """ + """Creates or modifies existing 3PAR host.""" # make sure we don't have the host already host = None hostname = self.common._safe_hostname(connector['host']) @@ -259,9 +261,11 @@ must be the same" % (cpg['domain'], self.configuration.hp3par_domain) # get persona from the volume type extra specs persona_id = self.common.get_persona_type(volume) # host doesn't exist, we have to create it - self._create_3par_iscsi_host(hostname, connector['initiator'], - self.configuration.hp3par_domain, - persona_id) + hostname = self._create_3par_iscsi_host(hostname, + connector['initiator'], + self.configuration. + hp3par_domain, + persona_id) host = self.common._get_3par_host(hostname) return host -- 2.45.2