使用Python通过ssh执行命令

213

我正在编写一个Python脚本来自动化一些命令行命令。目前,我正在执行类似以下的调用:

cmd = "some unix command"
retcode = subprocess.call(cmd,shell=True)

然而,我需要在远程机器上运行一些命令。手动操作时,我会使用ssh登录,然后运行命令。如何在Python中自动化这个过程?我需要使用已知的密码登录到远程机器,所以我不能只使用cmd = ssh user@remotehost,我想知道是否有适合此目的的模块?


2
你看过 Fabric 吗?它允许你使用 Python 在 SSH 上做各种远程操作。 - tomaski
我经常使用<a href="http://www.lag.net/paramiko/">paramiko</a>(很好用)和<a href="http://pexpect.sourceforge.net/pxssh.html">pxssh</a>(也很好用)。我会推荐其中任何一个。它们的工作方式略有不同,但在使用上有相当大的重叠部分。 - Eric Snow
这个回答解决了你的问题吗?Python处理SSH的库 - miken32
这是一个简单的包装类,用于调用sshsubprocess:https://gist.github.com/mamaj/a7b378a5c969e3e32a9e4f9bceb0c5eb - mamaj
18个回答

278

我会向你推荐 paramiko

请查看这个问题

ssh = paramiko.SSHClient()
ssh.connect(server, username=username, password=password)
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command(cmd_to_execute)
如果您正在使用SSH密钥,请执行以下操作:
k = paramiko.RSAKey.from_private_key_file(keyfilename)
# OR k = paramiko.DSSKey.from_private_key_file(keyfilename)

ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=host, username=user, pkey=k)

4
这里的假设是paramiko和(开放)ssh一样安全。 它是吗? - user239558
1
如果 SSH 密钥被交换了会怎么样? - Ardit
10
作为 Paramiko 的长期用户(但并非专家),我建议使用 Paramiko,但您应考虑您的使用情况以及您愿意学习多少。Paramiko 是非常底层的,你可能会很容易地陷入一个“命令运行帮助函数”的陷阱中,而没有完全理解你正在使用的代码。这意味着你可能会设计一个 def run_cmd(host, cmd): 来实现你想要的功能,但随着你的需求不断发展,你最终需要为新的用例更改帮助函数,从而改变了旧有用法的行为。因此请做好相应的计划。 - Scott Prive
7
对于未知主机错误,请在执行前使用ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())。 - Nemo
1
每个命令连接几秒钟时间都可能不太好。 - MrR
显示剩余6条评论

83

保持简单。不需要任何库。

import subprocess

