pippo主机管理

1. 主机管理

1.1 paramiko模块

Paramiko是SSHv2协议的Python(2.7,3.4+)实现,同时提供了客户端和服务器功能。尽管Paramiko利用Python C扩展进行了低级加密(Cryptography),但它本身是围绕SSH网络概念的纯Python接口,通过paramiko我们可以完成远程主机连接,指令执行、上传下载文件等操作。下面学习具体用法

官方网址: http://www.paramiko.org/

详细api接口文档:http://docs.paramiko.org/en/stable/

  • SSHClient的作用类似于Linux的ssh命令,是对SSH会话的封装,该类封装了传输(Transport),通道(Channel)及SFTPClient建立的方法(open_sftp),通常用于执行远程命令。
  • SFTPClient的作用类似与Linux的sftp命令,是对SFTP客户端的封装,用以实现远程文件操作,如文件上传、下载、修改文件权限等操作
pip install paramiko

Paramiko中的几个概念:

  • Client:ssh客户端短连接模式
  • Transport:可以建立ssh会话长连接模式
    • Channel:是一种类Socket,一种安全的SSH传输通道;
    • Transport:是一种加密的会话,使用时会同步创建了一个加密的Tunnels(通道),这个Tunnels叫做Channel;需要open_session来完成长连接对话。
    • Session:是client与Server保持连接的对象,用connect()/start_client()/start_server()开始会话。

简单示例

client模式,直接执行指令,类似于 ssh root@8.129.90.123 'pwd'

建立连接,发送指令,拿到指令结果,断开连接

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())  # 指纹记录的保存,自动做的

ssh.connect(hostname='47.95.38.42', port=22, username='root', password='zxy521', timeout=10) # 1. 直接密码远程连接的方式
#注意,如果你测试某个服务器的连接时,如果你本地已经配置了这个远程服务器的免密登录(公私钥模式),那么就不能测试出密码是否正确了,因为首先会通过公私钥模式登录,不会使用你的密码的。
# ssh.connect(hostname='39.102.132.191', port=22, username='root', pkey=pkey, timeout=10) # 2. 使用秘钥免密登录的方式
stdin, stdout, stderr = ssh.exec_command('pwd')
result = stdout.read()
print(result)
ssh.close()

transport模式

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

ssh.connect(hostname='47.95.38.42', port=22, username='root', password='zxy521521', timeout=10) # 1. 直接密码远程连接的方式
# ssh.connect(hostname='39.102.132.191', port=22, username='root', pkey=pkey, timeout=10)  #2. 使用秘钥免密登录的方式
cli = ssh.get_transport().open_session()  #开启会理工话  ,持续发指令
cli.settimeout(20)  # 20秒内没有交互了,会自动断开链接
# cli.exec_command('ifconfig')
cli.exec_command('ls')
stdout = cli.makefile("rb", -1)
content = stdout.read().decode()

print(content)

ssh.close()

通过paramiko封装类

都是在官网上找的

from paramiko.client import SSHClient, AutoAddPolicy
from paramiko.config import SSH_PORT
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import AuthenticationException
from io import StringIO


class SSH:
    def __init__(self, hostname, port=SSH_PORT, username='root', pkey=None, password=None, connect_timeout=10):
        if pkey is None and password is None:
            raise Exception('私钥或者密码必须有一个不为空')
        self.client = None
        self.arguments = {
            'hostname': hostname,  # 主机的ip地址
            'port': port,  # ssh链接端口  默认22端口
            'username': username,  # 连接主机的用户名,
            'password': password,  # 链接密码
            'pkey': RSAKey.from_private_key(StringIO(pkey)) if isinstance(pkey, str) else pkey,
            # 'pkey': pkey,  pkey为私钥文件信息,    # 1 密码链接, 2 公私钥链接(免密链接)
            'timeout': connect_timeout,  # 超时时间
        }
        # print(self.arguments)

    # 生成公私钥键值对
    @staticmethod
    def generate_key():
        key_obj = StringIO()  # 内存字符流
        key = RSAKey.generate(2048)  # 2028位的
        key.write_private_key(key_obj)  # 将key写到内存中
        return key_obj.getvalue(), 'ssh-rsa ' + key.get_base64()
        # 私钥字符串  和  公钥字符串

    # 将公钥上传到对应主机
    def add_public_key(self, public_key):
        # 600 是
        command = f'mkdir -p -m 700 ~/.ssh && \
        echo {public_key!r} >> ~/.ssh/authorized_keys && \
        chmod 600 ~/.ssh/authorized_keys'
        code, out = self.exec_command(command)
        if code != 0:
            raise Exception(f'添加公钥失败: {out}')

    # 检测连接并获取连接
    def ping(self):
        return self.get_client()

    # 直接获取连接
    def get_client(self):
        if self.client is not None:
            return self.client
        try:
            self.client = SSHClient()
            self.client.set_missing_host_key_policy(AutoAddPolicy)  # 指纹记录
            # print('2222self.client>>>>', self.client)
            self.client.connect(**self.arguments)  # 通过参数建立链接,如果链接不上,直接抛错误
            # print(333)
        except Exception as e:
            print(199888)
            return None
        # print('self.client>>>>',self.client)
        return self.client

    # 指定上文文件的路径
    # 给远程服务器上传我当前服务器的文件,用这个方法
    def put_file(self, local_path, remote_path):
        with self as cli:  # cli
            sftp = cli.open_sftp()  # sftp的一个连接模式,  ssh  sftp上传下载文件
            sftp.put(local_path, remote_path)
            sftp.close()

    def exec_command(self, command, timeout=1800, environment=None):
        command = 'set -e\n' + command  # set是如果执行不成功就不继续执行下面的指令
        with self as cli:  # __enter__方法,并将该方法的返回值给 as 后面的变量
            # cli -- self.client
            chan = cli.get_transport().open_session()
            chan.settimeout(timeout)
            #
            chan.set_combine_stderr(True)  # 正确和错误输出都在一个管道里面输出出来
            # if environment:
            #     str_env = ' '.join(f"{k}='{v}'" for k, v in environment.items())
            #     command = f'export {str_env} && {command}'
            chan.exec_command(command)
            stdout = chan.makefile("rb", -1)
            return chan.recv_exit_status(), self._decode(stdout.read())
            # chan.recv_exit_status()   0 -- 指令执行成功  1 -- 失败
            #

    # 上传文件,根据文件对象(文件句柄或类文件句柄)上传到指定目录下
    def put_file_by_fl(self, fl, remote_path, callback=None):
        with self as cli:
            sftp = cli.open_sftp()
            sftp.putfo(fl, remote_path, callback=callback)

    # 从远程主机下载文件到本地
    def download_file(self, local_path, remote_path):
        with self as cli:
            sftp = cli.open_sftp()
            sftp.get(remote_path, local_path)

    # 获取指定目录路径下的文件和文件夹列表详细信息,结果为列表,列表里面的每一项是from paramiko.sftp_attr import SFTPAttributes  类的对象,通过对象.属性可以获取对应的数据,比如获取修改时间用对象.st_mtime
    def list_dir_attr(self, path):
        with self as cli:
            sftp = cli.open_sftp()
            return sftp.listdir_attr(path)

    # 根据指定路径删除对应文件,没有对应文件会报错,有返回None
    def remove_file(self, path):
        with self as cli:
            sftp = cli.open_sftp()
            sftp.remove(path)

    # 删除远程主机上的目录
    def remove_dir(self, target_path):
        with self as cli:
            sftp = cli.open_sftp()
            sftp.rmdir(target_path)

    # 获取文件详情
    def file_stat(self, remote_path):
        with self as cli:
            sftp = cli.open_sftp()
            return sftp.stat(remote_path)

    # 编码方法
    def _decode(self, out: bytes):
        try:
            return out.decode()
        except UnicodeDecodeError:
            return out.decode('GBK')

    # with self: 先执行__enter__方法
    def __enter__(self):
        if self.client is not None:
            raise RuntimeError('已经建立连接了!!!')
        return self.get_client()

    # with self:语句体内容结束后执行如下方法 先执行__enter__方法
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.client.close()
        self.client = None

