Skip to content

Commit

Permalink
Raise usefully ambiguous error when every connect attempt fails.
Browse files Browse the repository at this point in the history
  • Loading branch information
bitprophet committed Feb 7, 2015
1 parent b42b533 commit f99e1d8
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 4 deletions.
24 changes: 20 additions & 4 deletions paramiko/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
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

Expand Down Expand Up @@ -256,8 +258,10 @@ def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=No
``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:
Expand All @@ -269,10 +273,22 @@ def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=No
# 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)
Expand Down
30 changes: 30 additions & 0 deletions paramiko/ssh_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -129,3 +131,31 @@ def __init__(self, command, error):
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

0 comments on commit f99e1d8

Please sign in to comment.