Skip to content

Commit 8e20f47

Browse files
authored
Conn refused (#339)
* Handle connection refused better when there are multiple available addresses from DNS resolution * Updated tests, changelog
1 parent 9c9b678 commit 8e20f47

File tree

7 files changed

+88
-43
lines changed

7 files changed

+88
-43
lines changed

Changelog.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
Change Log
22
============
33

4+
2.9.0
5+
+++++
6+
7+
Changes
8+
--------
9+
10+
* ``pssh.exceptions.ConnectionError`` is now the same as built-in ``ConnectionError`` and deprecated - to be removed.
11+
* Clients now continue connecting with all addresses in DNS list. In the case where an address refuses connection,
12+
other available addresses are attempted without delay.
13+
14+
For example where a host resolves to both IPv4 and v6 addresses while only one address is
15+
accepting connections, or multiple v4/v6 addresses where only some are accepting connections.
16+
* Connection actively refused error is no longer subject to retries.
17+
18+
419
2.8.0
520
+++++
621

pssh/clients/base/single.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ def _auth_retry(self, retries=1):
207207
if retries < self.num_retries:
208208
sleep(self.retry_delay)
209209
return self._auth_retry(retries=retries+1)
210-
msg = "Authentication error while connecting to %s:%s - %s"
211-
raise AuthenticationError(msg, self.host, self.port, ex)
210+
msg = "Authentication error while connecting to %s:%s - %s - retries %s/%s"
211+
raise AuthenticationError(msg, self.host, self.port, ex, retries, self.num_retries)
212212

213213
def disconnect(self):
214214
raise NotImplementedError
@@ -284,13 +284,25 @@ def _connect(self, host, port, retries=1):
284284
host, str(ex.args[1]), retries,
285285
self.num_retries)
286286
raise unknown_ex from ex
287-
family, _type, proto, _, sock_addr = addr_info[0]
287+
for i, (family, _type, proto, _, sock_addr) in enumerate(addr_info):
288+
try:
289+
return self._connect_socket(family, _type, proto, sock_addr, host, port, retries)
290+
except ConnectionRefusedError as ex:
291+
if i+1 == len(addr_info):
292+
logger.error("No available addresses from %s", [addr[4] for addr in addr_info])
293+
ex.args += (host, port)
294+
raise
295+
continue
296+
297+
def _connect_socket(self, family, _type, proto, sock_addr, host, port, retries):
288298
self.sock = socket.socket(family, _type)
289299
if self.timeout:
290300
self.sock.settimeout(self.timeout)
291301
logger.debug("Connecting to %s:%s", host, port)
292302
try:
293303
self.sock.connect(sock_addr)
304+
except ConnectionRefusedError:
305+
raise
294306
except sock_error as ex:
295307
logger.error("Error connecting to host '%s:%s' - retry %s/%s",
296308
host, port, retries, self.num_retries)

pssh/exceptions.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,7 @@ class UnknownHostError(Exception):
3535

3636

3737
UnknownHostException = UnknownHostError
38-
39-
40-
class ConnectionError(Exception):
41-
"""Raised on error connecting (connection refused/timed out)"""
42-
pass
43-
44-
38+
ConnectionError = ConnectionError
4539
ConnectionErrorException = ConnectionError
4640

4741

tests/native/test_parallel_client.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
from pssh.exceptions import UnknownHostException, \
3636
AuthenticationException, ConnectionErrorException, \
3737
HostArgumentException, SFTPError, SFTPIOError, Timeout, SCPError, \
38-
PKeyFileError, ShellError, HostArgumentError, NoIPv6AddressFoundError
38+
PKeyFileError, ShellError, HostArgumentError, NoIPv6AddressFoundError, \
39+
AuthenticationError
3940
from pssh.output import HostOutput
4041

4142
from .base_ssh2_case import PKEY_FILENAME, PUB_FILE
@@ -276,7 +277,7 @@ def test_pssh_client_hosts_list_part_failure(self):
276277
self.assertTrue(client.finished(output))
277278
self.assertEqual(len(hosts), len(output))
278279
self.assertIsNotNone(output[1].exception)
279-
self.assertEqual(output[1].exception.args[1], hosts[1])
280+
self.assertEqual(output[1].host, hosts[1])
280281
self.assertIsInstance(output[1].exception, ConnectionErrorException)
281282

282283
def test_pssh_client_timeout(self):
@@ -350,23 +351,23 @@ def test_pssh_client_long_running_command_exit_codes_no_stdout(self):
350351

