转载:Python Django撸个WebSSH操作Kubernetes Pod

涉及技术
- Kubernetes Stream:接收数据执行,提供实时返回数据流
- Django Channels:维持长连接,接收前端数据转给Kubernetes,同时将Kubernetes返回的数据发送给前端
- xterm.js:一个前端终端组件,用于模拟Terminal的界面显示
基本的数据流向是:用户 --> xterm.js --> django channels --> kubernetes stream,接下来看看具体的代码实现
Kubernetes Stream
Kubernetes本身提供了stream方法来实现exec的功能,返回的就是一个WebSocket可以使用的数据流,使用起来也非常方便,代码如下:
from kubernetes import client, config from kubernetes.stream import stream class KubeApi: def __init__(self, namespace='alodi'): config.load_kube_config("/ops/coffee/kubeconfig.yaml") self.namespace = namespace def pod_exec(self, pod, container=""): api_instance = client.CoreV1Api() exec_command = [ "/bin/sh", "-c", 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] ' '&& ([ -x /usr/bin/script ] ' '&& /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) ' '|| exec /bin/sh'] cont_stream = stream(api_instance.connect_get_namespaced_pod_exec, name=pod, namespace=self.namespace, container=container, command=exec_command, stderr=True, stdin=True, stdout=True, tty=True, _preload_content=False ) return cont_stream
这里的pod name可以通过list_namespaced_pod方法获取,代码如下:
def get_deployment_pod(self, RAND): api_instance = client.CoreV1Api() try: r = api_instance.list_namespaced_pod( namespace=self.namespace, label_selector="app=%s" % RAND ) return True, r except Exception as e: return False, 'Get Deployment: ' + str(e) state, data = self.get_deployment_pod(RAND) pod_name = data.items[0].metadata.name
list_namespaced_pod会列出namespace下所有pod的详细信息,这里传了两个参数,第一个namespace是必须的,表示我们要列出pod的namespace,第二个label_selector非必须,表示可以通过设置的标签过滤namespace下的pod,由于我们在创建的时候给每个deployment都添加了唯一的app=RAND的标签,所以这里可以过滤出来我们项目所对应的pod
一个deployment可能对应多个pod,获取到的data.items包含了所有的pod信息,为一个list列表,可根据需要取到对应pod的name
Django Channels
routing代码:
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path, re_path from medivh.consumers import SSHConsumer application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter([ re_path(r'^pod/(?P<name>\w+)', SSHConsumer), ]) ), })
正则匹配所有以pod开头的websocket连接,都交由名为SSHConsumer的Consumer处理,Consumer代码如下:
from channels.generic.websocket import WebsocketConsumer from medivh.backends.kube import KubeApi from threading import Thread class K8SStreamThread(Thread): def __init__(self, websocket, container_stream): Thread.__init__(self) self.websocket = websocket self.stream = container_stream def run(self): while self.stream.is_open(): if self.stream.peek_stdout(): stdout = self.stream.read_stdout() self.websocket.send(stdout) if self.stream.peek_stderr(): stderr = self.stream.read_stderr() self.websocket.send(stderr) else: self.websocket.close() class SSHConsumer(WebsocketConsumer): def connect(self): self.name = self.scope["url_route"]["kwargs"]["name"] # kube exec self.stream = KubeApi().pod_exec(self.name) kub_stream = K8SStreamThread(self, self.stream) kub_stream.start() self.accept() def disconnect(self, close_code): self.stream.write_stdin('exit\r') def receive(self, text_data): self.stream.write_stdin(text_data)
WebSSH可以看作是一个最简单的websocket长连接,每个连接建立后都是独立的,不会跟其他连接共享数据,所以这里不需要用到Group
当连接建立时通过self.scope获取到url中的name,传给Kubernetes API,同时会新起一个线程不断循环是否有新数据产生,如果有则发送给websocket
当websocket接收到数据就直接写入Kubernetes API,当websocket关闭则会发送个exit命令给Kubernetes
前端页面
前端主要用到了xterm.js,整体代码也比较简单
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Alodi | Pod Web SSH</title> <link rel="Shortcut Icon" href="/static/img/favicon.ico"> <link href="/static/plugins/xterm/xterm.css" rel="stylesheet" type="text/css"/> <link href="/static/plugins/xterm/addons/fullscreen/fullscreen.css" rel="stylesheet" type="text/css"/> </head> <body> <div id="terminal"></div> </body> <script src="/static/plugins/xterm/xterm.js"></script> <script src="/static/plugins/xterm/addons/fullscreen/fullscreen.js"></script> <script> var term = new Terminal({cursorBlink: true}); term.open(document.getElementById('terminal')); // xterm fullscreen config Terminal.applyAddon(fullscreen); term.toggleFullScreen(true); var socket = new WebSocket( 'ws://' + window.location.host + '/pod/{{ name }}'); socket.onopen = function () { term.on('data', function (data) { socket.send(data); }); socket.onerror = function (event) { console.log('error:' + e); }; socket.onmessage = function (event) { term.write(event.data); }; socket.onclose = function (event) { term.write('\n\r\x1B[1;3;31msocket is already closed.\x1B[0m'); // term.destroy(); }; }; </script> </html>
term.open初始化一个Terminal
term.on会将输入的内容全部实时的传递给后端
xterm.js有一个fullscreen的插件,引入之后可以配置fullscreen,否则可能页面只有一部分terminal窗口。 现在还有一个问题就是窗口大小无法调整。
调整窗口
terminal内容窗口的大小没有办法调整,这会导致的一个问题就是浏览器上可显示内容的区域太小,当查看/编辑文件时非常不便,就像下边这样,红色可视区域并没有被用到。


