diff options
-rw-r--r-- | contrib/python/podman/client.py | 148 | ||||
-rw-r--r-- | contrib/python/podman/libs/tunnel.py | 131 | ||||
-rw-r--r-- | contrib/python/requirements.txt | 6 | ||||
-rwxr-xr-x | contrib/python/test/test_runner.sh | 22 | ||||
-rw-r--r-- | contrib/python/test/test_system.py | 17 | ||||
-rw-r--r-- | install.md | 1 | ||||
-rw-r--r-- | vendor.conf | 2 | ||||
-rw-r--r-- | vendor/github.com/containers/storage/containers.go | 4 | ||||
-rw-r--r-- | vendor/github.com/containers/storage/containers_ffjson.go | 1 | ||||
-rw-r--r-- | vendor/github.com/containers/storage/pkg/directory/directory_unix.go | 2 |
10 files changed, 296 insertions, 38 deletions
diff --git a/contrib/python/podman/client.py b/contrib/python/podman/client.py index 89fcf5c15..c62c1430a 100644 --- a/contrib/python/podman/client.py +++ b/contrib/python/podman/client.py @@ -1,6 +1,6 @@ """A client for communicating with a Podman varlink service.""" -import contextlib -import functools +import os +from urllib.parse import urlparse from varlink import Client as VarlinkClient from varlink import VarlinkError @@ -10,6 +10,119 @@ from .libs.containers import Containers from .libs.errors import error_factory from .libs.images import Images from .libs.system import System +from .libs.tunnel import Context, Portal, Tunnel + + +class BaseClient(object): + """Context manager for API workers to access varlink.""" + + def __call__(self): + """Support being called for old API.""" + return self + + @classmethod + def factory(cls, + uri=None, + interface='io.projectatomic.podman', + *args, + **kwargs): + """Construct a Client based on input.""" + if uri is None: + raise ValueError('uri is required and cannot be None') + if interface is None: + raise ValueError('interface is required and cannot be None') + + local_path = urlparse(uri).path + if local_path == '': + raise ValueError('path is required for uri, format' + ' "unix://path_to_socket"') + + if kwargs.get('remote_uri') or kwargs.get('identity_file'): + # Remote access requires the full tuple of information + if kwargs.get('remote_uri') is None: + raise ValueError('remote is required, format' + ' "ssh://user@hostname/path_to_socket".') + remote = urlparse(kwargs['remote_uri']) + if remote.username is None: + raise ValueError('username is required for remote_uri, format' + ' "ssh://user@hostname/path_to_socket".') + if remote.path == '': + raise ValueError('path is required for remote_uri, format' + ' "ssh://user@hostname/path_to_socket".') + if remote.hostname is None: + raise ValueError('hostname is required for remote_uri, format' + ' "ssh://user@hostname/path_to_socket".') + + if kwargs.get('identity_file') is None: + raise ValueError('identity_file is required.') + + if not os.path.isfile(kwargs['identity_file']): + raise ValueError('identity_file "{}" not found.'.format( + kwargs['identity_file'])) + return RemoteClient( + Context(uri, interface, local_path, remote.path, + remote.username, remote.hostname, + kwargs['identity_file'])) + else: + return LocalClient( + Context(uri, interface, None, None, None, None, None)) + + +class LocalClient(BaseClient): + """Context manager for API workers to access varlink.""" + + def __init__(self, context): + """Construct LocalClient.""" + self._context = context + + def __enter__(self): + """Enter context for LocalClient.""" + self._client = VarlinkClient(address=self._context.uri) + self._iface = self._client.open(self._context.interface) + return self._iface + + def __exit__(self, e_type, e, e_traceback): + """Cleanup context for LocalClient.""" + if hasattr(self._client, 'close'): + self._client.close() + self._iface.close() + + if isinstance(e, VarlinkError): + raise error_factory(e) + + +class RemoteClient(BaseClient): + """Context manager for API workers to access remote varlink.""" + + def __init__(self, context): + """Construct RemoteCLient.""" + self._context = context + self._portal = Portal() + + def __enter__(self): + """Context manager for API workers to access varlink.""" + tunnel = self._portal.get(self._context.uri) + if tunnel is None: + tunnel = Tunnel(self._context).bore(self._context.uri) + self._portal[self._context.uri] = tunnel + + try: + self._client = VarlinkClient(address=self._context.uri) + self._iface = self._client.open(self._context.interface) + return self._iface + except Exception: + self._close_tunnel(self._context.uri) + raise + + def __exit__(self, e_type, e, e_traceback): + """Cleanup context for RemoteClient.""" + if hasattr(self._client, 'close'): + self._client.close() + self._iface.close() + + # set timer to shutdown ssh tunnel + if isinstance(e, VarlinkError): + raise error_factory(e) class Client(object): @@ -20,37 +133,26 @@ class Client(object): >>> import podman >>> c = podman.Client() >>> c.system.versions - """ - # TODO: Port to contextlib.AbstractContextManager once - # Python >=3.6 required + Example remote podman: + + >>> import podman + >>> c = podman.Client(uri='unix:/tmp/podman.sock', + remote_uri='ssh://user@host/run/podman/io.projectatomic.podman', + identity_file='~/.ssh/id_rsa') + """ def __init__(self, uri='unix:/run/podman/io.projectatomic.podman', - interface='io.projectatomic.podman'): + interface='io.projectatomic.podman', + **kwargs): """Construct a podman varlink Client. uri from default systemd unit file. interface from io.projectatomic.podman.varlink, do not change unless you are a varlink guru. """ - self._podman = None - - @contextlib.contextmanager - def _podman(uri, interface): - """Context manager for API workers to access varlink.""" - client = VarlinkClient(address=uri) - try: - iface = client.open(interface) - yield iface - except VarlinkError as e: - raise error_factory(e) from e - finally: - if hasattr(client, 'close'): - client.close() - iface.close() - - self._client = functools.partial(_podman, uri, interface) + self._client = BaseClient.factory(uri, interface, **kwargs) # Quick validation of connection data provided try: diff --git a/contrib/python/podman/libs/tunnel.py b/contrib/python/podman/libs/tunnel.py new file mode 100644 index 000000000..2cb178644 --- /dev/null +++ b/contrib/python/podman/libs/tunnel.py @@ -0,0 +1,131 @@ +"""Cache for SSH tunnels.""" +import collections +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() + + 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.""" + with self.lock: + now = time.time() + for entry, timeout in self.data: + if timeout < now: + self.__delitem__(entry) + 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.""" + cmd = [ + 'ssh', + '-nNT', + '-L', + '{}:{}'.format(self.context.local_socket, + self.context.remote_socket), + '-i', + self.context.identity_file, + 'ssh://{}@{}'.format(self.context.username, self.context.hostname), + ] + + if os.environ.get('PODMAN_DEBUG'): + cmd.append('-vvv') + + self._tunnel = subprocess.Popen(cmd, close_fds=True) + for i in range(5): + if os.path.exists(self.context.local_socket): + break + time.sleep(1) + 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.""" + print('Tunnel collapsed!') + if self._tunnel is None: + return + + self._tunnel.kill() + self._tunnel.wait(300) + os.remove(self.context.local_socket) + self._tunnel = None diff --git a/contrib/python/requirements.txt b/contrib/python/requirements.txt index 368a095b2..d294af3c7 100644 --- a/contrib/python/requirements.txt +++ b/contrib/python/requirements.txt @@ -1,3 +1,3 @@ -varlink>=25 -setuptools -dateutil +varlink>=26.1.0 +setuptools>=39.2.0 +python-dateutil>=2.7.3 diff --git a/contrib/python/test/test_runner.sh b/contrib/python/test/test_runner.sh index 4e5844831..602e0d6fd 100755 --- a/contrib/python/test/test_runner.sh +++ b/contrib/python/test/test_runner.sh @@ -13,13 +13,18 @@ if [[ ! -x ../../bin/podman ]]; then fi export PATH=../../bin:$PATH +function usage { + echo 1>&2 $0 [-v] [-h] [test.TestCase|test.TestCase.step] +} + while getopts "vh" arg; do case $arg in v ) VERBOSE='-v' ;; - h ) echo >2 $0 [-v] [-h] [test.TestCase|test.TestCase.step] ; exit 2 ;; + h ) usage ; exit 0;; + \? ) usage ; exit 2;; esac done -shift $((OPTIND-1)) +shift $((OPTIND -1)) function cleanup { # aggressive cleanup as tests may crash leaving crap around @@ -49,7 +54,7 @@ EOT } # Need locations to store stuff -mkdir -p ${TMPDIR}/{podman,crio,crio-run,cni/net.d,ctnr} +mkdir -p ${TMPDIR}/{podman,crio,crio-run,cni/net.d,ctnr,tunnel} # Cannot be done in python unittest fixtures. EnvVar not picked up. export REGISTRIES_CONFIG_PATH=${TMPDIR}/registry.conf @@ -102,11 +107,14 @@ ENTRYPOINT ["/tmp/hello.sh"] EOT export PODMAN_HOST="unix:${TMPDIR}/podman/io.projectatomic.podman" -PODMAN_ARGS="--storage-driver=vfs\ - --root=${TMPDIR}/crio\ - --runroot=${TMPDIR}/crio-run\ - --cni-config-dir=$CNI_CONFIG_PATH\ +PODMAN_ARGS="--storage-driver=vfs \ + --root=${TMPDIR}/crio \ + --runroot=${TMPDIR}/crio-run \ + --cni-config-dir=$CNI_CONFIG_PATH \ " +if [[ -n $VERBOSE ]]; then + PODMAN_ARGS="$PODMAN_ARGS --log-level=debug" +fi PODMAN="podman $PODMAN_ARGS" # document what we're about to do... diff --git a/contrib/python/test/test_system.py b/contrib/python/test/test_system.py index c0d30acd7..93fb9aded 100644 --- a/contrib/python/test/test_system.py +++ b/contrib/python/test/test_system.py @@ -1,14 +1,15 @@ import os import unittest - -import varlink +from urllib.parse import urlparse import podman +import varlink class TestSystem(unittest.TestCase): def setUp(self): self.host = os.environ['PODMAN_HOST'] + self.tmpdir = os.environ['TMPDIR'] def tearDown(self): pass @@ -22,6 +23,18 @@ class TestSystem(unittest.TestCase): with podman.Client(self.host) as pclient: self.assertTrue(pclient.system.ping()) + def test_remote_ping(self): + host = urlparse(self.host) + remote_uri = 'ssh://root@localhost/{}'.format(host.path) + + local_uri = 'unix:{}/tunnel/podman.sock'.format(self.tmpdir) + with podman.Client( + uri=local_uri, + remote_uri=remote_uri, + identity_file=os.path.expanduser('~/.ssh/id_rsa'), + ) as pclient: + pclient.system.ping() + def test_versions(self): with podman.Client(self.host) as pclient: # Values change with each build so we cannot test too much diff --git a/install.md b/install.md index b2f405336..c0767bb4d 100644 --- a/install.md +++ b/install.md @@ -41,6 +41,7 @@ yum install -y \ libgpg-error-devel \ libseccomp-devel \ libselinux-devel \ + make \ ostree-devel \ pkgconfig \ runc \ diff --git a/vendor.conf b/vendor.conf index 2fe12b67c..9ec863532 100644 --- a/vendor.conf +++ b/vendor.conf @@ -11,7 +11,7 @@ github.com/containerd/continuity master github.com/containernetworking/cni v0.6.0 github.com/containernetworking/plugins 1fb94a4222eafc6f948eacdca9c9f2158b427e53 github.com/containers/image ad33f7b73fbac0acf05b9e2cea021b61b4b0c3e0 -github.com/containers/storage 4993aae31ced3971f5b72f28c4e3fe38c34fa634 +github.com/containers/storage 51f1f85c2b7863b2fd361471f05b937fd8059124 github.com/coreos/go-systemd v14 github.com/cri-o/ocicni master github.com/cyphar/filepath-securejoin v0.2.1 diff --git a/vendor/github.com/containers/storage/containers.go b/vendor/github.com/containers/storage/containers.go index ec54a502e..ebc1e99a0 100644 --- a/vendor/github.com/containers/storage/containers.go +++ b/vendor/github.com/containers/storage/containers.go @@ -2,6 +2,7 @@ package storage import ( "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -278,7 +279,8 @@ func (r *containerStore) Create(id string, names []string, image, layer, metadat names = dedupeNames(names) for _, name := range names { if _, nameInUse := r.byname[name]; nameInUse { - return nil, ErrDuplicateName + return nil, errors.Wrapf(ErrDuplicateName, + fmt.Sprintf("the container name \"%s\" is already in use by \"%s\". You have to remove that container to be able to reuse that name.", name, r.byname[name].ID)) } } if err == nil { diff --git a/vendor/github.com/containers/storage/containers_ffjson.go b/vendor/github.com/containers/storage/containers_ffjson.go index aef6becfe..6e83808d4 100644 --- a/vendor/github.com/containers/storage/containers_ffjson.go +++ b/vendor/github.com/containers/storage/containers_ffjson.go @@ -1,5 +1,6 @@ // Code generated by ffjson <https://github.com/pquerna/ffjson>. DO NOT EDIT. // source: containers.go +// package storage diff --git a/vendor/github.com/containers/storage/pkg/directory/directory_unix.go b/vendor/github.com/containers/storage/pkg/directory/directory_unix.go index 397251bdb..05522d68b 100644 --- a/vendor/github.com/containers/storage/pkg/directory/directory_unix.go +++ b/vendor/github.com/containers/storage/pkg/directory/directory_unix.go @@ -1,4 +1,4 @@ -// +build linux freebsd solaris +// +build linux darwin freebsd solaris package directory |