3

Django实现WebSSH操作物理机或虚拟机

 2 years ago
source link: https://blog.ops-coffee.cn/s/a3ejjvttuujzwyk21ntbqq
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Django实现WebSSH操作物理机或虚拟机

我想用它替换掉xshell、crt之类的工具

WebSSH操作物理机或虚拟机

Django实现WebSSH操作Kubernetes Pod文章发布后,有小伙伴说咖啡哥,我们现在还没有用上Kubernetes,但我想通过浏览器连接我们的物理机和虚拟机该怎么办?

这就比较简单了,既然我们已经实现了浏览器操作Kubernetes的Pod,那么想想操作Pod和物理机虚拟机有什么区别呢?

整个数据流是一点没变:用户打开浏览器--》浏览器发送websocket请求给Django建立长连接--》Django与要操作的服务器建立SSH通道,实时的将收到的用户数据发送给SSH后的主机,并将主机执行的结果数据返回给浏览器

唯一不一样的地方就是Django与要操作的服务器建立SSH通道的方式,在Kubernetes中是通过Kubernetes提供的API建立的Stream流,而操作物理机或者虚拟机的时候我们可以使用Paramiko模块来建立SSH长连接隧道,Paramiko模块建立SSH长连接通道的方法如下:

# 实例化SSHClient
ssh = paramiko.SSHClient()

# 当远程服务器没有本地主机的密钥时自动添加到本地,这样不用在建立连接的时候输入yes或no进行确认
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

# 连接SSH服务器,这里以账号密码的方式进行认证,也可以用key
ssh.connect(hostname=host, port=port, username=username, password=password, timeout=8)

# 打开ssh通道,建立长连接
transport = ssh.get_transport()
self.ssh_channel = transport.open_session()

# 获取ssh通道,并设置term和终端大小
self.ssh_channel.get_pty(term=term, width=cols, height=rows)

# 激活终端,这样就可以正常登陆了
self.ssh_channel.invoke_shell()

连接建立,可以通过如下方法给SSH通道发送数据

self.ssh_channel.send(data)

当然SSH返回的数据也可以通过如下方法持续的输出给Websocket

while not self.ssh_channel.exit_status_ready():
    # SSH返回的数据需要转码为utf-8,否则json序列化会失败
    data = self.ssh_channel.recv(1024).decode('utf-8','ignore')
    if len(data) != 0:
        message = {'flag': 'success', 'message': data}
        self.websocket.send(json.dumps(message))
    else:
        break

有了这些信息,结合Django实现WebSSH操作Kubernetes Pod的文章,实现WebSSH浏览器操作物理机或者虚拟机就不算困难了,完整的Consumer代码如下:

import io
import json
import paramiko
from threading import Thread
from channels.generic.websocket import WebsocketConsumer
from cmdb.backends.sshargs import args


class SSHBridge(object):
    def __init__(self, websocket):
        self.websocket = websocket

    def connect(self, host, port, username, authtype, password=None, pkey=None, term='xterm', cols=80, rows=24):
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            if authtype == 2:
                pkey = paramiko.RSAKey.from_private_key(io.StringIO(pkey))
                ssh.connect(username=username, hostname=host, port=port, pkey=pkey, timeout=8)
            else:
                ssh.connect(hostname=host, port=port, username=username, password=password, timeout=8)
        except Exception as e:
            message = json.dumps({'flag': 'error', 'message': str(e)})
            self.websocket.send(message)

            return False

        # 打开一个ssh通道并建立连接
        transport = ssh.get_transport()
        self.ssh_channel = transport.open_session()
        self.ssh_channel.get_pty(term=term, width=cols, height=rows)
        self.ssh_channel.invoke_shell()

        # 连接建立一次,之后交互数据不会再进入该方法
        for i in range(2):
            recv = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
            message = json.dumps({'flag': 'success', 'message': recv})
            self.websocket.send(message)

    def close(self):
        try:
            self.websocket.close()
            self.ssh_channel.close()
        except BaseException as e:
            pass

    def _ws_to_ssh(self, data):
        try:
            self.ssh_channel.send(data)
        except OSError as e:
            self.close()

    def _ssh_to_ws(self):
        try:
            while not self.ssh_channel.exit_status_ready():
                data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
                if len(data) != 0:
                    message = {'flag': 'success', 'message': data}
                    self.websocket.send(json.dumps(message))
                else:
                    break
        except Exception as e:
            message = {'flag': 'error', 'message': str(e)}
            self.websocket.send(json.dumps(message))

            self.close()

    def shell(self, data):
        Thread(target=self._ws_to_ssh, args=(data,)).start()
        Thread(target=self._ssh_to_ws).start()