# Python 2
subprocess.Popen("ssh {user}@{host} {cmd}".format(user=user, host=host, cmd='ls -l'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()

# Python 3
subprocess.Popen(f"ssh {user}@{host} {cmd}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()

4
Subprocess 是自动化中的瑞士军刀。你可以将 Python 与 shell 脚本、sed、awk 和 grep 等无限可能结合使用。虽然你可以全部在 Python 中完成,但在 Python 中运行一个 grep 不是更好吗?而且你可以做到。 - Ronn Macc
30
如果你的SSH命令需要输入密码,你该如何在Python文件中提供密码? - BeingSuman
3
您可以使用SSH密钥认证进行操作。虽然我想您已经找到了解决方法。 - mmrbulbul
6
你可以使用“sshpass -p {密码} ssh {用户名}@{IP地址}”这个命令。 - Carsten
2
@BeingSuman f"echo {password} | ssh {user}@{host} {cmd}"。当然,只有在提示用户时才是安全的。 - Gulzar
显示剩余9条评论

57

8
一个很好的简单内置方法加一分。在我当前的设置中,我不想添加Python库,所以您的建议非常有价值,也非常直接。 - Philip Kearns
4
请确保您的远程主机已设置为无需密码的 SSH 登录。如果没有,您需要执行其他操作来管理身份验证。 - powerrox
subprocess.check_output -- 很棒的解决方案! - Tim S.
2
@powerrox 那些“其他的东西”是什么? - ealeon
2
@TimS。你可能需要包括适合你设置的身份验证处理方式。我使用expect在提示符处输入密码。然后有这个帖子提供其他解决方案:http://unix.stackexchange.com/questions/147329/answer-password-prompt-programmatically-via-shell-script - powerrox
1
请注意commands模块已经在Python 3中被移除。请使用subprocess模块,正如另一个答案中所提到的那样。 - Dylan Hogg

22

我发现 paramiko 过于底层,而 Fabric 不太适合用作库,因此我编写了自己的库称为 spur,它使用 paramiko 实现了一个稍微更好的接口:

import spur

shell = spur.SshShell(hostname="localhost", username="bob", password="password1")
result = shell.run(["echo", "-n", "hello"])
print result.output # prints hello

如果你需要在 shell 中运行:

shell.run(["sh", "-c", "echo -n hello"])

2
我决定尝试 spur。你可以生成额外的 shell 命令,最终得到:which 'mkdir' > /dev/null 2>&1 ; echo $?; exec 'mkdir' '-p' '/data/rpmupdate/20130207142923'。我也想要访问一个普通的 exec_command。还缺少运行后台任务的能力:nohup ./bin/rpmbuildpackages < /dev/null >& /dev/null &。例如,我使用模板生成一个 zsh 脚本(rpmbuildpackages),然后只需在机器上让它保持运行状态。也许监视这些后台作业的能力也很不错(将 PID 保存在某个 ~/.spur 文件中)。 - davidlt
1
spur 显然只能在 Unix 系统上工作,因为它依赖于 termios。有人知道一个适用于 Windows 的好库吗? - Gabriel
并不完全正确:如果您使用预编译安装程序,则可以安装paramiko和spur。我刚刚自己安装了... - ravemir
@Gabriel:最近的一个版本应该已经改善了在Windows上的支持。如果仍然无法正常工作,请随时提出问题。 - Michael Williamson
@davidlt:在构建SshShell时,现在有设置shell类型的选项。如果使用最小化shell通过传递shell_type=spur.ssh.ShellTypes.minimal,那么只会发送原始命令。直接实现后台任务可能有点超出Spur的范围,但是您应该能够通过调用shell来运行您描述的命令,例如 shell.run(["sh", "-c", "nohup ./bin/rpmbuildpackages < /dev/null >& /dev/null &"]) - Michael Williamson

11

所有人已经提到(推荐)使用 paramiko ,我只是分享一个Python代码(可以称之为API),它将允许您一次执行多个命令。

要在不同的节点上执行命令,请使用:Commands().run_cmd(host_ip,list_of_commands)

您会看到一个TODO,我保留它以防止任何命令执行失败时停止执行,我不知道该如何实现,请分享您的知识。

#!/usr/bin/python

import os
import sys
import select
import paramiko
import time


class Commands:
    def __init__(self, retry_time=0):
        self.retry_time = retry_time
        pass

    def run_cmd(self, host_ip, cmd_list):
        i = 0
        while True:
        # print("Trying to connect to %s (%i/%i)" % (self.host, i, self.retry_time))
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(host_ip)
            break
        except paramiko.AuthenticationException:
            print("Authentication failed when connecting to %s" % host_ip)
            sys.exit(1)
        except:
            print("Could not SSH to %s, waiting for it to start" % host_ip)
            i += 1
            time.sleep(2)

        # If we could not connect within time limit
        if i >= self.retry_time:
            print("Could not connect to %s. Giving up" % host_ip)
            sys.exit(1)
        # After connection is successful
        # Send the command
        for command in cmd_list:
            # print command
            print "> " + command
            # execute commands
            stdin, stdout, stderr = ssh.exec_command(command)
            # TODO() : if an error is thrown, stop further rules and revert back changes
            # Wait for the command to terminate
            while not stdout.channel.exit_status_ready():
                # Only print data if there is data to read in the channel
                if stdout.channel.recv_ready():
                    rl, wl, xl = select.select([ stdout.channel ], [ ], [ ], 0.0)
                    if len(rl) > 0:
                        tmp = stdout.channel.recv(1024)
                        output = tmp.decode()
                        print output

        # Close SSH connection
        ssh.close()
        return

def main(args=None):
    if args is None:
        print "arguments expected"
    else:
        # args = {'<ip_address>', <list_of_commands>}
        mytest = Commands()
        mytest.run_cmd(host_ip=args[0], cmd_list=args[1])
    return


if __name__ == "__main__":
    main(sys.argv[1:])

只需使用多进程并在它占用太多时间时终止该进程。另一方面,这也是一个问题,你可以在那里设置一个超时时间,我猜? - Ajay

10

被接受的答案对我没用,这是我使用的替代方案:

import paramiko
import os

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# ssh.load_system_host_keys()
ssh.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
ssh.connect("d.d.d.d", username="user", password="pass", port=22222)

ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("ls -alrt")
exit_code = ssh_stdout.channel.recv_exit_status() # handles async exit error 

for line in ssh_stdout:
    print(line.strip())

total 44
-rw-r--r--.  1 root root  129 Dec 28  2013 .tcshrc
-rw-r--r--.  1 root root  100 Dec 28  2013 .cshrc
-rw-r--r--.  1 root root  176 Dec 28  2013 .bashrc
...

或者,您可以使用sshpass

import subprocess
cmd = """ sshpass -p "myPas$" ssh user@d.d.d.d -p 12345 'my command; exit' """
print( subprocess.getoutput(cmd) )

参考资料:

  1. https://github.com/onyxfish/relay/issues/11
  2. https://dev59.com/tpffa4cB1Zd3GeqP-Jhz#61016663

注意事项:

  1. 确保至少通过ssh手动连接到远程系统一次(ssh root@ip),并接受公共密钥,这往往是无法使用paramiko或其他自动化ssh脚本进行连接的原因。

1
paramiko 解决方案工作良好。 - muthukumar

9

paramiko 终于在我添加了关于允许缺失主机密钥策略的额外行后成功运行,这是非常重要的一步(第5行):

import paramiko

p = paramiko.SSHClient()
# This script doesn't work for me unless the following line is added!
p.set_missing_host_key_policy(paramiko.AutoAddPolicy())   
p.connect("server", port=22, username="username", password="password")
stdin, stdout, stderr = p.exec_command("your command")
opt = stdout.readlines()
opt = "".join(opt)
print(opt)

请确保已安装paramiko包。 解决方案的原始来源:


4
我曾经使用过paramiko(不错)和pxssh(也不错)。我会推荐其中任何一个。它们的工作方式略有不同,但在使用上有较大的重叠部分。

5
pxssh的链接让人回到了过去,感觉很不错。 - Oben Sonne

3

首先:我惊讶地发现还没有人提到 fabric

其次:为了满足你所描述的这些要求,我实现了一个名叫 jk_simpleexec 的 Python 模块。它的目的是让运行命令更加容易。

让我简单地为您解释一下。

“在本地执行命令”的问题

我的 Python 模块 jk_simpleexec 提供了一个名为 runCmd(..) 的函数,可以在本地或远程执行 shell 命令。这非常简单。以下是本地执行命令的示例:

import jk_simpleexec

cmdResult = jk_simpleexec.runCmd(None, "cd / ; ls -la")

注意:默认情况下,返回的数据会自动裁剪,以删除STDOUT和STDERR中多余的空行。(当然,此行为可以被禁用,但是针对您的目的,正是这种行为您想要的。)

“处理结果”问题

您将收到一个包含返回代码、STDOUT和STDERR的对象。因此,处理结果非常容易。

在执行的命令可能存在并被启动,但可能无法完成其预期操作的情况下,这就是您想要做的。在最简单的情况下,如果您不关心STDOUT和STDERR,则您的代码可能看起来像这样:

cmdResult.raiseExceptionOnError("Something went wrong!", bDumpStatusOnError=True)

出于调试目的,您希望在某个时间将结果输出到 STDOUT,因此您可以这样做:

cmdResult.dump()

如果您想处理标准输出STDOUT,这同样也很简单。例如:

for line in cmdResult.stdOutLines:
    print(line)

“远程执行命令”问题

当然,我们可能希望在另一个系统上远程执行此命令。为此,我们可以使用相同的函数runCmd(..),但是需要先指定一个fabric连接对象。可以通过以下方式完成:

from fabric import Connection

REMOTE_HOST = "myhost"
REMOTE_PORT = 22
REMOTE_LOGIN = "mylogin"
REMOTE_PASSWORD = "mypwd"
c = Connection(host=REMOTE_HOST, user=REMOTE_LOGIN, port=REMOTE_PORT, connect_kwargs={"password": REMOTE_PASSWORD})

cmdResult = jk_simpleexec.runCmd(c, "cd / ; ls -la")

# ... process the result stored in cmdResult ...

c.close()

所有内容保持完全一致,但这次我们在另一台主机上运行此命令。这是有意的:我希望拥有一个统一的API,在将来决定从本地主机移动到另一台主机时,不需要修改软件。

密码输入问题

当然,还有密码问题。一些用户在上面提到了这个问题:我们可能想要请求执行此Python代码的用户输入密码。

为解决这个问题,我创建了一个自己的模块,名为jk_pwdinput,相当长一段时间以来都在使用。与普通的密码输入不同的是,jk_pwdinput会输出一些星号而不是什么都不打印。因此,对于您输入的每个密码字符,您都将看到一个星号。这样,您就更容易输入密码了。

下面是代码:

import jk_pwdinput

# ... define other 'constants' such as REMOTE_LOGIN, REMOTE_HOST ...

REMOTE_PASSWORD = jk_pwdinput.readpwd("Password for " + REMOTE_LOGIN + "@" + REMOTE_HOST + ": ")

(为了完整性:如果readpwd(..)返回None,则表示用户通过Ctrl+C取消了密码输入。在真实世界的情境中,你可能需要根据实际情况采取相应的行动。)

完整示例

以下是完整示例:

import jk_simpleexec
import jk_pwdinput
from fabric import Connection

REMOTE_HOST = "myhost"
REMOTE_PORT = 22
REMOTE_LOGIN = "mylogin"
REMOTE_PASSWORD = jk_pwdinput.readpwd("Password for " + REMOTE_LOGIN + "@" + REMOTE_HOST + ": ")
c = Connection(host=REMOTE_HOST, user=REMOTE_LOGIN, port=REMOTE_PORT, connect_kwargs={"password": REMOTE_PASSWORD})

cmdResult = jk_simpleexec.runCmd(
    c = c,
    command = "cd / ; ls -la"
)
cmdResult.raiseExceptionOnError("Something went wrong!", bDumpStatusOnError=True)

c.close()

最后的说明

我们拥有完整的解决方案:

  • 执行命令,
  • 通过同一API远程执行该命令,
  • 以安全、易用的方式输入密码创建连接。

以上代码对我来说解决了问题(希望对你也有帮助)。而且所有内容都是开源的:Fabric采用BSD-2-Clause许可,我的模块采用Apache-2许可。

使用的模块:

愉快地编程吧!;-)


1

完美运行...

import paramiko
import time

ssh = paramiko.SSHClient()
#ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('10.106.104.24', port=22, username='admin', password='')

time.sleep(5)
print('connected')
stdin, stdout, stderr = ssh.exec_command(" ")

def execute():
       stdin.write('xcommand SystemUnit Boot Action: Restart\n')
       print('success')

execute()

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