背景
Paramiko is a pure-Python [1] (2.7, 3.4+) implementation of the SSHv2 protocol [2], providing both client and server functionality. It provides the foundation for the high-level SSH library Fabric, which is what we recommend you use for common client use-cases such as running remote shell commands or transferring files.
Direct use of Paramiko itself is only intended for users who need advanced/low-level primitives or want to run an in-Python sshd.
以上是 paramiko 的官网介绍,简而言之,paramiko 是一个实用的 python ssh 库。
目前遇到一个问题,尝试用 paramiko 构建 ssh 连接,ssh 连接是基于 paramiko 的 transport 对象,因此需要先实例化 transport 对象,但是如果服务器宕机了,paramiko 会一直 pending 在实例化 transport 对象的过程中。
原因
Create a new SSH session over an existing socket, or socket-like object. This only creates the Transport
object; it doesn’t begin the SSH session yet. Use connect
or start_client
to begin a client session, or start_server
to begin a server session.
查看 paramiko 文档,发现 transport 是基于 socket 进行通信的,实例化 transport 对象需要传入 socket 对象或是类 socket 对象。而所谓类 socket 对象,其实就是 IP + port
的信息,这也是一般实例化 transport 的方法:
self.transport = paramiko.Transport((hostname, port))
self.transport.connect(username=username, password=password, **kwargs)
其实查看源码可以发现,这种类 socket 对象是 paramiko 对其做了向真实 socket 对象的转化:
...
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
hl = sock.split(":", 1)
self.hostname = hl[0]
if len(hl) == 1:
sock = (hl[0], 22)
else:
sock = (hl[0], int(hl[1]))
if type(sock) is tuple:
# connect to the given (host, port)
hostname, port = sock
self.hostname = hostname
reason = "No suitable address family"
addrinfos = socket.getaddrinfo(
hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM
)
for family, socktype, proto, canonname, sockaddr in addrinfos:
if socktype == socket.SOCK_STREAM:
af = family
# addr = sockaddr
sock = socket.socket(af, socket.SOCK_STREAM)
try:
retry_on_signal(lambda: sock.connect((hostname, port)))
except socket.error as e:
reason = str(e)
else:
break
else:
raise SSHException(
"Unable to connect to {}: {}".format(hostname, reason)
)
...
这时就会产生一个问题,paramiko 创建好 socket 对象后会测试连接,调用 retry_on_signal
方法,这个方法是个循环方法,直到连接通了才会停止。
def retry_on_signal(function):
"""Retries function until it doesn't raise an EINTR error"""
while True:
try:
return function()
except EnvironmentError as e:
if e.errno != errno.EINTR:
raise
但是,如果服务器宕机了,这个方法就会一直 pending 在这里…因为 paramiko 在实例化 socket 的时候并没有设置 socket 的 timeout 时间,即超时等待时间,而 socket 的默认 timeout 时间是 None…
socket.setdefaulttimeout(timeout)
Set the default timeout in seconds (float) for new socket objects. When the socket module is first imported, the default is None
. See settimeout()
for possible values and their respective meanings.
解决
解决思路是,既然实例化 transport 需要 socket 对象,那么就传一个真实的 socket 对象给构造函数,而不是让它帮我们实例化 socket。同时,我们在实例化 socket 的时候,同时调用 socket.timeout()
方法设置超时等待时间,以下代码参考 paramiko 的代码:
class SSH(object):
def __init__(self, hostname, username, password=None, port=22, **kwargs):
"""
ssh = SSH(host, username, password)
"""
self._username = username
self._password = password
timeout = kwargs.pop('timeout', 0)
self.transport = paramiko.Transport(_create_socket(hostname=hostname, port=port, timeout=timeout))
# self.transport = paramiko.Transport((hostname, port))
self.transport.connect(username=username, password=password, **kwargs)
self.ssh = paramiko.SSHClient()
self.ssh._transport = self.transport
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
def _create_socket(hostname, port, timeout=0):
"""
Create socket instance
If timeout>0, set the timeout of the instance, otherwise use the default timeout.
:param hostname:
:param port:
:param timeout:
:return:
"""
reason = "No suitable address family"
addrinfos = socket.getaddrinfo(
hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM
)
for family, socktype, proto, canonname, sockaddr in addrinfos:
if socktype == socket.SOCK_STREAM:
sock = socket.socket(family, socket.SOCK_STREAM)
if timeout > 0:
sock.settimeout(timeout)
try:
retry_on_signal(lambda: sock.connect((hostname, port)))
except socket.error as e:
reason = str(e)
else:
break
else:
raise SSHException(
"Unable to connect to {}: {}".format(hostname, reason)
)
return sock
if __name__ == '__main__':
ssh = SSH('192.168.1.123', 'test', 'test')
得到抛出的异常:
Traceback (most recent call last):
File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevd.py", line 1483, in _exec
pydev_imports.execfile(file, globals, locals) # execute the script
File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "/Users/alex/WorkSpace/repositories/hol-troubleshooting/scripts/ssh.py", line 240, in <module>
ssh = SSH('192.168.1.123', 'test', 'test')
File "/Users/alex/WorkSpace/repositories/hol-troubleshooting/scripts/ssh.py", line 37, in __init__
self.transport = paramiko.Transport(_create_socket(hostname=hostname, port=port, timeout=timeout))
File "/Users/alex/WorkSpace/repositories/hol-troubleshooting/scripts/ssh.py", line 201, in _create_socket
"Unable to connect to {}: {}".format(hostname, reason)
paramiko.ssh_exception.SSHException: Unable to connect to 192.168.1.123: timed out
仅供参考,欢迎交流。
参考
- paramiko 官网
- paramiko api 文档
- python socket api 文档