paramiko 堡垒机

时间:2023-03-09 09:13:28
paramiko 堡垒机

用paramiko写堡垒机

paramiko

paramiko模块,基于SSH用于连接远程服务器并执行相关操作。

基本用法

SSHClient

基于用户名密码连接:

基础用法:

import paramiko

# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='c1.salt.com', port=22, username='wupeiqi', password='123') # 执行命令
stdin, stdout, stderr = ssh.exec_command('ls')
# 获取命令结果
result = stdout.read() # 关闭连接
ssh.close()

SSHClient 封装 Transport

import paramiko

transport = paramiko.Transport(('hostname', 22))
transport.connect(username='wupeiqi', password='123') ssh = paramiko.SSHClient()
ssh._transport = transport stdin, stdout, stderr = ssh.exec_command('df')
print stdout.read() transport.close()

由此我们可以看出来,ssh执行命令时,我们可以使用sshclient transport两种方式来实现

基于公钥密钥连接:

基础用法:

import paramiko

private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')

# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='c1.salt.com', port=22, username='wupeiqi', key=private_key) # 执行命令
stdin, stdout, stderr = ssh.exec_command('df')
# 获取命令结果
result = stdout.read() # 关闭连接
ssh.close()

封装transport:

import paramiko

private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')

transport = paramiko.Transport(('hostname', 22))
transport.connect(username='wupeiqi', pkey=private_key) ssh = paramiko.SSHClient()
ssh._transport = transport stdin, stdout, stderr = ssh.exec_command('df') transport.close()

SFTPClient

用于连接远程服务器并执行上传下载

基于用户名密码上传下载:

import paramiko

transport = paramiko.Transport(('hostname',22))
transport.connect(username='wupeiqi',password='123') sftp = paramiko.SFTPClient.from_transport(transport)
# 将location.py 上传至服务器 /tmp/test.py
sftp.put('/tmp/location.py', '/tmp/test.py')
# 将remove_path 下载到本地 local_path
sftp.get('remove_path', 'local_path') transport.close()

基于公钥密钥上传下载:

import paramiko

private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')

transport = paramiko.Transport(('hostname', 22))
transport.connect(username='wupeiqi', pkey=private_key ) sftp = paramiko.SFTPClient.from_transport(transport)
# 将location.py 上传至服务器 /tmp/test.py
sftp.put('/tmp/location.py', '/tmp/test.py')
# 将remove_path 下载到本地 local_path
sftp.get('remove_path', 'local_path') transport.close()

有此看出,如果只做上传下载方式的话,我们只能使用transport,其实无论是ssh,还是sftp,都是调用了transport,基于socket实现的

生产需求:上传某文件并覆盖

