You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Python自动化SSH级联lxc-attach操作及密码应答

问题

如何实现下述级联操作流程的自动化,完成所有密码提示的自动应答?
目标流程:

Machine 1> ssh user2@machine2
   password:
Machine 2> lxc-attach -n 0x1000
Container> ssh user3@machine3
   password:
Machine 3> get_temperature.sh
   temperature is 65C
背景说明
  • 自动化脚本运行在Machine 1上,该主机是链路中唯一具备互联网访问能力、运行状态稳定的节点,可任意安装所需软件,整体自动化逻辑基于Python实现。
  • 后续级联的测试系统无互联网连接,会意外关机且内存被频繁擦除,所有手动修改的配置、上传的SSH密钥都无法长期留存。当前场景仅用于硬件测试,无权修改系统重置后的初始状态。
  • 级联系统架构图:
    级联系统架构图
已尝试方案及问题

paramiko方案

目前可成功连接Machine 2,但无法在Machine 2的容器内发起对Machine 3的SSH连接时自动应答密码提示,已尝试代码如下:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

ssh.connect('192.168.2.2', port=22, username='user2', password='pass2')
print("connect to machine 2")

channel = ssh.invoke_shell()
channel_data = ''
go = 1

while True: 
    if channel.recv_ready():
        channel_data = str(channel.recv(9999))
        print('Channel Data: ', channel_data)
    else:
        continue

    if channel_data.endswith("~ >'"):
        if go == 1:
            channel.send('lxc-attach -n 0x1000')
            channel.send('\n')
            go += 1
        else: 
            channel.send('ssh user3@192.168.3.2')
            print("sending ssh command")
    elif 'assword:' in channel_data:
        channel.send('pass3')
        channel.send('\n')
        print("put the pass")
        break

stdin, stdout, stderr = ssh.exec_command('pwd')
print(stdout)

fabric方案

配置gateway参数尝试连接Machine 3或Machine 4时,会抛出错误ChannelException(2, 'Connect failed');如果先执行lxc-attach命令,程序会永久挂起,已尝试代码如下:

import fabric

machine2 = fabric.Connection('192.168.2.2', user='user2', connect_kwargs={"password": "pass2"})
result = machine2.run('hostname')
print('machine2 hostname', result)
result = machine2.run('lxc-attach -n 0x1000')
print('machine2 lxc ', result)

machine3 = fabric.Connection('192.168.3.2', user='user3', gateway=machine2, connect_kwargs={"password": "pass3"})
machine4 = fabric.Connection('192.168.4.2', user='user4', gateway=machine3, connect_kwargs={"password": "pass4"})

result = machine3.run('hostname')
print('machine3 hostname', result)
现存卡点
  • 同类自动化密码登录问题的通用解决方案大多建议配置SSH密钥实现免密登录,完全不适用当前节点重置后配置无法留存的场景。
  • 常规SSH跳板机方案调试失败,核心卡点是lxc-attach启动的是原始shell,无法正常回传交互通道,不满足标准跳板机的端口转发要求。
  • Windows环境下pexpect运行存在兼容问题,调用接口时抛出module 'pexpect' has no attribute 'popen_spawn'错误,无法通过该库实现交互式shell模拟。
  • 核心诉求:获取和原生交互式shell行为完全一致的shell通道,完成全链路密码自动应答。

可行落地方案

方案1:修复paramiko交互逻辑,全流程在单PTY通道内执行

之前写的paramiko代码无法正常应答容器内SSH密码,核心原因有三点:

  1. 调用invoke_shell后没有申请伪终端(PTY),SSH客户端默认检测到连接非终端环境时,不会输出交互式密码输入提示,自然匹配不到assword:关键字
  2. 通道数据读取逻辑有缺陷:仅单次读取就绪数据、每次读取覆盖历史缓冲区,且通过固定字符串后缀判断shell提示符,很容易被登录欢迎语、终端控制字符、换行符干扰导致匹配失败
  3. 流程最后调用exec_command会新开独立的非交互通道,和之前invoke_shell打开的会话完全隔离,拿不到前序步骤进入容器、跳转SSH的会话状态

