基于linux Asciinema开发webssh的录像回放功能说明及内容记录

1.Asciinema 是一款开源免费的终端录制工具,它可以将命令行输入输出的任何内容加上时间保存在文件中,同时还提供方法在终端或者web浏览器中进行回放。Asciinema 的录制和播放都是基于文本的,相比传统的video有很多好处,例如录制文件体积小,在播放的过程中可以暂停复制其中的文本内容等等

2.官网地址:https://asciinema.org/explore/featured

   Github地址:https://github.com/asciinema

   Asciinema-player地址:https://github.com/asciinema/asciinema-player

   Asciinema-player下载地址:https://github.com/asciinema/asciinema-player/releases     基于web页面的播放器,只需要下载asciinema-player.css asciinema-player.js 导入到前端页面即可使用

3.安装Asciinema

python3 install asciinema

4.查看版本

/opt/python36/bin/asciinema --version

asciinema 2.0.2

5.参数说明

1.录制:rec,
2.播放:play,
3.以文件形式查看录制内容:cat,
4.上传文件到asciinema.org网站:upload、
5.asciinema.org账号认证:auth

6.录制,直接在linux命令行中执行

/opt/python36/bin/asciinema rec aaa.cast
参数说明:
--stdin 表示启用标准输入录制,比如输入的密码也会被记录,且文件流中会出现 i 表示stdin标准输入 或 o 表示stdout标准输出
--append 添加录制到已存在的文件中
--raw 保存原始STDOUT输出,无需定时信息等
--overwrite 如果文件已存在,则覆盖
-c 要记录的命令,默认为$SHELL
-e 要捕获的环境变量列表,默认为SHELL,TERM
-t 后跟数字,指定录像的title
-i 后跟数字,设置录制时记录的最大空闲时间
-y 所有提示都输入yes
-q 静默模式,加了此参数在进入录制或者退出录制时都没有提示
输入exit或按ctrl+D组合键退出录制

7.播放

/opt/python36/bin/asciinema play aaa.cast
参数说明

-s 后边跟数字,表示用几倍的速度来播放录像
-i 后边跟数字,表示在播放录像时空闲时间的最大秒数
在播放的过程中你可以通过空格来控制暂停或播放,也可以通过ctrl+c组合键来退出播放,当你按空格键暂停时,可以通过.号来逐帧显示接下来要播放的内容

8.文件说明

文件头header
{
"version": 2, "width": 233, "height": 57, "timestamp": 1589527986.4172204, "env": {"SHELL": "/bin/bash", "TERM": "linux"}, "title": "boamp_webssh_record"} 版本 播放器宽 播放器高 时间 环境 标题
文件内容

[0.5736286640167236, "o", "Last login: Fri May 15 15:29:58 2020 from 11.xx.xx.xx\r\r\n"]
[0.6025891304016113, "o", "Hello, \u9648\u5efa\u65872 !\r\n"]
[2.169491767883301, "o", "\u001b[?1034h[root@localhost:11.xx.xx.xx ~]# "]

9.如果有需要,将linux服务器上用户的所有操作过程都记录下来配置

echo "/opt/python36/bin/asciinema rec /tmp/$USER-$(date +%Y%m%d%H%M%S).cast -q" >> /etc/profile
source /etc/profile
这样每个用户登陆,都会记录他的所有操作内容

 10.Asciinema-player 播放器,web前端的使用

<html>
<head>
  ...
  <link rel="stylesheet" type="text/css" href="/asciinema-player.css" />
  ...
</head>
<body>
  ...
  <asciinema-player src="/demo.cast"></asciinema-player>
  ...
  <script src="/asciinema-player.js"></script>
</body>
</html>
参数说明