demo:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import paramiko
import uuid class SSHConnection(object): def __init__(self, host='172.16.103.191', port=22, username='wupeiqi',pwd='123'):
self.host = host
self.port = port
self.username = username
self.pwd = pwd
self.__k = None def create_file(self):
file_name = str(uuid.uuid4())
with open(file_name,'w') as f:
f.write('sb')
return file_name def run(self):
self.connect()
self.upload('/home/wupeiqi/tttttttttttt.py')
self.rename('/home/wupeiqi/tttttttttttt.py', '/home/wupeiqi/ooooooooo.py)
self.close() def connect(self):
transport = paramiko.Transport((self.host,self.port))
transport.connect(username=self.username,password=self.pwd)
self.__transport = transport def close(self): self.__transport.close() def upload(self,target_path):
# 连接,上传
file_name = self.create_file() sftp = paramiko.SFTPClient.from_transport(self.__transport)
# 将location.py 上传至服务器 /tmp/test.py
sftp.put(file_name, target_path) def rename(self, old_path, new_path): ssh = paramiko.SSHClient()
ssh._transport = self.__transport
# 执行命令
cmd = "mv %s %s" % (old_path, new_path,)
stdin, stdout, stderr = ssh.exec_command(cmd)
# 获取命令结果
result = stdout.read() def cmd(self, command):
ssh = paramiko.SSHClient()
ssh._transport = self.__transport
# 执行命令
stdin, stdout, stderr = ssh.exec_command(command)
# 获取命令结果
result = stdout.read()
return result ha = SSHConnection()
ha.run()

对于更多限制命令,需要在系统中设置:

位置:/etc/sudoers,代码:

Defaults    requiretty
Defaults:cmdb !requiretty

堡垒机

执行流程:

  1. 管理员为用户在服务器上创建账号(将公钥放置服务器,或者使用用户名密码)
  2. 用户登陆堡垒机,输入堡垒机用户名密码,现实当前用户管理的服务器列表
  3. 用户选择服务器,并自动登陆
  4. 执行操作并同时将用户操作记录

需要注意的是,如果想实现用户登录后直接操作,需要配置下堡垒机用户家目录的.bashrc文件:

/usr/bin/env python $PATH/s7.py
logout

将这两行假如即可.

看下实现方式:

简单调用:

import paramiko
import sys
import os
import socket
import select
import getpass tran = paramiko.Transport(('192.168.4.193', 22,))
tran.start_client()
tran.auth_password('root', '7ujm8ik,') # 打开一个通道
chan = tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell() #########
# 利用sys.stdin,肆意妄为执行操作
# 用户在终端输入内容,并将内容发送至远程服务器
# 远程服务器执行命令,并将结果返回
# 用户终端显示内容
#########

交互操作(无tab)

import paramiko
import sys
import os
import socket
import select
import getpass
from paramiko.py3compat import u tran = paramiko.Transport(('10.211.55.4', 22,))
tran.start_client()
tran.auth_password('wupeiqi', '123') # 打开一个通道
chan = tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell() while True:
# 监视用户输入和服务器返回数据
# sys.stdin 处理用户输入
# chan 是之前创建的通道,用于接收服务器返回信息
readable, writeable, error = select.select([chan, sys.stdin, ],[],[],1)
if chan in readable:
try:
x = u(chan.recv(1024))
if len(x) == 0:
print('\r\n*** EOF\r\n')
break
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in readable:
inp = sys.stdin.readline()
chan.sendall(inp) chan.close()
tran.close()

但此次我们会发现,没有tab补全,跟我们真是在shell里执行命令还是略有差距的,那么来第二个.

交互操作(有tab功能)

#!/usr/bin/env python
# -*-coding=utf-8-*-
# Auther:ccorz Mail:ccniubi@163.com Blog:http://www.cnblogs.com/ccorz/
# GitHub:https://github.com/ccorzorz import paramiko
import sys
import os
import socket
import select
import getpass
import termios
import tty
from paramiko.py3compat import u tran = paramiko.Transport(('192.168.4.193', 22,))
tran.start_client()
tran.auth_password('root', '7ujm8ik,') # 打开一个通道
chan = tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell() # 获取原tty属性
oldtty = termios.tcgetattr(sys.stdin)
try:
# 为tty设置新属性
# 默认当前tty设备属性:
# 输入一行回车,执行
# CTRL+C 进程退出,遇到特殊字符,特殊处理。 # 这是为原始模式,不认识所有特殊符号
# 放置特殊字符应用在当前终端,如此设置,将所有的用户输入均发送到远程服务器
tty.setraw(sys.stdin.fileno())
chan.settimeout(0.0) while True:
# 监视 用户输入 和 远程服务器返回数据(socket)
# 阻塞,直到句柄可读
r, w, e = select.select([chan, sys.stdin], [], [], 1)
if chan in r:
try:
x = u(chan.recv(1024))
if len(x) == 0:
print('\r\n*** EOF\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
chan.send(x) finally:
# 重新设置终端属性,必须设置,否则再次登录后无法使用
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) chan.close()
tran.close()

生产需求

在生产中,我们还需要更多的需求:

  1. 有日志记录,我们可以把管理员的每一条记录都记录到数据库中
  2. tab补全时,记录的中间会有空格之类的需要处理

我们来看下怎么实现:

import paramiko
import sys
import os
import socket
import getpass # from paramiko.py3compat import u # windows does not have termios...
try:
import termios
import tty
has_termios = True
except ImportError:
has_termios = False def interactive_shell(chan):
if has_termios:
posix_shell(chan)
else:
windows_shell(chan) def posix_shell(chan):
import select oldtty = termios.tcgetattr(sys.stdin)
try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
chan.settimeout(0.0)
f = open('handle.log','a+')
tab_flag = False
temp_list = []
while True:
r, w, e = select.select([chan, sys.stdin], [], [])
if chan in r:
try:
x = chan.recv(1024)
if len(x) == 0:
sys.stdout.write('\r\n*** EOF\r\n')
break
if tab_flag:
if x.startswith('\r\n'):
pass
else:
f.write(x)
f.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:
f.write(x)
f.flush()
chan.send(x) finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) def windows_shell(chan):
import threading sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n") def writeall(sock):
while True:
data = sock.recv(256)
if not data:
sys.stdout.write('\r\n*** EOF ***\r\n\r\n')
sys.stdout.flush()
break
sys.stdout.write(data)
sys.stdout.flush() writer = threading.Thread(target=writeall, args=(chan,))
writer.start() try:
while True:
d = sys.stdin.read(1)
if not d:
break
chan.send(d)
except EOFError:
# user hit ^Z or F6
pass def run():
# 获取当前登录用户
""" host_list = [
{'host': "192.168.11.139", 'username': 'oldboy', 'pwd': "123"},
{'host': "192.168.11.138", 'username': 'oldboy', 'pwd': "123"},
{'host': "192.168.11.137", 'username': 'oldboy', 'pwd': "123"},
]
for item in enumerate(host_list, 1):
print(item['host']) num = raw_input('序号:')
sel_host = host_list[int(num) -1]
hostname = sel_host['host']
username = sel_host['username']
pwd = sel_host['pwd']
print(hostname,username,pwd)
""" tran = paramiko.Transport((hostname, 22,))
tran.start_client()
tran.auth_password(username, pwd)
# 打开一个通道
chan = tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell() interactive_shell(chan) chan.close()
tran.close() if __name__ == '__main__':
run()

