最近在跟一个自动化发布平台的建设事项,其中 Linux 系统的远程控制通道则由我独立开发完成,其中涉及到了 Linux 系统远程命令和文件传输操作。
因为之前写 Linux 系统密码管理系统的时候,用的是 Paramiko 的 SSHClient。所以,我这次依然采用 Paramiko 来做实现,代码虽短,说起其中的坑,我也是一把辛酸一把泪的填上了。
先上完整代码:、
# -*- coding: utf-8 -*-
import os
import socket
import paramiko
import pysftp
'''
Name: remoteCtrl
Author: Jager @ zhangge.net
Description: remote command and file transfer API Base on paramiko and pysftp
Date: 2017-3-9 16:25:24
'''
class remoteCtrl(object):
# Description : remote command.
def command(self, ip, passwd, cmd, port=22, user='root', timeout=60):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(hostname=ip, port=int(port), username=user, password=passwd, timeout=timeout,allow_agent=False,look_for_keys=False)
# 连接超时
except socket.timeout as e:
return 502, e
# 密码错误
except paramiko.ssh_exception.AuthenticationException:
print "Password [%s] error" % passwd
client.close()
return 403, "Password [%s] error" % passwd
# 其他错误
except Exception as e:
print e
# 系统重装后会出现hostkey验证失败问题,需要先删除known_hosts中记录,用法:若返回503则重新下发即可
if "Host key for server" in str(e):
os.system('sed -i "/^\[%s].*/d" ~/.ssh/known_hosts' % ip)
client.close()
return 503,e
else:
client.close()
return 1,e
# 执行命令之前设置为utf8环境,其中 1>&2 尤为重要,可以解决paramiko远程执行后台脚本僵死的问题
stdin,stdout,stderr=client.exec_command("export LANG=en_US.UTF-8;export LC_ALL=en_US.UTF-8;%s 1>&2" % cmd)
result_info = ""
for line in stderr.readlines(): #因为有了 1>&2,所以读的是stderr
result_info += line
# 返回状态码和打屏信息
return stderr.channel.recv_exit_status(), result_info
# Description : paramiko & pysftp & sftp transfer.
def transfer(self,ip, passwd, src, dst, action='push', user = 'root' , port = 36000, timeout=60):
# 忽略hostkeys错误
cnopts = pysftp.CnOpts()
cnopts.hostkeys = None
# 若src以斜杠结尾,则去掉这个斜杠,是否是目录后面会有判断逻辑
if src[-1] == '/':
src = src[0:-1]
try:
with pysftp.Connection(ip, username=user, password=passwd, port=int(port), cnopts=cnopts) as sftp:
# 拉取文件或目录
if action == 'pull':
try:
# 判断远程来源是目录还文件
if sftp.isdir(src):
# 判断本地目录是否存在,若不存在则创建
if not os.path.exists(dst):
try:
os.makedirs(dst)
except Exception as e:
print e
pass
# 若为目录则分别取得父目录和需要操作的目录路径,进入父目录,然后执行sftp
parent_dir = src.rsplit('/',1)[0]
opt_dir = src.rsplit('/',1)[1]
sftp.chdir(parent_dir)
sftp.get_r(opt_dir,dst, preserve_mtime=True)
else:
# 拉取src远程文件到dst本地文件夹
if dst[-1] == '/':
# 判断本地目录是否存在,若不存在则创建
if not os.path.exists(dst):
try:
os.makedirs(dst)
except Exception as e:
print e
pass
os.chdir(dst)
sftp.get(src, preserve_mtime=True)
# 拉取src远程文件到dst本地文件
else:
file_dir = dst.rsplit('/',1)[0]
dst_file = dst.rsplit('/',1)[1] # 取得目标文件名称
# 判断本地目录是否存在,若不存在则创建
if not os.path.exists(file_dir):
try:
os.makedirs(file_dir)
except Exception as e:
print e
pass
os.chdir(file_dir)
sftp.get(src,dst_file, preserve_mtime=True)
except Exception as e:
return 1,e
else:
try:
# 判断本地文件是目录还是文件,若是目录则使用put_r 递归推送
if os.path.isdir(src):
# 判断目的目录是否存在,若不存在则创建
if not sftp.exists(dst):
try:
sftp.makedirs(dst)
except Exception as e:
print e
pass
sftp.put_r(src,dst,preserve_mtime=True)
# 否则先进入目标目录,然后使用put单文件推送
else:
# 推送src源文件到dst目的文件夹
if dst[-1] == '/':
# 判断目的目录是否存在,若不存在则创建
if not sftp.exists(dst):
try:
sftp.makedirs(dst)
except Exception as e:
print e
pass
sftp.chdir(dst)
sftp.put(src,preserve_mtime=True)
# 推送src源文件到dst目的文件
else:
file_dir = dst.rsplit('/',1)[0]
# 判断目的目录是否存在,若不存在则创建
if not sftp.exists(file_dir):
try:
sftp.makedirs(file_dir)
except Exception as e:
print e
pass
sftp.chdir(file_dir)
sftp.put(src,dst,preserve_mtime=True)
except Exception as e:
return 1,e
return 0, 'success'
except socket.timeout as e:
return 502,e
except paramiko.ssh_exception.AuthenticationException:
print "Password [%s] error" % passwd
client.close()
return 403,"Password [%s] error" % passwd
except Exception as e:
print e
# 系统重装后会出现hostkey验证失败问题,需要先删除known_hosts中记录
if "Host key for server" in str(e):
os.system('sed -i "/^\[%s].*/d" ~/.ssh/known_hosts' % ip)
client.close()
return 503,'Hostkeys Error'
else:
client.close()
return 1, e
简单说下用法:
# 先在Python脚本中载入,需要提前安装paramiko和pysftp插件(推荐pip命令安装)
from xxxx import remoteCtrl
# 执行远程命令,需要传入远程服务器ip地址、密码、命令、远程ssh端口,用户名和超时时间
myHandler = remoteCtrl()
ret, ret_info = myHandler.command(ip, password, cmd, port, user, timeout )
#### ret 表示最后一个命令的退出状态,ret_info 则是远程命令的打屏信息(含报错)
# 进行文件传输,需要传入远程服务器ip地址、密码、源文件路径、目标文件路径、传输动作(pull/push)、用户名、端口和超时时间
myHandler = remoteCtrl()
ret, ret_info = myHandler.transfer(ip, password, src, dst , action, user, port, timeout )
#### ret 表示传输结果,ret_info 是返回信息
代码很简单,不清楚的请注意代码中的注释,下面啰嗦下文件传输的说明:
①、规定目标文件夹(dst)必须以斜杠 / 结尾,否则识别为文件,而 src 因是实体存在,所以程序会自动判断是文件还是文件夹。
②、当执行本地文件夹推送至远程文件夹时,将不会保留本地文件夹名称,而是将本地文件夹内的所有文件推送到远程文件夹内,比如:
/data/srcdir/ 传送到 /data/dstdir/ ,结果是 srcdir 下的所有文件会存储在 dstdir
若想保留文件夹名称,请保证两端文件夹名称一致即可,比如:
/data/srcdir/ 推送到 /data/srcdir/
③、文件传输 demo:
将本地的/data/src.tar.gz 推送到 192.168.0.10 服务器的/data/files/dst.tar.gz
myHandler = remoteCtrl()
ret, ret_info = myHandler.transfer('192.168.0.10','123456','/data/src.tar.gz','/data/files/dst.tar.gz', 'push' )
Ps:若 action='pull'则表示将 src 拉取到本地的 dst。