由于工作中经常用到这两个库,不过基本上用的fabric来实现环境部署、预检、数据上传下发等操作。因此,本文重点介绍Fabric的使用(fabric2版本)。
paramiko
paramiko是使用SSHv2协议的三方库,提供了客户端和服务端的功能。
Welcome to Paramiko’s documentation! — Paramiko documentation
安装
pip install paramiko
paramiko中的几个重要组件:
SSHClient
:ssh服务器会话的高级封装,封装了Transport
,Channel
, andSFTPClient
SFTPClient
:基于一个已连通的Transport打开一个sftp会话,可实现对文件的操作(上传下载等)
Channel
:一种ssh传输的安全通道,类似socket
Transport
:一种协商加密的会话,会创建tunnels通道流,称为channels。多个channels可以在单个会话中多路复用
SSHClient
连接
- 通过密钥连接,有两种方式:
- 实例化
SSHClient
- 创建一个
Transport
加密通道
- 实例化
- 通过密码连接,有两种方式:
- 实例化
SSHClient
- 创建一个
Transport
加密通道
- 实例化
"""密钥连接(通过Transport连接)"""
private = paramiko.RSAKey.from_private_key_file('/xxx/xxx')
transport = paramiko.Transport((host, port))
transport.connect(username=user, pkey=private)
"""密钥连接(通过SSHClient连接)"""
private = paramiko.RSAKey.from_private_key_file('/xxx/xxx')
ssh_client = paramiko.SSHClient()
ssh_client.connect(hostname=host,port=22,username=user,pkey=private)
"""密码连接(通过Transport连接)"""
transport = paramiko.Transport((host, port))
transport.connect(username=user, password=password)
"""密码连接(通过SSHClient连接)"""
ssh_client = paramiko.SSHClient()
ssh_client.connect(hostname=host,port=22,username=user,password=password)
执行SSH命令
通过exec_command()
方法打开一个channel在服务器上执行命令,相当于我们使用ssh客户端工具执行。
若要执行多个命令可以通过 &&
连接,或者for循环调用exec_command()
,这将会打开多个channel。
stdin, stdout, stderr = ssh_client.exec_command("rm -i test1.txt && cd /home")
执行完成后会一次性返回stdout和stderr,同时可以通过stdout.channel.recv_exit_status()
获取执行结果,该方法是一个阻塞方法,如果想实时获取日志输出可以封装一下,代码如下:
def exec_cmd(ssh_client, commands, f):
"""
:param commands: 待执行命令列表
:param f: 文件对象
:return: 执行状态
"""
status = []
for cmd in commands:
stdin, stdout, stderr = ssh_client.exec_command(cmd)
f.write(f"start run command: {cmd}\n")
while True:
err, out = stderr.readline().strip(), stdout.readline().strip()
if not (out or err):
break
if out:
f.write(out + "\n")
if err:
f.write(err + "\n")
status.append(stdout.channel.recv_exit_status())
return status
关闭连接
ssh_client.close()
transport.close()
SFTPClient
创建SFTP客户端通道
transport = paramiko.Transport((host, port))
transport.connect(username=user, password=password)
# 基于已连通的transport创建一个SFTP客户端channel
# sftp = paramiko.SFTPClient.from_transport(transport)
sftp = transport.open_sftp_client()
# 或者通过SSHClient创建
ssh_client = paramiko.SSHClient()
ssh_client._transport = transport
sftp = ssh_client.open_sftp()
常用操作
listdir(path)
:遍历远程路径下的文件目录(只能遍历一级目录和文件)
listdir_attr(path)
:遍历远程路径下的文件目录,并且可以获取每个文件的详细属性
remove(path)
:删除文件
rmdir(path)
:删除目录
mkdir(path)
:创建目录
stat(path)
:获取文件属性信息(若为软链接文件,获取的是指向的源文件)
lstat(path)
:获取文件属性信息(若为软链接文件,获取的是自身文件)
get(remotepath, localpath)
:下载
put(localpath, remotepath)
:上传
检查远程文件是否存在
def normpath(path):
"""
由于windows和linux操作系统不同,路径格式会出现不统一的情况,反斜杠不处理的话会出现很多问题
替换windows路径中的\
:param path:
:return:
"""
if isinstance(path, Path):
path = str(path)
return path.replace('\\', '/')
def check_remote_path(remote_path, is_mkdir=False):
"""
判断目标机器路径是否存在
:param remote_path:
:param is_mkdir: 若为True,则会创建此路径
:return:
"""
remote_path = normpath(remote_path)
try:
sftp.lstat(remote_path)
return True
except FileNotFoundError:
if is_mkdir:
sftp.mkdir(remote_path)
else:
return False
判断远程路径是否为目录
def remote_path_isdir(remote_path):
"""
检查一个远程路径是否为目录
:param remote_path:
:return:
"""
attr = sftp.lstat(normpath(remote_path))
return stat.S_ISDIR(attr.st_mode)
上传
注意:远程目录必须存在,否则会报错
def get(remote_path, local_path):
"""
下载文件或目录
:param remote_path: 目标机器路径(注意路径反斜杠问题会报错)
:param local_path: 本地路径
:return:
"""
# 判断远程路径是否存在
if not check_remote_path(remote_path):
return False
local_path, remote_path = Path(local_path), Path(remote_path)
def find_files(remote_path, local_path):
for sftp_attr in sftp.listdir_attr(normpath(remote_path)):
filename = sftp_attr.filename
if filename.startswith('.'): # 过滤隐藏文件
continue
local_dir, remote_dir = local_path.joinpath(filename), remote_path.joinpath(filename)
# 若为目录,则递归调用
if stat.S_ISDIR(sftp_attr.st_mode): # st_mode判断文件类型(目录还是文件)
local_dir.mkdir(parents=True, exist_ok=True) # parents为True支持多级创建,exist_ok 存在就不创建
find_files(remote_dir, local_dir)
else:
local_dir.parent.mkdir(parents=True, exist_ok=True)
sftp.get(normpath(remote_dir), normpath(local_dir))
# 下载目录
if remote_path_isdir(remote_path):
find_files(remote_path, local_path)
# 下载单个文件,如果没有设置本地文件名,默认为远程路径中的名字
else:
if local_path.is_dir():
local_path.mkdir(parents=True, exist_ok=True)
local_path = local_path.joinpath(remote_path.name)
else:
local_path.parent.mkdir(parents=True, exist_ok=True)
sftp.get(normpath(remote_path), normpath(local_path))
return True
下载
注意:本地目录必须存在,否则会报错
def put(local_path, remote_path):
local_path, remote_path = Path(local_path), Path(remote_path)
if local_path.is_dir():
for path in local_path.rglob('[!.]*'):
# 拼接远程路径,relative_to获取相对路径
remote = remote_path.joinpath(path.relative_to(local_path))
if path.is_file():
check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建
sftp.put(normpath(path), normpath(remote))
# 上传单个文件
else:
check_remote_path(remote_path.parent, is_mkdir=True)
if remote_path_isdir(remote_path): # 若远程路径是一个目录,就将本地文件名作为默认名字
remote_path = remote_path.joinpath(local_path.name)
sftp.put(normpath(local_path), normpath(remote_path))
return True
关闭
sftp.close()
封装
class SSHConnection:
def __init__(self, host, user, password, port=22, mylogger=None):
self._host = host
self._user = user
self._password = password
self._port = port
self._transport = None
self._sftp = None
self._client = None
self.mylogger = mylogger
self.connect()
def connect(self):
# 密钥方式
# private = paramiko.RSAKey.from_private_key_file('/xxx/xxx')
# transport = paramiko.Transport((self._host, self._port))
# transport.connect(username=self._user, pkey=private)
# 密码连接方式(通过Transport连接,或 者通过SSHClient连接)
# transport 一种加密的会话,会创建一个加密通道
transport = paramiko.Transport((self._host, self._port))
transport.connect(username=self._user, password=self._password)
self._transport = transport
def exec_cmd(self, commands: list) -> List:
"""
:param commands: 待执行命令,支持list
:return: 执行状态
"""
# 实例化SSHClient
if not self._client:
self._client = paramiko.SSHClient()
self._client._transport = self._transport
status = []
for cmd in commands:
stdin, stdout, stderr = self._client.exec_command(cmd)
self.writer(f"start run command: {cmd}")
while True:
err, out = stderr.readline().strip(), stdout.readline().strip()
if not (out or err):
break
if out:
self.writer(out)
if err:
self.writer(err, level='error')
status.append(stdout.channel.recv_exit_status())
return status
def remote_path_isdir(self, remote_path):
"""
检查一个远程路径是否为目录
:param remote_path:
:return:
"""
attr = self._sftp.lstat(self.normpath(remote_path))
return stat.S_ISDIR(attr.st_mode)
def check_remote_path(self, remote_path, is_mkdir=False):
"""
判断目标机器路径是否存在
:param remote_path:
:param is_mkdir: 若为True,则会创建此路径
:return:
"""
remote_path = self.normpath(remote_path)
try:
self._sftp.lstat(remote_path)
return True
except FileNotFoundError:
if is_mkdir:
self._sftp.mkdir(remote_path)
else:
return False
@staticmethod
def check_local_path(local_path, is_mkdir=True):
"""
判断本地路径是否存在
:param local_path:
:param is_mkdir:
:return:
"""
if isinstance(local_path, Path):
local_path = str(local_path)
if not os.path.exists(local_path) and is_mkdir:
os.makedirs(local_path)
def writer(self, message, level=None):
"""
自定义写入文件方法,同时支持logger或文件对象
:param message:
:param level:
:return:
"""
if self.mylogger:
if isinstance(self.mylogger, logging.Logger):
if not level:
self.mylogger.info(message)
elif level == 'error':
self.mylogger.error(message)
else:
self.mylogger.warning(message)
else:
self.mylogger.write(message+"\n")
else:
print(message+"\n")
def get(self, remote_path, local_path):
"""
下载文件或目录
:param remote_path: 目标机器路径(注意路径反斜杠问题会报错)
:param local_path: 本地路径
:return:
"""
if not self._sftp:
# 创建一个已连通的sftp client
# self._sftp = paramiko.SFTPClient.from_transport(self._transport)
self._sftp = self._transport.open_sftp_client()
if not self.check_remote_path(remote_path):
self.writer(f"路径不存在:{remote_path}", level='error')
return False
local_path, remote_path = Path(local_path), Path(remote_path)
def find_files(remote_path, local_path):
for sftp_attr in self._sftp.listdir_attr(self.normpath(remote_path)):
filename = sftp_attr.filename
if filename.startswith('.'): # 过滤隐藏文件
continue
local_dir, remote_dir = local_path.joinpath(filename), remote_path.joinpath(filename)
# 若为目录,则递归调用
if stat.S_ISDIR(sftp_attr.st_mode): # st_mode判断文件类型(目录还是文件)
local_dir.mkdir(parents=True, exist_ok=True) # parents为True支持多级创建,exist_ok 存在就不创建
find_files(remote_dir, local_dir)
else:
local_dir.parent.mkdir(parents=True, exist_ok=True)
self._sftp.get(self.normpath(remote_dir), self.normpath(local_dir))
self.writer(f"download file {remote_dir} -> {local_dir} successful!")
# 下载目录
if self.remote_path_isdir(remote_path):
find_files(remote_path, local_path)
# 下载单个文件,如果没有设置本地文件名,默认 为远程路径中的名字
else:
if local_path.is_dir():
local_path.mkdir(parents=True, exist_ok=True)
local_path = local_path.joinpath(remote_path.name)
else:
local_path.parent.mkdir(parents=True, exist_ok=True)
self._sftp.get(self.normpath(remote_path), self.normpath(local_path))
self.writer(f"download file {remote_path} -> {local_path} successful!")
return True
def put(self, local_path, remote_path):
if not self._sftp:
self._sftp = paramiko.SFTPClient.from_transport(self._transport)
local_path, remote_path = Path(local_path), Path(remote_path)
if local_path.is_dir():
for path in local_path.rglob('[!.]*'):
# 拼接远程路径,relative_to获取相对路径
remote = remote_path.joinpath(path.relative_to(local_path))
if path.is_file():
self.check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建
self._sftp.put(self.normpath(path), self.normpath(remote))
self.writer(f"upload the file {path} successful!")
# 上传单个文件
else:
self.check_remote_path(remote_path.parent, is_mkdir=True)
if self.remote_path_isdir(remote_path): # 若远程路径是一个目录,就将本地文件名作为默认名字
remote_path = remote_path.joinpath(local_path.name)
self._sftp.put(self.normpath(local_path), self.normpath(remote_path))
self.writer(f"upload the file {local_path} successful!")
return True
def close(self):
if self._client:
self._client.close()
if self._transport:
self._transport.close()
if self._sftp:
self._sftp.close()
self._client, self._transport, self._sftp = None, None, None
@staticmethod
def normpath(path):
"""
由于windows和linux操作系统不同,路径格式会出现不统一的情况,反斜杠不处理的话会出现很多问题
替换windows路径中的\
:param path:
:return:
"""
if isinstance(path, Path):
path = str(path)
return path.replace('\\', '/')
fabric
fabric是基于paramiko的进一步封装,使用起来更加方便。fabric共有三个版本:fabric1、fabric2、fabric3,其中fabric3是非官方版本,所以不推荐使用,建议使用fabric2版本。
官方文档:https://www.fabfile.org/
安装
pip install fabric
或者pip install fabric2
,都是安装最新的官方版本。
使用
连接
from fabric import Connection
conn = Connection(f"{user}@{host}:{port}",
connect_kwargs={"password": password},
connect_timeout=5, # 5s超时
)
# 多个命令之间用&&或;连接
conn.run("ls")