银角究极版

珍藏下吧:

import paramiko
import sys
import os
import socket
import getpass from paramiko.py3compat import u # windows does not have termios...
try:
import termios
import tty
has_termios = True
except ImportError:
has_termios = False def interactive_shell(chan):
if has_termios:
posix_shell(chan)
else:
windows_shell(chan) def posix_shell(chan):
import select oldtty = termios.tcgetattr(sys.stdin)
try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
chan.settimeout(0.0)
log = open('handle.log', 'a+', encoding='utf-8')
flag = False
temp_list = []
while True:
r, w, e = select.select([chan, sys.stdin], [], [])
if chan in r:
try:
x = u(chan.recv(1024))
if len(x) == 0:
sys.stdout.write('\r\n*** EOF\r\n')
break
if flag:
if x.startswith('\r\n'):
pass
else:
temp_list.append(x)
flag = False
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in r:
x = sys.stdin.read(1)
import json if len(x) == 0:
break if x == '\t':
flag = True
else:
temp_list.append(x)
if x == '\r':
log.write(''.join(temp_list))
log.flush()
temp_list.clear()
chan.send(x) finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) def windows_shell(chan):
import threading sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n") def writeall(sock):
while True:
data = sock.recv(256)
if not data:
sys.stdout.write('\r\n*** EOF ***\r\n\r\n')
sys.stdout.flush()
break
sys.stdout.write(data)
sys.stdout.flush() writer = threading.Thread(target=writeall, args=(chan,))
writer.start() try:
while True:
d = sys.stdin.read(1)
if not d:
break
chan.send(d)
except EOFError:
# user hit ^Z or F6
pass def run():
default_username = getpass.getuser()
username = input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username hostname = input('Hostname: ')
if len(hostname) == 0:
print('*** Hostname required.')
sys.exit(1) tran = paramiko.Transport((hostname, 22,))
tran.start_client() default_auth = "p"
auth = input('Auth by (p)assword or (r)sa key[%s] ' % default_auth)
if len(auth) == 0:
auth = default_auth if auth == 'r':
default_path = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa')
path = input('RSA key [%s]: ' % default_path)
if len(path) == 0:
path = default_path
try:
key = paramiko.RSAKey.from_private_key_file(path)
except paramiko.PasswordRequiredException:
password = getpass.getpass('RSA key password: ')
key = paramiko.RSAKey.from_private_key_file(path, password)
tran.auth_publickey(username, key)
else:
pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))
tran.auth_password(username, pw) # 打开一个通道
chan = tran.open_session()
# 获取一个终端
chan.get_pty()
# 激活器
chan.invoke_shell() interactive_shell(chan) chan.close()
tran.close() if __name__ == '__main__':
run()

数据库模型

其实堡垒机的难点是在数据库的设计上

