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 有三大优势:

对比维度FTPSFTP
传输加密❌ 明文传输(不安全)✅ SSH 加密传输
认证方式密码明文SSH 密钥/密码双重认证
端口21(需额外开放)22(复用 SSH 端口)
防火墙友好度需开放多个端口(20/21)只需开放 22 端口
1.2 SSHClient vs SFTPClient 的关系
对比维度SSHClientSFTPClient
用途执行远程命令文件传输 + 远程文件管理
创建方式直接实例化通过已建立的 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
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}")

运行结果

✅ 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:

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()

运行结果

当前磁盘使用率:26%
✅ 文件上传成功!

三、文件上传——put() 方法

3.1 基本用法

put() 方法用于将本地文件上传到远程服务器:

sftp.put(localpath, remotepath, callback=None, confirm=True)

参数说明

参数类型说明
localpathstr本地文件路径(必填)
remotepathstr远程文件路径(必填)
callbackfunction进度回调函数(可选)
confirmbool上传完成后是否确认文件大小(默认 True)
3.2 基础上传示例
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()

运行结果

✅ 文件上传成功:syslogin.log → /root/files/syslogin.log
3.3 带进度回调的上传

对于大文件上传,显示进度可以提升用户体验:

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✅ 上传完成!")

运行效果

开始上传:large_file.tar.gz(256.00 MB)
  进度: 45.2% |█████████████████████░░░░░░░░░░░░░░░░░░░| 118362112/268435456 字节
✅ 上传完成!

四、文件下载——get() 方法

4.1 基本用法

get() 方法用于从远程服务器下载文件到本地:

sftp.get(remotepath, localpath, callback=None)
4.2 基础下载示例
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()

运行结果

✅ 文件下载成功:/root/files/linux.txt → E:\pyproject\运维\paramiko模块\linux.txt
4.3 批量下载(按文件名模式匹配)
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 目录管理完整示例
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()

运行结果

=== 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() 只能删除空目录,要删除非空目录需要递归处理:

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.SSHExceptionSSH 连接异常检查网络,重试或超时处理
paramiko.AuthenticationException认证失败检查用户名/密码/密钥
IOError文件读写错误检查磁盘空间、文件是否被占用
6.2 完整的异常处理模板
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)

#!/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()

运行结果

============================================================
  📦 堡垒机模式文件分发工具 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 关系图

┌─────────────────────────────────────────────────────────┐
│                    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
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