summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2015-02-06 19:33:06 -0800
committerJeff Forcier <jeff@bitprophet.org>2015-02-06 19:33:06 -0800
commitf99e1d8775be20c471c39f8d7a91126b8419381d (patch)
treecd19913be70ba5a3a51e15738c31252e1ff9fa7a
parentb42b5338de868c3a5343dd03a8ee3f8a968d2f65 (diff)
Raise usefully ambiguous error when every connect attempt fails.
Re #22
-rw-r--r--paramiko/client.py24
-rw-r--r--paramiko/ssh_exception.py30
2 files changed, 50 insertions, 4 deletions
diff --git a/paramiko/client.py b/paramiko/client.py
index b907d3b0..57e9919d 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -36,7 +36,9 @@ from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
from paramiko.resource import ResourceManager
from paramiko.rsakey import RSAKey
-from paramiko.ssh_exception import SSHException, BadHostKeyException
+from paramiko.ssh_exception import (
+ SSHException, BadHostKeyException, ConnectionError
+)
from paramiko.transport import Transport
from paramiko.util import retry_on_signal, ClosingContextManager
@@ -256,8 +258,10 @@ class SSHClient (ClosingContextManager):
``gss_deleg_creds`` and ``gss_host`` arguments.
"""
if not sock:
+ errors = {}
# Try multiple possible address families (e.g. IPv4 vs IPv6)
- for af, addr in self._families_and_addresses(hostname, port):
+ to_try = list(self._families_and_addresses(hostname, port))
+ for af, addr in to_try:
try:
sock = socket.socket(af, socket.SOCK_STREAM)
if timeout is not None:
@@ -269,10 +273,22 @@ class SSHClient (ClosingContextManager):
# Break out of the loop on success
break
except socket.error, e:
- # If the port is not open on IPv6 for example, we may still try IPv4.
- # Likewise if the host is not reachable using that address family.
+ # Raise anything that isn't a straight up connection error
+ # (such as a resolution error)
if e.errno not in (ECONNREFUSED, EHOSTUNREACH):
raise
+ # Capture anything else so we know how the run looks once
+ # iteration is complete. Retain info about which attempt
+ # this was.
+ errors[addr] = e
+
+ # Make sure we explode usefully if no address family attempts
+ # succeeded. We've no way of knowing which error is the "right"
+ # one, so we construct a hybrid exception containing all the real
+ # ones, of a subclass that client code should still be watching for
+ # (socket.error)
+ if len(errors) == len(to_try):
+ raise ConnectionError(errors)
t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds)
t.use_compression(compress=compress)
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index b99e42b3..7e6f2568 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -16,6 +16,8 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+import socket
+
class SSHException (Exception):
"""
@@ -129,3 +131,31 @@ class ProxyCommandFailure (SSHException):
self.error = error
# for unpickling
self.args = (command, error, )
+
+
+class ConnectionError(socket.error):
+ """
+ High-level socket error wrapping 1+ actual socket.error objects.
+
+ To see the wrapped exception objects, access the ``errors`` attribute.
+ ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1',
+ 22)``) and whose values are the exception encountered trying to connect to
+ that address.
+
+ It is implied/assumed that all the errors given to a single instance of
+ this class are from connecting to the same hostname + port (and thus that
+ the differences are in the resolution of the hostname - e.g. IPv4 vs v6).
+ """
+ def __init__(self, errors):
+ """
+ :param dict errors:
+ The errors dict to store, as described by class docstring.
+ """
+ addrs = errors.keys()
+ body = ', '.join([x[0] for x in addrs[:-1]])
+ tail = addrs[-1][0]
+ msg = "Unable to connect to port {0} at {1} or {2}"
+ super(ConnectionError, self).__init__(
+ msg.format(addrs[0][1], body, tail)
+ )
+ self.errors = errors