ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Paramiko实战:从SSH自动化到RPM打包全攻略

Paramiko实战:从SSH自动化到RPM打包全攻略 做运维自动化这几年但凡接到“写个脚本批量连服务器执行命令”这类需求我第一反应基本就是 Python 加 Paramiko。这个库在 Python 生态里的地位很稳它把 SSH 协议封装成了干净的 Python API几行代码就能完成远程登录、跑命令、传文件。但“会用”和“用好”是两回事——我见过不少同事在项目里直接 pip install paramiko 就开始调函数等到真要部署到内网服务器、打成 RPM 包、或者在生产环境遇到连接卡死、乱码、认证失败时才发现自己对这个库的了解只是冰山一角。这篇文章就把我实际项目中积累的东西一次讲清楚覆盖标题里的四个方向Paramiko 的核心使用场景、底层实现原理、从源码编译 RPM 的完整流程以及我踩过的一些坑。适合的人群很明确写自动化脚本的 Python 工程师、做批量运维平台的开发、以及那些刚接手遗留系统、需要离线安装 Python 库的运维同学。1. 为什么拿 Python 连 SSH 会绕不开 Paramiko1.1 它到底解决了什么问题先想想没有 Paramiko 时你要在脚本里远程操作服务器有多别扭。最原始的做法是调系统命令比如os.system(ssh userhost ls -l)。这会有几个硬伤密码没法自动传要么提前配免密钥匙要么用 expect 这种工具去模拟交互输出结果不好结构化处理你拿到的是一段混合了 stderr 和 stdout 的字符串异常处理基本靠字符串匹配比如“是不是报错就看有没有Permission denied”写起来又丑又慢。更麻烦的是文件传输想从一台机器拉个文件回来你得先scp到本地要么再写个 watcher 去盯目录链路一长就非常脆弱。Paramiko 做的事情是把这一切收进一个 Python 包里。你用一个SSHClient对象建立连接然后调用exec_command()执行远程命令用open_sftp()做文件传输。密码、密钥、端口、超时这些参数全部可以由代码控制返回值也是一个干净的字节流你可以直接decode()之后做日志、正则、解析 JSON 之类的后续操作。一句话总结它是 Python 世界里离原生 SSH 能力最近的库。1.2 真实项目里的典型场景Paramiko 最常见的场景是批量化运维。比如你有 20 台服务器需要检查磁盘水位、看看 Nginx 进程是否活着、收集一下最近登录记录。手工一台台连很蠢用 Ansible 又有点杀鸡用牛刀这时候一段三四十行的 Paramiko 脚本就能把结果汇总成表格。第二个高频场景是嵌入式应用里的远程通道。不是所有产品都适合装 Ansible 那么重的依赖但很多 Web 后台需要“在线终端”或者“远程执行”功能后端用 Paramiko 起一个 SSH 连接再通过 WebSocket 转发到前端交互体验和 Xshell 差不多。我做过的堡垒机/跳板机类项目里Paramiko 就是底层的连接引擎之一。第三个场景是网络设备管理。很多交换机、防火墙、路由器虽然叫“网络设备”但都支持 SSH 登录而且它们的命令行风格和 Linux 不完全一样用 Paramiko 的交互式 shell 模式去驱动比较顺手。比如登录华为设备后发system-view再把配置粘贴进去这套流程用invoke_shell()比用exec_command()稳定得多。文件收发同样非常常用定时从数据库服务器拉取备份或者向多台应用服务器分发配置文件。SFTP 通道就是 Paramiko 的SFTPClient提供的不需要再额外装scp命令而且全程可以编程控制目录、权限、异常重试。1.3 同类工具对比为什么很多时候还是选它方案优势劣势适合场景直接调 ssh/scp 命令依赖少系统自带交互、异常、结构化管理都差一次性手动操作expect/pexpect能模拟完整交互写得像“说相声”分支多时难维护旧的网络设备登录流程Fabric基于 Paramiko封装度高想精细控制底层连接时反而受限应用部署、简单任务链Ansible有 Playbook、有模块生态依赖较重二次开发嵌入平台成本高大批量配置管理Paramiko轻量、可控、直接对应 SSH 原始能力需要自己处理细节没有现成任务系统嵌入后端系统、定制自动化脚本我自己在项目里的选择标准很简单如果是纯批量部署走 Ansible如果是别人要我集成进他的管理平台或者做一个只有几十个功能的自动化小工具直接用 Paramiko。它不替你解决业务流程问题但给你提供了最稳定的 SSH/SFTP 底层操作能力这恰恰是多数自定义场景最需要的部分。2. 实现原理一次 SSH 连接的台前幕后2.1 SSH 握手过程拆解很多人用 Paramiko 就是这么调用的import paramiko ssh paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(192.168.1.10, usernameroot, password123456) stdin, stdout, stderr ssh.exec_command(uptime) print(stdout.read().decode()) ssh.close()这段代码背后SSH 连接建立其实经历了几个必要阶段。先是 TCP 层握手连上服务器的 22 端口。然后双方交换 SSH 版本字符串类似SSH-2.0-OpenSSH_8.7这一步决定了双方都能支持哪些协议能力。接下来是密钥交换常见算法包括 Diffie-Hellman 组交换、椭圆曲线算法如 Curve25519这步会协商出后续对称加密用的会话密钥。同时客户端需要拿到服务器的主机密钥用来验证“我连的确实是目标机器”防止中间人攻击。最后才是用户认证走密码、公钥或 keyboard-interactive 等方式。Paramiko 把这一整套过程封装在Transport层里。你可以浅显地理解成Transport是“一条已经建立好安全加密隧道”的底层通道凡是走SSHClient的方法最后都是通过这条隧道来收发二进制数据。隧道内部的数据是完整加密的外人抓包也看不到明文命令。2.2 Paramiko 的四个核心对象要理解和排查问题这几个类必须分清。SSHClient面向使用者的门面对象。你调用它的connect()、exec_command()、open_sftp()都是在对它操作。它内部管理着连接生命周期和传输通道。Transport真正执行 SSH 协议握手、维护加密隧道的对象。SSHClient会在connect()时创建一个Transport实例你可以通过ssh.get_transport()拿到它某些高级操作比如直接开新 channel 就得和它打交道。Channel基于已建立的 Transport 创建的独立通信管道。每执行一条命令Paramiko 都会开启一个新的 Channelstdin/stdout/stderr 都绑定在这个 Channel 上。SFTPClientSFTP 子系统的客户端。ssh.open_sftp()返回这个对象它专门负责文件操作底层走的也是 SSH 隧道只是协议不同。这个分层的意义在于连接是长生命周期的命令是短生命周期的。你一次connect()之后可以执行几十条命令每条命令都是一个 Channel但底层只有一条加密隧道。理解了这一点你就不会傻傻地为每条命令重复建立连接了。2.3 exec_command 为什么是“非交互”的刚开始用 Paramiko 的同学经常碰到一个困惑我执行cd /home ls是成功的但为什么执行完cd /home再执行pwd路径还是老样子还有人会发现source /etc/profile找不到环境变量或者输入top命令时输出很奇怪。原因在于exec_command()默认不申请伪终端PTY它只是打开一个 Channel 去执行一条单一命令。没有 PTY就不会有 shell 的交互式登录流程很多 shell 启动文件不会被加载每一条命令都是独立进程前一条命令的cd当然不会影响后一条。那什么时候需要invoke_shell()当你需要模拟一个交互式终端会话比如登录到设备命令行、进入某个 REPL、或者需要持续保持 shell 状态时就用它shell ssh.invoke_shell(termxterm, width180, height40) shell.send(system-view\n) time.sleep(1) output shell.recv(65535).decode() print(output)但交互式 shell 也有麻烦你必须自己处理命令提示符、等待输出完成、清理控制字符。所以原则是能不用交互模式就尽量不用用exec_command()一条条跑只有在必须维持会话状态的场景再考虑invoke_shell()。2.4 密钥认证与安全底线Paramiko 支持密码、公开密钥、keyboard-interactive 三种认证方式。密码最简单但脚本中硬编码密码是个很危险的习惯仓库明文泄露、日志打印、历史记录里都有风险。相比之下我更推荐密钥认证或者至少把密码放到环境变量或密钥管理服务里读取。公钥认证的核心是客户端持有私钥服务器保存公钥。Paramiko 加载私钥文件key paramiko.Ed25519Key.from_private_key_file(/root/.ssh/id_ed25519) ssh.connect(192.168.1.10, usernameappuser, pkeykey)要注意私钥文件权限必须收紧否则 Paramiko 在部分系统上会直接拒绝加载。另外私钥若有 passphrase需要在加载时提供。还有一个常被忽略的安全点set_missing_host_key_policy()。初学者的常见操作是AutoAddPolicy()意思是“遇到没见过的服务器指纹就直接信任并写入 known_hosts”。这在家庭实验室里方便在公网环境就非常危险中间人攻击可以直接伪装成目标服务器。生产环境的建议放在第 4 节详细说。3. 源码编译成 RPM从开发机到生产环境的最后一公里3.1 为什么要打 RPM 而不是 pip install很多 Python 开发者的本能是pip install paramiko。但生产环境往往不是这样的第一内网服务器通常不能直接访问 PyPI安全策略也不允许随便拉外部依赖第二线上环境的 Python 库版本必须受控。你开发机今天装了 paramiko 3.4过几天又升级到 3.5如果没有统一分发机制不同机器上的行为就会不一致第三纯净的 RPM 包体验最好yum install就能解决依赖、卸载、升级、校验运维人员不用去理解 Python 包的安装细节。所以当目标机是 RHEL、CentOS、Rocky、openEuler 这类 RPM 系系统时提前把 paramiko 打包成 RPM 再分发是我认为最稳妥的交付方式。3.2 编译前的环境准备打 RPM 不是说在开发机上跑两条命令就完事需要准备一台构建机。这台机器最好和目标环境的主版本一致Python 版本也要一致避免出现“在 Python 3.6 上编出的包跑到 Python 3.11 上无法导入”这种问题。构建机上先装 rpmbuild 工具链dnf install -y rpm-build python3-devel gcc gcc-c make dnf install -y python3-pyproject-rpm-macros python3-setuptools python3-wheelParamiko 依赖 cryptography、pynacl、bcrypt 这几个库其中 cryptography 有 C 扩展在打包 paramiko 时一般不需要连 cryptography 一起编译只要在 RPM 的 Requires 里声明依赖由包管理器自动安装预编译版本。但构建机需要能拿到 paramiko 的源码包。源码包怎么拿有条件的话直接从 PyPI 下载 tar.gz没条件就在能联网的机器上执行pip download paramiko3.4.0 --no-binary :all: -d ./packages把下载到的paramiko-3.4.0.tar.gz拷贝到构建机的rpmbuild/SOURCES/目录。rpmbuild 的标准目录结构通常长这样~/rpmbuild/ BUILD/ RPMS/ SOURCES/ SPECS/ SRPMS/3.3 SPEC 文件怎么写得稳一份最基本的 SPEC 文件是打包的核心也是最容易出错的地方。我以一个在 RHEL 9/CentOS Stream 9 上可用的版本为例Name: python-paramiko Version: 3.4.0 Release: 1%{?dist} Summary: SSH2 protocol library in pure Python License: LGPL-2.1-or-later URL: https://www.paramiko.org Source0: paramiko-%{version}.tar.gz BuildArch: noarch BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-wheel BuildRequires: pyproject-rpm-macros Requires: python3-cryptography 3.3 Requires: python3-pynacl 1.5 Requires: python3-bcrypt 3.2 %description Paramiko is a Python implementation of the SSHv2 protocol, providing both client and server functionality. %prep %autosetup -n paramiko-%{version} %build %pyproject_wheel %install %pyproject_install %files -n python3-%{name} %{python3_sitelib}/paramiko %{python3_sitelib}/paramiko-*.dist-info %{python3_sitelib}/__pycache__/* %changelog * Tue Jan 01 2024 Your Name youexample.com - 3.4.0-1 - Initial package build几个关键点我要单独说出来BuildArch: noarch因为 paramiko 本身是纯 Python 代码没有平台相关的二进制扩展所以可以声明 noarch。如果它依赖的 cryptography 需要二进制包那是 cryptography 自己的事不用拉进这个包。Requires把 cryptography、pynacl、bcrypt 三个依赖写清楚。RPM 包管理器会在安装时自动检查它们是否存在缺了就报错不会出现装了 paramiko 后 import 失败的问题。%files的路径要写对。用%pyproject_install安装后包体落在%{python3_sitelib}下。如果路径写错打出来的 RPM 会是空的安装后自然 import 不到。包名前缀python3-是 RPM 系的惯例方便区分 Python 2 和 Python 3 里同名库。如果不使用 pyproject-rpm-macros传统一点的写法也能用%build %py3_build %install %py3_install两者都能跑但 pyproject 宏更贴近当前 Python 生态我建议新项目直接用宏少操心。3.4 编译过程和验证清单进入 SPECS 目录执行cd ~/rpmbuild/SPECS rpmbuild -ba python-paramiko.spec如果构建顺利RPMS 子目录下会生成一个类似~/rpmbuild/RPMS/noarch/python3-paramiko-3.4.0-1.el9.noarch.rpm的文件。构建过程如果卡在缺少 BuildRequires按报错补装即可通常就那几个工具包。拿到 RPM 后不要急着批量分发先在一台和线上环境相同的测试机上验证dnf localinstall -y python3-paramiko-3.4.0-1.el9.noarch.rpm python3 -c import paramiko; print(paramiko.__version__)如果能正常打印版本号再做一次真实连接测试import paramiko ssh paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(127.0.0.1, port22, usernametest, passwordtest) stdin, stdout, stderr ssh.exec_command(echo rpm_ok) print(stdout.read().decode()) ssh.close()这套验证通过后再把 RPM 放到内网自己的软件仓库或者用dnf localinstall离线分发到目标机器。这里还有一个省事的技巧如果内网机器量大可以再打一个包含 paramiko 和它全部依赖的离线 RPM 目录用createrepo生成仓库元数据客户端配置指向内网源后直接dnf install后续升级也方便。4. 落地使用时最容易踩的坑4.1 主机指纹校验默认拦路还是裸奔Paramiko 对主机密钥的默认策略其实是RejectPolicy也就是说遇到没见过的服务器指纹会直接抛BadHostKeyException。新手为了省事一上来就AutoAddPolicy()等于放弃了校验。在上线公网环境或涉密项目时正确的做法是提前把目标机器的主机密钥保存到 known_hosts 里然后用RejectPolicy去连接。内网环境如果实在图方便可以在确定网络风险可控的前提下用 AutoAddPolicy但至少你得知道自己在做什么。我是这样记的校验主机指纹的目的是防止有人冒充你的服务器这和认证用户身份是两回事两个都重要。4.2 超时设置不设好就等着卡死Paramiko 最常见的故障就是“脚本挂住不动”。原因很单纯你没给socket设置超时而目标服务器网络异常或者防火墙只做丢包处理时TCP 连接会一直等下去。建议在connect()里就把几个超时参数一次配齐ssh.connect( hostname192.168.1.10, port22, usernameroot, passwordxxxx, timeout10, # TCP 建连超时 banner_timeout15, # SSH 版本协商超时 auth_timeout15, # 认证阶段超时 channel_timeout15, # Channel 建立超时 )命令执行阶段也要设超时stdin, stdout, stderr ssh.exec_command(yum update -y, timeout60)exec_command()的 timeout 是从调用开始等待命令返回的时间。对长时间任务比如编译、大数据量传输这个值要留足否则会误杀正常任务。另外如果某个命令就像黑洞一样永远不返回多线程配合stdout.channel.recv_exit_status()也不能解决所有问题必要时在外部套一层ThreadPoolExecutorfuture.result(timeoutN)该放弃就放弃。4.3 编码、伪终端和交互式命令Paramiko 的命令输出几乎都是bytes类型直接打印会看到b...新手经常一大半时间在跟这个较劲。正确姿势是拿到后立刻按目标系统编码 decodeout stdout.read().decode(utf-8, errorsreplace) err stderr.read().decode(utf-8, errorsreplace)不同发行版的默认编码不同有些老系统是 GBK有些是 UTF-8。直接指定errorsreplace可以避免因为个别字符解码失败导致进程崩溃代价是特殊字符显示成替换符日志审计足够了。再说伪终端。exec_command()默认不分配 PTY 的本质是好消息输出干净、没有控制字符。但是有两种情况必须加一是你执行sudo需要输入密码二是命令依赖tty环境比如部分交互式程序会检测终端。这时你可以给 exec_command 传get_ptyTruestdin, stdout, stderr ssh.exec_command(sudo -S whoami, get_ptyTrue) stdin.write(your_passwd\n) stdin.flush()但加了 PTY 之后输出里会混入\r\n和\x1b[开头的转义序列解析时要么先清洗要么不用它。4.4 并发批量执行怎么设计才不出错批量连接服务器时很多人最初写的是循环for ip in ip_list: ssh paramiko.SSHClient() ssh.connect(ip, ...) ...100 台机器串行跑那速度没法看。改成多线程后很多人又踩了另一个坑所有线程共用同一个SSHClient对象。Paramiko 的SSHClient和底层 Transport 并不是设计成线程安全的多个线程同时在一个连接上开 Channel轻则报错重则连接断开。正确的做法是每台服务器一个独立客户端用线程池控制并发数量from concurrent.futures import ThreadPoolExecutor, as_completed def check_one(ip): ssh paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(ip, port22, usernameuser, passwordpwd, timeout8) try: stdin, stdout, stderr ssh.exec_command(df -h /, timeout10) out stdout.read().decode(utf-8, errorsreplace) return ip, out finally: ssh.close() with ThreadPoolExecutor(max_workers10) as executor: futures [executor.submit(check_one, ip) for ip in ip_list] for future in as_completed(futures): ip, out future.result() print(ip, out)max_workers建议控制在 10 以内太多会把网卡、源端口和目标机器的连接数打爆反而得不偿失。4.5 常见错误速查表错误类型典型信息原因处理主机密钥BadHostKeyException服务器指纹变化或不在 known_hosts更新 known_hosts或临时用 AutoAddPolicy认证失败AuthenticationException密码错误、密钥不匹配、账号被锁定检查凭据确认服务器允许密码认证连接超时TimeoutError socket.timeout网络不通、端口被墙、防火墙丢包检查连通性增大 timeout版本协商SSHException Error reading SSH protocol banner对端不是 SSH 服务、banner 超时telnet 22 端口排查调整 banner_timeout依赖缺失No module named cryptography安装 paramiko 时依赖没装齐用 RPM 安装依赖或 pip install 完整依赖编码问题UnicodeDecodeError直接 decode bytes 用错了编码用 utf-8 errorsreplace线程错误Occasionally exceptions on shared transport多线程共用一个 SSHClient改成每连接一个客户端这些坑我基本都踩过一遍大多数不是 Paramiko 的 bug而是对 SSH 协议本身理解不到位。5. 我的一些工程习惯5.1 把 exec_command 包装成可靠的执行器裸用exec_command()的问题在于返回值是三个流对象你得记得去读而且exec_command()本身不抛命令执行失败的错误exit_status 是非 0 时也不会自动提醒你。所以我习惯在每个工具项目里加一个薄封装def run_cmd(ssh, cmd, timeout30): stdin, stdout, stderr ssh.exec_command(cmd, timeouttimeout) out stdout.read().decode(utf-8, errorsreplace) err stderr.read().decode(utf-8, errorsreplace) code stdout.channel.recv_exit_status() return code, out, err调用时统一判断返回码代码会干净很多。尤其是巡检脚本跑完不用逐行看输出去猜有没有问题直接看 return code 就行。这就是“二次封装”的典型思路。真正做运维平台时我还会在这个基础上加命令白名单、操作人记录、结果落库、审批流程。Paramiko 只负责打通隧道平台规则全在它上层。5.2 日志、审计和连接复用每一次远程操作都是风险操作尤其是在生产环境。我的习惯是日志里必留几条信息时间、目标 IP、执行用户、命令全文、返回码、执行耗时。这样出了问题能回溯到底是谁在什么时间执行了什么操作。还有一个连接复用的点。如果一个脚本要对同一台机器连续跑几十条命令反复connect()再close()是巨大的浪费。一次连接多个 Channel 是更合理的做法ssh paramiko.SSHClient() ssh.connect(...) try: for cmd in [df -h, free -m, uptime]: code, out, err run_cmd(ssh, cmd) print(cmd, code) finally: ssh.close()单条长连接还能减少对端服务器 sshd 进程的反复创建对登录限制严格的服务器更友好。5.3 记住它不万能组合用更合适Paramiko 不是银弹。它的优势是轻、可嵌入、接口接近 SSH 原生协议。但它本身没有任务调度能力、没有配置管理模块、也没有跨平台节点发现机制。如果一个业务逻辑要管理上千台服务器、做配置漂移对比、做 Playbook 编排那正确工具是 Ansible 这类更上层的自动化框架。Paramiko 的价值更多是在你确实需要“定制化”时作为底层积木存在。拿我自己举例小型工具和 Web 运维后台里Paramiko 是主力成体系的配置管理走 Ansible文件大规模分发走 rsyncParamiko 则用在特定数据抽取和同步脚本中。每个工具干自己最擅长的事组合起来反而比什么功能都想一把梭更稳定。另外如果只是临时想测试某台服务器能不能连、密钥对不对我一般先写个 10 行的 Python 脚本直接跑比跑到内网服务器上去rpm -qa快得多。工具的价值不在于多复杂在于解决问题时能不能减少摩擦。Paramiko 就是个例子短小直接但很可靠。
返回列表