使用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密码,核心原因有三点:
- 调用invoke_shell后没有申请伪终端(PTY),SSH客户端默认检测到连接非终端环境时,不会输出交互式密码输入提示,自然匹配不到
assword:关键字 - 通道数据读取逻辑有缺陷:仅单次读取就绪数据、每次读取覆盖历史缓冲区,且通过固定字符串后缀判断shell提示符,很容易被登录欢迎语、终端控制字符、换行符干扰导致匹配失败
- 流程最后调用
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匹配逻辑,不需要自己处理终端转义字符、提示符匹配问题,适合长期跑自动化测试场景:
- 确认Machine1的Windows系统已安装OpenSSH客户端(Win10 1809及以上版本默认自带,低版本可手动安装),再安装Windows移植版sshpass工具
- 编辑当前用户的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"
- 直接调用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

