前言:
如何向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()
结合起来!
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% 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 %}
---------------------------------------------------
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//定义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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@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交互式
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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交互式(同步)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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,为避免牵一发而动全身的,我采用了保守的方式(线程)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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响应给前端的数据,其实这样也可以把堡垒机用户所有操作记录下来而且较为详细;
用户行为日志
用户操作日志
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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表'
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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') ]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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')
------------------------------------------------------------------
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% 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 %}
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% 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 %}
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{% 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 %}