RESIZE_CHANNEL
kubectl exec有两个参数COLUMNS和LINES可以调整tty内容窗口的大小,命令如下:
kubectl exec -i -t $1 env COLUMNS=$COLUMNS LINES=$LINES bash
这实际上就是将COLUMNS和LINES两个环境变量传递到了容器内,由于Kubernetes stream底层也是通过kubernetes exec实现的,所以我们在启动容器时也将这两个变量传递进去就可以了,就像这样
exec_command = [ "/bin/sh", "-c", 'export LINES=20; export COLUMNS=100; ' 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] ' '&& ([ -x /usr/bin/script ] ' '&& /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) ' '|| exec /bin/sh']
添加了export LINES=20; export COLUMNS=100;,可以实现改变tty的输出大小,但这有个问题就是只能在建立链接时指定一次,不能动态的更新,也就是在一次websocket会话的过程中,如果页面大小改变了,后端输出的LINES和COLUMNS是无法随着改变的
在解决问题的过程中发现官方源码中有个RESIZE_CHANNEL的配置,同样可以控制窗口的大小,使用方法如下:
cont_stream = stream(api_instance.connect_get_namespaced_pod_exec, name=pod_name, namespace=self.namespace, container=container, command=exec_command, stderr=True, stdin=True, stdout=True, tty=True, _preload_content=False ) cont_stream.write_channel(4, json.dumps({"Height": int(rows), "Width": int(cols)}))
这样我们就可以修改stream输出的窗口大小了
xterm.js fit
一顿操作后,打开页面,咦?怎么页面不行,原来窗口的调整不仅需要调整stream输出数据的窗口大小,前端页面也要跟着一并调整
这里用到了xterm.js的另一个组件fit,fit可以调整终端大小的cols和rows适配父级元素
首先调整terminal块的宽度和高度为整个页面可视区域的大小,要让整个可视区域为终端窗口
document.getElementById('terminal').style.height = window.innerHeight + 'px';
然后引入fit组件,在term初始化之后执行fit操作
<script src="/static/plugins/xterm/xterm.js"></script> <script src="/static/plugins/xterm/addons/fit/fit.js"></script> <script> // 修改terminal的高度为body的高度 document.getElementById('terminal').style.height = window.innerHeight + 'px'; var term = new Terminal({cursorBlink: true}); term.open(document.getElementById('terminal')); // xterm fullscreen config Terminal.applyAddon(fit); term.fit(); console.log(term.cols, term.rows); </script>
fit之后就可以通过term.cols和term.rows取到xterm.js根据字体大小自动计算过的cols和rows的值了,然后把这两个值传递给kubernetes,kubernetes再根据这两个值输出窗口大小,这样前后端匹配就完美了
数据传递
xterm.js可以通过如下的方法动态的将cols和rows传递给后端
term.on('resize', size => {
socket.send('resize', [size.cols, size.rows]);
})
但当窗口由大变小时,之前输出的内容会有样式错乱,我为了方便直接在WebSocket连接建立时采用url传参的方式把cols和rows两个值传递给后端,kubernetes根据这两个值来设置输出内容的窗口大小,这样做的缺点是不会随着前端页面的变化动态的去调整后端stream输出窗口的大小,不过问题不大,如果页面调整大小,刷新下页面重新建立连接就可以啦,具体实现如下
首先需要修改的就是WebSocket的url地址
前端增加term.cols和term.rows两个参数的传递
var socket = new WebSocket(
'ws://' + window.location.host + '/pod/{{ name }}/'+term.cols+'/'+term.rows);
Routing增加两个参数的解析
re_path(r'^pod/(?P<name>\w+)/(?P<cols>\d+)/(?P<rows>\d+)$', SSHConsumer),
Consumer解析URL将对应参数传递给Kubernetes stream
class SSHConsumer(WebsocketConsumer): def connect(self): self.name = self.scope["url_route"]["kwargs"]["name"] self.cols = self.scope["url_route"]["kwargs"]["cols"] self.rows = self.scope["url_route"]["kwargs"]["rows"] # kube exec self.stream = KubeApi().pod_exec(self.name, cols=self.cols, rows=self.rows) kub_stream = K8SStreamThread(self, self.stream) kub_stream.start() self.accept()
最后Kubernetes stream接收参数并修改窗口大小
def pod_exec(self, RAND, container="", rows=24, cols=80): api_instance = client.CoreV1Api() exec_command = [ "/bin/sh", "-c", 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] ' '&& ([ -x /usr/bin/script ] ' '&& /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) ' '|| exec /bin/sh'] cont_stream = stream(api_instance.connect_get_namespaced_pod_exec, name=pod_name, namespace=self.namespace, container=container, command=exec_command, stderr=True, stdin=True, stdout=True, tty=True, _preload_content=False ) cont_stream.write_channel(4, json.dumps({"Height": int(rows), "Width": int(cols)})) return cont_stream
至此,每次WebSocket连接建立,前后端就会有一样的输出窗口大小
转自: https://www.cnblogs.com/37Y37/p/12564534.html
浙公网安备 33010602011771号