一、深入理解 paramiko
封装前奏
SSHClient()
部分源码
从上面的源码中不难看出,SSHClient()
的实例对象,最终也是使用了 Tansport()
的实例对象。
所以,我们也可以创建一个 Transport
的连接对象让 SSHClint
的对象使用
封装高潮
建立互信
ssh-keygen -t ecdsa -N ""
ssh-copy-id -i ~/.ssh/id_ecdsa.pub -f [email protected]
Centos7 系统采用了新的密钥算法,所以需要使用新的算法生成密钥对儿。
之后再用这个指定的公钥建立免密信任关系。
执行命令
import paramiko
# 创建一个本地当前用户的私钥对象
private_key = paramiko.ECDSAKey.from_private_key_file(
'/Users/yanshunjun/.ssh/id_ecdsa')')
# 创建一个传输对象
transport = paramiko.Transport(('172.16.153.10', 22))
transport.connect(username='root', pkey=private_key)
# 创建一个 ssh 客户端对象
ssh = paramiko.SSHClient()
# 使用传输对象
ssh._transport = transport
# 执行相关命令
stdin, stdout, stderr = ssh.exec_command('df')
# 获取正确的命令结果,要在关闭传输对象之前把命令的结果赋值给一个变量
r = str(stdout.read(),encoding='utf-8')
# 关闭传输对象
transport.close()
print(r)
传输文件
import paramiko
# 创建一个本地当前用户的私钥对象
private_key = paramiko.ECDSAKey.from_private_key_file(
'/Users/yanshunjun/.ssh/id_ecdsa')
# 创建一个传输对象
transport = paramiko.Transport(('172.16.153.10', 22))
# 使用刚才的传输对象创建一个传输文件的的连接对象
transport.connect(username='root', pkey=private_key)
sftp = paramiko.SFTPClient.from_transport(transport)
# 将 client.py 上传至服务器 /tmp/test.py
sftp.put('client.py', '/tmp/test.py')
# 将远程主机的文件 /tmp/test.py 下载到本地并命名为 some.txt
sftp.get('/tmp/test.py', 'some.txt')
transport.close()
合并
利用上面的共同点,我们可以写一个类,让执行命令和传输文件使用同一个传输对象。
这样我们就可以只需要和服务器建立一次连接,就可以同时实现执行命令和传输文件了。、
import paramiko
class MySSHClient(object):
def __init__(self,host, username, password=None, port=22, pkey_path=None):
self.username = username
self.password = password
self.host = host
self.port = port
self.pkey = pkey_path
self.private_key = None
self.__transport = None
self.sftp = None
self.ssh = None
def connect(self):
"""
创建一个传输对象
:return:
"""
if not self.host:
raise ConnectionError("服务器 ip 空")
self.__transport = paramiko.Transport((self.host, self.port))
if any([self.password,self.pkey]):
pass
else:
raise ConnectionError("密码或密钥为空")
if self.pkey:
self.private_key = paramiko.ECDSAKey.from_private_key_file(self.pkey)
# 使用刚才的传输对象创建一个传输文件的的连接对象
self.__transport.connect(username=self.username, pkey=self.private_key)
else:
self.__transport.connect(username=self.username, password=self.password)
def get_sftp(self):
self.sftp = paramiko.SFTPClient.from_transport(self.__transport)
def get_ssh(self):
"""
创建一个 ssh 客户端对象
:return:
"""
self.ssh = paramiko.SSHClient()
# 使用传输对象
self.ssh._transport = self.__transport
def put_file(self,local_file, remote):
if not self.__transport:
self.connect()
if not self.sftp:
self.get_sftp()
self.sftp.put(local_file, remote)
def get_file(self, remote, local_file):
if not self.__transport:
self.connect()
if not self.sftp:
self.get_sftp()
self.sftp.get(remote, local_file)
def exec_cmd(self, cmd):
# 执行相关命令
if not self.__transport:
self.connect()
if not self.ssh:
self.get_ssh()
# 执行命令
stdin, stdout, stderr = self.ssh.exec_command(cmd)
if stdout:
r = str(stdout.read(),encoding='utf-8')
else:
r = str(stderr.read(),encoding='utf-8')
print(r)
def close(self):
self.__transport.close()
使用示例
ssh = MySSHClient(
username='root',
pkey_path='/Users/yanshunjun/.ssh/id_ecdsa',
host='172.16.153.10'
)
ssh.exec_cmd('df')
ssh.put_file('some.txt', '/tmp/a.py')
ssh.exec_cmd('cat /tmp/a.py')
ssh.close()
二、堡垒机简介
三、设计
- 运维人员在每个服务器上创建不用权限的系统用户。
- 运维人员登录到堡垒机上,配置并建立和每个服务器的免密登录信任关系。
- 运维人员根据权限,在堡垒机上创建出来每个相关人员需要的账户,这个账号专门登录堡垒机。
- 相关人员使用自己授权的账户登录(输入用户名密码)到堡垒机,现实当前用户有权管理的服务器列表。
- 用户选择服务器,并自动登陆
- 执行操作并同时将用户操作记录
四、实现
输入回车时发送命令到远程服务器
# coding:utf-8
# 2019/3/19 15:15
import select,sys, socket
import paramiko
from paramiko.py3compat import u
private_key = paramiko.ECDSAKey.from_private_key_file('/Users/yanshunjun/.ssh/id_ecdsa')
transport = paramiko.Transport(('172.16.153.10', 22))
transport.start_client()
transport.auth_publickey(username='root', key=private_key)
# 打开一个通道
chanel = transport.open_session()
# 获取一个终端
chanel.get_pty()
# 激活器
chanel.invoke_shell()
while True:
# 监视用户输入和服务器返回数据
# sys.stdin 处理用户输入
# chan 是之前创建的通道,用于接收服务器返回信息
readable, writeable, error = select.select([chanel, sys.stdin, ], [], [], 1)
if chanel in readable:
try:
# 将 bytes 转换为 字符串
x = u(chanel.recv(1024))
if len(x) == 0:
print('\r\n******退出堡垒机******\r\n')
break
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in readable:
inp = sys.stdin.readline()
chanel.sendall(inp)
chanel.close()
transport.close()
每次输入一个字符就发送到远程服务器
# coding:utf-8
# 2019/4/14 14:39
import select,sys, socket, termios, tty
import paramiko
from paramiko.py3compat import u
private_key = paramiko.ECDSAKey.from_private_key_file('/Users/yanshunjun/.ssh/id_ecdsa')
transport = paramiko.Transport(('172.16.153.10', 22))
transport.start_client()
transport.auth_publickey(username='root', key=private_key)
# 打开一个通道
chanel = transport.open_session()
# 获取一个终端
chanel.get_pty()
# 激活器
chanel.invoke_shell()
# 获取原tty属性
old_tty = termios.tcgetattr(sys.stdin)
try:
# 为tty设置新属性
tty.setraw(sys.stdin.fileno())
chanel.settimeout(0.0)
while True:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e = select.select([chanel, sys.stdin], [], [], 1)
if chanel in r:
try:
x = u(chanel.recv(1024))
if len(x) == 0:
print('\r\n******退出堡垒机******\r\n')
break
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in r:
# 每次只读一个字符
x = sys.stdin.read(1)
if len(x) == 0:
break
chanel.send(x)
finally:
# 最后设置终端属性为最初的属性
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
五、完整版及使用方法
将下面的内容保存为一个 shark_paramiko.py
的文件,之后放到 /root/
目录下
在 ~/bashrc
文件的最后添加如下内容
/usr/bin/env python ~/shark_paramiko.py
logout
# coding:utf-8
# 2019/4/14 14:39
import os, select,sys, socket, termios, tty, getpass
import glob
import paramiko
from paramiko.py3compat import u
from paramiko.ssh_exception import SSHException
username = getpass.getuser()
inp_user = input("User:")
hostname = input("Host Name/IP:")
pwd = input("Password:")
if inp_user:
username = inp_user
user_home = os.environ['HOME']
pkey_dir = os.path.join(user_home, '.ssh')
pkey_path_li = glob.glob(pkey_dir+'/id*')
pkey_path_li = [i for i in pkey_path_li if not i.endswith('pub')]
pkey_path = pkey_path_li[0]
try:
private_key = paramiko.RSAKey.from_private_key_file(pkey_path)
except SSHException as e:
private_key = paramiko.ECDSAKey.from_private_key_file(pkey_path)
transport = paramiko.Transport(hostname)
transport.start_client()
if not pwd:
transport.auth_publickey(username=username, key=private_key)
else:
transport.auth_password(username=username, password=pwd)
# 打开一个通道
chanel = transport.open_session()
# 获取一个终端
chanel.get_pty()
# 激活器
chanel.invoke_shell()
# 获取原tty属性
old_tty = termios.tcgetattr(sys.stdin)
try:
# 为tty设置新属性
tty.setraw(sys.stdin.fileno())
chanel.settimeout(0.0)
while True:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e = select.select([chanel, sys.stdin], [], [], 1)
if chanel in r:
try:
x = u(chanel.recv(1024))
if len(x) == 0:
print('\r\n******退出堡垒机******\r\n')
break
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in r:
# 每次只读一个字符
x = sys.stdin.read(1)
if len(x) == 0:
break
chanel.send(x)
finally:
# 最后设置终端属性为最初的属性
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
chanel.close()
transport.close()
记录用户的操作
# coding:utf-8
# 2019/4/14 14:39
import os, select,sys, socket, termios, tty, getpass
import glob
import paramiko
from paramiko.py3compat import u
from paramiko.ssh_exception import SSHException
username = getpass.getuser()
inp_user = input("User:")
hostname = input("Host Name/IP:")
pwd = input("Password:")
if inp_user:
username = inp_user
user_home = os.environ['HOME']
pkey_dir = os.path.join(user_home, '.ssh')
pkey_path_li = glob.glob(pkey_dir+'/id*')
pkey_path_li = [i for i in pkey_path_li if not i.endswith('pub')]
pkey_path = pkey_path_li[0]
try:
private_key = paramiko.RSAKey.from_private_key_file(pkey_path)
except SSHException as e:
private_key = paramiko.ECDSAKey.from_private_key_file(pkey_path)
transport = paramiko.Transport(hostname)
transport.start_client()
if not pwd:
transport.auth_publickey(username=username, key=private_key)
else:
transport.auth_password(username=username, password=pwd)
# 打开一个通道
chanel = transport.open_session()
# 获取一个终端
chanel.get_pty()
# 激活器
chanel.invoke_shell()
# 获取原tty属性
old_tty = termios.tcgetattr(sys.stdin)
log = open('handler.log', 'a', encoding='utf-8')
try:
# 为tty设置新属性
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
chanel.settimeout(0.0)
tab_flag = False
while True:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e = select.select([chanel, sys.stdin], [], [], 1)
if chanel in r:
try:
x = u(chanel.recv(1024))
if len(x) == 0:
print('\r\n******退出堡垒机******\r\n')
break
if tab_flag:
if x.startswith('\r\n'):
print('yan>>>', [x])
pass
else:
log.write(x)
log.flush()
tab_flag = False
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in r:
# 每次只读一个字符
x = sys.stdin.read(1)
if len(x) == 0:
break
if x == '\t':
tab_flag = True
else:
log.write(x)
log.flush()
chanel.send(x)
finally:
# 最后设置终端属性为最初的属性
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
log.close()
chanel.close()
transport.close()