zoukankan      html  css  js  c++  java
  • 基于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
    
    "]
    [0.6025891304016113, "o", "Hello, u9648u5efau65872 !
    "]
    [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.cnx1b[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) + '
    ')
                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) + '
    '))        #写入执行输出输入的内容数据流
            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
  • 相关阅读:
    request和response使用
    oracle_to_char
    oracl_LTRIM_RITRIM
    convert
    jdbc
    oracle_trunc
    [python]glob模块中的glob()函数为什么返回空列表??
    win10 anaconda+tensorflow+keras
    Golang学习:sublime text3配置golang环境
    2018/12/05学习笔记
  • 原文地址:https://www.cnblogs.com/chenjw-note/p/12895913.html
Copyright © 2011-2022 走看看