cols: 播放终端的列数,默认为80,如果cast文件的header头有设置width,这里无需设置
rows: 播放终端的行数,默认为24,如果cast文件的header头有设置height,这里无需设置
autoplay: 是否自动开始播放,默认不会自动播放
preload: 预加载,如果你想为录像配音,这里可以预加载声音
loop: 是否循环播放,默认不循环
start-at: 从哪个地方开始播放,可以是123这样的秒数或者是1:06这样的时间点
speed: 播放的速度,类似于play命令播放时的-s参数
idle-time-limit: 最大空闲秒数,类似于play命令播放时的-i参数
poster: 播放之前的预览,可以是npt:1:06这样给定时间点的画面,也可以是data:text/plain,ops-coffee.cn这样给定的文字,其中文字支持ANSI编码,例如可以给文字加上颜色data:text/plain,\x1b[1;32mops-coffee.cn\x1b[1;0m
font-size: 文字大小,可以是small、medium、big或者直接是14px这样的css样式大小
theme: 终端颜色主题,默认是asciinema,也提供有tango、solarized-dark、solarized-light或者monokai可选择,当然你也可以自定义主题
title、author、author-url、author-img-url分别表示了录像的标题、作者、作者的主页、作者的头像,这些配置会在全屏观看录像时显示在标题栏中

11.整合到运维平台中

  1.在平台上调用目的主机上的命令执行录制:在主机上添加个脚本,每次连接自动进行录制,但这样不仅要在每台远程主机添加脚本,会很繁琐,而且录制的脚本文件都是放在远程主机上的,后续播放也很麻烦

  2.事实上录像文件不是录屏,只是一组组数据流,所以在webssh websocket 交互时记录数据,写到对应的cast文件中,再用Asciinema-player 播放器拿出来播放 【使用方案√】

  3.创建记录数据库代码

class RecordWebssh(models.Model):
    """webssh视频记录表"""
    user = models.CharField(max_length=128,blank=False,null=False,verbose_name="操作用户")
    host = models.CharField(max_length=128, blank=False, null=False, verbose_name="操作机器")
    record_filename = models.CharField(max_length=512, blank=False, null=False, verbose_name="操作记录文件名")
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")

    def __str__(self):
        return self.record_filename

    class Meta:
        verbose_name_plural = "webssh视频记录表"
        db_table = "recordwebssh"

  4.核心逻辑代码

import paramiko
from threading import Thread
from webssh.tools.tools import get_key_obj
import socket
import json
import time
import datetime
import os
from boamp.settings import logger
from boamp.settings import BASE_DIR
data_list = {}

class SSH:

    def __init__(self, host, user, websocker, message):
        self.host = host
        self.user = user
        self.websocker = websocker
        self.message = message
        self.time = time.time()     #获取起始时间戳
        self.date_time = datetime.datetime.now().strftime('%Y-%m-%d_%H%M')
        self.msg_list = data_list["%s_%s"%(self.host,self.date_time)] = []

    def record_webssh(self, host, user, type, data_list):
        try:
            record_dir_path = os.path.join(BASE_DIR,"static/webssh/record_webssh/")
            if not os.path.exists(record_dir_path):
                os.makedirs(record_dir_path)
            record_filename = '%s_%s_%s.cast' % (self.date_time,host, user)     #命名录像文件名
            record_filename_path = os.path.join(record_dir_path, record_filename)
            if type == 'header':        #是否是头部header内容,只写入一次头部header内容
                with open(record_filename_path, 'w') as f:
                        f.write(json.dumps(data_list) + '\n')
            else:
                with open(record_filename_path, 'a', buffering=1) as f:     #self.msg_list 必须为列表,如果使用字典格式会出现很多问题,已彩坑1天,换回列表就好了
                    for data in self.msg_list:
                        now_time = data[0]
                        message = data[1]
                        iodata = [now_time - self.time, 'o', message]       #生成数据流格式内容
                        f.write((json.dumps(iodata) + '\n'))        #写入执行输出输入的内容数据流
        except Exception as e:
            print(e)
            print('异常文件:%s ,异常行号:%s' % (e.__traceback__.tb_frame.f_globals['__file__'], e.__traceback__.tb_lineno))

    def connect(self, host, user, password, pkey=None, port=22, timeout=30,term='xterm', pty_width=80, pty_height=24):
        global data_list
        try:
            ssh_client = paramiko.SSHClient()
            ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

            if pkey:
                key = get_key_obj(paramiko.RSAKey, pkey_obj=pkey, password=password) or \
                      get_key_obj(paramiko.DSSKey, pkey_obj=pkey, password=password) or \
                      get_key_obj(paramiko.ECDSAKey, pkey_obj=pkey, password=password) or \
                      get_key_obj(paramiko.Ed25519Key, pkey_obj=pkey, password=password)

                print("使用密钥登陆")
                try:
                    ssh_client.connect(username=user, hostname=host, port=port, pkey=key, timeout=timeout)
                    logger.info("以[%s]用户通过[密钥]方式登陆机器[%s]成功"%(user,host))
                except Exception as e:
                    print("error:",e)
                    logger.warning("以[%s]用户通过[密钥]方式登陆机器[%s]失败,错误:%s" % (user, host,e))
            else:
                print("使用密码登陆")
                try:
                    ssh_client.connect(username=user, password=password, hostname=host, port=port, timeout=timeout)
                    logger.info("以[%s]用户通过[密码]方式登陆机器[%s]成功" % (user, host))
                except Exception as e:
                    print("error:",e)
                    logger.warning("以[%s]用户通过[密码]方式登陆机器[%s]失败,错误:%s" % (user, host, e))

            transport = ssh_client.get_transport()
            self.channel = transport.open_session()
            self.channel.get_pty(term=term, width=pty_width, height=pty_height)
            self.channel.invoke_shell()
            # 构建录像文件header
            header_data = {
                "version": 2,
                "width": 250,
                "height": 57,
                "timestamp": self.time,
                "env": {
                    "SHELL": "/bin/bash",
                    "TERM": "linux"
                },
                "title": "boamp_webssh_record"
            }
            self.record_webssh(host, user,'header', header_data)
            # 连接建立一次,之后交互数据不会再进入该方法

            for i in range(2):
                recv = self.channel.recv(102400).decode('utf-8')
                self.message['status'] = 0
                self.message['message'] = recv
                message = json.dumps(self.message)
                self.websocker.send(message)
                now_time = time.time()
                data_list_temp = [now_time,recv]
                self.msg_list.append(data_list_temp)
                self.record_webssh(host,user,'iodata',data_list)
                self.msg_list = []

        except socket.timeout as e:
            self.message['status'] = 1
            self.message['message'] = 'ssh connection timed out'
            message = json.dumps(self.message)
            self.websocker.send(message)
            self.websocker.close()
            now_time = time.time()
            data_list_temp = [now_time, self.message['message']]
            self.msg_list.append(data_list_temp)
            self.record_webssh(host, user, 'iodata', data_list)
            self.msg_list = []
        except Exception as e:
            print(e)
            print('异常文件:%s ,异常行号:%s' % (e.__traceback__.tb_frame.f_globals['__file__'], e.__traceback__.tb_lineno))
            self.message['status'] = 1
            self.message['message'] = str(e)
            message = json.dumps(self.message)
            self.websocker.send(message)
            self.websocker.close()
            now_time = time.time()
            data_list_temp = [now_time, self.message['message']]
            self.msg_list.append(data_list_temp)
            self.record_webssh(host, user, 'iodata', data_list)
            self.msg_list = []

    def resize_pty(self, cols, rows):
        self.channel.resize_pty(width=cols, height=rows)

    def django_to_ssh(self, data):
        try:
            self.channel.send(data)
            return
        except:
            self.close()

    def websocket_to_django(self):
        global data_list
        try:
            while True:
                data = self.channel.recv(1024).decode('utf-8','ignore')
                if len(data) != 0:
                    self.message['status'] = 0
                    self.message['message'] = data
                    message = json.dumps(self.message)
                    self.websocker.send(message)
                    print("===================",len(self.msg_list))
                    now_time = time.time()
                    data_list_temp = [now_time,data]
                    if len(self.msg_list) < 60:     #判断数据列表长度,60个内容就写入一次文件,避免了频繁写入文件,消耗io导致数据丢失的问题
                        self.msg_list.append(data_list_temp)
                    else:
                        self.msg_list.append(data_list_temp)
                        self.record_webssh(self.host, self.user, 'iodata', data_list)
                        self.msg_list = []
                else:
                    return
        except:
            self.close()

    def close(self):
        global data_list
        self.message['status'] = 1
        self.message['message'] = 'Close connection'
        message = json.dumps(self.message)
        now_time = time.time()
        data_list_temp = [now_time, self.message['message']]
        self.msg_list.append(data_list_temp)
        self.record_webssh(self.host, self.user, 'iodata', data_list)
        self.msg_list = []
        self.websocker.send(message)
        self.channel.close()
        self.websocker.close()

    def shell(self, data):
        Thread(target=self.django_to_ssh, args=(data,)).start()
        Thread(target=self.websocket_to_django).start()

  5.前端代码,实现Asciinema-player播放器

<!DOCTYPE html>
<html>
  
  <head>
    <meta charset="UTF-8">
    <title>record_webssh_play</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <link rel="stylesheet" href="/static/super_cmdb/css/font.css">
    <link rel="stylesheet" href="/static/super_cmdb/css/xadmin.css">
    <link rel="stylesheet" type="text/css" href="/static/super_cmdb/css/asciinema-player.css" />

  </head>
  
  <body class="layui-anim layui-anim-up">
    <div class="x-nav">
      <span class="layui-breadcrumb">
        <a href="">首页</a>
        <a href="">用户列表</a>
        <a>
          <cite>导航元素</cite></a>
      </span>
      <a class="layui-btn layui-btn-small" style="line-height:1.6em;margin-top:3px;float:right" href="javascript:location.replace(location.href);" title="刷新">
        <i class="layui-icon" style="line-height:33px"></i></a>
    </div>
    <div class="x-body">

        <asciinema-player title="录像回放" speed="1.5" autoplay author="xxxx" author-img-url="/static/super_cmdb/images/bg.png" src="/static/webssh/record_webssh/{{ record_filename }}"></asciinema-player>

    </div>
    <script type="text/javascript" src="/static/super_cmdb/js/jquery.min.js"></script>
    <script type="text/javascript" src="/static/super_cmdb/lib/layui/layui.js" charset="utf-8"></script>
    <script type="text/javascript" src="/static/super_cmdb/js/xadmin.js"></script>
    <script src="/static/super_cmdb/js/asciinema-player.js"></script>
  </body>

</html>

  6.效果展示

12.参考链接

https://ops-coffee.cn/s/oqzqgiq3unrnut-ngrh9lg
https://ops-coffee.cn/s/pcstabodjds8d15arwafza
https://www.cnblogs.com/37Y37/p/11909685.html

 

posted @ 2020-05-15 16:55  chenjianwen  阅读(759)  评论(0编辑  收藏  举报