1. 程式人生 > >堡壘機WebSSH進階之實時監控和強制下線

堡壘機WebSSH進階之實時監控和強制下線

這個功能我可以不用,但你不能沒有

前幾篇文章實現了對物理機、虛擬機器以及Kubernetes中Pod的WebSSH操作,可以方便的在web端對系統進行管理,同時也支援對所有操作進行全程錄影,以方便後續的檢視與審計

這一篇文章接著實現一個看起來很炫酷,但實際上你可能不會經常使用,又必須要存在的功能:實時監控使用者操作,在必要的時候將使用者踢下線

實時檢視操作

django通過channels實現websocket中有一個非常重要的概念叫layer,layer可以將多個channel合併成一個group,我們可以傳送訊息給group,那麼group裡的每個channel都能收到

關於Channel我有寫過兩篇文章結合聊天室和web端實現tail-f功能這兩個案例來詳細介紹,兩篇文章是『Django使用Channels實現WebSocket--上篇』和『Django使用Channels實現WebSocket--下篇』,對上邊提到的名詞一臉懵逼的朋友可以通過這兩篇文章來學習

之前的WebSSH僅是單連線,只需要客戶端和伺服器建立長連線,然後處理指令就ok了,我們並沒有啟用channel的layer,實際上也可以看作是單channel,但要實現監控的功能,就需要將操作者和管理員(監控者)的多個channel合併在一起組成一個group,這樣操作者的所有操作都可以傳送給這個group,同處於這個group內監控者就能實時收到訊息了,大概流程變化如下圖所示

接下來看下具體實現,以下所有程式碼均是在這篇文章的基礎上進行說明講解:『Django實現WebSSH操作物理機或虛擬機器』

首先我們要啟用layer,這個需要在settings.py中新增如下配置

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('ops-coffee.cn', 6379)],
        },
    },
}

然後將處理WebSSH連線名為SSHConsumer的Consumer做改造,以使其支援layer,程式碼如下

class SSHConsumer(WebsocketConsumer):
    def connect(self):
        # 格式化引數
        ssh_connect_args = args(self.scope)

        # 新建錄影記錄
        self.host = Host.objects.get(host=ssh_connect_args.get('host'))
        self.group_name = '%s-%s-%d' % (
            ssh_connect_args.get('host'), ssh_connect_args.get('username'), time.time())

        self.therecord = Record.objects.create(
            host=self.host,
            user=self.scope['user'],
            group=self.group_name,
            channel=self.channel_name,
            cols=ssh_connect_args.get('cols'),
            rows=ssh_connect_args.get('rows'),
            is_connecting=True
        )

        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )

        self.accept()

        # WebSocket連線成功後,連線ssh
        self.ssh = SSHBridge(self.therecord, websocket=self)
        self.ssh.connect(**ssh_connect_args)

    def disconnect(self, close_code):
        # 將連線狀態置為False
        self.therecord.is_connecting = False
        self.therecord.save()

        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )

        self.ssh.close()

    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', ''))

    def ssh_message(self, event):
        self.send(text_data=json.dumps(
            event['message']
        ))

在connect連線建立時新建一條記錄,儲存主機、使用者、group_namechannel_name以及初始視窗的colsrows資訊,同時標記is_connecting為True,這裡的group_name命名與文章『堡壘機的核心武器:WebSSH錄影實現』中我們定義的錄影檔名規則一致,另外將這篇文章中新建錄影記錄的操作從SSHBridge.record中給轉到了連線建立的connect中來,更合理也更方便

在disconnect連線關閉時,將is_connecting標記為False,這樣我們在前端頁面上就可以根據這個標記來判斷WebSSH是否正在連線,如果連線則展示監控和強制結束按鈕,否則展示播放和命令提取按鈕

同時添加個ssh_message方法,用來接收發送到組的資料

到這裡,我們已經將WebSSH改造成了支援layer的模式,那麼接下來就是要在使用者點選監控的時候將使用者與服務端建立的連線channel加入到上述group中

新建一個名為MonitorConsumer的consumer,主要用來處理監控連線

class MonitorConsumer(WebsocketConsumer):
    def connect(self):
        pk = self.scope['url_route']['kwargs'].get('id')
        self.group_name = Record.objects.get(id=pk).group

        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )

        self.accept()

        # 判斷使用者已經結束了這個webssh連線時就關閉監控
        self.connecting = Record.objects.get(id=pk).is_connecting
        if not self.connecting:
            self.close()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )

        self.close()

    def receive(self, text_data=None):
        pass

    def ssh_message(self, event):
        self.send(text_data=json.dumps(
            event['message']
        ))

MonitorConsumer與SSHConsumer有兩個地方不一樣,其一是SSHConsumer中我們直接新生成了個group_name,而MonitorConsumer中需要在connect時獲取到要監控的ID,然後通過ID拿到group_name,將monitor連線加入到這個group,其二是監控只能看,不能操作,所以也不需要前端傳送資料的term.on和Consumer的receive處理資料

最後需要修改SSHBridge方法中傳送給websocket的指令,從self.websocket.send改為傳送到group的模式,如下

async_to_sync(self.websocket.channel_layer.group_send)(
    self.group_name,
    {
        'type': 'ssh.message',
        'message': message
    }
)

至此,監控功能就算完成了,什麼?前端頁面怎麼弄?參考下之前的WebSSH介面,幾乎可以完全複製

踢使用者下線

踢使用者下線就比較簡單了,邏輯是點選頁面上的強制結束按鈕,給後端view傳送個請求帶上這條記錄的ID,view拿到ID後,通過ID找到group_name,然後向group傳送disconnect訊息,這個group裡的所有channel在收到disconnect訊息後就會斷開連線了

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

async_to_sync(get_channel_layer().group_send)(
    Record.objects.get(id=pk).group,
    {'type': 'disconnect'}
)

演示與說明

所有實現環環相扣,單看這一篇文章可能雲裡霧裡,不知所云,但你如果能把這個系列文章都給看下的話,我想實現個簡單的堡壘機應該沒有問題吧,更重要的是你會對websocket以及django中的Channels有著更加深刻的理解和運用

原本只是想給我最牛X的Alodi系統添加個WebSSH,可以方便開發或測試在專案執行過程中出現問題時提供一個快速的除錯途徑,沒想到這竟然寫了一個系列,實現了這麼多有趣好玩兒的功能

最後想起了這句成語:有意栽花花不開,無心插柳柳成蔭,真是奇妙~


相關文章推薦閱讀:

  • Django實現WebSSH操作Kubernetes Pod
  • Kubernetes WebSSH終端視窗自適應Resize