Refactor attach()/start() after podman changes

* Update examples
* Update/Clean up unittests
* Add Mixins for container attach()/start()

Signed-off-by: Jhon Honce <jhonce@redhat.com>

Closes: #1080
Approved by: baude
This commit is contained in:
Jhon Honce
2018-07-10 12:12:59 -07:00
committed by Atomic Bot
parent 7f3f491396
commit 86154b6538
7 changed files with 132 additions and 90 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Example: Run Alpine container and attach.""" """Example: Run top on Alpine container."""
import podman import podman
@ -8,10 +8,11 @@ print('{}\n'.format(__doc__))
with podman.Client() as client: with podman.Client() as client:
id = client.images.pull('alpine:latest') id = client.images.pull('alpine:latest')
img = client.images.get(id) img = client.images.get(id)
cntr = img.create() cntr = img.create(detach=True, tty=True, command=['/usr/bin/top'])
cntr.start() cntr.attach(eot=4)
try: try:
cntr.attach() cntr.start()
except BrokenPipeError: print()
print('Container disconnected.') except (BrokenPipeError, KeyboardInterrupt):
print('\nContainer disconnected.')

View File

@ -1,16 +1,11 @@
"""Exported method Container.attach().""" """Exported method Container.attach()."""
import collections
import fcntl import fcntl
import os import logging
import select
import signal
import socket
import struct import struct
import sys import sys
import termios import termios
import tty
CONMON_BUFSZ = 8192
class Mixin: class Mixin:
@ -20,10 +15,8 @@ class Mixin:
"""Attach to container's PID1 stdin and stdout. """Attach to container's PID1 stdin and stdout.
stderr is ignored. stderr is ignored.
PseudoTTY work is done in start().
""" """
if not self.containerrunning:
raise Exception('you can only attach to running containers')
if stdin is None: if stdin is None:
stdin = sys.stdin.fileno() stdin = sys.stdin.fileno()
@ -41,73 +34,42 @@ class Mixin:
) )
# This is the control socket where resizing events are sent to conmon # This is the control socket where resizing events are sent to conmon
ctl_socket = attach['sockets']['control_socket'] # attach['sockets']['control_socket']
self.pseudo_tty = collections.namedtuple(
'PseudoTTY',
['stdin', 'stdout', 'io_socket', 'control_socket', 'eot'])(
stdin,
stdout,
attach['sockets']['io_socket'],
attach['sockets']['control_socket'],
eot,
)
def resize_handler(signum, frame): @property
"""Send the new window size to conmon. def resize_handler(self):
"""Send the new window size to conmon."""
The method arguments are not used. def wrapped(signum, frame):
""" packed = fcntl.ioctl(self.pseudo_tty.stdout, termios.TIOCGWINSZ,
packed = fcntl.ioctl(stdout, termios.TIOCGWINSZ,
struct.pack('HHHH', 0, 0, 0, 0)) struct.pack('HHHH', 0, 0, 0, 0))
rows, cols, _, _ = struct.unpack('HHHH', packed) rows, cols, _, _ = struct.unpack('HHHH', packed)
logging.debug('Resize window({}x{}) using {}'.format(
rows, cols, self.pseudo_tty.control_socket))
# TODO: Need some kind of timeout in case pipe is blocked # TODO: Need some kind of timeout in case pipe is blocked
with open(ctl_socket, 'w') as skt: with open(self.pseudo_tty.control_socket, 'w') as skt:
# send conmon window resize message # send conmon window resize message
skt.write('1 {} {}\n'.format(rows, cols)) skt.write('1 {} {}\n'.format(rows, cols))
def log_handler(signum, frame): return wrapped
"""Send command to reopen log to conmon.
The method arguments are not used. @property
""" def log_handler(self):
with open(ctl_socket, 'w') as skt: """Send command to reopen log to conmon."""
def wrapped(signum, frame):
with open(self.pseudo_tty.control_socket, 'w') as skt:
# send conmon reopen log message # send conmon reopen log message
skt.write('2\n') skt.write('2\n')
try: return wrapped
# 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:
# TODO: socket.SOCK_SEQPACKET may not be supported in Windows
with socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) as skt:
# Prepare socket for communicating with conmon/container
skt.connect(io_socket)
skt.sendall(b'\n')
sources = [skt, stdin]
while sources:
readable, _, _ = select.select(sources, [], [])
if skt in readable:
data = skt.recv(CONMON_BUFSZ)
if not data:
sources.remove(skt)
# Remove source marker when writing
os.write(stdout, data[1:])
if stdin in readable:
data = os.read(stdin, CONMON_BUFSZ)
if not data:
sources.remove(stdin)
skt.sendall(data)
if eot in data:
sources.clear()
finally:
if original_attr:
termios.tcsetattr(stdout, termios.TCSADRAIN, original_attr)
signal.signal(signal.SIGWINCH, signal.SIG_DFL)

