与 Paramiko 的嵌套 SSH 会话

Posted

技术标签:

【中文标题】与 Paramiko 的嵌套 SSH 会话【英文标题】:Nested SSH session with Paramiko 【发布时间】:2010-12-27 01:40:27 【问题描述】:

我正在重写一个我写成 Python 的 Bash 脚本。该脚本的关键是

ssh -t first.com "ssh second.com very_remote_command"

我在使用 paramiko 进行嵌套身份验证时遇到问题。我无法找到任何处理我的确切情况的示例,但我能够在远程主机上找到使用 sudo 的示例。

The first method 写入标准输入

ssh.connect('127.0.0.1', username='jesse', password='lol')
stdin, stdout, stderr = ssh.exec_command("sudo dmesg")
stdin.write('lol\n')
stdin.flush()

The second 创建一个通道并使用类似套接字的 sendrecv

我能够让 stdin.writesudo 一起工作,但它不能与远程主机上的 ssh 一起工作。

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('first.com', username='luser', password='secret')
stdin, stdout, stderr = ssh.exec_command('ssh luser@second.com')
stdin.write('secret')
stdin.flush()
print '---- out ----'
print stdout.readlines()
print '---- error ----'
print stderr.readlines()

ssh.close()

...打印...

---- out ----
[]
---- error ----
['Pseudo-terminal will not be allocated because stdin is not a terminal.\r\n', 'Permission denied, please try again.\r\n', 'Permission denied, please try again.\r\n', 'Permission denied (publickey,password,keyboard-interactive).\r\n']

伪终端错误让我想起了原始命令中的 -t 标志,所以我切换到第二种方法,使用 Channel。而不是 ssh.exec_command 及更高版本,我有:

t = ssh.get_transport()
chan = t.open_session()
chan.get_pty()
print '---- send ssh cmd ----'
print chan.send('ssh luser@second.com')
print '---- recv ----'
print chan.recv(9999)
chan = t.open_session()
print '---- send password ----'
print chan.send('secret')
print '---- recv ----'
print chan.recv(9999)

...但它会打印 '---- send ssh cmd ----' 并一直挂起,直到我终止进程。

我是 Python 新手,对网络一无所知。在第一种情况下,为什么发送密码可以使用 sudo 而不是 ssh?提示不同吗? paramiko 是否适合这个库?

【问题讨论】:

【参考方案1】:

我设法找到了解决方案,但它需要一些手动工作。如果有人有更好的解决方案,请告诉我。

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('first.com', username='luser', password='secret')

chan = ssh.invoke_shell()

# Ssh and wait for the password prompt.
chan.send('ssh second.com\n')
buff = ''
while not buff.endswith('\'s password: '):
    resp = chan.recv(9999)
    buff += resp

# Send the password and wait for a prompt.
chan.send('secret\n')
buff = ''
while not buff.endswith('some-prompt$ '):
    resp = chan.recv(9999)
    buff += resp

# Execute whatever command and wait for a prompt again.
chan.send('ls\n')
buff = ''
while not buff.endswith('some-prompt$ '):
    resp = chan.recv(9999)
    buff += resp

# Now buff has the data I need.
print 'buff', buff

ssh.close()

需要注意的是,不是这个

t = ssh.get_transport()
chan = t.open_session()
chan.get_pty()

...你想要这个

chan = ssh.invoke_shell()

这让我想起了我小时候尝试编写 TradeWars 脚本并放弃编码十年的情景。 :)

【讨论】:

我正在尝试将您的解决方案用于类似的需求。但是 ('some-prompt$ ') 并不总是固定的..它总是有一个 '#' 但其他一些内容会不断变化..我该如何解释它? 如果以#结尾,则将some-prompt$ 改为#。但是,请确保检查尾随空格,即 ...endswith('# ')...endswith('#') 不同。 expect 实用程序(TCL 的东西)会帮助你吗?我相信 python 等价物是 pexpect... 我在没有部署程序的地方工作时遇到了这个问题。我不得不通过堡垒主机从生产服务器中收集文件,以查看部署了什么。在过去的六年里,我一直在部署代码版本的地方工作,而不是完全免费的部署。 (人们会从任何版本的代码库中直接选择要部署的文件,并且每个人都有权这样做。)我不再需要嵌套 ssh 会话,因为我可以将我的脚本放在远程主机上并在那里运行它,如果我想要。 我也更聪明。我很确定我可以使用 ssh 命令完成此操作。可能当时我不明白需要做的三个级别的转义。【参考方案2】:

这是一个仅使用 paramiko(和端口转发)的小示例:

import paramiko as ssh

class SSHTool():
    def __init__(self, host, user, auth,
                 via=None, via_user=None, via_auth=None):
        if via:
            t0 = ssh.Transport(via)
            t0.start_client()
            t0.auth_password(via_user, via_auth)
            # setup forwarding from 127.0.0.1:<free_random_port> to |host|
            channel = t0.open_channel('direct-tcpip', host, ('127.0.0.1', 0))
            self.transport = ssh.Transport(channel)
        else:
            self.transport = ssh.Transport(host)
        self.transport.start_client()
        self.transport.auth_password(user, auth)

    def run(self, cmd):
        ch = self.transport.open_session()
        ch.set_combine_stderr(True)
        ch.exec_command(cmd)
        retcode = ch.recv_exit_status()
        buf = ''
        while ch.recv_ready():
            buf += ch.recv(1024)
        return (buf, retcode)

# The example below is equivalent to
# $ ssh 10.10.10.10 ssh 192.168.1.1 uname -a
# The code above works as if these 2 commands were executed:
# $ ssh -L <free_random_port>:192.168.1.1:22 10.10.10.10
# $ ssh 127.0.0.1:<free_random_port> uname -a
host = ('192.168.1.1', 22)
via_host = ('10.10.10.10', 22)

ssht = SSHTool(host, 'user1', 'pass1',
    via=via_host, via_user='user2', via_auth='pass2')

print ssht.run('uname -a')

【讨论】:

您能否详细说明您要转发的端口以及在您的示例中执行转发的主机? 在代码中添加了注释,并做了一个小改动。转发。 User1 和 pass1 用于目标,vía 用于第一个主机。谢谢新浪。【参考方案3】:

您可以使用来自另一个 ssh 连接的通道创建 ssh 连接。详情请参阅here。

【讨论】:

谢谢。这是我找到的最好的方法。 这是最好的,应该标记为正确答案。【参考方案4】:

对于现成的解决方案,请查看 pxpect 项目中的 pxssh。查看 sshls.py 和 ssh_tunnel.py 示例。

http://www.noah.org/wiki/Pexpect

【讨论】:

pexpect 现在在 Windows 上不受支持【参考方案5】:

Sinas 的回答效果很好,但没有为我提供很长命令的所有输出。但是,使用 chan.makefile() 可以让我检索所有输出。

以下适用于需要 tty 并提示输入 sudo 密码的系统

ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
ssh.connect("10.10.10.1", 22, "user", "password")
chan=ssh.get_transport().open_session()
chan.get_pty()
f = chan.makefile()
chan.exec_command("sudo dmesg")
chan.send("password\n")
print f.read()
ssh.close()

【讨论】:

以上是关于与 Paramiko 的嵌套 SSH 会话的主要内容,如果未能解决你的问题,请参考以下文章

带有 Paramiko 的嵌套 SSH 退出 while 循环

使用 Paramiko 和 RSA 密钥文件的嵌套 SSH

Python学习记录-paramiko模块

CMDB资产采集

paramiko模块

Python paramiko模块使用解析实现ssh