1.2 功能展示

1.3 配置应用

创建app

python ../../manage.py startapp host

配置路由

path('host/', include('host.urls')),

生成数据库

utils/models.py 基础模型类

from django.db import models


class BaseModel(models.Model):

    is_show = models.BooleanField(default=True, verbose_name="是否显示")
    orders = models.IntegerField(default=1, verbose_name="排序")
    is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
    created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
    updated_time = models.DateTimeField(auto_now=True, verbose_name="修改时间")
    class Meta:
        # 数据库迁移时,设置了bstract = True的model类不会生成数据库表
        abstract = True

model.py

from django.db import models
from pippo_api.utils.model import BaseModel
from pippo_api.libs.ssh import SSH


# 全局密钥和共钥,所有用户都使用这个一对
class PkeyModel(BaseModel):
    key = models.CharField(max_length=50, unique=True)  # ssh私钥
    value = models.TextField()  # 秘钥的值
    desc = models.CharField(max_length=255, null=True)  # 简单描述

    def __repr__(self):
        return '<Pkey %r>' % self.key


# 主机表
class Host(BaseModel):
    # name = models.CharField(max_length=50,verbose_name='主机别名')
    # zone = models.CharField(max_length=50, verbose_name='主机类别')
    category = models.ForeignKey('HostCategory', on_delete=models.CASCADE, verbose_name='主机类别', related_name='hc')
    hostname = models.CharField(max_length=50, verbose_name='主机名称')
    ip_addr = models.CharField(blank=True, null=True, max_length=50, verbose_name='连接地址')
    port = models.IntegerField(verbose_name='端口')
    username = models.CharField(max_length=50, verbose_name='登录用户')
    # pkey = models.TextField(blank=True, null=True,verbose_name='ssh登录密钥')
    desc = models.CharField(max_length=255, null=True, verbose_name='描述', blank=True)

    def __str__(self):
        return self.hostname + ':' + self.ip_addr

    def get_ssh(self, pkey=None):
        # pkey = pkey or self.private_key

        return SSH(self.ip_addr, self.port, self.username, pkey)


# 主机类别表
class HostCategory(BaseModel):
    name = models.CharField(max_length=32, verbose_name='主机类别', unique=True)

    def __str__(self):
        return self.name
python manage.py makemigrations
python manage.py migrate

1.4 获取主机数据

view.py

from rest_framework.viewsets import ModelViewSet
from . import models
from .serializers import HostModelSerializer


class HostView(ModelViewSet):
    queryset = models.Host.objects.filter(is_show=True, is_deleted=False)
    serializer_class = HostModelSerializer


serializers.py

from rest_framework import serializers
from . import models


# 主页面
class HostModelSerializer(serializers.ModelSerializer):
    # 只展示主机类别名,不提交
    category_name = serializers.CharField(source='category.name', read_only=True)

    # {'category': 1, 'hostname':'root' ,'ip_addr': '127.1.1.1', 'port':22, desc:'asdf'}
    class Meta:
        model = models.Host
        # category指向的是id值
        fields = ['id', 'category', 'category_name', 'hostname', 'ip_addr', 'port', 'username', 'desc']

        extra_kwargs = {
            'category': {'write_only': 'True'}
        }


urls.py

from django.urls import path

from . import views

urlpatterns = [

    path('list/', views.HostView.as_view({'get': 'list', 'post': 'create'})),

]

添加模拟数据

postman测试

1.5 新建主机

用户提交数据到后,后端做校验,然后测试远程连接是否能通,并检查是否存在密钥,不存在则创建,然后将公钥保存在远程服务器上,pop密码之后然后保存主机信息到数据库中

serializer.py

from rest_framework import serializers
from . import models
from pippo_api.libs.ssh import SSH
from pippo_api.utils.check_host import valid_ssh
from pippo_api.utils.handle_key import AppSetting

# 主页面
class HostModelSerializer(serializers.ModelSerializer):
    # 只展示主机类别名,不提交
    category_name = serializers.CharField(source='category.name', read_only=True)
    password = serializers.CharField(write_only=True)
    # {'category': 1, 'hostname':'root' ,'ip_addr': '127.1.1.1', 'port':22, desc:'asdf'}
    class Meta:
        model = models.Host
        # category指向的是id值
        fields = ['id', 'category','password', 'category_name', 'hostname', 'ip_addr', 'port', 'username', 'desc']

        extra_kwargs = {
            'category': {'write_only': 'True'}
        }
    # 校验用户提交过来的数据,是否真实的能够链接某台主机,

    def validate(self, attrs):
        ip_addr = attrs.get('ip_addr')
        port = attrs.get('port')
        username = attrs.get('username')
        password = attrs.get('password')
        #调取封装好的方法做校验
        ret = valid_ssh(ip_addr, port, username, password)
        if not ret:

            raise serializers.ValidationError('参数校验失败,请检查输入的内容')

        return attrs

    #修改保存数据
    def create(self, validated_data):
        # print('>>>>>',validated_data)
        ip_addr = validated_data.get('ip_addr')
        port = validated_data.get('port')
        username = validated_data.get('username')
        password = validated_data.get('password')

        # 创建公私钥之前,我们先看看之前是否已经创建过公私钥了
        _cli = SSH(ip_addr, port, username, password=str(password))
        try:
            private_key = AppSetting.get('private_key')
            public_key = AppSetting.get('public_key')
        except KeyError:

            # 如果之前没有创建过,那么就创建公钥和私钥
            private_key, public_key = _cli.generate_key()
            # 将公钥和私钥保存到数据库中
            AppSetting.set('private_key', private_key, 'ssh private key')
            AppSetting.set('public_key', public_key, 'ssh public key')

        # 将公钥上传到服务器上保存

        _cli.add_public_key(public_key)

        # # 剔除密码字段,保存host记录
        password = validated_data.pop('password')
        new_host_obj = models.Host.objects.create(
            **validated_data
        )
        return new_host_obj
 

封装的验证方法

handle_key.py

from functools import lru_cache
from host.models import PkeyModel


class AppSetting:

    keys = ('public_key', 'private_key',)

    # 由于我们可能经常会执行这个get操作,所以我们使用了django的缓存机制,对方法的结果进行缓存,第二次调用 get()方法 时,并没有真正执行方法,而是直接返回缓存的结果,参数maxsize为最多缓存的次数,如果为None,则无限制,设置为2n时,性能最佳
    @classmethod
    @lru_cache(maxsize=64)
    def get(cls, key):  # public_key
        info = PkeyModel.objects.filter(key=key).first()
        if not info:
            raise KeyError(f'没有这个 {key!r}')
        return info.value

    @classmethod
    def set(cls, key, value, desc=None):  # public_key,  ('public_key', 'private_key',)
        if key in cls.keys:
            PkeyModel.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc})
        else:
            raise KeyError('key数据不正常')


