diff options
author | Jhon Honce <jhonce@redhat.com> | 2018-07-12 19:26:14 -0700 |
---|---|---|
committer | Jhon Honce <jhonce@redhat.com> | 2018-07-13 12:50:12 -0700 |
commit | 74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15 (patch) | |
tree | 75ba256d70545d79aa61d7c57c20df886be1555f /contrib/python/podman/libs | |
parent | 44b523c946c88e540b50d7ba59f441b5f8e0bad0 (diff) | |
download | podman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.tar.gz podman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.tar.bz2 podman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.zip |
Update python directories to better support setup.py
Signed-off-by: Jhon Honce <jhonce@redhat.com>
Diffstat (limited to 'contrib/python/podman/libs')
-rw-r--r-- | contrib/python/podman/libs/__init__.py | 54 | ||||
-rw-r--r-- | contrib/python/podman/libs/_containers_attach.py | 75 | ||||
-rw-r--r-- | contrib/python/podman/libs/_containers_start.py | 82 | ||||
-rw-r--r-- | contrib/python/podman/libs/containers.py | 245 | ||||
-rw-r--r-- | contrib/python/podman/libs/errors.py | 65 | ||||
-rw-r--r-- | contrib/python/podman/libs/images.py | 172 | ||||
-rw-r--r-- | contrib/python/podman/libs/system.py | 40 | ||||
-rw-r--r-- | contrib/python/podman/libs/tunnel.py | 138 |
8 files changed, 0 insertions, 871 deletions
diff --git a/contrib/python/podman/libs/__init__.py b/contrib/python/podman/libs/__init__.py deleted file mode 100644 index 3a8a35021..000000000 --- a/contrib/python/podman/libs/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Support files for podman API implementation.""" -import collections -import datetime -import functools - -from dateutil.parser import parse as dateutil_parse - -__all__ = [ - 'cached_property', - 'datetime_parse', - 'datetime_format', -] - - -def cached_property(fn): - """Decorate property to cache return value.""" - return property(functools.lru_cache(maxsize=8)(fn)) - - -class Config(collections.UserDict): - """Silently ignore None values, only take key once.""" - - def __init__(self, **kwargs): - """Construct dictionary.""" - super(Config, self).__init__(kwargs) - - def __setitem__(self, key, value): - """Store unique, not None values.""" - if value is None: - return - - if super().__contains__(key): - return - - super().__setitem__(key, value) - - -def datetime_parse(string): - """Convert timestamps to datetime. - - tzinfo aware, if provided. - """ - return dateutil_parse(string.upper(), fuzzy=True) - - -def datetime_format(dt): - """Format datetime in consistent style.""" - if isinstance(dt, str): - return datetime_parse(dt).isoformat() - elif isinstance(dt, datetime.datetime): - return dt.isoformat() - else: - raise ValueError('Unable to format {}. Type {} not supported.'.format( - dt, type(dt))) diff --git a/contrib/python/podman/libs/_containers_attach.py b/contrib/python/podman/libs/_containers_attach.py deleted file mode 100644 index df12fa998..000000000 --- a/contrib/python/podman/libs/_containers_attach.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Exported method Container.attach().""" - -import collections -import fcntl -import logging -import struct -import sys -import termios - - -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. - PseudoTTY work is done in start(). - """ - 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 - # 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, - ) - - @property - def resize_handler(self): - """Send the new window size to conmon.""" - - def wrapped(signum, frame): - packed = fcntl.ioctl(self.pseudo_tty.stdout, termios.TIOCGWINSZ, - struct.pack('HHHH', 0, 0, 0, 0)) - 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 - with open(self.pseudo_tty.control_socket, 'w') as skt: - # send conmon window resize message - skt.write('1 {} {}\n'.format(rows, cols)) - - return wrapped - - @property - def log_handler(self): - """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 - skt.write('2\n') - - return wrapped diff --git a/contrib/python/podman/libs/_containers_start.py b/contrib/python/podman/libs/_containers_start.py deleted file mode 100644 index ad9f32eab..000000000 --- a/contrib/python/podman/libs/_containers_start.py +++ /dev/null @@ -1,82 +0,0 @@ -"""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) diff --git a/contrib/python/podman/libs/containers.py b/contrib/python/podman/libs/containers.py deleted file mode 100644 index 6dc2c141e..000000000 --- a/contrib/python/podman/libs/containers.py +++ /dev/null @@ -1,245 +0,0 @@ -"""Models for manipulating containers and storage.""" -import collections -import functools -import getpass -import json -import signal -import time - -from ._containers_attach import Mixin as AttachMixin -from ._containers_start import Mixin as StartMixin - - -class Container(AttachMixin, StartMixin, collections.UserDict): - """Model for a container.""" - - def __init__(self, client, id, data): - """Construct Container Model.""" - super(Container, self).__init__(data) - - self._client = client - self._id = id - - with client() as podman: - self._refresh(podman) - - assert self._id == self.data['id'],\ - 'Requested container id({}) does not match store id({})'.format( - self._id, self.id - ) - - def __getitem__(self, key): - """Get items from parent dict.""" - return super().__getitem__(key) - - def _refresh(self, podman): - ctnr = podman.GetContainer(self._id) - super().update(ctnr['container']) - - for k, v in self.data.items(): - setattr(self, k, v) - if 'containerrunning' in self.data: - setattr(self, 'running', self.data['containerrunning']) - self.data['running'] = self.data['containerrunning'] - - return self - - def refresh(self): - """Refresh status fields for this container.""" - with self._client() as podman: - return self._refresh(podman) - - def processes(self): - """Show processes running in container.""" - with self._client() as podman: - results = podman.ListContainerProcesses(self.id) - yield from results['container'] - - def changes(self): - """Retrieve container changes.""" - with self._client() as podman: - results = podman.ListContainerChanges(self.id) - return results['container'] - - def kill(self, signal=signal.SIGTERM, wait=25): - """Send signal to container. - - default signal is signal.SIGTERM. - wait n of seconds, 0 waits forever. - """ - with self._client() as podman: - podman.KillContainer(self.id, signal) - timeout = time.time() + wait - while True: - self._refresh(podman) - if self.status != 'running': - return self - - if wait and timeout < time.time(): - raise TimeoutError() - - time.sleep(0.5) - - def _lower_hook(self): - """Convert all keys to lowercase.""" - - @functools.wraps(self._lower_hook) - def wrapped(input): - return {k.lower(): v for (k, v) in input.items()} - - return wrapped - - def inspect(self): - """Retrieve details about containers.""" - with self._client() as podman: - results = podman.InspectContainer(self.id) - obj = json.loads(results['container'], object_hook=self._lower_hook()) - return collections.namedtuple('ContainerInspect', obj.keys())(**obj) - - def export(self, target): - """Export container from store to tarball. - - TODO: should there be a compress option, like images? - """ - with self._client() as podman: - results = podman.ExportContainer(self.id, target) - return results['tarfile'] - - def commit(self, - image_name, - *args, - changes=[], - message='', - pause=True, - **kwargs): - """Create image from container. - - All changes overwrite existing values. - See inspect() to obtain current settings. - - Changes: - CMD=/usr/bin/zsh - ENTRYPOINT=/bin/sh date - ENV=TEST=test_containers.TestContainers.test_commit - EXPOSE=8888/tcp - LABEL=unittest=test_commit - USER=bozo:circus - VOLUME=/data - WORKDIR=/data/application - """ - # TODO: Clean up *args, **kwargs after Commit() is complete - try: - author = kwargs.get('author', getpass.getuser()) - except Exception: - author = '' - - for c in changes: - if c.startswith('LABEL=') and c.count('=') < 2: - raise ValueError( - 'LABEL should have the format: LABEL=label=value, not {}'. - format(c)) - - with self._client() as podman: - results = podman.Commit(self.id, image_name, changes, author, - message, pause) - return results['image'] - - def stop(self, timeout=25): - """Stop container, return id on success.""" - with self._client() as podman: - podman.StopContainer(self.id, timeout) - return self._refresh(podman) - - def remove(self, force=False): - """Remove container, return id on success. - - force=True, stop running container. - """ - with self._client() as podman: - results = podman.RemoveContainer(self.id, force) - return results['container'] - - def restart(self, timeout=25): - """Restart container with timeout, return id on success.""" - with self._client() as podman: - podman.RestartContainer(self.id, timeout) - return self._refresh(podman) - - def rename(self, target): - """Rename container, return id on success.""" - with self._client() as podman: - # TODO: Need arguments - results = podman.RenameContainer() - # TODO: fixup objects cached information - return results['container'] - - def resize_tty(self, width, height): - """Resize container tty.""" - with self._client() as podman: - # TODO: magic re: attach(), arguments - podman.ResizeContainerTty() - - def pause(self): - """Pause container, return id on success.""" - with self._client() as podman: - podman.PauseContainer(self.id) - return self._refresh(podman) - - def unpause(self): - """Unpause container, return id on success.""" - with self._client() as podman: - podman.UnpauseContainer(self.id) - return self._refresh(podman) - - def update_container(self, *args, **kwargs): - """TODO: Update container..., return id on success.""" - with self._client() as podman: - podman.UpdateContainer() - return self._refresh(podman) - - def wait(self): - """Wait for container to finish, return 'returncode'.""" - with self._client() as podman: - results = podman.WaitContainer(self.id) - return int(results['exitcode']) - - def stats(self): - """Retrieve resource stats from the container.""" - with self._client() as podman: - results = podman.GetContainerStats(self.id) - obj = results['container'] - return collections.namedtuple('StatDetail', obj.keys())(**obj) - - def logs(self, *args, **kwargs): - """Retrieve container logs.""" - with self._client() as podman: - results = podman.GetContainerLogs(self.id) - yield from results - - -class Containers(object): - """Model for Containers collection.""" - - def __init__(self, client): - """Construct model for Containers collection.""" - self._client = client - - def list(self): - """List of containers in the container store.""" - with self._client() as podman: - results = podman.ListContainers() - for cntr in results['containers']: - yield Container(self._client, cntr['id'], cntr) - - def delete_stopped(self): - """Delete all stopped containers.""" - with self._client() as podman: - results = podman.DeleteStoppedContainers() - return results['containers'] - - def get(self, id): - """Retrieve container details from store.""" - with self._client() as podman: - cntr = podman.GetContainer(id) - return Container(self._client, cntr['container']['id'], - cntr['container']) diff --git a/contrib/python/podman/libs/errors.py b/contrib/python/podman/libs/errors.py deleted file mode 100644 index b98210481..000000000 --- a/contrib/python/podman/libs/errors.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Error classes and wrappers for VarlinkError.""" -from varlink import VarlinkError - - -class VarlinkErrorProxy(VarlinkError): - """Class to Proxy VarlinkError methods.""" - - def __init__(self, message, namespaced=False): - """Construct proxy from Exception.""" - super().__init__(message.as_dict(), namespaced) - self._message = message - self.__module__ = 'libpod' - - def __getattr__(self, method): - """Return attribute from proxied Exception.""" - if hasattr(self._message, method): - return getattr(self._message, method) - - try: - return self._message.parameters()[method] - except KeyError: - raise AttributeError('No such attribute: {}'.format(method)) - - -class ContainerNotFound(VarlinkErrorProxy): - """Raised when Client can not find requested container.""" - - pass - - -class ImageNotFound(VarlinkErrorProxy): - """Raised when Client can not find requested image.""" - - pass - - -class ErrorOccurred(VarlinkErrorProxy): - """Raised when an error occurs during the execution. - - See error() to see actual error text. - """ - - pass - - -class RuntimeError(VarlinkErrorProxy): - """Raised when Client fails to connect to runtime.""" - - pass - - -error_map = { - 'io.projectatomic.podman.ContainerNotFound': ContainerNotFound, - 'io.projectatomic.podman.ErrorOccurred': ErrorOccurred, - 'io.projectatomic.podman.ImageNotFound': ImageNotFound, - 'io.projectatomic.podman.RuntimeError': RuntimeError, -} - - -def error_factory(exception): - """Map Exceptions to a discrete type.""" - try: - return error_map[exception.error()](exception) - except KeyError: - return exception diff --git a/contrib/python/podman/libs/images.py b/contrib/python/podman/libs/images.py deleted file mode 100644 index 334ff873c..000000000 --- a/contrib/python/podman/libs/images.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Models for manipulating images in/to/from storage.""" -import collections -import copy -import functools -import json -import logging - -from . import Config -from .containers import Container - - -class Image(collections.UserDict): - """Model for an Image.""" - - def __init__(self, client, id, data): - """Construct Image Model.""" - super(Image, self).__init__(data) - for k, v in data.items(): - setattr(self, k, v) - - self._id = id - self._client = client - - assert self._id == self.id,\ - 'Requested image id({}) does not match store id({})'.format( - self._id, self.id - ) - - def __getitem__(self, key): - """Get items from parent dict.""" - return super().__getitem__(key) - - def _split_token(self, values=None, sep='='): - return dict([v.split(sep, 1) for v in values if values]) - - def create(self, *args, **kwargs): - """Create container from image. - - Pulls defaults from image.inspect() - """ - details = self.inspect() - - config = Config(image_id=self.id, **kwargs) - config['command'] = details.containerconfig['cmd'] - config['env'] = self._split_token(details.containerconfig['env']) - config['image'] = copy.deepcopy(details.repotags[0]) - config['labels'] = copy.deepcopy(details.labels) - config['net_mode'] = 'bridge' - config['network'] = 'bridge' - - logging.debug('Image {}: create config: {}'.format(self.id, config)) - with self._client() as podman: - id = podman.CreateContainer(config)['container'] - cntr = podman.GetContainer(id) - return Container(self._client, id, cntr['container']) - - container = create - - def export(self, dest, compressed=False): - """Write image to dest, return id on success.""" - with self._client() as podman: - results = podman.ExportImage(self.id, dest, compressed) - return results['image'] - - def history(self): - """Retrieve image history.""" - with self._client() as podman: - for r in podman.HistoryImage(self.id)['history']: - yield collections.namedtuple('HistoryDetail', r.keys())(**r) - - # Convert all keys to lowercase. - def _lower_hook(self): - @functools.wraps(self._lower_hook) - def wrapped(input): - return {k.lower(): v for (k, v) in input.items()} - - return wrapped - - def inspect(self): - """Retrieve details about image.""" - with self._client() as podman: - results = podman.InspectImage(self.id) - obj = json.loads(results['image'], object_hook=self._lower_hook()) - return collections.namedtuple('ImageInspect', obj.keys())(**obj) - - def push(self, target, tlsverify=False): - """Copy image to target, return id on success.""" - with self._client() as podman: - results = podman.PushImage(self.id, target, tlsverify) - return results['image'] - - def remove(self, force=False): - """Delete image, return id on success. - - force=True, stop any running containers using image. - """ - with self._client() as podman: - results = podman.RemoveImage(self.id, force) - return results['image'] - - def tag(self, tag): - """Tag image.""" - with self._client() as podman: - results = podman.TagImage(self.id, tag) - return results['image'] - - -class Images(object): - """Model for Images collection.""" - - def __init__(self, client): - """Construct model for Images collection.""" - self._client = client - - def list(self): - """List all images in the libpod image store.""" - with self._client() as podman: - results = podman.ListImages() - for img in results['images']: - yield Image(self._client, img['id'], img) - - def build(self, dockerfile=None, tags=None, **kwargs): - """Build container from image. - - See podman-build.1.md for kwargs details. - """ - if dockerfile is None: - raise ValueError('"dockerfile" is a required argument.') - elif not hasattr(dockerfile, '__iter__'): - raise ValueError('"dockerfile" is required to be an iter.') - - if tags is None: - raise ValueError('"tags" is a required argument.') - elif not hasattr(tags, '__iter__'): - raise ValueError('"tags" is required to be an iter.') - - config = Config(dockerfile=dockerfile, tags=tags, **kwargs) - with self._client() as podman: - result = podman.BuildImage(config) - return self.get(result['image']['id']), \ - (line for line in result['image']['logs']) - - def delete_unused(self): - """Delete Images not associated with a container.""" - with self._client() as podman: - results = podman.DeleteUnusedImages() - return results['images'] - - def import_image(self, source, reference, message=None, changes=None): - """Read image tarball from source and save in image store.""" - with self._client() as podman: - results = podman.ImportImage(source, reference, message, changes) - return results['image'] - - def pull(self, source): - """Copy image from registry to image store.""" - with self._client() as podman: - results = podman.PullImage(source) - return results['id'] - - def search(self, id, limit=25): - """Search registries for id.""" - with self._client() as podman: - results = podman.SearchImage(id, limit) - for img in results['images']: - yield collections.namedtuple('ImageSearch', img.keys())(**img) - - def get(self, id): - """Get Image from id.""" - with self._client() as podman: - result = podman.GetImage(id) - return Image(self._client, result['image']['id'], result['image']) diff --git a/contrib/python/podman/libs/system.py b/contrib/python/podman/libs/system.py deleted file mode 100644 index c59867760..000000000 --- a/contrib/python/podman/libs/system.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Models for accessing details from varlink server.""" -import collections - -import pkg_resources - -from . import cached_property - - -class System(object): - """Model for accessing system resources.""" - - def __init__(self, client): - """Construct system model.""" - self._client = client - - @cached_property - def versions(self): - """Access versions.""" - with self._client() as podman: - vers = podman.GetVersion()['version'] - - client = '0.0.0' - try: - client = pkg_resources.get_distribution('podman').version - except Exception: - pass - vers['client_version'] = client - return collections.namedtuple('Version', vers.keys())(**vers) - - def info(self): - """Return podman info.""" - with self._client() as podman: - info = podman.GetInfo()['info'] - return collections.namedtuple('Info', info.keys())(**info) - - def ping(self): - """Return True if server awake.""" - with self._client() as podman: - response = podman.Ping() - return 'OK' == response['ping']['message'] diff --git a/contrib/python/podman/libs/tunnel.py b/contrib/python/podman/libs/tunnel.py deleted file mode 100644 index 9effdff6c..000000000 --- a/contrib/python/podman/libs/tunnel.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Cache for SSH tunnels.""" -import collections -import logging -import os -import subprocess -import threading -import time -import weakref - -Context = collections.namedtuple('Context', ( - 'uri', - 'interface', - 'local_socket', - 'remote_socket', - 'username', - 'hostname', - 'identity_file', -)) - - -class Portal(collections.MutableMapping): - """Expiring container for tunnels.""" - - def __init__(self, sweap=25): - """Construct portal, reap tunnels every sweap seconds.""" - self.data = collections.OrderedDict() - self.sweap = sweap - self.ttl = sweap * 2 - self.lock = threading.RLock() - self._schedule_reaper() - - def __getitem__(self, key): - """Given uri return tunnel and update TTL.""" - with self.lock: - value, _ = self.data[key] - self.data[key] = (value, time.time() + self.ttl) - self.data.move_to_end(key) - return value - - def __setitem__(self, key, value): - """Store given tunnel keyed with uri.""" - if not isinstance(value, Tunnel): - raise ValueError('Portals only support Tunnels.') - - with self.lock: - self.data[key] = (value, time.time() + self.ttl) - self.data.move_to_end(key) - - def __delitem__(self, key): - """Remove and close tunnel from portal.""" - with self.lock: - value, _ = self.data[key] - del self.data[key] - value.close(key) - del value - - def __iter__(self): - """Iterate tunnels.""" - with self.lock: - values = self.data.values() - - for tunnel, _ in values: - yield tunnel - - def __len__(self): - """Return number of tunnels in portal.""" - with self.lock: - return len(self.data) - - def _schedule_reaper(self): - timer = threading.Timer(interval=self.sweap, function=self.reap) - timer.setName('PortalReaper') - timer.setDaemon(True) - timer.start() - - def reap(self): - """Remove tunnels who's TTL has expired.""" - now = time.time() - with self.lock: - reaped_data = self.data.copy() - for entry in reaped_data.items(): - if entry[1][1] < now: - del self.data[entry[0]] - else: - # StopIteration as soon as possible - break - self._schedule_reaper() - - -class Tunnel(object): - """SSH tunnel.""" - - def __init__(self, context): - """Construct Tunnel.""" - self.context = context - self._tunnel = None - - def bore(self, id): - """Create SSH tunnel from given context.""" - ssh_opts = '-nNT' - if logging.getLogger().getEffectiveLevel() == logging.DEBUG: - ssh_opts += 'v' - else: - ssh_opts += 'q' - - cmd = [ - 'ssh', - ssh_opts, - '-L', - '{}:{}'.format(self.context.local_socket, - self.context.remote_socket), - '-i', - self.context.identity_file, - 'ssh://{}@{}'.format(self.context.username, self.context.hostname), - ] - logging.debug('Tunnel cmd "{}"'.format(' '.join(cmd))) - - self._tunnel = subprocess.Popen(cmd, close_fds=True) - for i in range(10): - # TODO: Make timeout configurable - if os.path.exists(self.context.local_socket): - break - time.sleep(0.5) - else: - raise TimeoutError('Failed to create tunnel using: {}'.format( - ' '.join(cmd))) - weakref.finalize(self, self.close, id) - return self - - def close(self, id): - """Close SSH tunnel.""" - if self._tunnel is None: - return - - self._tunnel.kill() - self._tunnel.wait(300) - os.remove(self.context.local_socket) - self._tunnel = None |