paramiko實(shí)現(xiàn)SSH交互式命令執(zhí)行

背景

需要批量在路由器上進(jìn)行配置,與網(wǎng)元建立SSH連接,同時(shí)存在交互操作。比如:鍵入Configure,進(jìn)入配置模式成功后才可以鍵入后續(xù)指令。

在這個(gè)過程中遇到很多坑,在此分享。

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í)行一條命令都會(huì)開啟一個(gè)新的“channel”,從而開啟一個(gè)新的session,這相當(dāng)于我們每執(zhí)行一次命令,都重新登錄了一次網(wǎng)元設(shè)備,這使得交互式無從談起。

def exec_command(
    self,
    command,
    bufsize=-1,
    timeout=None,
    get_pty=False,
    environment=None,
):
# 就是他,在這個(gè)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

所以我們要?jiǎng)?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)生的問題

以上是交互式連接的過程,但事情并不是一帆風(fēng)順的,在這個(gè)過程中還有兩個(gè)問題,這兩個(gè)問題都由交互結(jié)果獲取函數(shù)channel.recv引起,這將導(dǎo)致。

  1. 交互結(jié)果獲取存在延遲
  2. 錯(cuò)誤處理困難

原因

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的實(shí)現(xiàn)過程,歸根結(jié)底還是_ssl_io_loop 這個(gè)函數(shù)會(huì)遞歸的獲取ssh交互結(jié)果,形成一種循環(huán)。這種循環(huán)的結(jié)束條件就是接受到交互結(jié)果,或者是結(jié)果讀取異常。

如果我們?cè)趫?zhí)行命令之后立刻獲取結(jié)果,交互可能尚未產(chǎn)生結(jié)果,獲取失敗,結(jié)束循環(huán)(接收無效),channel.recv仿佛沒有執(zhí)行一樣。

這時(shí)我們就會(huì)想到,既然交互結(jié)果的獲取具有滯后性,那我們就編寫邏輯,使其等待或者循環(huán)等待。

這就會(huì)引發(fā)下一個(gè)問題,如果我們循環(huán)調(diào)用channel.recv時(shí),必須在獲取到交互結(jié)果后結(jié)束我們的循環(huán),不論這個(gè)交互的結(jié)果是不是你期望的;否則會(huì)進(jìn)入死循環(huán),程序無法推進(jìn)。

建議

  1. 循環(huán)調(diào)用channel.recv以獲取交互結(jié)果。
  2. 全面的結(jié)果處理,結(jié)果的處理取決于所交互設(shè)備的響應(yīng)內(nèi)容,一旦獲取交互結(jié)果,不論是期望與否都要及時(shí)退出我們的循環(huán);否則程序無法推進(jìn)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容