check_host.py

from pippo_api.libs.ssh import SSH


def valid_ssh(hostname, port, username, password):
    ssh = SSH(hostname, port=port, username=username, password=password)
    try:
        sss = ssh.ping()
        # print(f'{sss}1111111111')
    except Exception as e:
        # print(1111111111111111)
        return False

    return sss


1.6 主机类别获取

url地址

path('categorys/', views.HostCategoryView.as_view({'get': 'list', 'post': 'create'})),
re_path(r'categorys/(?P<pk>\d+)/', views.HostCategoryView.as_view({'put': 'update'})),

试图

class HostCategoryView(ModelViewSet):
    queryset = models.HostCategory.objects.filter(is_show=True, is_deleted=False)
    serializer_class = HostCategoryModelSerializer

序列化器

#获取主机类别
class HostCategoryModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.HostCategory
        fields = ['id', 'name']

1.7 批量导入导出

url.py

    re_path('^host_excel/', views.HostExcelView.as_view()),

view.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.views import APIView
from rest_framework.response import Response
from django.shortcuts import render, HttpResponse
from . import models
from .serializers import HostModelSerializer, HostCategoryModelSerializer
from pippo_api.utils.handle_excel import read_host_excel_data
import xlwt
from io import BytesIO, StringIO


class HostView(ModelViewSet):
    queryset = models.Host.objects.filter(is_show=True, is_deleted=False)
    serializer_class = HostModelSerializer


class HostCategoryView(ModelViewSet):
    queryset = models.HostCategory.objects.filter(is_show=True, is_deleted=False)
    serializer_class = HostCategoryModelSerializer


class HostExcelView(APIView):
    def post(self, request):
        host_excel = request.data.get('host_excel')

        # 创建一个excel的临时存储目录(根目录下创建tem_file),先保存excel文件到本地,然后读取数据,保存,最后删除excel文件。
        # sio = StringIO()
        sio = BytesIO()
        for i in host_excel:
            sio.write(i)

        # res_data = read_host_excel_data(file_path, default_password)
        res_data = read_host_excel_data(sio)
        # 拿到上传之后的数据之后,我们删掉上传上来的临时excel文件

        # import os
        # if os.path.exists(file_path):  # 如果文件存在
        #     # 删除文件,可使用以下两种方法。
        #     os.remove(file_path)
        #     # os.unlink(path)
        # else:
        #     print('没有该文件:%s' % file_path)  # 则返回文件不存在

        return Response(res_data)

    # 下载host数据excel文件
    def get(self, request):
        # 1 读取数据库数据
        all_host_data = models.Host.objects.all().values('id', 'category', 'hostname', 'ip_addr', 'port',
                                                         'username', 'desc', )
        print(all_host_data)

        # 2 写入excel并保存
        # 关于excel的操作,参考我的博客
        # 创建excel
        ws = xlwt.Workbook(encoding='utf-8')
        # 创建工作簿
        st = ws.add_sheet('主机数据')

        # 写标题行
        st.write(0, 0, 'id')
        st.write(0, 1, 'category')
        st.write(0, 2, 'hostname')
        st.write(0, 3, 'ip_addr')
        st.write(0, 4, 'port')
        st.write(0, 5, 'username')
        st.write(0, 6, 'desc')
        st.write(0, 7, 'evrironments')

        # 写入数据,从第一行开始
        excel_row = 1
        for host_obj in all_host_data:
            st.write(excel_row, 0, host_obj.get('id'))
            st.write(excel_row, 1, host_obj.get('category'))
            st.write(excel_row, 2, host_obj.get('hostname'))
            st.write(excel_row, 3, host_obj.get('ip_addr'))
            st.write(excel_row, 4, host_obj.get('port'))
            st.write(excel_row, 5, host_obj.get('username'))
            st.write(excel_row, 6, host_obj.get('desc'))
            st.write(excel_row, 7, host_obj.get('evrironments'))
            excel_row += 1

        # sio = BytesIO()
        from io import BytesIO
        # 将数据写入io数据流,不用在本地生成excel文件,不然效率就低了
        sio = BytesIO()
        ws.save(sio)
        sio.seek(0)
        # print(sio.getvalue())

        # 3 将excel数据响应回客户端
        response = HttpResponse(sio.getvalue(), content_type='application/vnd.ms-excel')
        # response['Content-Disposition'] = 'attachment; filename=xx.xls'

        # 文件名称中文设置
        from django.utils.encoding import escape_uri_path
        response['Content-Disposition'] = 'attachment; filename={}'.format(escape_uri_path('主机列表数据.xls'))
        response.write(sio.getvalue())  # 必须要给response写入一下数据,不然不生效

        return response


handle_excel.py

from host.serializers import HostModelSerializer
from host.models import HostCategory
import xlrd


def read_host_excel_data(host_excel_file_path):
    # SIO  default_password
    # print(host_excel_file_path.getvalue())
    # data = xlrd.open_workbook(本地文件路径)
    # open_workbook无法处理xlsx的文件,需要改为xls的文件

    data = xlrd.open_workbook(file_contents=host_excel_file_path.getvalue())

    # 根据索引获取第一个sheet工作簿
    sheet = data.sheet_by_index(0)
    rows_count = sheet.nrows  # 数据行数

    # 查询出所有分类数据
    category_list = HostCategory.objects.values_list('id', 'name')
    # print(category_list)

    host_info_list = []
    for row_number in range(1, rows_count):
        one_row_dict = {}
        # print(sheet.cell(row_number, 0))  # 类型:值,  参数:(行号,列号)
        # print(sheet.cell_type(row_number, 0))  # 单元格数据类型
        # print(sheet.cell_value(row_number, 0))  #获取值
        category = sheet.cell_value(row_number, 0)

        # 由于拿到的是分类名称,所以我们要找到对应名称的分类id,才能去数据库里面存储
        for category_data in category_list:
            # print(category_data[1],type(category_data[1]),category,type(category))
            if category_data[1].strip() == category.strip():
                one_row_dict['category'] = category_data[0]
                break

        # category_id = category
        # one_row_dict['category'] = category_id
        # 注意:数据列要对应
        one_row_dict['hostname'] = sheet.cell_value(row_number, 1)
        one_row_dict['ip_addr'] = sheet.cell_value(row_number, 2)
        one_row_dict['port'] = sheet.cell_value(row_number, 3)
        one_row_dict['username'] = sheet.cell_value(row_number, 4)

        # 如果该条记录中没有密码数据,那么使用用户填写的默认密码,如果默认密码也没有,那么报错
        # pwd = sheet.cell_value(row_number, 5)
        # print(sheet.cell_value(row_number, 5),type(sheet.cell_value(row_number, 5)))
        excel_pwd = sheet.cell_value(row_number, 5)
        # 这样强转容易报错,最好捕获一下异常,并记录单元格位置,给用户保存信息时,可以提示用户哪个单元格的数据有问题

        one_row_dict['password'] = excel_pwd
        # print(one_row_dict)

        one_row_dict['desc'] = sheet.cell_value(row_number, 6)

        host_info_list.append(one_row_dict)

    # 校验主机数据
    # 将做好的主机信息字典数据通过我们添加主机时的序列化器进行校验
    res_data = {}  # 存放上传成功之后需要返回的主机数据和某些错误信息数据
    serializers_host_res_data = []
    res_error_data = []
    print(host_info_list)
    for k, host_data in enumerate(host_info_list):  # [{},{}]
        s_obj = HostModelSerializer(data=host_data)
        # print(s_obj.is_valid())
        if s_obj.is_valid():
            new_host_obj = s_obj.save()

            serializers_host_res_data.append(new_host_obj)
        else:
            # 报错,并且错误信息中应该体验错误的数据位置
            res_error_data.append({'error': f'该{k + 1}行数据有误,其他没有问题的数据,已经添加成功了,请求失败数据改完之后,重新上传这个错误数据,成功的数据不需要上传了'})

    # 再次调用序列化器进行数据的序列化,返回给客户端
    s2_obj = HostModelSerializer(instance=serializers_host_res_data, many=True)
    # print(s2_obj.data)
    res_data['data'] = s2_obj.data
    res_data['error'] = res_error_data

    return res_data


