使用Paramiko进行嵌套SSH会话

37

我正在将我写的一个Bash脚本改写成Python。那个脚本的关键是

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

我正在使用paramiko进行嵌套认证时遇到问题。我没有找到任何处理我的精确情况的示例,但是我找到了在远程主机上使用sudo的示例。 第一种方法写入stdin。
ssh.connect('127.0.0.1', username='jesse', password='lol')
stdin, stdout, stderr = ssh.exec_command("sudo dmesg")
stdin.write('lol\n')
stdin.flush()

第二个示例创建一个频道并使用类似套接字的发送接收

我能够在sudo上使用stdin.write,但在远程主机上的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标志,因此我切换到第二种方法,使用通道。而不是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是否是正确的库?

5个回答

33
我找到了一个解决方案,但需要一些手动操作。如果有更好的解决方案,请告诉我。
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()

The thing to note is that instead of this

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

你希望这样做

chan = ssh.invoke_shell()

这让我想起小时候尝试编写TradeWars脚本,最终放弃了编码十年。


期望效用程序(TCL工具)能帮到你吗?我相信它的Python等效工具是pexpect... - Jimbo
我曾在一个没有部署程序的地方工作时遇到了这个问题。我不得不通过堡垒主机从生产服务器上查看已部署的内容。过去六年里,我一直在那些部署代码版本而不是随意部署的地方工作。(人们会从代码库的任何版本中选择要部署的文件,每个人都有权限这样做。)我不再需要嵌套ssh会话,因为如果需要,我可以将我的脚本放在远程主机上并在那里运行它。 - mqsoh
还有,我更聪明。我很确定我可以用ssh命令完成这个任务。可能当时我没有理解需要进行三层转义的概念。 - mqsoh
如果您的 while 循环挂起,请在调用 shell 命令后设置 chan.settimeout(3) 并处理 socket.timeout 异常。 - simno
@Praneeth:chan是一种套接字类型,因此recv只需要获取任意数量的数据。在文档中,我发现应该使用2的幂次方。另外,有一个无意义的赋值。现在我会这样做:buff += chan.recv(4096)。https://docs.python.org/3/library/socket.html#socket.socket.recv - mqsoh
显示剩余3条评论

17

这是一个仅使用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')

您能详细说明一下,在您的示例中,您要转发哪个端口以及由哪个主机执行转发吗? - Mike Pennington
在代码中添加了一条注释,并对转发进行了小的更改。 - Sinas
User1和pass1是用于目标,vía是用于第一个主机。谢谢Sina。 - Hermandroid

7
您可以使用来自另一个ssh连接的通道创建ssh连接。有关更多详细信息,请参见此处

谢谢。这是我找到的最好的方法。 - Hermandroid
这是最好的答案,应该标记为正确答案。 - Matthew Moisen

1

对于一个现成的解决方案,可以查看 pxpect 项目中的 pxssh。可以参考 sshls.py 和 ssh_tunnel.py 的示例。

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


pexpect现已不支持Windows操作系统。 - Mohit Gupta

0

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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接