前言
需求所致需要做平台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就更进一步了,后续有机会再更新。