
简介这是Paramiko 1.17.1版本的Python源码包面向需要在Python代码中直接调用SSH协议完成远程服务器操作的开发者和运维人员。Paramiko底层基于cryptography库实现SSHv2协议可替代手工执行ssh命令的方式支持远程命令执行、文件传输、SFTP会话等常见自动化场景也可作为学习SSH协议实现与Python网络编程的参考素材。压缩包共179个文件主体为74个py源码文件其余包括txt说明、html文档、doctree索引、png示意图以及测试用密钥等辅助资料整体仅1.31MB安装轻量、目录结构清晰便于直接阅读源码或快速集成到已有项目中。已有323人学习下载适合具备基础Python语法能力、正在搭建自动化运维或批量服务器管理脚本的读者参考。透过源码目录可快速了解Transport、Channel、SFTP、Server等核心模块的组织方式对二次开发、定制SSH客户端或排查连接问题均有直接帮助。1. 为什么你会拿到一份 paramiko-1.17.1.tar.gz如果你手里有一份 paramiko-1.17.1.tar.gz多半是从某台离线服务器、内部软件源或者老项目的 vendor 目录里翻出来的。它是 SSH 协议在 Python 侧的客户端实现不装 OpenSSH 命令行也能远程执行命令、传文件、做批量巡检。和现在主流的 paramiko 2.x 相比这个版本依赖更传统、体积更小不少存量项目至今还锁死在它上面。你可能会觉得“老版本就是该淘汰”但对只有 Python 2.7 或 Python 3.6 的环境来说它反而是少有的能一次跑通的选择。这篇文章不做科普直接从解包安装讲到线上踩坑顺手把连接池和自动重连一起补上。2. 把 paramiko-1.17.1.tar.gz 装上三行命令与依赖核对2.1 解包前先自查环境依赖别让 pycrypto 编译卡整晚拿到 tarball 别急着解包第一步是看机器上现在有哪些加密相关库。paramiko 是 SSH 协议的高层封装真正干活的是它底下的加密后端。这个版本有两个可选后端一是历史包袱 PyCrypto二是后来官方推荐的 cryptography。两个都没有安装后 import 都会成功但你一调用 connect() 就会报 “No suitable encryption algorithm” 之类的问题很容易被误判成服务器配置有问题。# 先确认三个关键依赖是否存在缺哪个补哪个 python -c import Crypto; print(pycrypto ok) || echo 缺少 pycrypto python -c import cryptography; print(cryptography ok) || echo 缺少 cryptography python -c import ecdsa; print(ecdsa ok) || echo 缺少 ecdsa这三条命令本身不装任何东西只是探测。我一般会先把结果截下来再决定怎么装主包如果 cryptography 已经存在那就让它作为后端避免去碰 PyCrypto 的老源码编译如果两个都没有优先补 cryptography因为它在现代 Python 环境下编译成功率高很多。关于依赖的选择我踩过几次坑后形成了下表。依赖作用我遇到过的现象pycrypto老版本常用加密后端负责 RSA/DSA 加解密新版编译器上编译失败错乱报在src/MD2.c之类的地方cryptography更现代的加密后端封装 OpenSSL版本过新时和 1.17.1 的 ABI 兼容性不稳定偶发导入失败ecdsa部分密钥类型认证时需要缺了后某些密钥登不上报错却指向认证失败绕了远路依赖探测完成后再进解包安装。以下是最小安装路径tar -xzf paramiko-1.17.1.tar.gz cd paramiko-1.17.1 python setup.py install cd .. python -c import paramiko; print(paramiko.__version__)第四行是关键验证如果这里输出了 1.17.1说明导入路径和依赖都正常如果报 ImportError把报错信息里提到的模块名记下来十有八九是上面三个依赖缺了一个。2.2 用 virtualenv 装 1.17.1老库不挑新解释器得给它挑环境1.17.1 发布时主要面对 Python 2.7 和 Python 3.3 到 3.5放到现在的 Python 3.9、3.10 上虽然也能 import但 setup.py 里用到的 setuptools 接口早变了直接python setup.py install经常在“构建 pycrypto”这一步先挂掉。这不是操作问题是老代码和新构建工具之间的版本摩擦。我的习惯是先建一个独立虚拟环境强制指定解释器版本再进 tarball 目录安装。虚拟环境的好处是依赖不会污染系统 Python将来这台机器要升级体系无关痛痒最稳妥。# 以 Python 3.6 为例建一个名为 sshop 的虚拟环境 python3.6 -m venv sshop source sshop/bin/activate pip install --upgrade pip setuptools wheel然后把 tarball 拷贝到这台机器按 2.1 的顺序解包安装。别忘了先跑一遍依赖探测命令因为虚拟环境默认是干净的几乎肯定缺依赖。如果你确实只能用 Python 3.10 或更高版本我不推荐死磕这个老包常见做法是放弃 tarball直接pip install paramiko1.17.1试一次pip 会拉取更合适的 wheel 或者自动处理编译依赖再不行就升级到 2.x 系列因为 2.x 的核心 API 和 1.17.1 差别不大迁移成本能接受。提示老版本库最大的风险不在功能而在加密后端。能选 cryptography 就不要碰 PyCrypto 源码编译这是我能给你的最直接建议。3. 远程命令自动化exec_command 的连接建立与三流读写3.1 最小可用脚本连接参数逐个说清楚paramiko 的 SSHClient 是日常使用入口。绝大多数远程命令场景只需要五样东西地址、端口、用户名、认证凭据、超时设置。先给一个能直接复制的最小脚本再解释为什么参数是这么传的。import paramiko client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( hostname192.0.2.10, port22, usernamedeploy, passwordyour_password, timeout10, banner_timeout30, auth_timeout15, ) stdin, stdout, stderr client.exec_command(df -h /data) exit_status stdout.channel.recv_exit_status() print(stdout.read().decode(utf-8, errorsreplace)) print(stderr.read().decode(utf-8, errorsreplace)) print(exit:, exit_status) finally: client.close()这段脚本里有两个容易被忽略的点。第一set_missing_host_key_policy(paramiko.AutoAddPolicy())的含义是“遇到未知主机密钥时自动接受”。在 1.17.1 里不设置这个第一次连接某台新机器会直接提示 host key 未确认哪怕你密码完全正确也连不上。对做自动化任务来说这个策略能省掉 known_hosts 管理的麻烦但代价是不防中间人所以我只在内部网络或测试环境用。第二banner_timeout和auth_timeout是分开的。banner_timeout控制 TCP 建连后等待 SSH 协议 banner 的时间auth_timeout控制认证阶段整体耗时。老版 paramiko 默认值偏小跨机房操作时经常触发误报我会显式把这两个值传到 10 秒以上。关于命令执行方式exec_command 是“单次执行、拿退出码”的模型适合跑固定脚本而 invoke_shell 是伪终端交互模型输出乱、状态多不适合自动化。我一般只在调试交互式程序时才用 invoke_shell平时一律 exec_command。3.2 输出读取顺序一次把 stdout/stderr 读完别等 channel 卡死3.1 的脚本对“输出量小于几百 KB”的命令没问题但一旦命令输出超过通道缓冲区你就会看到程序像死了一样卡住。这个坑在 1.17.1 里非常典型原因是 SSH channel 的接收缓冲是有限制容量的stdout 和 stderr 是两个独立流如果你先读 stdout 等到 EOF而 stderr 那边已经攒满了远程进程写不进 stderr整条通道就互相等了。我现在的习惯是只要不确定输出量就用循环把两个流交替抽干而不是直接调 read() 读到底。参考代码如下import time import paramiko client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(192.0.2.10, port22, usernamedeploy, passwordyour_password, timeout10) _, stdout, stderr client.exec_command(find /opt -name *.log 2/dev/null) chan stdout.channel chan.settimeout(0.1) out_chunks [] err_chunks [] while not chan.exit_status_ready() or chan.recv_ready() or chan.recv_stderr_ready(): if chan.recv_ready(): out_chunks.append(chan.recv(4096)) if chan.recv_stderr_ready(): err_chunks.append(chan.recv_stderr(4096)) time.sleep(0.01) exit_code chan.recv_exit_status() print(b.join(out_chunks).decode(utf-8, errorsreplace)) print(b.join(err_chunks).decode(utf-8, errorsreplace)) print(exit:, exit_code)循环的退出条件必须写全等退出状态就绪并且两个接收缓冲区都空了才停止。time.sleep(0.01)是给 CPU 让路如果命令执行时间短可以调成 0.05如果远程脚本要跑几分钟这个空转完全无所谓真正影响体验的是你一直不读数据导致的阻塞。如果远程命令的输出量就是不大你也可以用更省事的方式在命令末尾加21把 stderr 合并到 stdout然后用一次 read() 读全。这个办法的缺点是你无法区分正常输出和错误输出只适合看执行结果的场景。3.3 带 sudo 的命令get_pty 与 stdin 输入绕过交互自动化脚本里跑sudo是最容易翻车的场景因为 sudo 会去读当前终端的密码提示。不带终端执行时它可能直接报 “sudo: no tty present and no askpass program specified”或者卡在密码输入上。paramiko 的做法是让 exec_command 开一个伪终端并把密码写入 stdin。关键在于两个点一是get_ptyTrue必须带二是 sudo 要加-S参数让 sudo 强制从标准输入读密码。import paramiko client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(192.0.2.10, port22, usernamedeploy, passwordyour_password, timeout10) stdin, stdout, stderr client.exec_command(sudo -S systemctl restart nginx, get_ptyTrue) stdin.write(your_password\n) stdin.flush() out stdout.read().decode(utf-8, errorsreplace) err stderr.read().decode(utf-8, errorsreplace) print(stdout:, out) print(stderr:, err)写入密码后必须flush()否则数据可能还留在 Python 缓冲区sudo 一直等不到输入。另一个坑是如果密码输错一次sudo 会在同一次会话里再问一次而你的脚本只写了一次密码进程又会卡住。常见做法是在写入密码后再等一小段用stdin.channel.send(your_password\n)补发一次或者干脆把要执行的权限收敛到专用用户不依赖 sudo。我自己的经验分界线是一条命令带 sudo 可以这么处理一次脚本里十个命令都带 sudo别再硬刚直接在服务器上给部署用户配好 sudoers 免密或者用远程调度工具去解决权限模型代码会干净很多。4. SFTP 文件传输把文件传上服务器的完整动作4.1 上传下载的最小代码与目录准备SFTP 是 paramiko 里第二个高频入口用途是传文件。很多人直接在 exec_command 里拼scp但那样还得依赖远程机器的 scp 配置SFTP 只要 SSH 服务正常就能用。先看一个最小完整脚本覆盖“上传、下载、建目录、关闭”四个动作import paramiko client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(192.0.2.10, port22, usernamedeploy, passwordyour_password, timeout10) sftp client.open_sftp() # 远端目录不存在时先手动建避免 put 直接报错 try: sftp.stat(/data/backup) except IOError: sftp.mkdir(/data/backup) sftp.put(local_app.tar.gz, /data/backup/local_app.tar.gz) sftp.get(/data/backup/local_app.tar.gz, ./downloaded_app.tar.gz) sftp.close() client.close()open_sftp()返回的 SFTPClient 对象底层用的是同一条 SSH 连接所以不要先 close 连接再调 sftp 方法顺序必须在连接存活期间完成。mkdir只负责建一层目录如果父目录/data也没有会抛 IOError。常见做法是分段建立先 stat/data没有就 mkdir/data再处理/data/backup。对目录层级深的场景可以写成循环逐级 stat 和 mkdir比指望一次调用完成更可靠。还有一个用得上的细节put 和 get 默认是覆盖式写入不会做校验。要确认落盘文件是否正确最土的方法是在传完后用 exec_command 跑一次 md5sum 对比不过这多一次往返适合对小文件做关键校验。4.2 大文件传输进度回调与断点续传的落实方法传大文件时最怕两件事一是看不到进度像死机二是传一半断了要重头再来。paramiko 的 SFTP 接口天然支持进度回调这是 1.17.1 就有的能力不用额外打补丁。def progress(transferred, total): percent transferred * 100.0 / total print(fprogress: {percent:.1f}% ({transferred}/{total})) sftp.put(/home/user/big_site.tar, /data/backup/big_site.tar, callbackprogress)callback 的两个参数是已传输字节数和总字节数。注意这个回调是在主线程里同步触发的也就是说它会被阻塞在网络 I/O 上不要在回调里做太重的事比如再写日志、再调接口否则会拖慢传输本身。我一般只在回调里打印一行百分比多了不干。断点续传的思路和 HTTP 类似先读远端文件大小和本地对比跳过已传部分只追传剩余部分。下面是一个简化版本适合单文件续传SOURCE /home/user/big_site.tar DEST /data/backup/big_site.tar local_size os.path.getsize(SOURCE) try: remote_size sftp.stat(DEST).st_size except IOError: remote_size 0 offset remote_size if remote_size local_size else 0 with open(SOURCE, rb) as local_file: remote_file sftp.open(DEST, ab) local_file.seek(offset) while True: chunk local_file.read(65536) if not chunk: break remote_file.write(chunk) remote_file.close()这个方案能工作但有两个边界要注意一是远端文件比本地文件大时把 offset 归零重新全量传别用本地大小去覆盖一个更大的文件二是每次 write 都是一次网络往返chunk 太小会慢64 KB 是个不错的起点对百兆以上文件可以再调大到 128 KB 或 256 KB。如果你经常要传一堆碎文件别一个个循环 put。压缩成一个 tar 再传传输次数从几百次降成一次出错概率也低很多。传完再在远端解包这是运维里很成熟的做法。提示SFTP 的 put 和 get 都不保证断点续传的原子性传完以后最好用 stat 再比对一次文件大小对重要文件是值得的。5. paramiko 1.17.1 的常见踩坑与排查思路5.1 报错 Error reading SSH protocol banner先调时间再看中间链路现象连接一台跨机房的服务器时偶尔抛Error reading SSH protocol banner重试几次又成功给人感觉像玄学。原因paramiko 在 TCP 建连后会等待服务器发来 SSH banner 文本这个等待是有时限的。机房远、服务器慢、或者经过负载均衡转发时banner 晚到几秒老版本默认值可能兜不住。解决把 connect 参数里的banner_timeout调大。我一般直接给 30 秒同时把timeout保持为 10 秒这两个参数不冲突前者管协议 banner后者管整条 TCP 通道。调到 30 秒后这个错误基本绝迹。如果调大后仍然偶发就要检查中间链路是不是有设备在做 TCP 代理或连接复用这和 paramiko 无关属于网络架构问题只能从链路侧解决。5.2 SSH 命令行能连代码里却 Authentication failed现象在终端里ssh userhost秒进换成 paramiko 用同一账户、同一密码登录却报Authentication failed而且日志完全看不出哪一步断了。原因一paramiko 1.17.1 不会自动读取~/.ssh/config你在命令行里配好的别名、端口、跳板机设置代码里全部无效。必须显式把 hostname、port、username 传给 connect。原因二如果你没有传key_filenameparamiko 会尝试默认密钥再用密码认证顺序不对时可能把密码验证机会耗光。常见做法是只传密码password或者只传密钥key_filename不要混着传。原因三新版本 OpenSSH 生成的私钥默认格式是OPENSSH PRIVATE KEYparamiko 1.17.1 对这类新格式支持有限会直接判定密钥非法。先转回 PEM 格式再传。# 把 OpenSSH 新格式私钥转成 PEM 格式 ssh-keygen -p -m PEM -f /path/to/id_ed25519执行后会提示输入旧密码然后要求输入新密码。如果你原本没有口令直接回车就行如果原本有口令输入原口令后再回车可以保留原口令。转换后文件内容还是那个密钥只是格式变成了老版 paramiko 能认的样子。5.3 exec_command 之后 read() 卡住是管道缓冲不是死机现象执行一个命令比如find / -name core输出量很大代码停在stdout.read()不返回远程命令看起来完成了程序却干等。原因SSH channel 的接收缓冲区有容量上限stdout 和 stderr 是独立通道。你先读 stdout等它 EOF可是 stderr 或 stdout 的另一部分堆积超过缓冲区上限远程进程写不动整个通道就互相卡死。解决按第 3.2 节的循环方式交替读取 stdout 和 stderr直到退出状态就绪且两个缓冲区都空。如果不在乎区分两路输出就在远程命令末尾加21让它变成单流一次 read() 读到底。这个问题的本质不是死锁而是阻塞所以看到卡住先不要 kill 进程加一层循环读取往往就能救回来。5.4 新环境编译依赖时pycrypto 源码构建翻车现象全新 Python 3.6 虚拟环境里按 2.1 的流程装依赖pip install pycrypto在编译阶段报错错误信息指向某种 C 源码文件看起来完全没法修。原因pycrypto 停更多年新版 GCC 对旧 C 代码的检查更严格加上缺失某些编译头文件源码构建很容易挂。不是你的机器坏了是工具链太新。解决最省事的路径是绕过 pycrypto直接装 cryptography并让 paramiko 使用 cryptography 后端。老版本 paramiko 在 import 时会优先探测后端只要 cryptography 存在且能被正确识别就不会去真找 pycrypto。如果因为项目规定必须用 pycrypto我的备选方案是找一个较老的 Python 环境或者用预编译 wheel源码编译放在最后再试。血泪经验告诉我不要在构建这一步死磕超过半小时直接换后端更划算。6. 批量任务前先给 SSH 连接加一层池与自动重连批量运维的常见形态是一个循环里连几十台机器执行同样的命令。如果每台机器都重新建连接三次握手加大密钥交换加在一起的开销非常可观。而且 paramiko 的连接对象不是线程安全的多线程并发时不能共享同一个实例否则运气不好就会出现“偶发断连”的怪问题。我的做法是做一个轻量连接池启动时建立 N 条连接放队列线程需要时借一条用完归还借出前顺手执行一个无副作用的命令探测连接是否还活着死了就自动重建。下面是一个落到代码的简化版import queue import paramiko class SSHConnPool: def __init__(self, host, username, password, n4): self.host host self.username username self.password password self._idle queue.Queue() for _ in range(n): self._idle.put(self._new()) def _new(self): c paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(self.host, port22, usernameself.username, passwordself.password, timeout8, banner_timeout20) return c def borrow(self): c self._idle.get() try: _, out, _ c.exec_command(echo ok, timeout3) out.read() return c except Exception: try: c.close() except Exception: pass return self._new() def give(self, c): self._idle.put(c)用法是初始化时传机器地址、账号密码和连接数每个线程取一条连接执行命令执行完放回去。借出的探测命令只做一次echo ok成本很低换来的是脚本在远程服务重启后仍能继续跑不至于整个任务在第三台机器就中断。这个方案也有边界如果一台机器本身负载很高echo ok也可能超时被误判成连接断开而重建连接。我通常会把探测超时放宽到 5 秒并且只在执行批量任务前启用不用于高频小命令。我现在养成的习惯是任何批量脚本第一版就把连接池和重连写进去而不是等跑到一半报错再补。前者只是多十几行代码后者要面对一堆机器状态不一致的烂摊子。把连接生命周期管好远程脚本的上限就从“能跑通”变成了“能长时间跑”。希望帮到你。本文还有配套的精品资源点击获取