杰瑞科技汇

Python Paramiko交互如何实现远程命令执行?

paramiko 是一个强大的 Python 库,用于实现 SSHv2 协议,它不仅可以让你执行远程命令,还可以进行文件传输(SFTP)和建立 SSH 隧道。

Python Paramiko交互如何实现远程命令执行?-图1
(图片来源网络,侵删)

“交互”这个词在 SSH 场景下通常有两种含义:

  1. 执行单条命令并获取其输出:这是最常见的用法,也是自动化运维的基础,你发送一个命令(如 ls -l),然后读取并打印服务器返回的结果。
  2. 模拟一个真实的终端会话:这更复杂,用于需要持续交互的场景,比如登录路由器、进入 sudo 密码提示、使用 top 命令或运行交互式脚本(如 vimbash)。

下面我们将分别详细讲解这两种交互方式,并提供完整的代码示例。


环境准备

确保你已经安装了 paramiko 库,如果没有,可以通过 pip 安装:

pip install paramiko

执行单条命令并获取输出(最常用)

这是最直接、最常用的方式,它通过 SSHClient 对象的 exec_command() 方法实现,这个方法会返回三个对象:

Python Paramiko交互如何实现远程命令执行?-图2
(图片来源网络,侵删)
  1. stdin: 标准输入流,你可以向它写入数据(在 sudo 时输入密码)。
  2. stdout: 标准输出流,你需要从这里读取命令的返回结果。
  3. stderr: 标准错误流,如果命令执行失败,错误信息会在这里。

基本示例

import paramiko
# --- 1. 创建 SSHClient 对象 ---
ssh = paramiko.SSHClient()
# --- 2. 自动添加主机密钥 (不推荐用于生产环境) ---
# 第一次连接时,会提示你是否信任服务器的 host key。
# 为了方便测试,我们可以自动添加。
# 生产环境中,应该使用 ssh.get_host_keys().add() 来手动添加。
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
    # --- 3. 连接服务器 ---
    # 请替换为你的服务器信息
    hostname = "your_server_ip"
    port = 22
    username = "your_username"
    password = "your_password" # 或者使用密钥认证
    print(f"正在连接到 {hostname}...")
    ssh.connect(hostname, port=port, username=username, password=password)
    print("连接成功!")
    # --- 4. 执行命令 ---
    command = "ls -l /tmp"
    print(f"正在执行命令: '{command}'")
    # exec_command 会返回三个流
    stdin, stdout, stderr = ssh.exec_command(command)
    # --- 5. 获取输出 ---
    # stdout.read() 会读取所有输出,并解码为字符串
    output = stdout.read().decode('utf-8')
    error = stderr.read().decode('utf-8')
    if error:
        print(f"命令执行出错:\n{error}")
    else:
        print("命令执行成功,输出如下:")
        print(output)
except paramiko.AuthenticationException:
    print("认证失败,请检查用户名和密码。")
except paramiko.SSHException as e:
    print(f"SSH 连接或命令执行出错: {e}")
except Exception as e:
    print(f"发生未知错误: {e}")
finally:
    # --- 6. 关闭连接 ---
    if ssh:
        ssh.close()
        print("连接已关闭。")

关键点说明

  • ssh.set_missing_host_key_policy(): 这是处理“未知主机密钥”问题的关键。AutoAddPolicy 会自动接收并保存服务器的公钥,这会带来“中间人攻击”的风险,在生产环境中,你应该预先知道服务器的公钥指纹,并使用 ssh.get_host_keys().add() 来添加它。
  • 认证方式: 除了密码,paramiko 还支持更安全的密钥对认证。
    # 使用密钥对认证
    private_key_path = '/path/to/your/private_key'
    key = paramiko.RSAKey.from_private_key_file(private_key_path)
    ssh.connect(hostname, username=username, pkey=key)
  • stdin, stdout, stderr: 这三个流必须在 with 语句中或手动关闭,以释放资源。exec_command 在所有流关闭后才会返回,上面的例子中,read() 方法会一直等待直到流关闭,所以无需手动关闭,但养成良好的习惯总是好的。

模拟真实终端交互(高级用法)

当你需要执行一个需要持续输入输出的命令时(sudo suhtop 或一个交互式安装脚本),exec_command 就无能为力了,这时,你需要使用 paramiko.Channel 来建立一个持久的交互式会话。

这种方式的核心是:

  1. 创建一个 Channel
  2. 在一个循环中,持续地从 stdout 读取服务器的输出。
  3. 根据输出内容,判断下一步该发送什么命令或输入到 stdin
  4. 处理超时,避免程序无限等待。

