1. 程式人生 > >Django實現WebSSH操作Kubernetes Pod

Django實現WebSSH操作Kubernetes Pod

優秀的系統都是根據反饋逐漸完善出來的

上篇文章介紹了我們為了應對安全和多分支頻繁測試的問題而開發了一套Alodi系統,Alodi可以通過一個按鈕快速構建一套測試環境,生成一個臨時訪問地址,詳細資訊可以看這一篇文章:Alodi:為了保密我開發了一個系統

系統上線後,SSH登陸控制檯成了一個迫切的需求,Kubernetes的Dashboard控制檯雖然有WebSSH的功能,但卻沒辦法跟Alodi系統相結合,決定在Alodi中整合WebSSH的功能,先來看看最後實現的效果吧

涉及技術

  • 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

之前有兩篇文章詳細介紹過Django Channels,不瞭解的可以先檢視:Django使用Channels實現WebSocket--上篇和Django使用Channels實現WebSocket--下篇,最重要的兩部分程式碼如下

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視窗

目前仍然遇到一個視窗大小無法調整的問題沒有解決,初步判斷是後端Kubernetes傳回的資料決定的,查詢了相關資料,找到kubectl命令可以通過新增COLUMNSLINES的env來設定

#!/bin/sh
if [ "$1" = "" ]; then
  echo "Usage: kshell <pod>"
  exit 1
fi
COLUMNS=`tput cols`
LINES=`tput lines`
TERM=xterm
kubectl exec -i -t $1 env COLUMNS=$COLUMNS LINES=$LINES TERM=$TERM bash

但Kubernetes Python API的Stream沒有找到配置的地方,如果你知道,麻煩告訴我


相關文章推薦閱讀:

  • Django+Echarts畫圖例項
  • Django+JWT實現Token認證