前言:
如何向Leader體現出運維人員的工作價值?工單!如何自動記錄下他們的操作,堡壘機!我看了網上有說 GateOne是一款開源的堡壘機解決方案,但是部署上之后發現了一個痛點, 我如何在不使用 公鑰、私鑰的前提下,基於web shh 實現 點擊按鈕進行一鍵登錄--------》使用xshell一樣使用Linux ------》退出之后記錄操作日志,我可以修改GateOne的源碼!但感覺自己實力不足,所以當下我想利用Django+websocket+paramiko+gevent.....能否zzzzzz實現?
一、WebSocket配合terms.js
terms.js是在前端模擬ssh終端的開源框架
把terms.js和websocket框架的回調函數
onmessage()
socket.onclose()
onclose()
send()
結合起來!

{% extends "arya/layout.html" %} {% block out_js %} <script src="/static/pligin/datatables/jquery.dataTables.min.js"></script> <script src="/static/pligin/datatables/dataTables.bootstrap.min.js"></script> <script src="/static/pligin/term.js"></script> <script src="/static/iron_ssh.js"></script> <!---引入打開WebSocke的JavaScript代碼 IronSSHClient類--> {% endblock %} {% block content %} <div class="table-responsive"> <div id="page-content"> <div class="panel col-lg-9"> <div class="panel-heading"> <h3 class="panel-title">主機列表</h3> </div> <div class="panel-body"> <div class="table-responsive"> <table id="host_table" class="table table-hover table-bordered table-striped"> <thead> <tr> <th>IDC</th> <th>Hostname</th> <th>IP</th> <th>Port</th> <th>Username</th> <th>操作</th> </tr> </thead> <tbody id="hostlist"> {% for host in hosts %} <tr> <td>{{ host.host.idc }}</td> <td>{{ host.host.hostname }}</td> <td>{{ host.host.ip_addr }}</td> <td>{{ host.host.port }}</td> <td>{{ host.host_user.username }}</td> <td> <button onclick="open_websocket({{ host.pk }},this)" type="button" class="btn btn-success">連接 </button> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> <div id="term"> </div> <div hidden="hidden" id="disconnect"> <button type="button" class="btn btn-danger" id="close_connect" onclick="close_ssh_termial()">關閉連接</button> </div> {% endblock %} {% block in_js %} <script> /* Datatables是一款jquery表格插件。它是一個高度靈活的工具,可以將任何HTML表格添加高級的交互功能。 */ function set_tables() { $('#host_table').DataTable({ "paging": true, <!-- 允許分頁 --> "lengthChange": true, <!-- 允許改變每頁顯示的行數 --> "searching": true, <!-- 允許內容搜索 --> "ordering": true, <!-- 允許排序 --> "info": true, <!-- 顯示信息 --> "autoWidth": true }); } set_tables(); CUURENT_WEB_SOCKEY=''; function open_terminal(options) { $('#page-content').hide(); //點擊連接按鈕隱藏表格 $('#disconnect').show(); var client = new IronSSHClient(); //這里相當於執行了iron_ssh.js中的代碼 CUURENT_WEB_SOCKEY=client; var term = new Terminal( { cols: 80, rows: 24, handler: function (key) { client.send(key); }, screenKeys: true, useStyle: true, cursorBlink: true }); term.open(); //打開ssh終端 $('.terminal').detach().appendTo('#term'); //把ssh終端放入 #term div標簽中 term.write('開始連接......'); client.connect( //調用connect連接方法,把option的方法擴展了傳進去 $.extend(options, { onError: function (error) { term.write('錯誤: ' + error + '\r\n'); }, onConnect: function () { term.write('\r'); }, onClose: function () { client.close_web_soket(); term.write('對方斷開了連接.......'); {# close_ssh_termial() //關閉ssh命令 終端#} }, //term.destroy(); onData: function (data) { term.write(data); } } ) ); } function open_websocket(pk, self) { //點擊連接按鈕創建web_ssh 通道 var options = {host_id: pk}; open_terminal(options)//打開1個模塊ssh的終端 } function close_ssh_termial() {//關閉ssh命令終端 CUURENT_WEB_SOCKEY.close_web_soket(); $('#term').empty(); $('#page-content').show(); //點擊連接按鈕隱藏表格 $('#disconnect').hide(); } </script> {% endblock %}
---------------------------------------------------

//定義1個js的原型 function IronSSHClient() { } //增加生成URL的方法 IronSSHClient.prototype._generateURL = function (options) { if (window.location.protocol == 'https:'){ var protocol = 'wss://'; } else { var protocol = 'ws://'; } // ws://192.168.1.108:8000/host/3/ var url = protocol + window.location.host + '/audit/host/'+ encodeURIComponent(options.host_id) + '/'; return url; }; //連接websocket IronSSHClient.prototype.connect = function (options) { var server_socket = this._generateURL(options); if (window.WebSocket) { this._web_socket = new WebSocket(server_socket) //創建1個websocket對象 } else if (window.MozWebSocket) { this._web_socket = new new MozWebSocket(server_socket) //如果是火狐瀏覽器使用這種方式:創建1個websocket對象 } else { options.onError('當前瀏覽器不支持WebSocket'); //如果用戶的瀏覽器不支持 websocket return } this._web_socket.onopen = function () { //連接建立時觸發 options.onConnect(); }; this._web_socket.onmessage = function (event) { //客戶端接收服務端數據時觸發(event) var data = JSON.parse(event.data.toString()); // console.log(data); if (data.error !== undefined) { //如果發過來的錯誤信息 options.onError(data.error);//執行opetion中的error方法 } else { //正常數據 options.onData(data.data); } }; this._web_socket.onclose = function (event) { //關閉websocket的方法 options.onClose(); }; }; //websocket 發送數據 IronSSHClient.prototype.send = function (data) { //websocket發送數據的方法 this._web_socket.send(JSON.stringify({'data':data})); //注意O,我發得可是字典!! }; IronSSHClient.prototype.close_web_soket = function (data) { //websocket發送數據的方法 this._web_socket.close(); }; // web_socket_client = new IronSSHClient();
二、Django+dwebsocket

@accept_websocket def connect_host(request,user_bind_host_id): # print(request.environ) try: if request.is_websocket(): while True: message = request.websocket.wait()#一直等待前端發生數據過來!! if message: request.websocket.send(message)
三、paramiko交互式

import paramiko import time trans = paramiko.Transport(('172.17.10.113', 22)) # 【坑1】 如果你使用 paramiko.SSHClient() cd后會回到連接的初始狀態 trans.start_client() # 用戶名密碼方式 trans.auth_password(username='root', password='xxxxxx123') # 打開一個通道 channel = trans.open_session() channel.settimeout(7200) # 獲取一個終端 channel.get_pty() # 激活器 channel.invoke_shell() while True: cmd = input('---------> ').strip() channel.send(cmd + '\r') time.sleep(0.2) rst = channel.recv(1024) rst = rst.decode('utf-8') print(rst) # 通過命令執行提示符來判斷命令是否執行完成 if 'yes/no' in rst: channel.send('yes\r') # 【坑3】 如果你使用絕對路徑,則會在home路徑建立文件夾導致與預期不符 time.sleep(0.5) ret = channel.recv(1024) ret = ret.decode('utf-8') print(ret) break channel.close() trans.close()
四、WebSocket+Paramiko交互式(同步)

import json,time,paramiko from . import models from dwebsocket.decorators import accept_websocket from django.shortcuts import render,HttpResponse,redirect def hosts_list(request): current_user=models.UserInfo.objects.get(username=request.session.get('username')) current_audit__user =models.Account.objects.filter(user=current_user).first() if current_user: hosts=current_audit__user.host_user_binds.all() return render(request,'hosts_list.html',locals()) @accept_websocket def connect_host(request,user_bind_host_id): # print(request.environ) try: if request.is_websocket(): #來了1個WebSocket創建1個SSHSocket,在它們兩個開始同步 對話 ssh_socket = paramiko.Transport(('172.17.10.113', 22)) ssh_socket.start_client() ssh_socket.auth_password(username='root', password='xxxxxx123') channel = ssh_socket.open_session() channel.get_pty() channel.invoke_shell() while True: message = request.websocket.wait()#一直等待前端發生數據過來!! if len(message)>1: cmd=json.loads(message) #------------------------------- channel.send(cmd['data']) #--------------------------------- data = channel.recv(1024) if len(data)>1: request.websocket.send(json.dumps({'data': data.decode()})) # 把前端發送的數據,返回前段的數據 # time.sleep(2) except Exception: print('客戶端已經斷開了連接!')
五、WebSocket+Paramiko交互式+Gevent模塊(協程異步)
本來打算使用Gevent模塊開協程進行切換的,但是gevent的模塊的from gevent import monkey;monkey.patch_all()Django項目中所有用到得庫,還得換uwsgi,為避免牽一發而動全身的,我采用了保守的方式(線程)

import paramiko import threading import json class Web_and_SSH(object): def __init__(self,host_user_bind_obj,websocket): self.host_user_bind_obj=host_user_bind_obj self.ip=self.host_user_bind_obj.host.ip_addr self.port=int(self.host_user_bind_obj.host.port) self.login_user=self.host_user_bind_obj. host_user.username self.password=self.host_user_bind_obj.host_user.password self.web_socket = websocket self.cmd_string = '' def open_shh_socket(self): try: # trans = paramiko.Transport(('172.17.10.113', 22)) # 【坑1】 如果你使用 paramiko.SSHClient() cd后會回到連接的初始狀態 # print(self.ip,) trans = paramiko.Transport((self.ip,self.port)) # 【坑1】 如果你使用 paramiko.SSHClient() cd后會回到連接的初始狀態 trans.start_client() # 用戶名密碼方式 # print(self.login_user,self.password) #xxxxxx123 trans.auth_password(username=self.login_user,password=self.password) # 打開一個通道 channel = trans.open_session() # 獲取一個終端 channel.get_pty() channel.invoke_shell() self.ssh_socket=channel # print(self.ssh_socket) except Exception as e: print(e) self.web_socket.send(json.dumps({'error':str(e)},ensure_ascii=False)) self.ssh_socket.close() raise def web_to_ssh(self): # print('--------------->') try: while True: message= self.web_socket.wait() if not message: return cmd = json.loads(message) if 'data' in cmd: self.ssh_socket.send(cmd['data']) self.cmd_string += cmd['data'] finally: self.close() def ssh_to_web(self): # print('<-------------------') try: while True: data = self.ssh_socket.recv(1024) if not data: return self.web_socket.send(json.dumps({'data':data.decode()})) # print(self.cmd_string) finally: self.close() def _bridge(self): t1 = threading.Thread(target=self.web_to_ssh) t2 = threading.Thread(target=self.ssh_to_web) t1.start() t2.start() t1.join() t2.join() def shell(self): self.open_shh_socket() self._bridge() self.close() def close(self): self.ssh_socket.close()

from audit import Bridge from . import models from dwebsocket.decorators import accept_websocket from django.shortcuts import render,HttpResponse,redirect def hosts_list(request): current_user=models.UserInfo.objects.get(username=request.session.get('username')) current_audit__user =models.Account.objects.filter(user=current_user).first() if current_user: hosts=current_audit__user.host_user_binds.all() return render(request,'hosts_list.html',locals()) @accept_websocket def connect_host(request,user_bind_host_id): if request.is_websocket(): #來了1個WebSocket創建1個SSHSocket,在它們兩個開始同步 對話 user_bind_host_id=models.HostUserBind.objects.get(pk=user_bind_host_id) obj=Bridge.Web_and_SSH(user_bind_host_id,request.websocket) obj.open_shh_socket() obj.shell()
六.用戶行為日志+運維日志
我在想怎么在使用了web socket的前提下 記錄用戶輸入的command,這樣做的痛點是使用了web socket協議之后 數據傳輸是 水流式的( 如果你執行了1個df命令,就會有d 、f 、\r 傳輸到后端),還要繼續做數據處理,即便我拿到這些命令意義也不是很大;
突然我放棄了,我不這么搞了,我要這么搞!
我記錄web socket響應給前端的數據,其實這樣也可以把堡壘機用戶所有操作記錄下來而且較為詳細;
用戶行為日志
用戶操作日志

from django.db import models from cmdb.models import UserInfo # Create your models here. class IDC(models.Model): name = models.CharField(max_length=64,unique=True) def __str__(self): return self.name class Meta: verbose_name_plural = "IDC機房" class Host(models.Model): """存儲所有主機信息""" hostname = models.CharField(max_length=64,unique=True) ip_addr = models.GenericIPAddressField(unique=True) port = models.IntegerField(default=22) idc = models.ForeignKey("IDC") enabled = models.BooleanField(default=True) def __str__(self): return "%s-%s" %(self.hostname,self.ip_addr) class Meta: verbose_name_plural = "主機" class HostGroup(models.Model): """主機組""" name = models.CharField(max_length=64,unique=True) host_user_binds = models.ManyToManyField("HostUserBind") def __str__(self): return self.name class Meta: verbose_name_plural = "主機組" class HostUser(models.Model): """存儲遠程主機的用戶信息 root 123 root abc root sfsfs """ auth_type_choices = ((0,'ssh-password'),(1,'ssh-key')) auth_type = models.SmallIntegerField(choices=auth_type_choices) username = models.CharField(max_length=32) password = models.CharField(blank=True,null=True,max_length=128) def __str__(self): return "%s-%s-%s" %(self.get_auth_type_display(),self.username,self.password) class Meta: unique_together = ('username','password') verbose_name_plural = "用戶+密碼表" class HostUserBind(models.Model): """綁定主機和用戶""" host = models.ForeignKey("Host") host_user = models.ForeignKey("HostUser") def __str__(self): return "%s-%s" %(self.host,self.host_user) class Meta: unique_together = ('host','host_user') verbose_name_plural = "主機+用戶+密碼表" class SessionLog(models.Model): ''' 記錄每個用戶 每次操作的記錄 ''' account=models.ForeignKey('Account',verbose_name='執行任務的用戶') host_user_bind=models.ForeignKey('HostUserBind',verbose_name='執行的任務所在服務器') operation_type_choices= ((0, '交互式操作'), (1, '批量操作')) operation_type=models.SmallIntegerField(choices=operation_type_choices,default=0,verbose_name='操作類型') start_date=models.CharField(max_length=255,verbose_name='開始時間') end_date=models.DateTimeField(auto_now_add=True,verbose_name='結束時間') is_work_order=models.BooleanField(default=False) def __str__(self): return '%s %s-%s-%s-%s'%(self.start_date,self.account,self.host_user_bind.host.ip_addr,self.host_user_bind.host_user.username,self.get_operation_type_display()) class Meta: verbose_name_plural = '操作記錄' class AuditLog(models.Model): """記錄用戶 每次操作執行的命令""" session = models.ForeignKey("SessionLog") cmd = models.TextField(verbose_name='執行了哪些命令') date = models.DateTimeField(auto_now_add=True) def __str__(self): return "%s-%s" %(self.session,self.cmd) class Meta: verbose_name_plural = '操作執行的命令' class Account(models.Model): """堡壘機賬戶 user.account.host_user_bind """ user = models.OneToOneField(UserInfo,verbose_name='運維平台用戶') enabled = models.BooleanField(default=True,verbose_name='當前用戶是否被禁用') host_user_binds = models.ManyToManyField("HostUserBind",blank=True,verbose_name='用戶下的權限') host_groups = models.ManyToManyField("HostGroup",blank=True,verbose_name='用戶下的權限組') def __str__(self): return "%s" %(self.user.username) class Meta: verbose_name_plural = '堡壘機用戶' class CronTable(models.Model): '''主機的Cron 任務表''' host_user=models.ForeignKey('HostUserBind',verbose_name='1服務器+1用戶+1cron+1行記錄') task_name=models.CharField(max_length=255,verbose_name='任務名稱',blank=True,null=True) task_tag= models.CharField(max_length=255, verbose_name='任務功能說明',blank=True, null=True) cron_expression = models.CharField(max_length=255, verbose_name='任務表達式', blank=True, null=True) available=models.BooleanField(verbose_name='當前cron任務是否可用') last_execute_available = models.BooleanField(default=True, verbose_name='上一次執行是否執行成功') last_execute_log = models.TextField(verbose_name='上次次執行日志', blank=True, null=True) next_execute_time = models.CharField(max_length=255, verbose_name='下次執行時間',blank=True, null=True) cron_execute=((0,'shell'),(1,'http-get')) pass1 = models.CharField(max_length=255,verbose_name='預留字段1',blank=True, null=True) pass2 = models.CharField(max_length=255,verbose_name='預留字段2',blank=True, null=True) # class Meta: # verbose_name_plural = 'crontab表'

from django.conf.urls import url from . import views urlpatterns = [ url(r'^hosts_list/$',views.hosts_list,name='hosts_list'),#/audit/hosts_list/ #('host/<int:user_bind_host_id>/', views.connect #(?P<n1>\d+)/ url(r'^host/(?P<user_bind_host_id>\d+)/$',views.connect_host,name='connect_host'),#/audit/hosts_list/ url(r'^user/activity/logs/$',views.activity_log, name='users_activity_log_url'),#/audit//user/operation/logs/ url(r'^user/operation/logs/$',views.operation_log, name='users_operation_log_url') ]

from audit import Bridge from . import models from dwebsocket.decorators import accept_websocket from django.shortcuts import render,HttpResponse,redirect def hosts_list(request): current_user=models.UserInfo.objects.get(username=request.session.get('username')) current_audit__user =models.Account.objects.filter(user=current_user).first() if current_user: hosts=current_audit__user.host_user_binds.all() return render(request,'hosts_list.html',locals()) @accept_websocket def connect_host(request,user_bind_host_id): if request.is_websocket(): #來了1個WebSocket創建1個SSHSocket,django在它們2個之間, 協調異步對話 user_bind_host_id=models.HostUserBind.objects.get(pk=user_bind_host_id) obj=Bridge.Web_and_SSH(user_bind_host_id,request.websocket,request,models) obj.open_shh_socket() obj.shell() obj.add_logs() def activity_log(request):#用戶行為日志 pk=request.GET.get('pk') host_user_bind_pk=pk SessionLogs=models.SessionLog.objects.filter(host_user_bind__pk=pk).order_by('-pk') return render(request,'activity_logs.html',locals()) def operation_log(request):#用戶操作日 pk = request.GET.get('pk') host_user_bind_pk=request.GET.get('next') AuditLogs = models.AuditLog.objects.filter(session__pk=pk).order_by('-pk') return render(request,'operation_logs.html',locals()) def generate_work_order(request):#運維日志生成工單 return HttpResponse('ok')
------------------------------------------------------------------

{% extends "arya/layout.html" %} {% block out_js %} <script src="/static/pligin/datatables/jquery.dataTables.min.js"></script> <script src="/static/pligin/datatables/dataTables.bootstrap.min.js"></script> <script src="/static/pligin/term.js"></script> <script src="/static/iron_ssh.js"></script> <!---引入打開WebSocke的JavaScript代碼 IronSSHClient類--> {% endblock %} {% block content %} <div class="table-responsive"> <div id="page-content"> <div class="panel col-lg-9"> <div class="panel-heading"> <h3 class="panel-title">主機列表</h3> </div> <div class="panel-body"> <div class="table-responsive"> <table id="host_table" class="table table-hover table-bordered table-striped"> <thead> <tr> <th>IDC</th> <th>Hostname</th> <th>IP</th> <th>Port</th> <th>Username</th> <th>操作</th> </tr> </thead> <tbody id="hostlist"> {% for host in hosts %} <tr> <td>{{ host.host.idc }}</td> <td>{{ host.host.hostname }}</td> <td><a href="{% url 'users_activity_log_url'%}?pk={{ host.pk }}">{{ host.host.ip_addr }}</a> </td> <td>{{ host.host.port }}</td> <td>{{ host.host_user.username }}</td> <td> <button onclick="open_websocket({{ host.pk }},this)" type="button" class="btn btn-success">連接 </button> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> <div id="term"> </div> <div hidden="hidden" id="disconnect"> <button type="button" class="btn btn-danger" id="close_connect" onclick="close_ssh_termial()">關閉連接</button> </div> {% endblock %} {% block in_js %} <script> /* Datatables是一款jquery表格插件。它是一個高度靈活的工具,可以將任何HTML表格添加高級的交互功能。 */ function set_tables() { $('#host_table').DataTable({ "paging": true, <!-- 允許分頁 --> "lengthChange": true, <!-- 允許改變每頁顯示的行數 --> "searching": true, <!-- 允許內容搜索 --> "ordering": true, <!-- 允許排序 --> "info": true, <!-- 顯示信息 --> "autoWidth": true }); } set_tables(); CUURENT_WEB_SOCKEY=''; function open_terminal(options) { $('#page-content').hide(); //點擊連接按鈕隱藏表格 $('#disconnect').show(); var client = new IronSSHClient(); //這里相當於執行了iron_ssh.js中的代碼 CUURENT_WEB_SOCKEY=client; var term = new Terminal( { cols: 80, rows: 24, handler: function (key) { client.send(key); }, screenKeys: true, useStyle: true, cursorBlink: true }); term.open(); //打開ssh終端 $('.terminal').detach().appendTo('#term'); //把ssh終端放入 #term div標簽中 term.write('開始連接......'); client.connect( //調用connect連接方法,把option的方法擴展了傳進去 $.extend(options, { onError: function (error) { term.write('錯誤: ' + error + '\r\n'); }, onConnect: function () { term.write('\r'); }, onClose: function () { client.close_web_soket(); term.write('對方斷開了連接.......'); {# close_ssh_termial() //關閉ssh命令 終端#} }, //term.destroy(); onData: function (data) { term.write(data); } } ) ); } function open_websocket(pk, self) { //點擊連接按鈕創建web_ssh 通道 var options = {host_id: pk}; open_terminal(options)//打開1個模塊ssh的終端 } function close_ssh_termial() {//關閉ssh命令終端 CUURENT_WEB_SOCKEY.close_web_soket(); $('#term').empty(); $('#page-content').show(); //點擊連接按鈕隱藏表格 $('#disconnect').hide(); } </script> {% endblock %}

{% extends "arya/layout.html" %} {% block out_js %} <script src="/static/pligin/datatables/jquery.dataTables.min.js"></script> <script src="/static/pligin/datatables/dataTables.bootstrap.min.js"></script> {% endblock %} {% block content %} <a class='btn btn-primary btn-sm' href="/audit/hosts_list/">返回</a> <div class="table-responsive"> <div id="page-content"> <div class="panel col-lg-9"> <div class="panel-heading"> <h3 class="panel-title">用戶行為日志</h3> </div> <div class="panel-body"> <div class="table-responsive"> <table id="users_activity_log_show" class="table table-hover table-bordered table-striped"> <thead> <tr> <th>開始時間</th> <th>結束時間</th> <th>運維用戶</th> <th>方式</th> <th>登錄</th> <th>服務器</th> <th>操作</th> </tr> </thead> <tbody> {% for log in SessionLogs %} <tr> <td>{{ log.start_date }}</td> <td>{{ log.end_date }}</td> <td> {{ log.account.user.username }}</td> <td> {{ log.get_operation_type_display }}</td> <td>{{ log.host_user_bind.host_user.username }}</td> <td>{{ log.host_user_bind.host.ip_addr }}</td> <td style="text-align: center"> <a class='btn btn-primary btn-sm' href="{% url 'users_operation_log_url' %}?pk={{ log.pk }}&next={{ host_user_bind_pk }}">更多</a> {% if request.session.username == log.account.user.username %} <a class='btn btn-success btn-sm' href="{% url 'users_operation_log_url' %}?pk={{ log.pk }}&next={{ host_user_bind_pk }}">工單</a> </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> {% endblock %} {% block in_js %} <script> function set_tables() { $('#users_activity_log_show').DataTable({ "paging": true, <!-- 允許分頁 --> "lengthChange": true, <!-- 允許改變每頁顯示的行數 --> "searching": true, <!-- 允許內容搜索 --> "ordering": true, <!-- 允許排序 --> "info": true, <!-- 顯示信息 --> "autoWidth": true }); } set_tables() </script> {% endblock %}

{% extends "arya/layout.html" %} {% block content %} <a class='btn btn-primary btn-sm' href="{% url 'users_activity_log_url'%}?pk={{ host_user_bind_pk}}">返回</a> <div class="table-responsive"> <div id="page-content"> <div class="panel col-lg-9"> <div class="panel-heading"> <h3 class="panel-title">運維日志</h3> </div> {% for log in AuditLogs %} <h3>{{ log.date }}</h3> <pre style="background-color: black;color: white"> {{ log.cmd }} </pre> {% endfor %} </div> </div> </div> {% endblock %}