交互式登录示例(sudo

这个例子模拟了输入 sudo 命令后,等待密码提示,然后输入密码的过程。

import paramiko
import time
import select
def interactive_shell(hostname, username, password, sudo_password=None):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        print(f"正在连接到 {hostname}...")
        ssh.connect(hostname, port=22, username=username, password=password)
        print("连接成功!")
        # --- 1. 创建一个交互式 Channel ---
        channel = ssh.invoke_shell()
        print("已进入交互式 Shell。")
        # 设置一个超时,避免无限等待
        # 等待5秒内没有输出就认为当前命令执行完毕
        timeout = 5
        # --- 2. 交互式循环 ---
        while True:
            # 使用 select 来检查 channel 是否有数据可读
            # 这比直接 channel.recv() 更高效,可以避免阻塞
            if channel.recv_ready():
                # 读取并打印服务器的输出
                output = channel.recv(4096).decode('utf-8')
                print(output, end='')
                # 如果输出中包含密码提示,就输入密码
                if sudo_password and "password for" in output.lower():
                    print("检测到 sudo 密码提示,正在输入密码...")
                    channel.send(sudo_password + '\n')
                    # 输入密码后,清空 sudo_password 变量,避免重复输入
                    sudo_password = None
            # 检查 channel 是否关闭
            if channel.exit_status_ready():
                print("\n服务器已关闭连接。")
                break
            # 短暂休眠,避免 CPU 占用过高
            time.sleep(0.1)
    except paramiko.AuthenticationException:
        print("认证失败,请检查用户名和密码。")
    except paramiko.SSHException as e:
        print(f"SSH 连接或命令执行出错: {e}")
    except Exception as e:
        print(f"发生未知错误: {e}")
    finally:
        if ssh:
            ssh.close()
            print("连接已关闭。")
# --- 使用示例 ---
if __name__ == "__main__":
    # 请替换为你的信息
    HOSTNAME = "your_server_ip"
    USERNAME = "your_username"
    PASSWORD = "your_password"
    SUDO_PASSWORD = "your_sudo_password" # 如果不需要 sudo,可以设为 None
    interactive_shell(HOSTNAME, USERNAME, PASSWORD, SUDO_PASSWORD)

交互式 Shell 示例 (top)

这个例子展示了如何启动一个持续输出的命令,并在一段时间后终止它。

import paramiko
import time
def run_interactive_command(hostname, username, password, command, duration=10):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        ssh.connect(hostname, port=22, username=username, password=password)
        channel = ssh.invoke_shell()
        print(f"正在执行命令: '{command}' 并运行 {duration} 秒...")
        # 发送命令
        channel.send(command + '\n')
        start_time = time.time()
        while True:
            # 检查是否超时
            if time.time() - start_time > duration:
                print("\n时间到,正在终止命令...")
                # 发送 Ctrl+C 来终止命令
                channel.send('\x03') # \x03 是 EOT (End of Transmission) 字符,通常代表 Ctrl+C
                break
            # 读取并打印输出
            if channel.recv_ready():
                output = channel.recv(1024).decode('utf-8')
                print(output, end='')
            time.sleep(0.1)
        # 等待一会儿,确保命令被终止
        time.sleep(2)
        print("\n命令执行结束。")
    except Exception as e:
        print(f"发生错误: {e}")
    finally:
        if ssh:
            ssh.close()
            print("连接已关闭。")
# --- 使用示例 ---
if __name__ == "__main__":
    HOSTNAME = "your_server_ip"
    USERNAME = "your_username"
    PASSWORD = "your_password"
    # 运行 top 命令 5 秒
    run_interactive_command(HOSTNAME, USERNAME, PASSWORD, "top", duration=5)

总结与最佳实践

特性 exec_command() invoke_shell()
用途 执行简单的、一次性的命令 模拟真实终端,用于持续交互
返回值 三个独立的流 (stdin, stdout, stderr) 一个 Channel 对象,通过 send()recv() 交互
复杂度 简单,易于使用 复杂,需要处理循环、超时和输出解析
适用场景 文件操作 (ls, cp)、软件安装 (yum install)、系统信息查询 (df -h) sudosutophtopvim、交互式脚本
资源管理 命令执行完毕后自动关闭流 需要手动管理 Channel 的生命周期

最佳实践建议:

  1. 优先使用 exec_command():对于绝大多数自动化任务,exec_command() 是更简单、更安全、更高效的选择。
  2. 仅在必要时使用 invoke_shell():只有当你明确需要模拟一个真实用户坐在终端前操作的场景时,才使用 invoke_shell()
  3. 处理异常和资源:始终使用 try...finallywith 语句来确保 SSHClientChannel 被正确关闭,防止资源泄露。
  4. 考虑安全性:避免在代码中硬编码密码,优先使用 SSH 密钥对进行认证,如果必须使用密码,可以考虑使用环境变量或配置文件来存储。
  5. 处理输出编码:服务器返回的输出编码可能是 utf-8,也可能是其他编码(如 gbk),使用 .decode('utf-8', errors='ignore') 可以避免因编码问题导致的程序崩溃。
分享:
扫描分享到社交APP
上一篇
下一篇