#!/usr/bin/env python
# -*-coding=utf-8-*-
# Auther:ccorz Mail:ccniubi@163.com Blog:http://www.cnblogs.com/ccorz/
# GitHub:https://github.com/ccorzorz from sqlalchemy import create_engine, and_, or_, func, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, DateTime
from sqlalchemy.orm import sessionmaker, relationship engine = create_engine("mysql+pymysql://root:7ujm8ik,@192.168.4.193:3306/test13", max_overflow=5)
Base = declarative_base() # 生成一个SqlORM 基类 class UserProfile2HostUser(Base):
__tablename__ = 'userprofile_2_hostuser'
id = Column(Integer, primary_key=True, autoincrement=True)
userprofile_id = Column(Integer, ForeignKey('user_profile.id'),primary_key=True)
hostuser_id = Column(Integer, ForeignKey('host_user.id'),primary_key=True)
# userprofile = relationship('UserProfile',secondary=lambda :) class Host(Base):
__tablename__ = 'host'
id = Column(Integer, primary_key=True, autoincrement=True)
hostname = Column(String(64), unique=True, nullable=False)
ip_addr = Column(String(128), unique=True, nullable=False)
port = Column(Integer, default=22) class HostUser(Base):
__tablename__ = 'host_user'
id = Column(Integer, primary_key=True, autoincrement=True)
AuthTypes = [
(u'ssh-passwd', u'SSH/Password'),
(u'ssh-key', u'SSH/KEY'),
]
auth_type = Column(String(64))
username = Column(String(64), nullable=False)
password = Column(String(255))
host_id = Column(Integer, ForeignKey('host.id'))
host = relationship('Host', backref='uu')
__table_args__ = (UniqueConstraint(u'host_id', u'username', name='_host_username_uc'),) class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(64), unique=True, nullable=False) #
# obj = session.query(HostUser.username,HostUser.password,Host.hostname,Host.port).join(Host).filter(HostUser.id == 1).first()
# (用户名,密码,主机名,端口) class UserProfile(Base):
__tablename__ = 'user_profile'
id = Column(Integer, primary_key=True)
username = Column(String(64), unique=True, nullable=False)
# 存密码感觉没什么卵用
# password = Column(String(255),nullable=False)
# 如果是一个人只能在一个组下
group_id = Column(Integer, ForeignKey('group.id'))
#需要这么加secondary,否则插入数据时会报错
host_list = relationship('HostUser', secondary=lambda :UserProfile2HostUser.__table__, backref='userprofiles') """
# 输入用户名和密码:
#
obj = session.query(UserProfile).filter(username=输入的用户名, password=输入的密码).first()
if not obj:
# 堡垒机登录用户对象
# 输入这个人的所有机器
obj.host_list # 当前堡垒机登录用户,所有的服务器用户名
#
for item in obj.host_list:
# item,是一个HostUser对象
item.password,item.username,
# item.host 对象,host对象
item.host.hostname,item.host.port
# item 目标机器HostUser对象
host_obj = input(:...)
session.add(AuditLog(userprofile_id=obj.id,hostuser_id = host_obj.id, "ifconfig"))
""" class Log(Base):
__tablename__ = 'log'
id = Column(Integer, primary_key=True)
userprofile_id = Column(Integer, ForeignKey('user_profile.id'))
hostuser_id = Column(Integer, ForeignKey('host_user.id'))
cmd = Column(String(255))
date = Column(DateTime) # class Session:
# session = None
# def __init__(self):
# self.engine = create_engine("mysql+pymysql://root:7ujm8ik,@192.168.4.193:3306/test13", max_overflow=5)
# self.ss = sessionmaker(bind=self.engine)
# self.obj = ss()
# self.Session.session = obj
#
# @classmethod
# def instance(cls):
# if not cls.session:
# cls()
# return cls.session # ss = Session()
# 定义初始化数据库函数
def init_db():
Base.metadata.create_all(engine) # init_db() # 删除数据库函数
def drop_db():
Base.metadata.drop_all(engine) # drop_db() # 实例化数据库操作对象为session
Session = sessionmaker(bind=engine)
ss = Session()
#
# ss.add_all([
# Group(id=1, name='DBA'),
# Group(id=2, name='SA')
# ])
#
# ss.add_all([
# UserProfile(id=1,username='chengc',group_id=2),
# UserProfile(id=2,username='root',group_id=2)
# ])
#
# ss.add_all([
# Host(id=1,hostname='test',ip_addr='192.168.4.193',port=22),
# Host(id=2,hostname='zhongrt1',ip_addr='223.202.101.164',port=43228)
# ])
#
# ss.add_all([
# HostUser(id=1,auth_type='pwd',username='root',password='7ujm8ik,',host_id=1),
# HostUser(id=2,auth_type='pwd',username='root',password='asdf',host_id=2)
# ])
#
# ss.add_all([
# UserProfile2HostUser(userprofile_id=1,hostuser_id=1),
# UserProfile2HostUser(userprofile_id=2,hostuser_id=1)
# ])
# #
# ss.add_all([Group(id=3,name='SB')])
ss.commit()