class SSHConsumer(WebsocketConsumer):
    def connect(self):
        self.pk = self.scope['url_route']['kwargs'].get('id')
        self.query = self.scope.get('query_string')
        self.user = self.scope['user']

        self.accept()

        # ssh_connect_args为SSH连接需要的参数
        ssh_connect_args = args(self.pk, self.user, self.query)

        self.ssh = SSHBridge(websocket=self)
        self.ssh.connect(**ssh_connect_args)

    def disconnect(self, close_code):
        self.ssh.close()

    def receive(self, text_data=None):
        text_data = json.loads(text_data)
        self.ssh.shell(data=text_data.get('data', ''))

动态调整终端窗口大小

看了Kubernetes WebSSH终端窗口自适应Resize文章,小伙伴又说了,你这只能在连接建立时确定终端窗口的大小,如果我中途调整了浏览器的大小,显示就乱了,这该怎么办?

不要着急,接下来就让我们看看怎么让终端窗口随着浏览器大小的调整而改变,上边的文章中已经说过,终端窗口的大小需要浏览器和后端返回的Terminal大小保持一致,单单调整页面窗口大小或者后端返回的Terminal窗口大小都是不行的,那么从这两个方向来说明该如何动态调整窗口的大小

首先Paramiko模块建立的SSH通道可以通过resize_pty来动态改变返回Terminal窗口的大小,使用方法如下:

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

然后Django的Channels每次接收到前端发过来的数据时,判断一下窗口是否有变化,如果有变化则调用上边的方法动态改变Terminal输出窗口的大小

我在实现时会给传过来的数据加个flag,如果flag是resize,则调用resize_pty的方法动态调整窗口大小,否则就正常调用执行命令的方法,代码如下:

def receive(self, text_data=None):
    text_data = json.loads(text_data)

    if text_data.get('flag') == 'resize':
        self.ssh.resize_pty(cols=text_data['cols'], rows=text_data['rows'])
    else:
        self.ssh.shell(data=text_data.get('data', ''))

后端都搞定了,那么来看看前端如何处理吧

首先有一个terminal_size的方法根据浏览器窗口大小除以每个字符所占用的大小计算出cols和rows的值,无论是xterm.js还是Paramiko都是根据这两个值来调整窗口大小的

function terminal_size() {
    return {
        cols: Math.floor($('#terminal').width() / 9),
        rows: Math.floor($(window).height() / 17),
    }
}

然后通过$(window).resize()来检测浏览器窗口的变化,一旦发生变化,则发送一个带resize标记的数据给Django,同时传递的数据还有新的cols和rows

// terminal resize
$(window).resize(function () {
    let cols = terminal_size().cols;
    let rows = terminal_size().rows;

    send_data = JSON.stringify({
        'flag': 'resize',
        'cols': cols,
        'rows': rows
    });

    socket.send(send_data);
    term.resize(cols, rows)
})

最后通过term.resize来调整xterm渲染的窗口的大小

这样一个完整的动态调整窗口大小的方案就完成了

演示与源码

20191101.webssh.gif

我写了个简单的Demo来实现上边的功能,Demo写完发现还挺好用,我就扩展了一下添加了内网的物理机和虚拟机,历史原因,有些是账号密码认证,有些是密钥认证,我都给兼容了一下,最终实现的效果如上图所示

项目里边要记录主机的密码,为了安全这个密码是通过RSA加密存放在数据库的,每次使用的时候进行解密,加解密的实现,可参考这篇文章 Django开发密码管理表实例【附源码】

最后,如果你对这个简单的小玩意感兴趣,想要自己实现,却遇到了一些问题,可以通过公众号后台加我微信获取源码


能看到这里一定是真爱,关注一下吧

wx.sou1.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK