Fixes 3PAR Host already exists error.

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
This commit is contained in:
Jim Branen 2013-05-24 09:30:10 -07:00
parent 1df6516dce
commit 9b6dc3dc58
4 changed files with 152 additions and 43 deletions

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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