Python 学习笔记(第十四期)——运维自动化(下·中篇):远程文件传输——paramiko进阶篇

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

📑 本期目录

  1. SFTPClient 概述
  2. 建立 SFTP 会话(from_transport
  3. 文件上传(put 方法)
  4. 文件下载(get 方法)
  5. 远程目录操作(mkdir/rmdir/rename/listdir/stat)
  6. 异常处理与最佳实践
  7. 综合实战:远程文件分发工具
  8. 总结与知识点一览表

正文

一、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次),提高成功率
记录操作日志 记录上传/下载的时间、文件大小、结果,便于审计

七、综合实战:堡垒机模式下的文件分发

需求描述 :在生产环境中,运维人员通常不能直接访问业务服务器,而是需要先登录堡垒机(跳板机),再从堡垒机跳转到目标服务器。本实战实现"通过堡垒机向目标服务器分发文件"的完整流程:

  1. 本地 → 堡垒机(上传):通过 SFTP 将文件从本地上传到堡垒机的临时目录
  2. 堡垒机 → 目标服务器(分发):通过 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    │       │
│  └───────────────┘             └───────────────┘       │
└─────────────────────────────────────────────────────────┘

学习/生产最佳实践

  1. 复用 Transport :如果需要同时执行命令和传输文件,先用 Transport 建立认证通道,然后复用同一个 Transport 分别创建 SSHClient 和 SFTPClient,减少握手开销。
  2. 上传前检查磁盘空间 :使用 exec_command("df -h /target") 检查目标分区剩余空间,避免上传失败。
  3. 使用 stat() 验证上传结果 :上传完成后调用 sftp.stat(remotepath) 确认文件已存在且大小正确。
  4. 大文件使用进度回调 :通过 callback 参数提供实时进度反馈,方便监控传输状态。
  5. 及时释放资源 :操作完成后按顺序关闭 sftp.close()transport.close(),避免连接泄漏。
  6. 记录操作日志:文件传输是运维敏感操作,建议记录时间、用户、文件名、大小、目标路径等完整信息。

🔜 下期预告

本期我们深入学习了 SFTPClient 的远程文件传输能力------从 Transport 创建会话、文件上传下载、远程目录管理,到异常处理和最佳实践,最后通过"堡垒机模式文件分发"实战将 SSHClient 和 SFTPClient 有机结合起来。

下一期 ,我们将进入 paramiko 的高阶篇------堡垒机自动化运维 。你将深入学习 invoke_shell 交互式会话的原理与应用,掌握如何通过堡垒机实现"命令执行 + 文件传输"的完整自动化方案,并处理各种复杂的交互式场景(密码输入、命令提示符检测、超时处理等)。敬请期待!


本期(paramiko 进阶篇)到此结束。 如果您在使用过程中发现任何问题或有改进建议,欢迎随时提出!
--- Compiled and Authored by Whisky --- July 21 st, 2026

相关推荐
math_hongfan1 小时前
鸿蒙Flutter setState机制深入理解
学习·flutter·华为·harmonyos·鸿蒙
其实防守也摸鱼1 小时前
补天SRC新手入门指南:从0到1的漏洞挖掘之路
网络·python·学习·安全·web安全·数据挖掘·挖洞
lupai1 小时前
手机在网状态查询 API 新手实战指南
大数据·python·智能手机
imc.111 小时前
linux基础IO
linux·运维·服务器
躺不平的理查德1 小时前
让Ubuntu 的第二块网卡 ens37 永久固定为 192.168.30.111
运维·服务器
其美杰布-富贵-李2 小时前
Spring Boot 依赖注入说明文档
java·spring boot·python
bitbrowser2 小时前
Facebook 验证反复回到原页面,应该从哪里排查
运维·服务器·facebook
Land03292 小时前
AI网页元素变化无法自动修复?自带元素自愈的自动化解决方案
运维·人工智能·ai·自动化·rpa
fengyehongWorld2 小时前
Jenkins 安装与简单配置
运维·jenkins