1.8 编辑删除

url.py

re_path(r'list/(?P<pk>\d+)/', views.HostView.as_view({'get': 'retrieve', 'delete': 'destroy','put':'partial_update'})),

serializer.py

        # extra_kwargs = {
        #     'category': {'write_only': 'True'}
        # }

注释掉即可

1.9 页面展示

Host.vue

<template>
  <!--  卡片-->
  <a-card style="width: 100%">
    <div class="top">
      <!--      栅格,选择器-->
      <a-row>
        <a-col :span="5">
          主机类别:
          <a-select default-value="" style="width:60%;margin: 0 8px 8px 0" @change="handleChange">
            <a-select-option value="">全部</a-select-option>
            <a-select-option :value="host_category.name"
                             v-for="(host_category,category_index) in category_data" :key="category_index">
              {{ host_category.name }}
            </a-select-option>
          </a-select>
        </a-col>
        <a-col :span="5">
          主机别名:
          <a-input placeholder="请输入" allow-clear style="width: 60%" v-model="host_name"/>

        </a-col>
        <a-col :span="5">
          主机地址:
          <a-input placeholder="请输入" allow-clear style="width: 60%" v-model="host_link"/>
        </a-col>
        <a-col :span="5">
          <a-button type="primary" icon="search" @click="onSearch" style="margin-right: 20px">
            搜索
          </a-button>
          <a-button type="primary" icon="sync" onclick="location.reload()">
            刷新
          </a-button>

        </a-col>
      </a-row>


    </div>
    <div class="middle" style="margin-top: 15px">
      <a-button type="primary" icon="plus" @click="showModal">新建</a-button>
      <a-modal v-model="visible" title="新建主机" @ok="handleOk" @cancel="handleCancel" width="800px" ok-text="确认"
               cancel-text="取消">
        <a-form-model
            ref="ruleForm"
            :model="add_host.form"
            :rules="add_host.rules"
            :label-col="add_host.labelCol"
            :wrapper-col="add_host.wrapperCol"
        >
          <a-form-model-item ref="category" label="主机类别" prop="category">
            <a-row>
              <a-col :span="13">
                <a-select placeholder="请选择" label-in-value @change="handleChange22">
                  <a-select-option :value="cate_data.id" v-for="(cate_data,cate_index) in category_data"
                                   :key="cate_index">
                    {{ cate_data.name }}
                  </a-select-option>
                </a-select>
              </a-col>
              <a-col :span="4" :offset="3">
                <button type="button" class="ant-btn ant-btn-link" @click="sshowModal"><span>添加类别</span></button>
                <a-modal v-model="svisible" title="添加类别" @ok="cataOk" ok-text="确认" cancel-text="取消">
                  <a-alert type="info" message="请确认要添加的主机类别不存在。" banner closable/>
                  <div style="margin-top: 10px">
                    <a-input placeholder="请输入主机类别" v-model="catas"/>
                  </div>
                </a-modal>
              </a-col>
              <a-col :span="4">
                <button type="button" class="ant-btn ant-btn-link" @click="ssshowModal"><span>编辑类别</span></button>
                <a-modal v-model="ssvisible" title="编辑类别" @ok="sshandleOk" ok-text="确认" cancel-text="取消">
                  <div>
                    <a-alert type="info" message="请选择左侧的下拉框,然后修改对应属性。" banner closable/>
                  </div>
                  <div style="margin-top: 10px">
                    <a-input v-model="edit_cata"/>
                  </div>
                </a-modal>
              </a-col>
            </a-row>
          </a-form-model-item>
          <a-form-model-item ref="hostname" label="主机名称" prop="hostname">
            <a-input v-model="add_host.form.hostname"/>
          </a-form-model-item>
          <a-form-model-item label="用户名称" prop="username">
            <a-input addon-before="用户名" v-model="add_host.form.username"/>
          </a-form-model-item>
          <a-form-model-item label="链接地址" prop="ip_addr">
            <a-input addon-before="@" v-model="add_host.form.ip_addr"/>
          </a-form-model-item>
          <a-form-model-item label="主机端口" prop="port">
            <a-input addon-before="端口" v-model="add_host.form.port"/>
          </a-form-model-item>
          <a-form-model-item ref="password" label="连接密码" prop="password">
            <a-input
                v-model="add_host.form.password"/>
          </a-form-model-item>

          <a-form-model-item label="描述">
            <a-input v-model="add_host.form.desc" type="textarea"/>
          </a-form-model-item>

        </a-form-model>
      </a-modal>
      <a-button type="primary" icon="import" style="margin-left: 20px;" @click="import_showModal">批量导入</a-button>
      <a-modal v-model="import_visible" title="批量导入" width="800px">
        <div>
          <a-alert type="info" message="导入或输入的密码仅作首次验证使用,并不会存储密码。" banner closable/>
        </div>
        <a-form v-bind="formItemLayout">

          <a-form-item label="模板下载" extra="请下载使用该模板填充数据后导入">
            <a href='../../../主机导入模板.xls'>主机导入模板.xlsx</a>
          </a-form-item>
          <a-form-item label="导入数据">
            <a-upload accept=".xls" :file-list="excel_fileList" :remove="handleExcelRemove"
                      :before-upload="beforeExcelUpload">


              <a-button>
                <a-icon type="upload"/>
                上传文件
              </a-button>
            </a-upload>
          </a-form-item>

        </a-form>
        <template slot="footer">
          <a-button key="back" @click="import_handleCancel">
            取消导入
          </a-button>
          <a-button key="submit" type="primary" :loading="uploading" @click="import_handleOk"
                    :disabled="excel_fileList.length === 0">
            {{ uploading ? '正在上传' : '导入' }}
          </a-button>

        </template>
      </a-modal>
      <a style="color:green; margin-left: 10px" href="http://127.0.0.1:8000/host/host_excel/"> 导出主机列表数据</a>

    </div>
    <div class="down" style="margin-top: 20px">
      <a-table :columns="columns" :data-source="host_data2" rowKey="id">
        <a slot="action" slot-scope="text,scope" href="javascript:;">

          <a-button type="link" size="small" @click="edit_showModal">
            <router-link :to="'/hippo/host/'+ scope.id + '/'">编辑</router-link>
          </a-button>
          <a-modal v-model="edit_visible" title="编辑主机" @ok="edit_handleOk" @cancel="edit_handleCancel" width="800px"
                   ok-text="确认"
                   cancel-text="取消">
            <a-form-model
                :label-col="add_host.labelCol"
                :wrapper-col="add_host.wrapperCol">
              <a-form-model-item label="主机类别">
                <a-row>
                  <a-col :span="13">
                    <a-select :value="edit_data.category" placeholder="请选择">
                      <a-select-option :value="cate_data.id" v-for="(cate_data,cate_index) in category_data"
                                       :key="cate_index">
                        {{ cate_data.name }}
                      </a-select-option>
                    </a-select>
                  </a-col>
                </a-row>
              </a-form-model-item>
              <a-form-model-item label="主机名称">
                <a-input v-model="edit_data.hostname"/>
              </a-form-model-item>
              <a-form-model-item label="用户名称">
                <a-input addon-before="用户名" v-model="edit_data.username"/>
              </a-form-model-item>
              <a-form-model-item label="链接地址">
                <a-input addon-before="@" v-model="edit_data.ip_addr"/>
              </a-form-model-item>
              <a-form-model-item label="主机端口">
                <a-input addon-before="端口" v-model="edit_data.port"/>
              </a-form-model-item>
              <a-form-model-item label="连接密码" prop="password">
                <a-input
                    v-model="edit_password"/>
              </a-form-model-item>

              <a-form-model-item label="描述">
                <a-input v-model="edit_data.desc" type="textarea"/>
              </a-form-model-item>

            </a-form-model>
          </a-modal>

          <a-popconfirm title="确定要删除么?" ok-text="Yes" cancel-text="No" @confirm="zzonSearch">
            <a-button type="link" size="small">
              <router-link :to="'/hippo/host/'+ scope.id + '/'">删除</router-link>
            </a-button>
          </a-popconfirm>

          <a-button type="link" size="small">
            <router-link :to="'/hippo/console/'+ scope.id + '/'">console</router-link>
          </a-button>
        </a>
      </a-table>
    </div>
    <div>
      <a-modal v-model="res_error" title="通知" @ok="error_ex"
               @cancel="error_ex">
        <div v-for="res_da in error_data">
          <p> {{ res_da }}</p>
        </div>
      </a-modal>
    </div>
  </a-card>