351352
def test_pssh_client_retries(self):
352353
"""Test connection error retries"""
353-
listen_port = self.make_random_port()
354+
# listen_port = self.make_random_port()
354355
expected_num_tries = 2
355-
client = ParallelSSHClient([self.host], port=listen_port,
356-
pkey=self.user_key,
356+
client = ParallelSSHClient([self.host], port=self.port,
357+
pkey=b"fake",
357358
num_retries=expected_num_tries,
358359
retry_delay=.1,
359360
)
360-
self.assertRaises(ConnectionErrorException, client.run_command, 'blah')
361+
self.assertRaises(AuthenticationError, client.run_command, 'blah')
361362
try:
362363
client.run_command('blah')
363-
except ConnectionErrorException as ex:
364+
except AuthenticationError as ex:
365+
max_tries = ex.args[-2:][0]
364366
num_tries = ex.args[-1:][0]
365-
self.assertEqual(expected_num_tries, num_tries,
366-
msg="Got unexpected number of retries %s - "
367-
"expected %s" % (num_tries, expected_num_tries,))
367+
self.assertEqual(expected_num_tries, max_tries)
368+
self.assertEqual(expected_num_tries, num_tries)
368369
else:
369-
raise Exception('No ConnectionErrorException')
370+
raise Exception('No AuthenticationError')
370371

371372
def test_sftp_exceptions(self):
372373
# Port with no server listening on it on separate ip
@@ -380,7 +381,8 @@ def test_sftp_exceptions(self):
380381
try:
381382
cmd.get()
382383
except Exception as ex:
383-
self.assertEqual(ex.args[1], self.host)
384+
self.assertEqual(ex.args[2], self.host)
385+
self.assertEqual(ex.args[3], port)
384386
self.assertIsInstance(ex, ConnectionErrorException)
385387
else:
386388
raise Exception("Expected ConnectionErrorException, got none")
@@ -859,7 +861,7 @@ def test_identical_hosts_in_host_list(self):
859861
_host_stdout = list(host_out.stdout)
860862
self.assertListEqual(_host_stdout, expected_stdout)
861863

862-
def test_connection_error_exception(self):
864+
def test_connection_error(self):
863865
"""Test that we get connection error exception in output with correct arguments"""
864866
# Make port with no server listening on it on separate ip
865867
host = '127.0.0.3'
@@ -874,13 +876,7 @@ def test_connection_error_exception(self):
874876
for host_output in output:
875877
exit_code = host_output.exit_code
876878
self.assertEqual(exit_code, None)
877-
try:
878-
raise output[0].exception
879-
except ConnectionErrorException as ex:
880-
self.assertEqual(ex.args[1], host)
881-
self.assertEqual(ex.args[2], port)
882-
else:
883-
raise Exception("Expected ConnectionErrorException")
879+
self.assertIsInstance(output[0].exception, ConnectionError)
884880

885881
def test_bad_pkey_path(self):
886882
self.assertRaises(PKeyFileError, ParallelSSHClient, [self.host], port=self.port,

tests/native/test_single_client.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import shutil
2121
import tempfile
2222
from tempfile import NamedTemporaryFile
23+
24+
import pytest
2325
from pytest import raises
2426
from unittest.mock import MagicMock, call, patch
2527
from hashlib import sha256
@@ -36,7 +38,7 @@
3638
)
3739
from pssh.exceptions import (AuthenticationException, ConnectionErrorException,
3840
SessionError, SFTPIOError, SFTPError, SCPError, PKeyFileError, Timeout,
39-
AuthenticationError, NoIPv6AddressFoundError,
41+
AuthenticationError, NoIPv6AddressFoundError, ConnectionError
4042
)
4143

4244
from .base_ssh2_case import SSH2TestCase
@@ -89,6 +91,10 @@ def _sftp_exc(local_file, remote_file):
8991
self.assertRaises(
9092
SFTPIOError, client.copy_remote_file, 'fake_remote_file_not_exists', 'local')
9193

94+
def test_conn_refused(self):
95+
with pytest.raises(ConnectionRefusedError):
96+
SSHClient('127.0.0.99', port=self.port, num_retries=1, timeout=1)
97+
9298
@patch('pssh.clients.base.single.socket')
9399
def test_ipv6(self, gsocket):
94100
# As of Oct 2021, CircleCI does not support IPv6 in its containers.
@@ -102,18 +108,41 @@ def test_ipv6(self, gsocket):
102108
_sock = MagicMock()
103109
gsocket.socket.return_value = _sock
104110
sock_con = MagicMock()
111+
sock_con.side_effect = ConnectionRefusedError
105112
_sock.connect = sock_con
106113
getaddrinfo = MagicMock()
107114
gsocket.getaddrinfo = getaddrinfo
108115
getaddrinfo.return_value = [(
109116
socket.AF_INET6, socket.SocketKind.SOCK_STREAM, socket.IPPROTO_TCP, '', addr_info)]
110-
with raises(TypeError):
111-
# Mock object as a file descriptor will raise TypeError
117+
with raises(ConnectionError):
112118
client = SSHClient(host, port=self.port, pkey=self.user_key,
113119
num_retries=1)
114120
getaddrinfo.assert_called_once_with(host, self.port, proto=socket.IPPROTO_TCP)
115121
sock_con.assert_called_once_with(addr_info)
116122

123+
@patch('pssh.clients.base.single.socket')
124+
def test_multiple_available_addr(self, gsocket):
125+
host = '127.0.0.1'
126+
addr_info = (host, self.port)
127+
gsocket.IPPROTO_TCP = socket.IPPROTO_TCP
128+
gsocket.socket = MagicMock()
129+
_sock = MagicMock()
130+
gsocket.socket.return_value = _sock
131+
sock_con = MagicMock()
132+
sock_con.side_effect = ConnectionRefusedError
133+
_sock.connect = sock_con
134+
getaddrinfo = MagicMock()
135+
gsocket.getaddrinfo = getaddrinfo
136+
getaddrinfo.return_value = [
137+
(socket.AF_INET, socket.SocketKind.SOCK_STREAM, socket.IPPROTO_TCP, '', addr_info),
138+
(socket.AF_INET, socket.SocketKind.SOCK_STREAM, socket.IPPROTO_TCP, '', addr_info),
139+
]
140+
with raises(ConnectionError):
141+
client = SSHClient(host, port=self.port, pkey=self.user_key,
142+
num_retries=1)
143+
getaddrinfo.assert_called_with(host, self.port, proto=socket.IPPROTO_TCP)
144+
assert sock_con.call_count == len(getaddrinfo.return_value)
145+
117146
def test_no_ipv6(self):
118147
try:
119148
SSHClient(self.host,
@@ -357,7 +386,7 @@ def test_password_auth_failure(self):
357386
raise AssertionError
358387

359388
def test_retry_failure(self):
360-
self.assertRaises(ConnectionErrorException,
389+
self.assertRaises(ConnectionError,
361390
SSHClient, self.host, port=12345,
362391
num_retries=2, _auth_thread_pool=False,
363392
retry_delay=.1,

tests/ssh/test_parallel_client.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,14 @@ def test_pssh_client_hosts_list_part_failure(self):
238238
self.assertIsNotNone(output[1].exception,
239239
msg="Failed host %s has no exception in output - %s" % (hosts[1], output,))
240240
self.assertTrue(output[1].exception is not None)
241-
self.assertEqual(output[1].exception.args[1], hosts[1])
241+
self.assertEqual(output[1].host, hosts[1])
242+
self.assertEqual(output[1].exception.args[-2], hosts[1])
242243
try:
243244
raise output[1].exception
244245
except ConnectionErrorException:
245246
pass
246247
else:
247-
raise Exception("Expected ConnectionError, got %s instead" % (
248-
output[1].exception,))
248+
raise Exception("Expected ConnectionError, got %s instead" % (output[1].exception,))
249249

250250
def test_pssh_client_timeout(self):
251251
# 1ms timeout
@@ -316,14 +316,13 @@ def test_connection_error_exception(self):
316316
num_retries=1)
317317
output = client.run_command(self.cmd, stop_on_errors=False)
318318
client.join(output)
319-
self.assertIsNotNone(output[0].exception,
320-
msg="Got no exception for host %s - expected connection error" % (
321-
host,))
319+
self.assertIsInstance(output[0].exception, ConnectionErrorException)
320+
self.assertEqual(output[0].host, host)
322321
try:
323322
raise output[0].exception
324323
except ConnectionErrorException as ex:
325-
self.assertEqual(ex.args[1], host)
326-
self.assertEqual(ex.args[2], port)
324+
self.assertEqual(ex.args[-2], host)
325+
self.assertEqual(ex.args[-1], port)
327326
else:
328327
raise Exception("Expected ConnectionErrorException")
329328

tests/ssh/test_single_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from datetime import datetime
2222

2323
from gevent import sleep, Timeout as GTimeout, spawn
24-
from ssh.session import Session
24+
# from ssh.session import Session
2525
from ssh.exceptions import AuthenticationDenied
2626
from pssh.exceptions import AuthenticationException, ConnectionErrorException, \
2727
SessionError, SFTPIOError, SFTPError, SCPError, PKeyFileError, Timeout, \

0 commit comments

Comments
 (0)