前言:
如何向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 %}