Do What You Can’t

Python 实现开启 tty 的交互式 Kuberntes 容器控制

· Read in about 2 min · (379 Words)
tty kubernetes

熟悉 dockerkubectl 的同学们,很大概率使用过 -it 的交互方式,效果是通过一个可交互的伪终端来实现对目标容器的控制。

今天介绍一下,如何通过 Python 程序实现类似 kubectl exec -it 的 Kubernetes 容器控制程序。

基本原理

在开始控制 Kubernetes 容器之前,我们首先要了解下伪终端或 pty 的基本概念及原理。

pty 定义

The pty module defines operations for handling the pseudo-terminal concept: starting another process and being able to write to and read from its controlling terminal programmatically.

伪终端就是通过 fork 一个新的进程,并能够通过程序控制从原始控制终端中读取或是写入数据。

新进程的写入 stdin 会重定向到原生终端的写入 stdin,新进程的输出 stdoutstderr 读取自于原始控制终端的 stdoutstderr

读取和写入部分看起来特别像是管道的概念,但是不同的就是原始进程是一个 termial,除输入和输出外, 还有窗口大小控制、termial mode(raw, cbreak) 还包括 alternative buffer mode(vim 使用的)这些终端独有的特性。

关于 dockerkubectl 来说,当开启了 tty 的容器控制时,实际上都是通过客户端和 HTTP Server 间建立的一个连接来实现的控制及 I/O 管道的数据交换。

更多 Golang 实现细节,可以直接去查看相关的客户端代码实现。

Python 客户端实现

如果你也刚好选用 Python 在实现或者维护了一个 Python 编写的客户端,需要封装实现类似 kubectl exec -it 的效果, 好消息是,Kubernetes 的官方 Python 库(https://github.com/kubernetes-client/python) 就已经支持了这种交互,只是没有明确的使用样例。

我参考 «Pseudo Terminals in Python» 实现了一个开启了 tty 并支持 resize WINDOW 的 Python 客户端实现,有需求的可以看一下,希望有帮助。

show me the code

话不多说,直接上代码:

fork 部分:

    def spawn(self, argv=None):
        """
        Create a spawned process.
        Based on the code for pty.spawn().
        """
        if not argv:
            argv = [os.environ['SHELL']]

        pid, master_fd = pty.fork()
        self.master_fd = master_fd
        if pid == pty.CHILD:
            os.execlp(argv[0], *argv)

        old_handler = signal.signal(signal.SIGWINCH, self._signal_winch)
        try:
            mode = tty.tcgetattr(pty.STDIN_FILENO)
            tty.setraw(pty.STDIN_FILENO)
            restore = 1
        except tty.error:    # This is the same as termios.error
            restore = 0
        self._init_fd()
        try:
            self._copy()
        except (IOError, OSError):
            if restore:
                tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)

        self.k8s_stream.close()
        self.k8s_stream = None
        if self.master_fd:
            os.close(self.master_fd)
            self.master_fd = None
        signal.signal(signal.SIGWINCH, old_handler)

I/O 部分:

    def _copy(self):
        """
        Main select loop. Passes all data to self.master_read() or
               self.stdin_read().
        """
        assert self.k8s_stream is not None
        k8s_stream = self.k8s_stream
        while True:
            try:
                rfds, wfds, xfds = select.select([pty.STDIN_FILENO,
                                                  k8s_stream.sock.sock],
                                                 [], [])
            except select.error as e:
                no = e.errno if six.PY3 else e[0]
                if no == errno.EINTR:
                    continue

            if pty.STDIN_FILENO in rfds:
                data = os.read(pty.STDIN_FILENO, 1024)
                self.stdin_read(data)
            if k8s_stream.sock.sock in rfds:
                # read from k8s_stream
                if k8s_stream.peek_stdout():
                    self.master_read(k8s_stream.read_stdout())
                # error occurs
                if k8s_stream.peek_channel(3):
                    break

resize 部分(佩服 Python 客户端基于 Websocket 实现的多路复用,对接 RESIZE_CHANNEL):

    def _set_pty_size(self):
        """
        Sets the window size of the child pty based on the window size of
               our own controlling terminal.
        """
        packed = fcntl.ioctl(pty.STDOUT_FILENO,
                             termios.TIOCGWINSZ,
                             struct.pack('HHHH', 0, 0, 0, 0))
        rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
        self.k8s_stream.write_channel(4,
                                      json.dumps({"Height": rows,
                                                  "Width": cols}))

完整代码,请戳这里:https://github.com/kubernetes-client/python/pull/515/

我已经将基于 Kubernetes Python client 实现的 tty 的例子提交到了上面的 PR。

有兴趣的欢迎围观,发现问题还请不吝赐教。

同时,我录制了一个 try out 的上手视频,里边包含了想要查看运行效果的基本步骤, 包括 clone 代码,mkvirtualenv, pip 安装依赖, 运行样例等。

cast

复制并浏览器打开:https://asciinema.org/a/fOznfIWkZcYdEslY1iJs4HeZc

整体效果受限于当时的设备和环境,可能尺寸偏大,不便于查看,望见谅。

另:其中调整窗口尺寸部分 asciinema 好像还不能很好的支持,所以视频中没有体现出效果, 大家亲自尝试的时候,可以通过终端的多行输出看出容器中的 tty 尺寸是会随着当前的终端尺寸变化而调整的。

Refs:

Comments