</template>

<script>
const columns = [
  {title: 'id', dataIndex: 'id', key: 'id'},
  {title: '类别', dataIndex: 'category_name', key: 'category_name'},
  {title: '主机名称', dataIndex: 'hostname', key: 'hostname'},
  {title: '连接地址', dataIndex: 'ip_addr', key: 'ip_addr'},
  {title: '端口号', dataIndex: 'port', key: 'port'},
  {title: '描述', dataIndex: 'desc', key: 'desc'},
  {title: '操作', dataIndex: '', key: 'x', scopedSlots: {customRender: 'action'}},
];

export default {
  name: "Host",
  data() {
    return {
      //上传信息

      formItemLayout: {
        labelCol: {span: 6},
        wrapperCol: {span: 14},
      },
      edit_password: "",
      excel_fileList: [],
      edit_visible: false,
      error_data: "",
      edit_cata: "", //编辑分类
      res_error: false,
      catas: "", //添加主机类别
      host_data: [], //主机数据
      host_data2: [], //主机数据
      category_data: [], //分类信息
      host_name: "",
      host_link: "",
      category_name: "",
      columns,
      visible: false, //新建
      ssvisible: false, //添加分类
      svisible: false, //编辑分类
      import_visible: false,
      uploading: false,
      edit_data: {},
      add_host: {
        labelCol: {span: 4},
        wrapperCol: {span: 14},
        other: '',
        form: {
          category: undefined,
          hostname: "",
          username: "",
          ip_addr: "",
          port: "22",
          desc: '',
          password: '',
        },
        rules: {
          category: [{required: true, message: '请选择主机类型', trigger: 'change'}],
          hostname: [{required: true, message: '请输入主机名', trigger: 'change'}],
          password: [{required: true, message: '请输入密码', trigger: 'change'}],
          username: [{required: true, message: '请输入用户名', trigger: 'change'}],
          ip_addr: [{required: true, message: '请输入链接地址', trigger: 'change'}],
          port: [{required: true, message: '请输入端口号', trigger: 'change'}],
        },
      }
    }
  },
  created() {
    this.get_host();
    this.get_category_data();
  },
  methods: {
    //input输入框
    handleChange(v) {
      this.category_name = v

    }
    ,

    //获取list列表
    get_host() {
      this.axios.get('/host/list/'
      ).then(res => {
            this.host_data = res.data
            this.host_data2 = res.data
          }
      )
    }
    ,
    get_category_data() {
      this.axios.get('/host/categorys'
      ).then(res => {
            this.category_data = res.data
          }
      )
    }
    ,

    //弹出新建对话框
    showModal() {
      this.visible = true;
    }
    ,
    edit_showModal() {
      this.axios.get(`/host/list/${this.$route.params.id}/`
      ).then(res => {
        this.edit_data = res.data
      })
      this.edit_visible = true;

    }
    ,
    ssshowModal() {
      this.ssvisible = true;
    }
    ,
    sshowModal() {
      this.svisible = true;
    }
    ,
    //新建主机
    handleOk(e) {
      this.$refs.ruleForm.validate(valid => {
        if (valid) {
          this.axios.post('/host/list/', this.add_host.form
          ).then(res => {
                this.host_data.push(res.data);
                this.$message.info('Success')
                this.$refs.ruleForm.resetFields();
                this.visible = false;
              }
          ).catch(error => {
            this.$message.error("请求参数有误!")
          })

        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    //编辑主机
    edit_handleOk(e) {
      delete this.edit_data.category_name
      console.log(this.edit_password)
      this.edit_data["password"] = this.edit_password
      this.axios.put(`/host/list/${this.$route.params.id}/`, this.edit_data
      ).then(res => {
            this.get_host()
            this.edit_visible = false
            this.$message.success("修改成功。")
          }
      ).catch(error => {
        this.$message.error("修改失败")
      })
    },
    // {category: 13, hostname: "zbb", username: "root", ip_addr: "47.95.38.42", port: "22", desc: "123213",…}

    //添加主机类型
    cataOk(e) {
      if (this.catas) {
        this.axios.post('/host/categorys/', {
          name: this.catas
        }).then(res => {
          this.category_data.push(res.data)
        }).catch(error => {
          this.$message.error("主机类别重复!")
        })
        this.svisible = false;
      } else {
        this.$message.error("请输入正确的参数。")
      }

    },
    sshandleOk(e) {
      if (this.edit_cata) {
        this.axios.put('/host/categorys/' + this.add_host.form.category + "/", {
          name: this.edit_cata
        }).then(res => {
          this.get_category_data();
        }).catch(error => {
          this.$message.error("编辑失败!")
        })
        this.ssvisible = false;
      } else {
        this.$message.error("请输入正确的参数。")
      }
    },
    handleCancel(e) {
      this.$refs.ruleForm.resetFields();
      this.visible = false;
    }
    ,
    edit_handleCancel(e) {
      this.edit_visible = false;
    },
    //批量导入对话框
    import_showModal() {
      this.import_visible = true;
    }
    ,
    //上传文件
    import_handleOk(e) {
      const formData = new FormData();
      this.excel_fileList.forEach((file_value, file_index) => {
        formData.append('host_excel', file_value);
      });
      this.uploading = true;
      this.axios.post('/host/host_excel/', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        }
      }).then(res => {
        this.uploading = false
        this.import_visible = false
        this.excel_fileList = []
        this.error_data = res.data.error
        this.host_data2 = this.host_data.concat(res.data.data);
        console.log(res.data)

        if (!this.error_data[0]) {
          this.$message.success("添加成功")

        } else {
          this.res_error = true
        }


      }).catch(error => {
        this.uploading = false
        this.$message.error('添加失败')

      })

    },
    import_handleCancel(e) {
      this.import_visible = false;
      this.excel_fileList = []
    }
    ,

    onSearch(value) {
      //模糊查询
      this.host_data2 = this.host_data
      if (this.category_name) {
        this.host_data2 = this.host_data2.filter(value => value.category_name.includes(this.category_name))
      }
      console.log(this.host_data2)
      if (this.host_name) {
        this.host_data2 = this.host_data2.filter(value => value.hostname.includes(this.host_name))
      }
      console.log(this.host_data2)
      if (this.host_link) {
        this.host_data2 = this.host_data2.filter(value => value.ip_addr.includes(this.host_link))

      }
      console.log(this.host_data2)
    },


    // 选中的想要上传的文件进行删除
    handleExcelRemove(file) {
      const index = this.excel_fileList.indexOf(file);
      const newFileList = this.excel_fileList.slice();
      newFileList.splice(index, 1);
      this.excel_fileList = newFileList;
    },
    // 上传之前将所有文件列表赋值给excel_fileList数据属性
    beforeExcelUpload(file) {
      // console.log('file>>>>>', file)
      this.excel_fileList = [...this.excel_fileList, file];
      // this.excel_fileList = this.excel_fileList.push(file);
      return false;
    },
    error_ex(e) {
      this.res_error = false;
      this.excel_fileList = []
    },

    //删除主机
    zzonSearch() {
      this.axios.delete(`/host/list/${this.$route.params.id}/`
      ).then(res => {
        this.get_host()
      })
    },
    handleChange22(value) {
      this.edit_cata = value.label
      this.add_host.form.category = value.key
    }

  },


}
;
</script>

<style scoped>

</style>

路由

    {
        path: '/hippo',
        component: Hippo,
        children: [
            {
                path: '',
                component: Home,
            }, {
                path: 'host/:id/',//动态路由匹配
                component: Host,
            }, {
                path: 'host/',
                component: Host,
            }

        ]

    },

1.10 Console功能

Xterm.js安装

npm install xterm

xterm.js初始化

在main.js文件中加上如下内容

import 'xterm/css/xterm.css'
import 'xterm/lib/xterm'

新建Console.vue

<template>
  <div class="hello">
    <div id="terminal"></div>
  </div>
</template>

<script>
  import { Terminal } from 'xterm'

  export default {
    name: 'Console',
    data() {
      return {}
    },

    mounted() {
      // var term = new Terminal(); 初始化Terminal页面
      var term = new Terminal({
       rendererType: "canvas", //渲染类型
       rows: 40, //行数
       convertEol: true, //启用时,光标将设置为下一行的开头
       scrollback:100,//终端中的回滚量
       disableStdin: false, //是否应禁用输入。
       cursorStyle: 'underline', //光标样式
       cursorBlink: true, //光标闪烁
       theme: {
         foreground: '#ffffff', //字体
         background: '#060101', //背景色
         cursor: 'help',//设置光标
       }
     });
      // 建立websocket
      // let ws = new WebSocket('ws://127.0.0.1:8000/ws/ssh/58/');
      let ws = new WebSocket(`ws://127.0.0.1:8000/ws/ssh/${this.$route.params.id}/`);
      var keyWord = '';  // 拼接用户输入的内容
      ws.onmessage = function (event) {
        if (!keyWord){
          //所要执行的操作
          term.write(event.data);
        }else {
          keyWord=''
          // 对响应回来的数据进行一些加工处理,筛选出结果内容部分
          let a = event.data.replace(event.data.split('\r\n',1)[0],'');
          let b = a.split('\r\n',-1).slice(0,-1).join('\r\n');
          term.write(b);
        }

      }
      term.prompt = () => {
        term.write('\r\n');
        // term.write('\r\n$ ')
        msg = '';
      }
      term.onKey(e => { //监听用户的键盘操作
        // console.log(e)
        const ev = e.domEvent
        const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey

        // console.log('>>>>',ev.keyCode);
        if (ev.keyCode === 13) {
          // console.log(keyWord);
          // 按下回车键进行指令的发送
          ws.send(keyWord);

        } else if (ev.keyCode === 8) {
          // Do not delete the prompt
          if (term._core.buffer.x > 2) {
            term.write('\b \b')
          }
        } else if (printable) {
          term.write(e.key);
          keyWord += e.key
        }
      })
      term.open(document.getElementById('terminal'));

    }

  }
</script>

<style scoped>

</style>


路由配置

import Console from "@/views/Console";
			{
                path: 'console/:id/', //动态路由匹配
                component: Console,
            },

1.11 后端配置websocket

windows安装channel

1.下载channel.whl包安装:

https://pypi.org/project/channels/#files

下载Twisted的.whl格式:

https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

pip install Twisted.whl
pip install channels.whl

配置settings

INSTALLED_APPS = ['channels',] #注册
# 配置channel的通道层
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": ["redis://:zxy5221@47.95.32:6379/0"],  # 需修改
        },
    },
}