修复后的实现逻辑:

  • 建立Machine2的SSH连接后,第一时间为shell通道申请PTY,保证后续所有交互式命令的密码提示都能正常输出
  • 维护滚动的接收缓冲区,循环读取通道数据,不要每次读取就覆盖历史内容
  • 所有命令发送、输出读取都在同一个shell通道内完成,不要混用exec_command新开通道
  • 简化shell提示符匹配规则,不要写死固定后缀,通过通用的行尾提示符(# 、$ )判断会话就绪状态

核心修复代码示例:

import paramiko
import time

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.2.2', port=22, username='user2', password='pass2', timeout=10)
# 申请带规格的伪终端,保证交互式提示正常输出
channel = ssh.invoke_shell(width=200, height=50)
channel.get_pty()
buffer = ""

def read_channel():
    global buffer
    while channel.recv_ready():
        data = channel.recv(1024).decode('utf-8', errors='ignore')
        buffer += data
        print(data, end="")
    time.sleep(0.2)
    while channel.recv_ready():
        data = channel.recv(1024).decode('utf-8', errors='ignore')
        buffer += data
        print(data, end="")

# 等待Machine2 shell就绪,进入容器
read_channel()
channel.send('lxc-attach -n 0x1000\n')
buffer = ""
# 等待容器shell就绪,发起Machine3 SSH连接
while True:
    read_channel()
    if buffer.rstrip().endswith('# ') or buffer.rstrip().endswith('$ '):
        channel.send('ssh user3@192.168.3.2\n')
        break
buffer = ""
# 匹配密码提示,发送密码
while True:
    read_channel()
    if 'assword:' in buffer:
        channel.send('pass3\n')
        break
buffer = ""
# 等待Machine3 shell就绪,执行测温脚本
while True:
    read_channel()
    if buffer.rstrip().endswith('# ') or buffer.rstrip().endswith('$ '):
        channel.send('get_temperature.sh\n')
        break
buffer = ""
# 读取脚本输出结果
while True:
    read_channel()
    if 'temperature is' in buffer:
        break

注意:shell提示符的匹配规则可根据实际环境调整,避免因为环境差异导致匹配失败。

方案2:包装原生SSH代理跳转,绕开Python层交互处理

这个方案稳定性远高于手动写shell匹配逻辑,不需要自己处理终端转义字符、提示符匹配问题,适合长期跑自动化测试场景:

  1. 确认Machine1的Windows系统已安装OpenSSH客户端(Win10 1809及以上版本默认自带,低版本可手动安装),再安装Windows移植版sshpass工具
  2. 编辑当前用户的SSH配置文件(路径为C:\Users\当前用户名\.ssh\config),把lxc-attach进入容器的步骤包装成自定义代理跳转规则,让SSH原生处理通道转发
    配置内容示例:
Host machine3
    HostName 192.168.3.2
    User user3
    ProxyCommand ssh user2@192.168.2.2 "lxc-attach -n 0x1000 -n -- ssh -W %h:%p user3@192.168.3.2"
  1. 直接调用sshpass填充密码,一条命令即可完成全链路跳转和脚本执行:
sshpass -p pass3 ssh machine3 "get_temperature.sh"

如果需要级联跳转更多层机器,只要按照相同逻辑在ProxyCommand里叠加跳转规则即可,不需要修改Python层逻辑。如果要和Python代码集成,直接用subprocess调用上述命令就能拿到脚本输出。

之前fabric方案报错的原因

fabric的gateway参数仅支持标准SSH协议的跳板跳转,要求中间跳板节点可以直接响应SSH端口转发请求。链路中存在lxc-attach进入容器的非SSH跳转步骤,不符合fabric网关的实现逻辑,自然会抛出连接失败错误。如果要使用fabric,也可以参考方案2的思路,把lxc-attach的跳转规则写到系统SSH配置里,fabric会自动读取配置完成连接,不需要手动指定gateway参数。


内容的提问来源于stack exchange,提问作者Alphy13

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:57:11