View File

@ -0,0 +1,82 @@
"""Exported method Container.start()."""
import logging
import os
import select
import signal
import socket
import sys
import termios
import tty
CONMON_BUFSZ = 8192
class Mixin:
"""Publish start() for inclusion in Container class."""
def start(self):
"""Start container, return container on success.
Will block if container has been detached.
"""
with self._client() as podman:
results = podman.StartContainer(self.id)
logging.debug('Started Container "{}"'.format(
results['container']))
if not hasattr(self, 'pseudo_tty') or self.pseudo_tty is None:
return self._refresh(podman)
logging.debug('Setting up PseudoTTY for Container "{}"'.format(
results['container']))
try:
# save off the old settings for terminal
tcoldattr = termios.tcgetattr(self.pseudo_tty.stdin)
tty.setraw(self.pseudo_tty.stdin)
# initialize container's window size
self.resize_handler(None, sys._getframe(0))
# catch any resizing events and send the resize info
# to the control fifo "socket"
signal.signal(signal.SIGWINCH, self.resize_handler)
except termios.error:
tcoldattr = None
try:
# TODO: Is socket.SOCK_SEQPACKET supported in Windows?
with socket.socket(socket.AF_UNIX,
socket.SOCK_SEQPACKET) as skt:
# Prepare socket for use with conmon/container
skt.connect(self.pseudo_tty.io_socket)
sources = [skt, self.pseudo_tty.stdin]
while sources:
logging.debug('Waiting on sources: {}'.format(sources))
readable, _, _ = select.select(sources, [], [])
if skt in readable:
data = skt.recv(CONMON_BUFSZ)
if data:
# Remove source marker when writing
os.write(self.pseudo_tty.stdout, data[1:])
else:
sources.remove(skt)
if self.pseudo_tty.stdin in readable:
data = os.read(self.pseudo_tty.stdin, CONMON_BUFSZ)
if data:
skt.sendall(data)
if self.pseudo_tty.eot in data:
sources.clear()
else:
sources.remove(self.pseudo_tty.stdin)
finally:
if tcoldattr:
termios.tcsetattr(self.pseudo_tty.stdin, termios.TCSADRAIN,
tcoldattr)
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
return self._refresh(podman)

View File

@ -7,9 +7,10 @@ import signal
import time import time
from ._containers_attach import Mixin as AttachMixin from ._containers_attach import Mixin as AttachMixin
from ._containers_start import Mixin as StartMixin
class Container(collections.UserDict, AttachMixin): class Container(AttachMixin, StartMixin, collections.UserDict):
"""Model for a container.""" """Model for a container."""
def __init__(self, client, id, data): def __init__(self, client, id, data):
@ -143,12 +144,6 @@ class Container(collections.UserDict, AttachMixin):
message, pause) message, pause)
return results['image'] return results['image']
def start(self):
"""Start container, return container on success."""
with self._client() as podman:
podman.StartContainer(self.id)
return self._refresh(podman)
def stop(self, timeout=25): def stop(self, timeout=25):
"""Stop container, return id on success.""" """Stop container, return id on success."""
with self._client() as podman: with self._client() as podman:

