From 2cb881fa58a6a8fe6c89ec2885f01ebb3d74d871 Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Fri, 25 May 2018 15:18:43 -0700 Subject: Implement container attach Signed-off-by: Jhon Honce --- contrib/python/podman/libs/__init__.py | 6 +- contrib/python/podman/libs/_containers_attach.py | 111 +++++++++++++++++++++++ contrib/python/podman/libs/containers.py | 10 +- contrib/python/test/podman_testcase.py | 3 +- contrib/python/test/test_containers.py | 20 +++- 5 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 contrib/python/podman/libs/_containers_attach.py (limited to 'contrib/python') diff --git a/contrib/python/podman/libs/__init__.py b/contrib/python/podman/libs/__init__.py index 9db5dacf5..3a8a35021 100644 --- a/contrib/python/podman/libs/__init__.py +++ b/contrib/python/podman/libs/__init__.py @@ -1,4 +1,5 @@ """Support files for podman API implementation.""" +import collections import datetime import functools @@ -12,12 +13,11 @@ __all__ = [ def cached_property(fn): - """Cache return to save future expense.""" + """Decorate property to cache return value.""" return property(functools.lru_cache(maxsize=8)(fn)) -# Cannot subclass collections.UserDict, breaks varlink -class Config(dict): +class Config(collections.UserDict): """Silently ignore None values, only take key once.""" def __init__(self, **kwargs): diff --git a/contrib/python/podman/libs/_containers_attach.py b/contrib/python/podman/libs/_containers_attach.py new file mode 100644 index 000000000..54e6a009e --- /dev/null +++ b/contrib/python/podman/libs/_containers_attach.py @@ -0,0 +1,111 @@ +"""Exported method Container.attach().""" + +import fcntl +import os +import select +import signal +import socket +import struct +import sys +import termios +import tty + +CONMON_BUFSZ = 8192 + + +class Mixin: + """Publish attach() for inclusion in Container class.""" + + def attach(self, eot=4, stdin=None, stdout=None): + """Attach to container's PID1 stdin and stdout. + + stderr is ignored. + """ + if stdin is None: + stdin = sys.stdin.fileno() + + if stdout is None: + stdout = sys.stdout.fileno() + + with self._client() as podman: + attach = podman.GetAttachSockets(self._id) + + # This is the UDS where all the IO goes + io_socket = attach['sockets']['io_socket'] + assert len(io_socket) <= 107,\ + 'Path length for sockets too long. {} > 107'.format( + len(io_socket) + ) + + # This is the control socket where resizing events are sent to conmon + ctl_socket = attach['sockets']['control_socket'] + + def resize_handler(signum, frame): + """Send the new window size to conmon. + + The method arguments are not used. + """ + packed = fcntl.ioctl(stdout, termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)) + rows, cols, _, _ = struct.unpack('HHHH', packed) + + with open(ctl_socket, 'w') as skt: + # send conmon window resize message + skt.write('1 {} {}\n'.format(rows, cols)) + + def log_handler(signum, frame): + """Send command to reopen log to conmon. + + The method arguments are not used. + """ + with open(ctl_socket, 'w') as skt: + # send conmon reopen log message + skt.write('2\n') + + try: + # save off the old settings for terminal + original_attr = termios.tcgetattr(stdout) + tty.setraw(stdin) + + # initialize containers window size + resize_handler(None, sys._getframe(0)) + + # catch any resizing events and send the resize info + # to the control fifo "socket" + signal.signal(signal.SIGWINCH, resize_handler) + except termios.error: + original_attr = None + + try: + # Prepare socket for communicating with conmon/container + with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as skt: + skt.connect(io_socket) + skt.sendall(b'\n') + + sources = [skt, stdin] + while sources: + readable, _, _ = select.select(sources, [], []) + for r in readable: + if r is skt: + data = r.recv(CONMON_BUFSZ) + if not data: + sources.remove(skt) + + # Remove source marker when writing + os.write(stdout, data[1:]) + elif r is stdin: + data = os.read(stdin, CONMON_BUFSZ) + if not data: + sources.remove(stdin) + + skt.sendall(data) + + if eot in data: + sources.clear() + break + else: + raise ValueError('Unknown source in select') + finally: + if original_attr: + termios.tcsetattr(stdout, termios.TCSADRAIN, original_attr) + signal.signal(signal.SIGWINCH, signal.SIG_DFL) diff --git a/contrib/python/podman/libs/containers.py b/contrib/python/podman/libs/containers.py index 96ec6be37..a350a128a 100644 --- a/contrib/python/podman/libs/containers.py +++ b/contrib/python/podman/libs/containers.py @@ -6,8 +6,10 @@ import json import signal import time +from ._containers_attach import Mixin as AttachMixin -class Container(collections.UserDict): + +class Container(collections.UserDict, AttachMixin): """Model for a container.""" def __init__(self, client, id, data): @@ -46,12 +48,6 @@ class Container(collections.UserDict): with self._client() as podman: return self._refresh(podman) - def attach(self, detach_key=None, no_stdin=False, sig_proxy=True): - """Attach to running container.""" - with self._client() as podman: - # TODO: streaming and port magic occur, need arguments - podman.AttachToContainer() - def processes(self): """Show processes running in container.""" with self._client() as podman: diff --git a/contrib/python/test/podman_testcase.py b/contrib/python/test/podman_testcase.py index fc99f26ce..f96a3a013 100644 --- a/contrib/python/test/podman_testcase.py +++ b/contrib/python/test/podman_testcase.py @@ -62,7 +62,8 @@ class PodmanTestCase(unittest.TestCase): cmd = ['podman'] cmd.extend(podman_args) - cmd.extend(['run', '-d', 'alpine', 'sleep', '500']) + # cmd.extend(['run', '-d', 'alpine', 'sleep', '500']) + cmd.extend(['run', '-dt', 'alpine', '/bin/sh']) PodmanTestCase.alpine_process = subprocess.Popen( cmd, stdout=PodmanTestCase.alpine_log, diff --git a/contrib/python/test/test_containers.py b/contrib/python/test/test_containers.py index 5c7c9934b..87d43adb4 100644 --- a/contrib/python/test/test_containers.py +++ b/contrib/python/test/test_containers.py @@ -1,11 +1,9 @@ import os import signal -import time import unittest from test.podman_testcase import PodmanTestCase import podman -from podman import datetime_parse class TestContainers(PodmanTestCase): @@ -65,8 +63,22 @@ class TestContainers(PodmanTestCase): self.pclient.containers.get("bozo") def test_attach(self): - with self.assertRaisesNotImplemented(): - self.alpine_ctnr.attach() + # StringIO does not support fileno() so we had to go old school + input = os.path.join(self.tmpdir, 'test_attach.stdin') + output = os.path.join(self.tmpdir, 'test_attach.stdout') + + with open(input, 'w+') as mock_in, open(output, 'w+') as mock_out: + # double quote is indeed in the expected place + mock_in.write('echo H"ello, World"; exit\n') + mock_in.seek(0, 0) + + self.alpine_ctnr.attach( + stdin=mock_in.fileno(), stdout=mock_out.fileno()) + + mock_out.flush() + mock_out.seek(0, 0) + output = mock_out.read() + self.assertIn('Hello', output) def test_processes(self): actual = list(self.alpine_ctnr.processes()) -- cgit v1.2.3-54-g00ecf