背景
需要批量在路由器上進行配置浑测,與網(wǎng)元建立SSH連接芥颈,同時存在交互操作。比如:鍵入Configure
角钩,進入配置模式成功后才可以鍵入后續(xù)指令吝沫。
在這個過程中遇到很多坑,在此分享递礼。
SSHClient
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=device, port=22, username=username, password=password)
stdin, stdout, stderr = ssh.exec_command("configure", bufsize=1024)
res, err = stdout.read(), stderr.read()
result = res if res else err
print(result)
SSHClient是最簡單的命令行執(zhí)行方式惨险,簡單的參數(shù)填寫,優(yōu)雅的結(jié)果處理脊髓,這很難不讓人趕緊上手一試辫愉。
但遺憾的是SSHClient不支持交互式命令執(zhí)行,其原因在于其exec_command
方法每次執(zhí)行一條命令都會開啟一個新的“channel”将硝,從而開啟一個新的session恭朗,這相當于我們每執(zhí)行一次命令,都重新登錄了一次網(wǎng)元設(shè)備袋哼,這使得交互式無從談起冀墨。
def exec_command(
self,
command,
bufsize=-1,
timeout=None,
get_pty=False,
environment=None,
):
# 就是他,在這個open_session的說明中也有描述:Request a new channel to the server,
# of type ``"session"``.
chan = self._transport.open_session(timeout=timeout)
if get_pty:
chan.get_pty()
chan.settimeout(timeout)
if environment:
chan.update_environment(environment)
chan.exec_command(command)
stdin = chan.makefile_stdin("wb", bufsize)
stdout = chan.makefile("r", bufsize)
stderr = chan.makefile_stderr("r", bufsize)
return stdin, stdout, stderr
所以我們要創(chuàng)建固定channel涛贯,從而創(chuàng)建固定session
交互式連接
# Create a new SSH session over an existing socket, or socket-like object.
trans = paramiko.Transport((devcie, 22))
trans.start_client()
trans.auth_password(username, password)
# 新建channel
channel = trans.open_session(timeout=1200)
# 獲取終端
channel.get_pty()
# 激活終端
channel.invoke_shell()
# 執(zhí)行命令
channel.send(command)
# 結(jié)果獲取
result = channel.recv(10240)
result = result.decode("utf-8")
產(chǎn)生的問題
以上是交互式連接的過程诽嘉,但事情并不是一帆風順的,在這個過程中還有兩個問題弟翘,這兩個問題都由交互結(jié)果獲取函數(shù)channel.recv
引起虫腋,這將導(dǎo)致。
- 交互結(jié)果獲取存在延遲
- 錯誤處理困難
原因
def read(self, len=1024, buffer=None):
return self._wrap_ssl_read(len, buffer)
def recv(self, len=1024, flags=0):
if flags != 0:
raise ValueError("non-zero flags not allowed in calls to recv")
return self._wrap_ssl_read(len)
def _wrap_ssl_read(self, len, buffer=None):
try:
return self._ssl_io_loop(self.sslobj.read, len, buffer)
except ssl.SSLError as e:
if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs:
return 0 # eof, return 0.
else:
raise
def _ssl_io_loop(self, func, *args):
"""Performs an I/O loop between incoming/outgoing and the socket."""
should_loop = True
ret = None
while should_loop:
errno = None
try:
# 就是這里稀余,遞歸的入口
ret = func(*args)
except ssl.SSLError as e:
if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE):
# WANT_READ, and WANT_WRITE are expected, others are not.
raise e
errno = e.errno
buf = self.outgoing.read()
self.socket.sendall(buf)
if errno is None:
should_loop = False
elif errno == ssl.SSL_ERROR_WANT_READ:
buf = self.socket.recv(SSL_BLOCKSIZE)
if buf:
self.incoming.write(buf)
else:
self.incoming.write_eof()
return ret
以上是channel.recv
的實現(xiàn)過程悦冀,歸根結(jié)底還是_ssl_io_loop
這個函數(shù)會遞歸的獲取ssh交互結(jié)果,形成一種循環(huán)睛琳。這種循環(huán)的結(jié)束條件就是接受到交互結(jié)果盒蟆,或者是結(jié)果讀取異常踏烙。
如果我們在執(zhí)行命令之后立刻獲取結(jié)果,交互可能尚未產(chǎn)生結(jié)果历等,獲取失敗讨惩,結(jié)束循環(huán)(接收無效),channel.recv
仿佛沒有執(zhí)行一樣寒屯。
這時我們就會想到荐捻,既然交互結(jié)果的獲取具有滯后性,那我們就編寫邏輯寡夹,使其等待或者循環(huán)等待处面。
這就會引發(fā)下一個問題,如果我們循環(huán)調(diào)用channel.recv
時菩掏,必須在獲取到交互結(jié)果后結(jié)束我們的循環(huán)魂角,不論這個交互的結(jié)果是不是你期望的;否則會進入死循環(huán)患蹂,程序無法推進或颊。
建議
- 循環(huán)調(diào)用
channel.recv
以獲取交互結(jié)果。 - 全面的結(jié)果處理传于,結(jié)果的處理取決于所交互設(shè)備的響應(yīng)內(nèi)容囱挑,一旦獲取交互結(jié)果,不論是期望與否都要及時退出我們的循環(huán)沼溜;否則程序無法推進平挑。