View File

@ -3,6 +3,7 @@ import collections
import copy import copy
import functools import functools
import json import json
import logging
from . import Config from . import Config
from .containers import Container from .containers import Container
@ -37,11 +38,8 @@ class Image(collections.UserDict):
Pulls defaults from image.inspect() Pulls defaults from image.inspect()
""" """
with self._client() as podman: details = self.inspect()
details = self.inspect()
# TODO: remove network settings once defaults implemented in service
# Inialize config from parameters, then add image information
config = Config(image_id=self.id, **kwargs) config = Config(image_id=self.id, **kwargs)
config['command'] = details.containerconfig['cmd'] config['command'] = details.containerconfig['cmd']
config['env'] = self._split_token(details.containerconfig['env']) config['env'] = self._split_token(details.containerconfig['env'])
@ -49,8 +47,8 @@ class Image(collections.UserDict):
config['labels'] = copy.deepcopy(details.labels) config['labels'] = copy.deepcopy(details.labels)
config['net_mode'] = 'bridge' config['net_mode'] = 'bridge'
config['network'] = 'bridge' config['network'] = 'bridge'
config['work_dir'] = '/tmp'
logging.debug('Image {}: create config: {}'.format(self.id, config))
with self._client() as podman: with self._client() as podman:
id = podman.CreateContainer(config)['container'] id = podman.CreateContainer(config)['container']
cntr = podman.GetContainer(id) cntr = podman.GetContainer(id)

View File

@ -72,14 +72,18 @@ class TestContainers(PodmanTestCase):
mock_in.write('echo H"ello, World"; exit\n') mock_in.write('echo H"ello, World"; exit\n')
mock_in.seek(0, 0) mock_in.seek(0, 0)
self.alpine_ctnr.attach( ctnr = self.pclient.images.get(self.alpine_ctnr.image).container(
stdin=mock_in.fileno(), stdout=mock_out.fileno()) detach=True, tty=True)
ctnr.attach(stdin=mock_in.fileno(), stdout=mock_out.fileno())
ctnr.start()
mock_out.flush() mock_out.flush()
mock_out.seek(0, 0) mock_out.seek(0, 0)
output = mock_out.read() output = mock_out.read()
self.assertIn('Hello', output) self.assertIn('Hello', output)
ctnr.remove(force=True)
def test_processes(self): def test_processes(self):
actual = list(self.alpine_ctnr.processes()) actual = list(self.alpine_ctnr.processes())
self.assertGreaterEqual(len(actual), 2) self.assertGreaterEqual(len(actual), 2)
@ -133,8 +137,7 @@ class TestContainers(PodmanTestCase):
def test_commit(self): def test_commit(self):
# TODO: Test for STOPSIGNAL when supported by OCI # TODO: Test for STOPSIGNAL when supported by OCI
# TODO: Test for message when supported by OCI # TODO: Test for message when supported by OCI
details = self.pclient.images.get( details = self.pclient.images.get(self.alpine_ctnr.image).inspect()
self.alpine_ctnr.inspect().image).inspect()
changes = ['ENV=' + i for i in details.containerconfig['env']] changes = ['ENV=' + i for i in details.containerconfig['env']]
changes.append('CMD=/usr/bin/zsh') changes.append('CMD=/usr/bin/zsh')
changes.append('ENTRYPOINT=/bin/sh date') changes.append('ENTRYPOINT=/bin/sh date')

View File

@ -62,6 +62,7 @@ class TestImages(PodmanTestCase):
actual = self.alpine_image.container() actual = self.alpine_image.container()
self.assertIsNotNone(actual) self.assertIsNotNone(actual)
self.assertEqual(actual.status, 'configured') self.assertEqual(actual.status, 'configured')
ctnr = actual.start() ctnr = actual.start()
self.assertIn(ctnr.status, ['running', 'exited']) self.assertIn(ctnr.status, ['running', 'exited'])