Python 运维笔记 -- docker container web terminal

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/duxiangwushirenfei/article/details/83183936

前言

需求所致需要做平台container的web terminal。因为现在的一些配套Dashboard不好做权限和资源管理。实现原理上,前端开web socket 后端保持双向通信,服务端完成容器对接。初衷是Kubernetes 所启动的pod中container连接,本节我们先从docker做起,后期有机会在介绍如何实现Kubernetes的container web terminal。
在此特别感谢下面工程的作者,给了很好的启示。
参考工程:https://github.com/hctech/docker-web-terminal
笔者在此基础上修改后能够正常运行的demo工程地址
https://github.com/wushirenfei/web_terminal_docker
研发环境Python3.6,MacOS

基础信息

WebSocket

还是捎带说明下吧,websocket 不是平日说的 tcp socket连接,tcp是传输层的,websocket和http一样是应用层协议。但是与http不同的是:
1,首先http是无状态的,但是websocket是有一个握手建立连接的过程,是一种有状态的连接协议。
2,http是单向通信,一定是client端发起请求后server响应,server无法主动发送消息,因此以前有之前 python + reds 长轮询 的方式用以模拟服务端推送信息。而WebSocket的通道是类似socket连接的全双工通信。此处实现terminal的钱后端实时信息传输便使用WebSocket。

docker API

docker除了terminal操作外还提供了API接口,python有docker的package封装了docker的API。其中提供了 exec_create用以执行命令。
详情参加:docker/docker-py
但是版本必须要稍微高点的版本,参考工程中的docker版本在MacOS上调试时无法正常运行,使用docker==3.5.0版本方能放才可以调试。

实现

demo基础构成

没有修改参考工程的基本逻辑结构,沿用下来,谢谢原作者提供的demo。前端xTerm做terminal连接,后端Flask做基础Web 服务,调用docker API包开启和docker container连接。Flask WebSocket的主线服务负责接收socket的信息往container中发送,另开启线程负责接收docker container 的stdin, stdout流中内容回发到前端socket中。具体可以参见https://github.com/wushirenfei/web_terminal_docker调试通过后个人的demo。

Web 服务端

app主服务端代码如下:

import conf

from werkzeug import serving
from flask_sockets import Sockets
from flask import Flask, render_template
from utility.myDocker import ClientHandler, DockerStreamThread, BeatWS


app = Flask(__name__)
sockets = Sockets(app)


@app.route('/')
def index():
    return render_template('index.html')


@sockets.route('/echo')
def echo_socket(ws):
    dockerCli = ClientHandler(base_url=conf.DOCKER_HOST, timeout=10, version='1.38')
    terminalExecId = dockerCli.creatTerminalExec(conf.CONTAINER_ID)
    terminalStream = dockerCli.startTerminalExec(terminalExecId)._sock

    terminalThread = DockerStreamThread(ws, terminalStream)
    terminalThread.start()
    beat_thread = BeatWS(ws, dockerCli.client)
    beat_thread.start()

    try:
        while not ws.closed:
            message = ws.receive()
            if message is not None:
                sed_msg = bytes(message, encoding='utf-8')
                if sed_msg != b'__ping__':
                    terminalStream.send(bytes(message, encoding='utf-8'))
    except Exception as err:
        print(err)
    finally:
        ws.close()
        terminalStream.close()
        dockerCli.dockerClient.close()


@serving.run_with_reloader
def run_server():
    app.debug = True
    from gevent import pywsgi
    from geventwebsocket.handler import WebSocketHandler
    server = pywsgi.WSGIServer(
        listener=('0.0.0.0', 5000),
        application=app,
        handler_class=WebSocketHandler)
    server.serve_forever()


if __name__ == '__main__':
    run_server()

其中开启DockerStreamThread线程用以接收docker标准流信息反馈到web端,具体实现如下:

import time
import docker
import threading
from socket import timeout


class ClientHandler(object):

    def __init__(self, **kwargs):
        self.dockerClient = docker.APIClient(**kwargs)

    @property
    def client(self):
        return self.dockerClient

    def creatTerminalExec(self, containerId):
        execCommand = [
            "/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']
        execOptions = {
            "tty": True,
            "stdin": True,
            "stdout": True
        }

        execId = self.dockerClient.exec_create(containerId, execCommand, **execOptions)
        return execId["Id"]

    def startTerminalExec(self, execId):
        return self.dockerClient.exec_start(execId, socket=True, tty=True)


class DockerStreamThread(threading.Thread):
    def __init__(self, ws, terminalStream):
        super(DockerStreamThread, self).__init__()
        self.ws = ws
        self.terminalStream = terminalStream

    def run(self):
        while not self.ws.closed:
            try:
                dockerStreamStdout = self.terminalStream.recv(2048)
                if dockerStreamStdout is not None:
                    self.ws.send(str(dockerStreamStdout, encoding='utf-8'))
                else:
                    print("docker daemon socket is close")
                    self.ws.close()
            except timeout:
                print('Receive from docker timeout.')
            except Exception as e:
                print("docker daemon socket err: %s" % e)
                self.ws.close()
                break


class BeatWS(threading.Thread):
    def __init__(self, ws, docker_client):
        super(BeatWS, self).__init__()
        self.ws = ws
        self.docker_client = docker_client

    def run(self):
        while not self.ws.closed:
            time.sleep(2)
            self.docker_client.ping()

遇到问题

环境

1,原始工程中用的是docker 2.5的包,结果调试时候怎么都通不过,给出的错误提示也未能找到解决方法,最终无法解决了升级docker到新版的3.5,顺利调试通过,即便在docker降级回2.5也能通过了,估计是某个依赖版本低了。
2,docker配置,在MacOS,conf文件中直接给的是docker的sock连接,并不是tcp的配置,如果想修改docker的damean.json开放tcp会出问题,直接docker就异常退出了。如果有兴趣可以去CentOS上试试。

心跳保持

对前端稍作了修改,加了一个setInterval ping 用以保持前端ws和服务端的连接,同时在服务端开启BeatWS,调用docker的ping方法保持和container的连接。docker的ping保持连接没有测试,尚不知晓会不会保持住和container的连接,有机会可以测试下。

bash挂起

docker 对于异常退出的bash是不会自动kill的,就会这么一直挂着,而且docker和宿主机实际是共用进程资源的,这种异常退出非常耗资源。

在这里插入图片描述

调试时没有控制情况下,一堆 xTerm开的bash,可以在容器内看到。所以在主线代码中用大异常包揽,无论如何要close container的sock。

有了这些了解再尝试去转做一个 Kubernetes 的 contaienr web terminal就更进一步了,后续有机会再更新。

猜你喜欢

转载自blog.csdn.net/duxiangwushirenfei/article/details/83183936