我想用它替换掉xshell、crt之类的工具前端
Django实现WebSSH操做Kubernetes Pod文章发布后,有小伙伴说咖啡哥,咱们如今尚未用上Kubernetes,但我想经过浏览器链接咱们的物理机和虚拟机该怎么办?web
这就比较简单了,既然咱们已经实现了浏览器操做Kubernetes的Pod,那么想一想操做Pod和物理机虚拟机有什么区别呢?shell
整个数据流是一点没变:用户打开浏览器--》浏览器发送websocket请求给Django创建长链接--》Django与要操做的服务器创建SSH通道,实时的将收到的用户数据发送给SSH后的主机,并将主机执行的结果数据返回给浏览器数据库
惟一不同的地方就是Django与要操做的服务器创建SSH通道的方式,在Kubernetes中是经过Kubernetes提供的API创建的Stream流,而操做物理机或者虚拟机的时候咱们能够使用Paramiko
模块来创建SSH长链接隧道,Paramiko
模块创建SSH长链接通道的方法以下:json
# 实例化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渲染的窗口的大小
这样一个完整的动态调整窗口大小的方案就完成了
我写了个简单的Demo来实现上边的功能,Demo写完发现还挺好用,我就扩展了一下添加了内网的物理机和虚拟机,历史缘由,有些是帐号密码认证,有些是密钥认证,我都给兼容了一下,最终实现的效果如上图所示
项目里边要记录主机的密码,为了安全这个密码是经过RSA加密存放在数据库的,每次使用的时候进行解密,加解密的实现,可参考这篇文章 Django开发密码管理表实例【附源码】
最后,若是你对这个简单的小玩意感兴趣,想要本身实现,却遇到了一些问题,能够经过公众号后台加我微信获取源码
相关文章推荐阅读: