前言

承接上一篇docker container web terminal,实现Kubernetes的Pod中运行容器terminal连接。所不同的是后端使用的是python kubernetes的package。完整的工程地址请参见:web_terminal_kubernetes

Kubernetes Client

Python kubernetes package直接可以pip安装:

 $ pip install kubernetes

Github 官方地址:https://github.com/kubernetes-client/python

kubernetes client连接服务所用的认证信息区别传统的形式,而是对kubectl的一个封装,需要指定连接集群的主机API访问地址,此外就是比较重要的用户认证。

keystone 认证

如果访问集群实接入了keystone,那么在使用kubernetes client连接时提供 username 和 password 即可,keystone会通过此两项信息得知用户身份,但用户的权限范围仍然是kubernetes控制。
对应的python连接方式如下:

from kubernetes import client

kub_conf = client.Configuration()
kub_conf.host = api_host
kub_conf.username = 'your_username'
kub_conf.password = 'your_password'
kub_client = client.ApiClient(configuration=kub_conf)

证书认证

笔者所访问集群并未接入keystone认证,而是用证书形式,类似于在机器上配置kubectl config 时使用的指定地址和ca的方式进行API访问。
对应的python连接方式如下:

from kubernetes import client

kub_conf = client.Configuration()
kub_conf.host = api_host
kub_conf.ssl_ca_cert = 'your_ssl_ca_cert_path'
kub_conf.cert_file = 'your_cert_file_path'
kub_conf.key_file = 'your_key_file_path'
kub_client = client.ApiClient(configuration=kub_conf)
注意:上述 ssl_ca_cert,cert_file,key_file所需要提供的都是证书在机器上的存放path。

Kubernetes API

kubernetes更新比较快,能够获取到的信息,以及提供的API都应去Github上实时查阅。kubernetes.client.CoreV1Api提供的API参见:https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md
此处调用CoreV1Api 下的 connect_get_namespaced_pod_exec方法用以连接Pod中container。

stream 回调

connect_get_namespaced_pod_exec方法都是一个API形式的服务,并非返回socket对象开启连接发送流数据,所以需要借助 kubernetes.stream ,将执行方法和参数传入,返回websocket。

Kubernetes连接实现

class K8SClient(KubernetesAPI):

    def __init__(self, api_host, ssl_ca_cert, key_file, cert_file):
        super(K8SClient, self).__init__(
            api_host, ssl_ca_cert, key_file, cert_file)

    @staticmethod
    def gen_ca():
        ssl_ca_cert = os.path.join(
            os.path.dirname(os.path.dirname(__file__)),
            '_credentials/kubernetes_dev_ca_cert')
        key_file = os.path.join(
            os.path.dirname(os.path.dirname(__file__)),
            '_credentials/kubernetes_dev_key')
        cert_file = os.path.join(
            os.path.dirname(os.path.dirname(__file__)),
            '_credentials/kubernetes_dev_cert')

        return ssl_ca_cert, key_file, cert_file

    def terminal_start(self, namespace, pod_name, container):
        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']

        container_stream = stream(
            self.client_core_v1.connect_get_namespaced_pod_exec,
            name=pod_name,
            namespace=namespace,
            container=container,
            command=command,
            stderr=True, stdin=True,
            stdout=True, tty=True,
            _preload_content=False
        )

        return container_stream

KubernetesAPI 对 kubernetes client做了简单封装。
注意:
1,connect_get_namespaced_pod_exec 传入的参数标准输入 stdin,标准输出stdout,和标准错误 stderr都开启
2,tty参数一定要开启,此参数标示 以类似 kubectl exec的形式运行命令。
3,_preload_content,if False, the urllib3.HTTPResponse object will be returned without reading/decoding response data. Default is True。原文注释,标识是否需要编解码。

遇到问题

异常挂起

先前提过container的开启的bash如果没有退出,会直接挂起,之前提出的大异常包括范围,只能去关闭与容器的连接,但是bash没有退出。
1,后端服务大异常包裹,并且在检测到前台ws关闭追加 ‘exit\r’ 模拟用户退出终端。
2,在浏览器的退出和刷新时弹出提示,提醒用户退出终端。
但是模拟exit退出解决不了嵌套,诸如用户进入container中,开启python shell,之后并不手动退出,而是直接关闭浏览器,那么此时就会有一个bash挂起

nginx部署

以nginx代理此服务,配置正确后,界面加载均正常显示,唯独开启socket时会出现异常,后发现需要加载额外配置。
在nginx的conf文件中,可以参照以下配置:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 80;
    server_name localhost your_domain;
    root your_root_path

    location /terminal/ {
       proxy_pass http://localhost:5000;
       proxy_set_header Host $host;
       proxy_http_version 1.1;
       proxy_ignore_headers X-Accel-Buffering;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection "Upgrade";
       proxy_set_header X-Real-IP $remote_addr;
    }
}

注意:
一定要设置代理访问的协议是http1.1 proxy_http_version 1.1
因为之前有提过,ws是需要先发送http1.1的请求进行握手操作,之后才能开启连接。

Python Tips

Flask

API拆分

Flask算是个人用的比较少框架了,但是很轻,很容易使用。
Flask对于app稍大一点是可以进行拆分的,可以用Blueprint进行拆分,示例如下:
独立api文件如下

import json
from flask import Blueprint

a_api = Blueprint('a_api', __name__)
a_ws = Blueprint('a_api', __name__)

@k8s_api.route('/your_route', methods=['GET'])
def inner_terminal():
    return json.dumps('api hello')

@a_ws.route('/your_ws', methods=['GET'])
def inner_terminal(ws):
    return json.dumps('ws hello')

主文app.py件中加载

from api import a_api, a_ws
app = Flask(__name__, static_folder='static',
            static_url_path='/terminal/static')
sockets = Sockets(app)

app.register_blueprint(k8s_api)
sockets.register_blueprint(k8s_ws)

按此思路完成了对Flask大app的拆分。

静态文件

在上文示例中,new Flask对象中可以设置静态文件所在的文件夹,已经请求文件的url前缀,在前台文件中加载静态文件可以形如:

<script src="{{ url_for('static', filename='xterm/dist/xterm.js') }}"></script>

在实际访问时便是以
http://localhost:5000/terminal/static/xterm/dist/xterm.js 的地址去加载js文件了。
url_for 函数用以生成url访问地址。

获取路径

通常想获取路径的方法可以引入 os库,但是:

import os
os.getcwd()
os.path.dirname(__file__) 

是不一样的。
os.path.dirname(file) 是获取当前执行py文件的路行。
os.getcwd() 是获取外层调用者的路径。即触发os.getcwd()运行的脚本文件地址。倘若直接运行当前文件执行os.getcwd()和os.path.dirname(file) 获取的路径是一样的,但是如果是在内层函数中操作os.getcwd(),则获取到的是主调该函数调用者所在的文件路径。

Logo

开源、云原生的融合云平台

更多推荐