Python paramiko库实战:统一SSH与SFTP操作及交互式终端实现
本文详解Python第三方库paramiko的三种核心用法:密码登录、密钥免密登录及SFTP文件传输。重点解析如何通过SSHClient直接调用open_sftp方法,避免Transport与SSHClient分离使用的繁琐。此外,提供基于invoke_shell的交互式终端Demo,展示命令执行、文件操作及常见兼容性问题,助力高效远程运维开发。
介绍
关于paramiko的具体定义,读者可参考其他技术博客或文档,此处不再展开,直接切入核心应用场景。
本次测试所基于的环境版本信息如下:
- python 3.9
- paramiko 2.7.2
- centos 8
三种常用方式
paramiko库在实际开发中主要涵盖以下三种常用操作模式:
- 使用密码进行登录
- 使用密钥免密码登录
- SFTP 传输文件
在众多教程中,SFTP文件传输部分往往显得较为割裂:登录阶段使用SSHClient类,而文件传输却切换到Transport类。笔者也曾长期沿用这种模式。
若读者也采用此法,是否感到些许违和?使用Python的初衷之一便是降低心智负担,为何同一功能要分裂为两种实现方式?是否存在更统一的解决方案?
网络上常见的“统一”做法是:先实例化Transport类,再实例化SSHClient类,最后将Transport实例注入到SSHClient中。这种操作逻辑上总让人觉得不够直观。
关键点:通过查阅源码可知,SSHClient类本身已直接提供SFTP传输文件的实例化方法,直接调用即可,代码结构因此变得清爽简洁。
使用密码进行登录
import paramiko
hostname = 'localhost'
port = 22
username = 'aaron'
# 看密码就知道我是用的redhat系linux系统
password = 'redhat'
# 实例化SSHClient类
ssh = paramiko.SSHClient()
# 远程主机没有本地密钥时的处理规则,主要有三个
# AutoAddPolicy:直接建立连接,不进行yes/no的确认
# WarningPolicy:直接建立连接,但是会提示是新连接
# RejectPolicy:拒绝未知的连接,依赖系统密钥的信息。默认选项。
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接到服务器
ssh.connect(hostname, port, username, password)
# 执行命令,获取标准输入、标准输出、标准错误输出,均为流式输入输出
# 函数原型为 exec_command(self, command, bufsize=-1, timeout=None, get_pty=False, environment=None, )
# 理论上可以通过标准输入,也就是下面的额stdin变量完成连续输入
# 同时参数中有布尔型参数 get_pty 可以指定是否获取 tty 通道,这样阻塞输入,比如sudo输入密码什么的都能做。貌似就可以做成你想要的任何东西。
# 但是以上两点没有验证,貌似比较麻烦,我太懒了-_-|||
#
# 另外,exec_command方法每次都是新开一个通道执行命令,执行完成后状态消失。SSHClient类还提供一个invoke_shell方法,这个方法可以连续输入命令。
# 这两个的区别主要是 invoke_shell使用SSH shell通道,而exec_command使用SSH exec通道。
# shell通道就是常用的终端软件登陆的通道,登陆变量都会进行加载比如 ~/bashrc 等
# 而 exec通道 则不进行加载登陆文件,相当于linux桌面系统上右键开terminal一样。
# 如果你还是不懂,没关系,invoke_shell nb就完事了
stdin, stdout, stderr = ssh.exec_command('df')
# 打印输出
print(stdout.read().decode())
# 不要忘记关闭连接
ssh.close()
使用密钥免密码登录
此处演示使用密钥文件登录。为覆盖一般场景,示例中为密钥文件设置了密码;若仅需纯免密登录,则无需设置密码。
在客户端生成密钥对,并将公钥传输至服务器端。
ssh-keygen -t rsa # 这里设置密码为redhat_rsa,这里是给密钥设置密码,如果想免密,不设置密码即可 ssh-copy-id -i ~/.ssh/id_rsa.pub aaron@localhost
import paramiko
hostname = 'localhost'
port = 22
username = 'aaron'
# 这里是密钥文件的密码
password = 'redhat_rsa'
# 密钥文件的位置,可以是列表,paramiko会把列表里文件顺序尝试,登陆上位置
private_key_path = '/home/aaron/.ssh/id_rsa'
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 这里网上很多先设置pkey传入,但是直接传文件路径也可以,还简单。
# 我使用的和网上不同,另一个版本请自行搜索,资料n多
# 如果没有密钥,则不需添加password
# look_for_keys默认为True,就是会找你 .ssh 目录下有没有合适的密钥文件
# 也就是说如果密钥文件存在,但是你传 key_filename 时传错了,不影响,paramiko已经替你想好了,这才是正经 python 应有的待遇,舒服!
ssh.connect(hostname, port, username=username, password=password, key_filename=private_key_path, look_for_keys=False)
stdin, stdout, stderr = ssh.exec_command('ip a')
print(stdout.read().decode())
ssh.close()
SFTP 传输文件
import paramiko
hostname = 'localhost'
port = 22
username = 'aaron'
password = 'redhat'
# 还是SSHClient登陆,以上两种方式都可以。
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname, port, username, password)
# 重头戏,直接使用打开方法即可
sftp = ssh.open_sftp()
# do something
# 从这里到下面的ssh.close()为止都是sftp能做的事情,具体能做啥,请看下一个代码段,这里只列举上传(put) 下载(get) 文件,这两个也比较重要
# 回调函数,没想到吧,上传下载还能有回调函数
# 参数一定,都是传入的两个size,int型数据
# size 已传输文件累计大小
# file_size 文件总大小
def callback(size, file_size):
print(f"目前传输文件比例: {size} / {file_size}")
# 上传文件,参数都给你们了,看看啥意思就行了
# 主要就是这个confirm, 如果定义会检测一下上传到服务器文件大小和本地大小是否一致,默认False
stat = sftp.put(localpath='/tmp/s.avi', remotepath='/tmp/a.avi', callback=callback, confirm=True)
print(stat)
# 下载文件,同样参数都给你们了,看看啥意思就行了
sftp.get(localpath='/tmp/s.avi', remotepath='/tmp/a.avi', callback=callback)
ssh.close()
SFTP模块支持的高级功能说明。
# 列出当前路径下有什么文件,默认path="."
print(sftp.listdir())
# 列出当前路径下文件属性,默认path="."
attrs = sftp.listdir_attr()
print(attrs)
print("="*20)
# listdir函数就是遍历的每个属性的filename
print(attrs[0].filename)
print(attrs[0].longname)
print(attrs[0].st_atime)
print(attrs[0].st_mtime)
print(attrs[0].st_gid)
print(attrs[0].st_uid)
print(attrs[0].st_mode)
# 就是 listdir_attr 的迭代器版本
attrs = sftp.listdir_iter()
print("="*20)
for i in attrs:
print(i.filename)
print(i.longname)
print(i.st_atime)
print(i.st_mtime)
print(i.st_gid)
print(i.st_uid)
print(i.st_mode)
# 和内置open用法基本相同,只不过是打开外部文件
with sftp.open("a.txt", "w") as f:
f.write("aaa")
# 删除文件,只能删除文件,删除目录使用rmdir函数。文件不存在则报错。
sftp.remove("/home/aaron/a.txt")
# 文件改名,类似于move
sftp.rename("/home/aaron/as.txt", "/tmp/soon.txt",)
# 符合posix标准的改名,没有测试
sftp.posix_rename("/home/aaron/as.txt", "/tmp/soon.txt",)
# 新建目录
sftp.mkdir("/home/aaron/as")
# 删除目录,类似rmdir,删除的必须为空目录
sftp.rmdir("/home/aaron/as")
# 返回单个文件的attr信息,如果是软连接则直接返回真实文件信息
stat = sftp.stat("/tmp/soon.txt")
print(stat)
# 测试和stat差不多,如果是软链接则返回软链接文件信息
stat = sftp.lstat("/tmp/soon.txt")
print(stat)
# 修改权限,权限为八进制数,需要把权限换算为十进制数。比如下面的例子就是权限333
sftp.chmod("/tmp/soon.txt", 219)
# 修改属主和属组,属主和属组为gid和uid表示。需要有权限。
sftp.chown("/tmp/soon.txt", 0, 0)
# 设置atime和mtime,如果传入None,则设置为当前时间。否则必须传入两个元素的元组或数组,分别为 (atime, mtime)
sftp.utime("/tmp/soon.txt", None)
import time
sftp.utime("/tmp/soon.txt", (time.time(), time.time()))
# 读取软链接指定的目标
print(sftp.readlink("/etc/rc.local"))
# 读取软连接制定目标的绝对路径
print(sftp.normalize("/etc/rc.local"))
# 切换工作路径。SFTP没有工作路径的概念,但是paramiko进行了模拟。如果设置了路径,所有的相对路径都是根据这个路径来的。如果想要切换回去传入None即可。
sftp.chdir("/tmp")
# 获取当前的工作路径。如果没有使用chdir切换过,则会返回None
print(sftp.getcwd())
终端交互Demo演示
笔者一直希望构建类似xshell的终端工具。相比之下,macOS上的iterm或iterm2并未达到宣传中的完美程度,或许是因为未曾体验过更优秀的工具。
然而,每次执行exec_command命令时,进程均从家目录启动,无法直接切换目录,操作极为不便。此前进展缓慢,直到发现invoke_shell方法,问题才迎刃而解。
import time
from threading import Thread
import paramiko
# 接收消息并打印的函数
# 返回的消息会分成好几段,如果只是发送命令后直接打印是打印不全的,这里直接循环检测缓冲区,有结果就打印。
def recv_and_print(channel):
# 定义全局变量,recv_func_flag 此接收函数退出标志,cmd 当前执行命令
global recv_func_flag, cmd
while recv_func_flag:
# 如果此次命令是exit并且退出完成,则设置退出标志
if cmd == "exit" and channel.exit_status_ready():
# 打印退出状态,为int型数字
print(f"此次退出状态:{channel.recv_exit_status()}")
# 退出标志置为假
recv_func_flag = False
# 吐过缓冲区有数据
if channel.recv_ready():
# 接收数据
response = channel.recv(1024).decode().strip()
# 需要注意的是接收的数据会把传入的命令也返回一遍,这里我们只保留自己打在屏幕上的,不要传回的,所以传回的数据如果和命令相同则不打印,略过
if response != cmd:
print(response, end="")
print("接收函数退出......")
# 定义全局变量
recv_func_flag = True
cmd = ""
# ssh登陆,老一套东西
hostname = 'localhost'
port = 22
username = 'aaron'
password = 'redhat010;'
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname, port, username, password)
# 获取invoke_shell
invoke_shell = ssh.invoke_shell()
# 接收函数使用另一个线程运行,因为和当前主线程一同退出,所以不需要join方法
t = Thread(target=recv_and_print, args=(invoke_shell,))
t.start()
# 主线程退出标志
flag = True
while flag:
# 输入命令
cmd = input()
# 输入命令必须有回车才会执行,这里我发送的是linux命令,\r之后能执行命令,如果系统不同,需要测试\r\n等回车字符
invoke_shell.send(f"{cmd}\r")
# 如果命令是exit则退出循环
if cmd == "exit":
flag = False
# 检测接收函数已经退出,这里停止0.5s是因为退出命令发送给服务器,服务器会返回注销的信息,之后检测接收函数才会完全退出,认为设置了一个等待时间,这个时间因为是本机,设置的相对不长,如果是其他主机,需要根据网络以及超时情况进行设置
while recv_func_flag:
time.sleep(0.5)
# 别忘了关闭ssh
ssh.close()
该Demo可直接运行并输入命令,体验如同通过终端直接登录服务器。
系统中定义的alias别名(如ll等)亦可正常使用。
目前尚未实现双击Tab补全或Ctrl+C中断等功能,留待读者进一步完善。
该Demo目前仍存在若干未解决的问题。受限于时间,暂不深入修复,待后续实际需求时再行探究。
- 换行处理存在波动,有时正常有时异常。观察发现,每次发送数据时,命令、结果及信息提示符的合并或分离发送并无固定规律。这可能与Linux发行版差异或TCP通信机制有关,具体原因尚不明确(建议读者自行测试验证)。
- 显示信息采用print(response, end="")输出,退出时亦复用相同逻辑。这导致系统注销信息与函数提示信息“接收函数退出......”直接拼接(注:此处本可插入回车符,但测试已终止,不再深究)。
- su命令可正常执行,输入密码不影响流程,但密码会以明文形式显示。
- 承接上条,su命令的退出存在障碍:使用exit退出时,整个函数随之结束。暂不修复此问题。
至此,关于Python第三方库paramiko的介绍告一段落。如需了解更多相关内容,请搜索本站往期文章或继续浏览后续内容,感谢大家的支持!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













