paramiko实现SSH交互式命令执行

背景

需要批量在路由器上进行配置,与网元建立SSH连接,同时存在交互操作。比如:键入Configure,进入配置模式成功后才可以键入后续指令。

在这个过程中遇到很多坑,在此分享。

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是最简单的命令行执行方式,简单的参数填写,优雅的结果处理,这很难不让人赶紧上手一试。

但遗憾的是SSHClient不支持交互式命令执行,其原因在于其exec_command方法每次执行一条命令都会开启一个新的“channel”,从而开启一个新的session,这相当于我们每执行一次命令,都重新登录了一次网元设备,这使得交互式无从谈起。

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

所以我们要创建固定channel,从而创建固定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()
# 执行命令
channel.send(command)
# 结果获取
result = channel.recv(10240)
result = result.decode("utf-8")

产生的问题

以上是交互式连接的过程,但事情并不是一帆风顺的,在这个过程中还有两个问题,这两个问题都由交互结果获取函数channel.recv引起,这将导致。

  1. 交互结果获取存在延迟
  2. 错误处理困难

原因

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的实现过程,归根结底还是_ssl_io_loop 这个函数会递归的获取ssh交互结果,形成一种循环。这种循环的结束条件就是接受到交互结果,或者是结果读取异常。

如果我们在执行命令之后立刻获取结果,交互可能尚未产生结果,获取失败,结束循环(接收无效),channel.recv仿佛没有执行一样。

这时我们就会想到,既然交互结果的获取具有滞后性,那我们就编写逻辑,使其等待或者循环等待。

这就会引发下一个问题,如果我们循环调用channel.recv时,必须在获取到交互结果后结束我们的循环,不论这个交互的结果是不是你期望的;否则会进入死循环,程序无法推进。

建议

  1. 循环调用channel.recv以获取交互结果。
  2. 全面的结果处理,结果的处理取决于所交互设备的响应内容,一旦获取交互结果,不论是期望与否都要及时退出我们的循环;否则程序无法推进。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容