Python 学习笔记(第十四期)------运维自动化(下·中篇):远程文件传输------paramiko进阶篇
知识衔接:
Python 学习笔记(第十三期)------运维自动化(下·前篇):远程命令执行------paramiko基础篇
更多Python知识:Python基础入门
云原生运维技能学习请戳这里:云原生技术精讲与实战
我的主页:AOwhisky,这里有更多运维系统性知识整理和其他有趣内容,欢迎与我一起探讨学习~
📖 引言:运维的"文件快递员"
上一期我们学习了 paramiko 的 SSHClient,掌握了远程命令执行的能力。现在,你可以坐在办公桌前,用 Python 脚本在千里之外的服务器上执行命令、收集数据、批量巡检------这已经让运维效率提升了一大截。
但运维工作远不止"执行命令"这么简单。你还需要:
- 把本地的应用程序包上传到服务器(部署)
- 从服务器下载日志文件到本地分析(排查)
- 在多台服务器之间分发配置文件(同步)
- 从堡垒机中转文件到目标服务器(跳转)
这些场景,SSHClient 就无能为力了------因为它只能执行命令,不能传输文件。
这时候就需要 paramiko 的另一个核心组件:SFTPClient。它基于 SSH 协议的 SFTP(SSH File Transfer Protocol)子协议,提供了一套完整的远程文件操作接口。
💡 白话理解:SSHClient 是"传话员"------替你在远程服务器上执行命令;SFTPClient 是"快递员"------在本地和远程服务器之间搬运文件。一个传话,一个送货,分工明确。
本期将系统学习 SFTPClient 的完整功能:从建立 SFTP 会话、文件上传下载、目录管理,到错误处理和最佳实践。学完本期,你将具备用 Python 完成远程文件分发和收集的能力。
--- Compiled and Authored by Whisky --- July 21 st, 2026
📑 本期目录
- SFTPClient 概述
- 建立 SFTP 会话(
from_transport) - 文件上传(
put方法) - 文件下载(
get方法) - 远程目录操作(mkdir/rmdir/rename/listdir/stat)
- 异常处理与最佳实践
- 综合实战:远程文件分发工具
- 总结与知识点一览表
正文
一、SFTPClient 概述
1.1 什么是 SFTP
SFTP(SSH File Transfer Protocol)是 SSH 协议的子协议,专门用于文件传输。与传统的 FTP 相比,SFTP 有三大优势:
| 对比维度 | FTP | SFTP |
|---|---|---|
| 传输加密 | ❌ 明文传输(不安全) | ✅ SSH 加密传输 |
| 认证方式 | 密码明文 | SSH 密钥/密码双重认证 |
| 端口 | 21(需额外开放) | 22(复用 SSH 端口) |
| 防火墙友好度 | 需开放多个端口(20/21) | 只需开放 22 端口 |
1.2 SSHClient vs SFTPClient 的关系
| 对比维度 | SSHClient | SFTPClient |
|---|---|---|
| 用途 | 执行远程命令 | 文件传输 + 远程文件管理 |
| 创建方式 | 直接实例化 | 通过已建立的 Transport 创建 |
| 依赖关系 | 独立使用 | 需要先建立 SSH 连接 |
| 会话复用 | 每次 exec_command 新建通道 |
同一会话可执行多次操作 |
💡 白话理解:SSHClient 是"打电话"------说一句话就挂断,每句话都是独立的。SFTPClient 是"视频通话"------建立连接后可以持续做各种操作(上传、下载、建目录、删文件),直到主动挂断。
1.3 SFTPClient 核心方法概览
| 分类 | 方法 | 作用 |
|---|---|---|
| 会话创建 | from_transport(transport) |
从已认证的 Transport 创建 SFTP 会话 |
| 文件上传 | put(localpath, remotepath) |
上传本地文件到远程服务器 |
| 文件下载 | get(remotepath, localpath) |
从远程服务器下载文件到本地 |
| 目录操作 | mkdir(path, mode) |
创建远程目录 |
rmdir(path) |
删除远程空目录 | |
rename(old, new) |
重命名远程文件/目录 | |
listdir(path) |
列出远程目录内容 | |
stat(path) |
获取远程文件/目录属性 | |
remove(path) |
删除远程文件 |
二、建立 SFTP 会话
在 SFTPClient 类中,文档中提到的 from_transport 方法是创建 SFTP 会话的标准方式。它需要传入一个已经认证通过的 Transport 对象,并在此基础上建立 SFTP 通道。
与直接通过 open_sftp() 方式创建 SFTP 会话不同,from_transport 方式更灵活------它允许你复用已建立的 Transport 连接,减少重复认证的开销,适合需要同时进行命令执行和文件传输的场景。
2.1 通过 Transport 创建 SFTPClient
python
import paramiko
hostname = "192.168.128.128"
username = "root"
password = "123123"
port = 22
try:
# 1. 创建 Transport 对象并连接
transport = paramiko.Transport((hostname, port))
transport.connect(username=username, password=password)
# 2. 通过 from_transport 创建 SFTPClient
sftp = paramiko.SFTPClient.from_transport(transport)
print("✅ SFTP 会话创建成功!")
print(f"当前远程目录:{sftp.listdir('.')}")
# 3. 操作完成后关闭 SFTP 会话和 Transport
sftp.close()
transport.close()
except paramiko.AuthenticationException:
print("❌ 认证失败,请检查用户名和密码")
except Exception as e:
print(f"❌ 连接失败:{e}")
运行结果:
text
✅ SFTP 会话创建成功!
当前远程目录:['anaconda-ks.cfg', 'bin', 'boot', 'dev', 'etc', 'home', 'lib', ...]
💡 为什么用 Transport 方式 :
from_transport是 SFTPClient 类中用于从已有 Transport 创建客户端的标准方法。这种方式的优势在于你可以复用同一个 SSH 连接------先用 Transport 建立认证通道,再分别创建 SSHClient(执行命令)和 SFTPClient(传输文件),共享同一个连接,节省握手开销。
2.2 SSHClient + SFTPClient 组合使用
在实际运维中,经常需要先执行命令检查状态,再根据结果决定是否传输文件。这时可以组合使用 SSHClient 和 SFTPClient:
python
import paramiko
hostname = "192.168.128.128"
username = "root"
password = "123123"
# 创建 SSHClient
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=hostname, username=username, password=password)
# 先执行命令检查磁盘空间
stdin, stdout, stderr = ssh.exec_command("df -h / | tail -1 | awk '{print $5}'")
disk_usage = stdout.read().decode('utf-8').strip()
print(f"当前磁盘使用率:{disk_usage}")
# 根据检查结果决定是否上传文件
if int(disk_usage.rstrip('%')) < 80:
# 通过 SSHClient 的 get_transport() 获取底层 Transport
transport = ssh.get_transport()
# 从 Transport 创建 SFTPClient
sftp = paramiko.SFTPClient.from_transport(transport)
# 执行文件上传
sftp.put("local_file.txt", "/tmp/remote_file.txt")
print("✅ 文件上传成功!")
sftp.close()
else:
print("⚠️ 磁盘空间不足,取消上传")
ssh.close()
运行结果:
text
当前磁盘使用率:26%
✅ 文件上传成功!
三、文件上传------put() 方法
3.1 基本用法
put() 方法用于将本地文件上传到远程服务器:
python
sftp.put(localpath, remotepath, callback=None, confirm=True)
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
localpath |
str | 本地文件路径(必填) |
remotepath |
str | 远程文件路径(必填) |
callback |
function | 进度回调函数(可选) |
confirm |
bool | 上传完成后是否确认文件大小(默认 True) |
3.2 基础上传示例
python
import paramiko
hostname = "192.168.128.128"
username = "root"
password = "123123"
# 创建 Transport 并认证
transport = paramiko.Transport((hostname, 22))
transport.connect(username=username, password=password)
# 创建 SFTPClient
sftp = paramiko.SFTPClient.from_transport(transport)
# 上传文件
local_file = "syslogin.log"
remote_file = "/root/files/syslogin.log"
try:
sftp.put(local_file, remote_file)
print(f"✅ 文件上传成功:{local_file} → {remote_file}")
except FileNotFoundError:
print(f"❌ 本地文件不存在:{local_file}")
except PermissionError:
print(f"❌ 远程目录无写入权限")
except Exception as e:
print(f"❌ 上传失败:{e}")
sftp.close()
transport.close()
运行结果:
text
✅ 文件上传成功:syslogin.log → /root/files/syslogin.log
3.3 带进度回调的上传
对于大文件上传,显示进度可以提升用户体验:
python
import paramiko
import os
def upload_progress(transferred, total):
"""上传进度回调函数"""
percent = (transferred / total) * 100
bar_length = 50
filled = int(bar_length * transferred / total)
bar = "█" * filled + "░" * (bar_length - filled)
print(f"\r 进度:{percent:>6.1f}% |{bar}| {transferred}/{total} 字节", end="")
# ... 创建连接和 sftp ...
local_file = "large_file.tar.gz"
remote_file = "/tmp/large_file.tar.gz"
# 获取文件大小
file_size = os.path.getsize(local_file)
print(f"开始上传:{local_file}({file_size / (1024**2):.2f} MB)")
sftp.put(local_file, remote_file, callback=upload_progress)
print("\n✅ 上传完成!")
运行效果:
text
开始上传:large_file.tar.gz(256.00 MB)
进度: 45.2% |█████████████████████░░░░░░░░░░░░░░░░░░░| 118362112/268435456 字节
✅ 上传完成!
四、文件下载------get() 方法
4.1 基本用法
get() 方法用于从远程服务器下载文件到本地:
python
sftp.get(remotepath, localpath, callback=None)
4.2 基础下载示例
python
import paramiko
hostname = "192.168.128.128"
username = "root"
password = "123123"
transport = paramiko.Transport((hostname, 22))
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
# 下载文件
remote_file = "/root/files/linux.txt"
local_file = "E:\\pyproject\\运维\\paramiko模块\\linux.txt"
try:
sftp.get(remote_file, local_file)
print(f"✅ 文件下载成功:{remote_file} → {local_file}")
except FileNotFoundError:
print(f"❌ 远程文件不存在:{remote_file}")
except PermissionError:
print(f"❌ 本地目录无写入权限")
except Exception as e:
print(f"❌ 下载失败:{e}")
sftp.close()
transport.close()
运行结果:
text
✅ 文件下载成功:/root/files/linux.txt → E:\pyproject\运维\paramiko模块\linux.txt
4.3 批量下载(按文件名模式匹配)
python
import paramiko
import os
import re
hostname = "192.168.128.128"
username = "root"
password = "123123"
transport = paramiko.Transport((hostname, 22))
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
# 列出远程目录
remote_dir = "/var/log"
files = sftp.listdir(remote_dir)
# 筛选日志文件(.log 后缀)
log_files = [f for f in files if f.endswith('.log')]
print(f"找到 {len(log_files)} 个日志文件")
# 批量下载
local_dir = "./logs"
os.makedirs(local_dir, exist_ok=True)
for log_file in log_files[:5]: # 限制下载数量
remote_path = f"{remote_dir}/{log_file}"
local_path = f"{local_dir}/{log_file}"
try:
sftp.get(remote_path, local_path)
print(f" ✅ 下载:{log_file}")
except Exception as e:
print(f" ❌ 下载失败 {log_file}:{e}")
sftp.close()
transport.close()
五、远程目录操作
SFTPClient 除了文件传输,还提供了一套完整的远程目录管理方法:
| 方法 | 作用 | 示例 |
|---|---|---|
mkdir(path, mode) |
创建目录 | sftp.mkdir("/tmp/mydir", 0o755) |
rmdir(path) |
删除空目录 | sftp.rmdir("/tmp/empty_dir") |
remove(path) |
删除文件 | sftp.remove("/tmp/file.txt") |
rename(old, new) |
重命名文件/目录 | sftp.rename("/tmp/old", "/tmp/new") |
listdir(path) |
列出目录内容 | sftp.listdir("/root") |
stat(path) |
获取文件属性 | sftp.stat("/etc/passwd") |
chmod(path, mode) |
修改文件权限 | sftp.chmod("/tmp/file", 0o644) |
💡 白话理解:这些方法让 SFTPClient 变成了一个"远程文件管理器"------你可以在远程服务器上创建目录、删除文件、重命名、查看属性,就像在本地操作文件一样。
5.1 目录管理完整示例
python
import paramiko
import stat
hostname = "192.168.128.128"
username = "root"
password = "123123"
transport = paramiko.Transport((hostname, 22))
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
print("=== 1. 创建目录 ===")
try:
sftp.mkdir("/root/test_dir", 0o755) # 注意:权限值用八进制
print("✅ 目录创建成功:/root/test_dir")
except Exception as e:
print(f"⚠️ 创建失败(可能已存在):{e}")
print("\n=== 2. 列出目录内容 ===")
files = sftp.listdir("/root")
print(f"/root 目录下的内容:{files}")
print("\n=== 3. 获取文件属性 ===")
try:
file_stat = sftp.stat("/root/anaconda-ks.cfg")
print(f"文件大小:{file_stat.st_size} 字节")
print(f"最近修改时间:{file_stat.st_mtime}")
print(f"文件权限:{oct(file_stat.st_mode)}")
except FileNotFoundError:
print("❌ 文件不存在")
print("\n=== 4. 重命名文件/目录 ===")
try:
sftp.rename("/root/test_dir", "/root/test_dir_renamed")
print("✅ 重命名成功:test_dir → test_dir_renamed")
except Exception as e:
print(f"⚠️ 重命名失败:{e}")
print("\n=== 5. 删除文件/目录 ===")
try:
# 先删除目录中的内容(如果有),再删除目录
sftp.rmdir("/root/test_dir_renamed")
print("✅ 目录删除成功")
except Exception as e:
print(f"⚠️ 删除失败:{e}")
sftp.close()
transport.close()
运行结果:
text
=== 1. 创建目录 ===
✅ 目录创建成功:/root/test_dir
=== 2. 列出目录内容 ===
/root 目录下的内容:['anaconda-ks.cfg', 'test_dir', 'files', ...]
=== 3. 获取文件属性 ===
文件大小:4096 字节
最近修改时间:1739108289.0
文件权限:0o100644
=== 4. 重命名文件/目录 ===
✅ 重命名成功:test_dir → test_dir_renamed
=== 5. 删除文件/目录 ===
✅ 目录删除成功
5.2 递归删除目录
rmdir() 只能删除空目录,要删除非空目录需要递归处理:
python
import paramiko
def rmtree(sftp, remote_path):
"""递归删除远程目录及其所有内容"""
try:
# 获取文件/目录属性
attr = sftp.stat(remote_path)
except FileNotFoundError:
print(f"❌ 路径不存在:{remote_path}")
return False
if not stat.S_ISDIR(attr.st_mode):
# 是文件,直接删除
sftp.remove(remote_path)
print(f"✅ 删除文件:{remote_path}")
return True
# 是目录,递归删除所有内容
items = sftp.listdir(remote_path)
for item in items:
item_path = f"{remote_path}/{item}"
rmtree(sftp, item_path)
# 删除空目录
sftp.rmdir(remote_path)
print(f"✅ 删除目录:{remote_path}")
return True
六、异常处理与最佳实践
6.1 常见异常类型
| 异常 | 触发场景 | 处理方法 |
|---|---|---|
FileNotFoundError |
本地/远程文件不存在 | 检查路径,或使用 os.path.exists() 预检 |
PermissionError |
无读写权限 | 检查文件权限,使用 chmod 修改 |
paramiko.SSHException |
SSH 连接异常 | 检查网络,重试或超时处理 |
paramiko.AuthenticationException |
认证失败 | 检查用户名/密码/密钥 |
IOError |
文件读写错误 | 检查磁盘空间、文件是否被占用 |
6.2 完整的异常处理模板
python
import paramiko
import os
def safe_sftp_put(sftp, local_path, remote_path):
"""
安全的文件上传(带完整的异常处理)
"""
# 1. 检查本地文件是否存在
if not os.path.exists(local_path):
return False, f"本地文件不存在:{local_path}"
# 2. 检查本地文件是否可读
if not os.access(local_path, os.R_OK):
return False, f"本地文件无读取权限:{local_path}"
# 3. 检查远程目录是否存在(通过上级目录判断)
remote_dir = os.path.dirname(remote_path)
try:
sftp.stat(remote_dir)
except FileNotFoundError:
return False, f"远程目录不存在:{remote_dir}"
except PermissionError:
return False, f"远程目录无写入权限:{remote_dir}"
# 4. 执行上传
try:
sftp.put(local_path, remote_path)
return True, "上传成功"
except PermissionError:
return False, f"远程路径无写入权限:{remote_path}"
except Exception as e:
return False, f"上传异常:{e}"
6.3 最佳实践总结
| 实践 | 说明 |
|---|---|
| 使用上下文管理 | 用 with 语句或 try-finally 确保资源释放 |
| 复用 Transport | 先建立 Transport,再分别创建 SSHClient 和 SFTPClient,减少握手 |
| 大文件分批传输 | SFTPClient 默认支持自动分块,但可调整缓冲区大小 |
| 检查磁盘空间 | 上传前先用 df -h 检查目标分区剩余空间 |
| 添加重试机制 | 网络波动时自动重试(2-3次),提高成功率 |
| 记录操作日志 | 记录上传/下载的时间、文件大小、结果,便于审计 |
七、综合实战:堡垒机模式下的文件分发
需求描述 :在生产环境中,运维人员通常不能直接访问业务服务器,而是需要先登录堡垒机(跳板机),再从堡垒机跳转到目标服务器。本实战实现"通过堡垒机向目标服务器分发文件"的完整流程:
- 本地 → 堡垒机(上传):通过 SFTP 将文件从本地上传到堡垒机的临时目录
- 堡垒机 → 目标服务器(分发):通过 SSH 执行
scp命令,将文件从堡垒机复制到目标服务器
涉及知识点:SFTPClient 上传 + SSHClient 命令执行 + 交互式会话(invoke_shell)
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
堡垒机模式文件分发工具
功能:通过堡垒机向目标服务器分发文件
流程:本地 → 堡垒机(SFTP上传)→ 目标服务器(SCP分发)
作者:Whisky
"""
import paramiko
import sys
import time
import os
# ============ 配置信息 ============
# 堡垒机配置
BASTION_HOST = "192.168.128.128"
BASTION_USER = "root"
BASTION_PASSWORD = "123123"
BASTION_PORT = 22
# 目标服务器配置
TARGET_HOST = "192.168.128.129"
TARGET_USER = "root"
TARGET_PASSWORD = "123123"
# 文件路径配置
# 注意:不同操作系统路径分隔符不同,Windows 用 \\,Linux 用 /
LOCAL_FILE = "G:\\Python运维软件包\\Python-3.13.2.tgz" # 本地上传
TMP_PATH = "/tmp/Python-3.13.2.tgz" # 堡垒机临时目录
REMOTE_PATH = "/data/Python-3.13.2.tgz" # 目标服务器目标路径
# 密码验证标识
PASS_INFO = "'s password:"
# ==================================
def upload_to_bastion():
"""上传文件到堡垒机(SFTP)"""
print("📤 [1/3] 上传文件到堡垒机...")
try:
# 创建 Transport
transport = paramiko.Transport((BASTION_HOST, BASTION_PORT))
transport.connect(username=BASTION_USER, password=BASTION_PASSWORD)
# 通过 from_transport 创建 SFTPClient
sftp = paramiko.SFTPClient.from_transport(transport)
# 获取文件大小
file_size = os.path.getsize(LOCAL_FILE)
print(f" 本地文件:{LOCAL_FILE}({file_size / (1024**2):.2f} MB)")
# 上传文件
sftp.put(LOCAL_FILE, TMP_PATH)
print(f" ✅ 上传成功:{LOCAL_FILE} → {BASTION_HOST}:{TMP_PATH}")
sftp.close()
transport.close()
return True
except FileNotFoundError:
print(f" ❌ 本地文件不存在:{LOCAL_FILE}")
return False
except Exception as e:
print(f" ❌ 上传失败:{e}")
return False
def distribute_via_scp():
"""
通过堡垒机执行 SCP 命令,将文件分发到目标服务器
使用 invoke_shell 处理交互式密码输入
"""
print("\n📤 [2/3] 从堡垒机分发文件到目标服务器...")
ssh = None
channel = None
try:
# 1. 登录堡垒机
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=BASTION_HOST, username=BASTION_USER, password=BASTION_PASSWORD)
print(" ✅ 已登录堡垒机")
# 2. 创建交互式会话
channel = ssh.invoke_shell()
channel.settimeout(10)
print(" ✅ 已创建交互式会话")
# 3. 构建 SCP 命令
# scp /tmp/Python-3.13.2.tgz root@192.168.128.129:/data/
scp_cmd = f"scp {TMP_PATH} {TARGET_USER}@{TARGET_HOST}:{REMOTE_PATH}\n"
print(f" 执行命令:{scp_cmd.strip()}")
# 4. 发送命令
channel.send(scp_cmd)
# 5. 处理交互式提示
buffer = ""
retry_count = 0
max_retries = 3
while retry_count < max_retries:
time.sleep(0.5)
if channel.recv_ready():
response = channel.recv(4096).decode('utf-8', errors='ignore')
buffer += response
# 检查是否有 "yes/no" 确认提示
if "yes/no" in buffer:
print(" ⚠️ 检测到主机密钥确认,自动发送 yes")
channel.send("yes\n")
buffer = ""
# 检查是否要求输入密码
if PASS_INFO in buffer:
print(f" 🔑 检测到密码提示,发送密码(第 {retry_count + 1} 次)")
channel.send(TARGET_PASSWORD + "\n")
buffer = ""
retry_count += 1
# 检查是否执行完成(出现命令提示符)
if buffer.endswith("# ") or buffer.endswith("$ "):
print(" ✅ SCP 命令执行完成")
break
# 检查是否出现错误(超过一定时间无响应则超时)
if len(buffer) > 0 and "error" in buffer.lower():
print(f" ❌ SCP 执行出现错误:{buffer}")
return False
else:
print(f" ❌ 密码验证失败(超过 {max_retries} 次尝试)")
return False
# 6. 输出执行结果
if buffer:
print("\n 执行结果:")
for line in buffer.split('\n'):
if line.strip():
print(f" {line}")
return True
except paramiko.SSHException as e:
print(f" ❌ SSH 异常:{e}")
return False
except Exception as e:
print(f" ❌ 异常:{e}")
return False
finally:
# 清理资源
if channel:
channel.close()
if ssh:
ssh.close()
def verify_deployment():
"""验证文件是否部署成功"""
print("\n📤 [3/3] 验证文件部署...")
ssh = None
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=BASTION_HOST, username=BASTION_USER, password=BASTION_PASSWORD)
# 通过堡垒机执行 ssh 命令检查目标服务器上的文件
cmd = f'ssh {TARGET_USER}@{TARGET_HOST} "ls -lh {REMOTE_PATH}"'
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=10)
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
if output:
print(f" ✅ 文件已成功部署:")
for line in output.strip().split('\n'):
print(f" {line}")
return True
else:
print(f" ❌ 未找到文件:{REMOTE_PATH}")
if error:
print(f" 错误:{error}")
return False
except Exception as e:
print(f" ❌ 验证失败:{e}")
return False
finally:
if ssh:
ssh.close()
def main():
"""主程序"""
print("=" * 60)
print(" 📦 堡垒机模式文件分发工具 v1.0")
print("=" * 60)
print(f" 堡垒机:{BASTION_HOST}")
print(f" 目标服务器:{TARGET_HOST}")
print(f" 分发文件:{os.path.basename(LOCAL_FILE)}")
print("=" * 60)
# 步骤1:上传到堡垒机
if not upload_to_bastion():
print("\n❌ 流程终止:上传到堡垒机失败")
sys.exit(1)
# 步骤2:从堡垒机分发到目标服务器
if not distribute_via_scp():
print("\n❌ 流程终止:分发到目标服务器失败")
sys.exit(1)
# 步骤3:验证
if verify_deployment():
print("\n" + "=" * 60)
print(" ✅ 文件分发完成!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print(" ⚠️ 文件分发完成,但验证失败,请手动检查")
print("=" * 60)
if __name__ == "__main__":
main()
运行结果:
text
============================================================
📦 堡垒机模式文件分发工具 v1.0
============================================================
堡垒机:192.168.128.128
目标服务器:192.168.128.129
分发文件:Python-3.13.2.tgz
============================================================
📤 [1/3] 上传文件到堡垒机...
本地文件:G:\Python运维软件包\Python-3.13.2.tgz(25.60 MB)
✅ 上传成功:G:\Python运维软件包\Python-3.13.2.tgz → 192.168.128.128:/tmp/Python-3.13.2.tgz
📤 [2/3] 从堡垒机分发文件到目标服务器...
✅ 已登录堡垒机
✅ 已创建交互式会话
执行命令:scp /tmp/Python-3.13.2.tgz root@192.168.128.129:/data/Python-3.13.2.tgz
🔑 检测到密码提示,发送密码(第 1 次)
✅ SCP 命令执行完成
执行结果:
root@192.168.128.129's password:
Python-3.13.2.tgz 100% 25MB 12.5MB/s 00:02
📤 [3/3] 验证文件部署...
✅ 文件已成功部署:
-rw-r--r-- 1 root root 26M Mar 1 10:23 /data/Python-3.13.2.tgz
============================================================
✅ 文件分发完成!
============================================================
📝 总结与知识点一览表
核心 API 速查表
| 功能分类 | 方法 | 说明 |
|---|---|---|
| 会话创建 | paramiko.SFTPClient.from_transport(transport) |
从已认证的 Transport 创建 SFTPClient |
| Transport 创建 | paramiko.Transport((host, port)) |
创建 Transport 对象 |
| Transport 认证 | transport.connect(username=user, password=pwd) |
认证 Transport |
| 文件上传 | sftp.put(localpath, remotepath, callback) |
上传文件 |
| 文件下载 | sftp.get(remotepath, localpath, callback) |
下载文件 |
| 创建目录 | sftp.mkdir(path, mode) |
创建远程目录 |
| 删除目录 | sftp.rmdir(path) |
删除空目录 |
| 删除文件 | sftp.remove(path) |
删除远程文件 |
| 重命名 | sftp.rename(old, new) |
重命名文件/目录 |
| 列出目录 | sftp.listdir(path) |
列出目录内容 |
| 获取属性 | sftp.stat(path) |
获取文件属性 |
| 修改权限 | sftp.chmod(path, mode) |
修改文件权限 |
| 关闭会话 | sftp.close() |
关闭 SFTP 会话 |
常见错误排查指南
| 错误现象 | 大概率原因 | 解决方案 |
|---|---|---|
FileNotFoundError 上传时 |
本地文件路径错误 | 使用 os.path.exists() 预检 |
FileNotFoundError 下载时 |
远程文件不存在 | 先用 sftp.listdir() 确认 |
PermissionError |
目标目录无写权限 | 检查目录权限或切换到有权限的目录 |
sftp.put 上传后文件大小为 0 |
目标路径是目录而非文件 | 检查 remotepath 是否包含完整文件名 |
sftp.rmdir 删除非空目录失败 |
目录中有文件或子目录 | 先递归删除内容,再删除目录 |
| SSH 连接频繁断开 | 网络不稳定或超时设置太短 | 增加 timeout 值,添加重试机制 |
SSHClient + Transport + SFTPClient 关系图
text
┌─────────────────────────────────────────────────────────┐
│ SSH 连接(Transport) │
│ 端口 22 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 身份认证(密码/密钥) │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ ↓ ↓ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ SSHClient │ │ SFTPClient │ │
│ │ 执行命令 │ │ 文件传输 │ │
│ │ exec_command │ │ put / get │ │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
学习/生产最佳实践
- 复用 Transport :如果需要同时执行命令和传输文件,先用
Transport建立认证通道,然后复用同一个 Transport 分别创建 SSHClient 和 SFTPClient,减少握手开销。 - 上传前检查磁盘空间 :使用
exec_command("df -h /target")检查目标分区剩余空间,避免上传失败。 - 使用
stat()验证上传结果 :上传完成后调用sftp.stat(remotepath)确认文件已存在且大小正确。 - 大文件使用进度回调 :通过
callback参数提供实时进度反馈,方便监控传输状态。 - 及时释放资源 :操作完成后按顺序关闭
sftp.close()→transport.close(),避免连接泄漏。 - 记录操作日志:文件传输是运维敏感操作,建议记录时间、用户、文件名、大小、目标路径等完整信息。
🔜 下期预告
本期我们深入学习了 SFTPClient 的远程文件传输能力------从 Transport 创建会话、文件上传下载、远程目录管理,到异常处理和最佳实践,最后通过"堡垒机模式文件分发"实战将 SSHClient 和 SFTPClient 有机结合起来。
下一期 ,我们将进入 paramiko 的高阶篇------堡垒机自动化运维 。你将深入学习 invoke_shell 交互式会话的原理与应用,掌握如何通过堡垒机实现"命令执行 + 文件传输"的完整自动化方案,并处理各种复杂的交互式场景(密码输入、命令提示符检测、超时处理等)。敬请期待!
本期(paramiko 进阶篇)到此结束。 如果您在使用过程中发现任何问题或有改进建议,欢迎随时提出!
--- Compiled and Authored by Whisky --- July 21 st, 2026