ASGI_APPLICATION = "pippo_api.asgi.application"

asgi.py

"""
WSGI config for pippo_api project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""

import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
from django.urls import path
from host.consumers import SSHConsumer

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pippo_api.settings')

application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('ws/ssh/<int:id>/', SSHConsumer.as_asgi()),
        ]
        )
    )
    # Just HTTP for now. (We can add other protocols later.)
})


consumers.py

消费者模型

from channels.generic.websocket import WebsocketConsumer
from host.models import Host
from threading import Thread
import json
from host.models import PkeyModel


class SSHConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = None
        self.chang = None
        self.ssh = None

    # 1 请求来了自动触发父类connect方法,我们继承拓展父类的connect方法,因为我们和客户端建立连接的同时,就可以和客户端想要操作的主机建立一个ssh连接通道。
    def connect(self):
        self.id = self.scope['url_route']['kwargs']['id']
        # print(self.scope['url_route'])
        # print('>>>>id', self.id)
        print('connect连接来啦74')
        self.accept()  # 建立websocket连接
        self._init()  # 建立和主机的ssh连接

    # 2 建立和主机的ssh连接
    def _init(self):
        # self.send(bytes_data=b'Connecting ...\r\n')
        self.send('Connecting ...\r\n')
        host = Host.objects.filter(pk=self.id).first()
        print(host)
        if not host:
            self.send(text_data='Unknown host\r\n')
            self.close()
        try:
            pkey = PkeyModel.objects.get(key='private_key').value
            self.ssh = host.get_ssh(pkey).get_client()
        except Exception as e:
            self.send(f'Exception: {e}\r\n')
            self.close()
            return

        self.chang = self.ssh.invoke_shell(term='xterm')  # invoke_shell激活shell终端模式,也就是长连接模式,exec_command()函数是将服务器执行完的结果一次性返回给你;invoke_shell()函数类似shell终端,可以将执行结果分批次返回,所以我们接受数据时需要循环的取数据

        self.chang.transport.set_keepalive(30)  # 连接中没有任何信息时,该连接能够维持30秒

        # 和主机的连接一旦建立,主机就会将连接信息返回给服务端和主机的连接通道中,并且以后我们还要在这个通道中进行指令发送和指令结果的读取,所以我们开启单独的线程,去连接中一直等待和获取指令执行结果的返回数据
        Thread(target=self.loop_read).start()

    # 3 接受连接返回结果并返回给客户端     # 5 接受主机指令的执行结果,并返回给客户端
    def loop_read(self):

        while True:
            data = self.chang.recv(32 * 1024) #没有指令就一直等这
            print(data)
            if not data:
                self.close(3333)
                break
            self.send(data.decode())

    # 4 接受客户端发送过来的指令,并发送给主机执行指令
    def receive(self, text_data=None, bytes_data=None):
        data = text_data or bytes_data
        print('receive:  xxxxxx', data, type(data))
        if data:
            self.chang.send(data + '\r\n')

    def disconnect(self, code):
        self.chang.close()
        self.ssh.close()
        print('Connection close')


1.12 文件管理

url.py

re_path('^list/(?P<pk>\d+)/', views.HostView.as_view({'get': 'retrieve'})),

展示文件管理目录

urls.py

re_path(r'^file/(?P<pk>\d+)/', views.HostFileView.as_view({'get': 'get_folders','post': 'upload_file', 'delete': 'delete_file'})),

Host/views.py

from pippo_api.utils.handle_key import AppSetting
from rest_framework.viewsets import ViewSet
from rest_framework import status

class HostFileView(ViewSet):
    # 方法分发之前,先获取要操作的主机id和链接
    def dispatch(self, request, *args, **kwargs):
        pk = kwargs.get('pk')
        host_obj = models.Host.objects.get(pk=pk)
        pkey = AppSetting.get('private_key')
        cli = host_obj.get_ssh(pkey)
        self.cli = cli
        ret = super().dispatch(request, *args, **kwargs)

        return ret

    # get_folders获取某个目录的文件和文件夹信息列表
    #主要是前端发送的请求ls或者ls-a
    def get_folders(self, request, pk):
        cmd = request.query_params.get('cmd')

        res_code, res_data = self.cli.exec_command(cmd)
        # print('!!!!!!!', res_code,res_data)
        return Response([res_code, res_data])

    def download_file(self, request, pk, file_path):
        pass

    def upload_file(self, request, pk):
        folder_path = request.query_params.get('folder_path')

        file_obj = request.FILES.get('file')
        folder_path += f'/{file_obj.name}'
        # print(folder_path)
        file_size = file_obj.size

        try:
            self.cli.put_file_by_fl(file_obj, folder_path, self.file_upload_callback)
        except:
            return Response({'error': '文件上传失败,请联系管理员或者查看一下用户权限'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        return Response({'msg': 'ok'})

    def delete_file(self, request, file_path):
        pass

    def file_upload_callback(self, n, k):
        print('>>>>>>>>>>>', n, k)

<template>
  <div class="console">
    <div class="console_header">
      <div class="info">
        {{host_info.hostname}} | {{host_info.username}}@{{host_info.ip_addr}}:{{host_info.port}}
      </div>
      <div class="file_part">
        <button type="button" class="ant-btn ant-btn-primary" @click="showDrawer">
          <a-icon type="folder-open"/>
          文件管理器
        </button>
      </div>
    </div>

    <div class="file_show">

      <div>

        <a-drawer
          title="文件管理器"
          :width="900"
          :visible="visible"
          :body-style="{ paddingBottom: '80px' }"
          @close="onClose"
        >
          <div class="file_nav">
            <div>
              <a-breadcrumb>
                <a-breadcrumb-item>
                  <a-icon @click="back_folder('/',1)" type="home"/>
                </a-breadcrumb-item>

                <a-breadcrumb-item v-for="(folder_path,f_index) in path" :key="f_index" v-show="folder_path!=='/'">
                  <span style="cursor: pointer;" @click="back_folder(folder_path,f_index)">{{folder_path}}</span>
                </a-breadcrumb-item>
              </a-breadcrumb>
            </div>
            <div style="display: flex; align-items: center;">
              <span>显示隐藏文件:</span>
              <a-switch @change="switch_on_off" checked-children="开" un-checked-children="关"/>
              <div style="margin-left: 10px">
                <a-upload
                  name="file"
                  :multiple="true"
                  :action="this.$settings.Host +  visit_url+'?folder_path=' + folder_path_str"
                  :headers="headers"
                  @change="handleChange"
                >
                  <a-button type="primary">
                    <a-icon type="upload"/>
                    上传文件
                  </a-button>
                </a-upload>
              </div>

            </div>


          </div>
          <div>
            <a-table
              :columns="columns"
              :data-source="data"
              :pagination="false"
              :scroll="{ y: 400 }"
            >

              <a slot="name" slot-scope="text,record"> <!-- record表示该条记录,是个字典 -->
                <span @click="join_folder(text)" v-if="record.file_attr.substr(0,1)==='d'">
                  <a-icon type="folder"/>

                {{ text }}
                </span>
                <span v-else>
                  <a-popconfirm placement="top" ok-text="下载" cancel-text="取消" @confirm="confirm(text)">
                    <template slot="title">
                      <p>确认下载该文件吗?</p>
                      <p>{{ text }}</p>
                    </template>

                      <a-icon type="file"/>
                      {{ text }}

                  </a-popconfirm>
                </span>
              </a>


            </a-table>
          </div>
        </a-drawer>
      </div>
    </div>


    <div id="terminal"></div>
  </div>
</template>

<script>
  import {Terminal} from 'xterm'

  const columns = [

    {
      title: '名称',
      dataIndex: 'file_name',
      width: 300,
      scopedSlots: {customRender: 'name'},
    },
    {
      title: '大小',
      dataIndex: 'file_size',

    },
    {
      title: '修改时间',
      dataIndex: 'file_modify_time',
      width: 200,
    },
    {
      title: '属性',
      dataIndex: 'file_attr',
      width: 150,
      scopedSlots: {customRender: 'file_attr'},
    },
    {
      title: '操作',
      dataIndex: 'action',
    },
  ];

  const data = [];
  // for (let i = 0; i < 3; i++) {
  //   data.push({
  //     key: i,
  //     name: `home`,
  //     age: 32,
  //     address: `London, Park Lane no. ${i}`,
  //   });
  // }
  export default {
    name: 'Console',
    data() {
      return {
        host_info: {},
        visible: false,
        headers: {
          authorization: 'authorization-text',
        },
        ws: null, // websocket连接
        data,
        columns,
        path: ['/',], // 默认是根路径,获取根路径下所有的文件和文件夹 ls-l /
        // shell_or_folder:0, // 判断用户是在操作shell还是在操作文件管理器
        file_folder_list: [], // 存放目录和文件信息数据
        // cmd1:`\\ls -l -h --time-style '+%Y/%m/%d %H:%M:%S'`,
        // cmd2:`\\ls -l -h -a --time-style '+%Y/%m/%d %H:%M:%S'`,
        ls_cmd: "\\ls -l -h --time-style '+%Y/%m/%d %H:%M:%S'",
        folder_path_str: '/',
        visit_url:'',
      }
    },
    methods: {
      // 下载文件
      confirm(text){
        // 拼接文件下载路径
        let file_path = `${this.folder_path_str}/${text}`;
        console.log(file_path);
        // 发送请求,下载文件数据
        this.axios.get(`/host/file2/${this.$route.params.id}/`
        ).then()

      },


			// 显示隐藏文件和隐藏显示文件
      switch_on_off(e) {
        // console.log('>>>',e);  // true\false
        if (e) {
          // 开启显示隐藏文件
          this.ls_cmd = `\\ls -l -h -a --time-style '+%Y/%m/%d %H:%M:%S'`
          this.send_show_folder_cmd(this.folder_path_str, this.ls_cmd);

        } else {
          // 关闭显示隐藏文件
          this.ls_cmd = `\\ls -l -h --time-style '+%Y/%m/%d %H:%M:%S'`
          this.send_show_folder_cmd(this.folder_path_str, this.ls_cmd);
        }

      },

      back_folder(text, f_index) {
        // this.path = this.path.slice(0,f_index+1);
        this.path = this.path.slice(0, f_index);

        this.join_folder(text);
      },

			// 拼接访问的目录路径
      join_folder(text) {
        // console.log(text);
        // 发请求获取路径下的文件或者文件夹信息
        // this.$axios.get(`${this.settings.host}/`)
        // 拼接路径
        // console.log(text);
        // this.path.push(text);
        // let now_year = (new Date()).getFullYear();


        this.file_folder_list = [];

        if (text === '/') {
          this.path = ['/',]
        } else {
          this.path.push(text);
        }
        // console.log('>>>>>',this.path);

        let folder_path = this.path.join('/');
        // folder_path = '/'
        if (this.path.length > 1) {
          folder_path = folder_path.slice(1);
        }
        this.folder_path_str = folder_path;
        // console.log(this.path,'|||||',folder_path)
        // console.log(this.ws);
        // folder_path = '/home'
        // this.ws.send(`xx*ls -l ${folder_path}`);
        this.send_show_folder_cmd(this.folder_path_str, this.ls_cmd);

      },

      // 发送ls指令
      send_show_folder_cmd(folder_path, cmd) {
        this.axios.get(this.visit_url, {
          params: {
            cmd: `${cmd} ${folder_path}`,
          }
        }).then((res) => {
          console.log('>>>>>>',this.folder_path_str);
          console.log('>>>>>>',res);
          this.data = [];
          let data = res.data;
          // console.log(data);
          let data_l = data[1].split('\n').slice(1);

          // console.log('///',data_l);
          data_l.forEach((file_info, file_index) => {
            // console.log(v);
            if (file_info) {
              // console.log(file_info,file_index);
              //["drwxr-xr-x", "2", "root", "root", "4096", "2020/09/14", "17:34:06", "bin"]
              let files_list = file_info.trim().split(/\s+/);
              // console.log(files_list);
              let a_list = files_list.slice(5, 7);
              let timer = a_list.join(' ');

              this.data.push({
                key: `${files_list[7] + 1}`,
                file_name: files_list[7],  //[-1], 不支持负数索引
                file_size: files_list[4],
                file_modify_time: timer,
                file_attr: files_list[0],

              })

            }


          })
          // this.file_folder_list = this.file_folder_list.concat(data_l_2);

        }).catch((error) => {
          console.log('报错啦!!!');
        })
      },


      show_comand_result() {
        let pk = this.$route.params.id;
        this.axios.get(`/host/list/${pk}/`)
          .then((res) => {
            // console.log(res);
            this.host_info = res.data;
          })
          .catch((error) => {
            this.$message.error("获取信息失败!")
          })

        // var term = new Terminal();
        var term = new Terminal({
          rendererType: "canvas", //渲染类型
          rows: 40, //行数
          convertEol: true, //启用时,光标将设置为下一行的开头
          scrollback: 100,//终端中的回滚量
          disableStdin: false, //是否应禁用输入。
          cursorStyle: 'underline', //光标样式
          cursorBlink: true, //光标闪烁
          theme: {
            foreground: '#ffffff', //字体
            background: '#060101', //背景色
            cursor: 'help',//设置光标
          }
        });
        // let ws = new WebSocket(`ws://127.0.0.1:8000/ws/ssh/${this.$route.params.id}/`);
        let ws = new WebSocket('ws://'+this.$settings.Host2+'/ws/ssh/'+this.$route.params.id+'/');
        this.ws = ws;
        var keyWord = '';
        this.ws.onmessage = function (event) {
          // msg += event.data
          // term.prompt();
          // console.log('收到消息:' + event.data)
          if (!this.visible) {
            if (!keyWord) {
              //所要执行的操作
              term.write(event.data);
            } else {
              keyWord = ''
              // 对响应回来的数据进行一些加工处理,筛选出结果内容部分
              let a = event.data.replace(event.data.split('\r\n', 1)[0], '');
              let b = a.split('\r\n', -1).slice(0, -1).join('\r\n');
              term.write(b);
            }
          } else {
            // console.log('>>>>>>',event.data);
          }

        }
        term.prompt = () => {
          term.write('\r\n');
          // term.write('\r\n$ ')
          msg = '';
        };


        term.onKey(e => {
          // console.log(e)
          const ev = e.domEvent
          const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey

          // console.log('>>>>', ev.keyCode);
          if (ev.keyCode === 13) {
            // ws.send()
            // console.log(keyWord);
            this.ws.send(keyWord);
            // keyWord=''

          } else if (ev.keyCode === 8) {
            // Do not delete the prompt
            if (term._core.buffer.x > 2) {
              term.write('\b \b')
            }
          } else if (printable) {
            term.write(e.key);
            keyWord += e.key
          }
        })
        term.open(document.getElementById('terminal'));
        // term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')


      },


      afterVisibleChange(val) {
        console.log('visible', val);
      },
      showDrawer() {
        this.visible = true;
        this.visit_url = `/host/file/${this.$route.params.id}/`;
        this.join_folder('/');
      },
      onClose() {
        this.visible = false;
      },
      handleChange(info) {
        if (info.file.status !== 'uploading') {
          // console.log(info.file, info.fileList);

        }
        if (info.file.status === 'done') {
          this.send_show_folder_cmd(this.folder_path_str, this.ls_cmd);
          this.$message.success(`${info.file.name} file uploaded successfully`);


        } else if (info.file.status === 'error') {
          this.$message.error(`${info.file.name} file upload failed.${info.file.response.error}`);
        }
      },
    },

    mounted() {
      this.show_comand_result();

    },


  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .console_header {
    display: flex;
    align-items: center;
    height: 46px;
    justify-content: space-between;
    padding: 0 10px;
    background-color: #e6f7ff;
  }

  .file_nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
  }

</style>


posted @ 2021-03-03 10:53  追梦nan  阅读(318)  评论(0编辑  收藏  举报