summaryrefslogtreecommitdiff
path: root/contrib/python
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/.gitignore3
-rw-r--r--contrib/python/CHANGES.txt1
-rw-r--r--contrib/python/LICENSE.txt13
-rw-r--r--contrib/python/MANIFEST.in2
-rw-r--r--contrib/python/Makefile15
-rw-r--r--contrib/python/README.md15
-rw-r--r--contrib/python/podman/__init__.py22
-rw-r--r--contrib/python/podman/client.py81
-rw-r--r--contrib/python/podman/libs/__init__.py96
-rw-r--r--contrib/python/podman/libs/containers.py211
-rw-r--r--contrib/python/podman/libs/errors.py58
-rw-r--r--contrib/python/podman/libs/images.py137
-rw-r--r--contrib/python/podman/libs/system.py40
-rw-r--r--contrib/python/requirements.txt2
-rw-r--r--contrib/python/setup.py37
-rw-r--r--contrib/python/test/__init__.py0
-rw-r--r--contrib/python/test/podman_testcase.py106
-rw-r--r--contrib/python/test/test_containers.py186
-rw-r--r--contrib/python/test/test_images.py151
-rw-r--r--contrib/python/test/test_libs.py46
-rwxr-xr-xcontrib/python/test/test_runner.sh116
-rw-r--r--contrib/python/test/test_system.py49
22 files changed, 1387 insertions, 0 deletions
diff --git a/contrib/python/.gitignore b/contrib/python/.gitignore
new file mode 100644
index 000000000..2fc8ce3da
--- /dev/null
+++ b/contrib/python/.gitignore
@@ -0,0 +1,3 @@
+build
+dist
+*.egg-info
diff --git a/contrib/python/CHANGES.txt b/contrib/python/CHANGES.txt
new file mode 100644
index 000000000..2bac1c867
--- /dev/null
+++ b/contrib/python/CHANGES.txt
@@ -0,0 +1 @@
+v0.1.0, 2018-05-11 -- Initial release.
diff --git a/contrib/python/LICENSE.txt b/contrib/python/LICENSE.txt
new file mode 100644
index 000000000..decfce56d
--- /dev/null
+++ b/contrib/python/LICENSE.txt
@@ -0,0 +1,13 @@
+Copyright 2018 Red Hat, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/contrib/python/MANIFEST.in b/contrib/python/MANIFEST.in
new file mode 100644
index 000000000..72e638cb9
--- /dev/null
+++ b/contrib/python/MANIFEST.in
@@ -0,0 +1,2 @@
+prune test/
+include README.md
diff --git a/contrib/python/Makefile b/contrib/python/Makefile
new file mode 100644
index 000000000..d0ec60687
--- /dev/null
+++ b/contrib/python/Makefile
@@ -0,0 +1,15 @@
+PYTHON ?= /usr/bin/python3
+
+.PHONY: python-podman
+python-podman:
+ $(PYTHON) setup.py bdist
+
+.PHONY: integration
+integration:
+ test/test_runner.sh
+
+.PHONY: clean
+clean:
+ $(PYTHON) setup.py clean --all
+ rm -rf podman.egg-info dist
+ find . -depth -name __pycache__ -exec rm -rf {} \;
diff --git a/contrib/python/README.md b/contrib/python/README.md
new file mode 100644
index 000000000..747e71559
--- /dev/null
+++ b/contrib/python/README.md
@@ -0,0 +1,15 @@
+# podman - pythonic library for working with varlink interface to Podman
+
+### Status: Active Development
+
+See [libpod](https://github.com/projectatomic/libpod)
+
+
+###
+
+To build the podman wheel:
+
+```sh
+cd ~/libpod/contrib/pypodman
+python3 setup.py clean -a && python3 setup.py bdist_wheel
+```
diff --git a/contrib/python/podman/__init__.py b/contrib/python/podman/__init__.py
new file mode 100644
index 000000000..5a0356311
--- /dev/null
+++ b/contrib/python/podman/__init__.py
@@ -0,0 +1,22 @@
+"""A client for communicating with a Podman server."""
+import pkg_resources
+
+from .client import Client
+from .libs import datetime_format, datetime_parse
+from .libs.errors import (ContainerNotFound, ErrorOccurred, ImageNotFound,
+ RuntimeError)
+
+try:
+ __version__ = pkg_resources.get_distribution('podman').version
+except Exception:
+ __version__ = '0.0.0'
+
+__all__ = [
+ 'Client',
+ 'ContainerNotFound',
+ 'datetime_format',
+ 'datetime_parse',
+ 'ErrorOccurred',
+ 'ImageNotFound',
+ 'RuntimeError',
+]
diff --git a/contrib/python/podman/client.py b/contrib/python/podman/client.py
new file mode 100644
index 000000000..8a1acdd9b
--- /dev/null
+++ b/contrib/python/podman/client.py
@@ -0,0 +1,81 @@
+"""A client for communicating with a Podman varlink service."""
+import contextlib
+import functools
+
+from varlink import Client as VarlinkClient
+from varlink import VarlinkError
+
+from .libs import cached_property
+from .libs.containers import Containers
+from .libs.errors import error_factory
+from .libs.images import Images
+from .libs.system import System
+
+
+class Client(object):
+ """A client for communicating with a Podman varlink service.
+
+ Example:
+
+ >>> import podman
+ >>> c = podman.Client()
+ >>> c.system.versions
+ """
+
+ # TODO: Port to contextlib.AbstractContextManager once
+ # Python >=3.6 required
+
+ def __init__(self,
+ uri='unix:/run/podman/io.projectatomic.podman',
+ interface='io.projectatomic.podman'):
+ """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 children 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)
+
+ # Quick validation of connection data provided
+ if not System(self._client).ping():
+ raise ValueError('Failed varlink connection "{}/{}"'.format(
+ uri, interface))
+
+ def __enter__(self):
+ """Return `self` upon entering the runtime context."""
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Raise any exception triggered within the runtime context."""
+ return None
+
+ @cached_property
+ def system(self):
+ """Manage system model for podman."""
+ return System(self._client)
+
+ @cached_property
+ def images(self):
+ """Manage images model for libpod."""
+ return Images(self._client)
+
+ @cached_property
+ def containers(self):
+ """Manage containers model for libpod."""
+ return Containers(self._client)
diff --git a/contrib/python/podman/libs/__init__.py b/contrib/python/podman/libs/__init__.py
new file mode 100644
index 000000000..ab8fb94a8
--- /dev/null
+++ b/contrib/python/podman/libs/__init__.py
@@ -0,0 +1,96 @@
+"""Support files for podman API implementation."""
+import datetime
+import re
+import threading
+
+__all__ = [
+ 'cached_property',
+ 'datetime_parse',
+ 'datetime_format',
+]
+
+
+class cached_property(object):
+ """cached_property() - computed once per instance, cached as attribute.
+
+ Maybe this will make a future version of python.
+ """
+
+ def __init__(self, func):
+ """Construct context manager."""
+ self.func = func
+ self.__doc__ = func.__doc__
+ self.lock = threading.RLock()
+
+ def __get__(self, instance, cls=None):
+ """Retrieve previous value, or call func()."""
+ if instance is None:
+ return self
+
+ attrname = self.func.__name__
+ try:
+ cache = instance.__dict__
+ except AttributeError: # objects with __slots__ have no __dict__
+ msg = ("No '__dict__' attribute on {}"
+ " instance to cache {} property.").format(
+ repr(type(instance).__name__), repr(attrname))
+ raise TypeError(msg) from None
+
+ with self.lock:
+ # check if another thread filled cache while we awaited lock
+ if attrname not in cache:
+ cache[attrname] = self.func(instance)
+ return cache[attrname]
+
+
+def datetime_parse(string):
+ """Convert timestamp to datetime.
+
+ Because date/time parsing in python is still pedantically stupid,
+ we rip the input string apart throwing out the stop characters etc;
+ then rebuild a string strptime() can parse. Igit!
+
+ - Python >3.7 will address colons in the UTC offset.
+ - There is no ETA on microseconds > 6 digits.
+ - And giving an offset and timezone name...
+
+ # match: 2018-05-08T14:12:53.797795191-07:00
+ # match: 2018-05-08T18:24:52.753227-07:00
+ # match: 2018-05-08 14:12:53.797795191 -0700 MST
+ # match: 2018-05-09T10:45:57.576002 (python isoformat())
+
+ Some people, when confronted with a problem, think “I know,
+ I'll use regular expressions.” Now they have two problems.
+ -- Jamie Zawinski
+ """
+ ts = re.compile(r'^(\d+)-(\d+)-(\d+)'
+ r'[ T]?(\d+):(\d+):(\d+).(\d+)'
+ r' *([-+][\d:]{4,5})? *')
+
+ x = ts.match(string)
+ if x is None:
+ raise ValueError('Unable to parse {}'.format(string))
+
+ # converting everything to int() not worth the readablity hit
+ igit_proof = '{}T{}.{}{}'.format(
+ '-'.join(x.group(1, 2, 3)),
+ ':'.join(x.group(4, 5, 6)),
+ x.group(7)[0:6],
+ x.group(8).replace(':', '') if x.group(8) else '',
+ )
+
+ format = '%Y-%m-%dT%H:%M:%S.%f'
+ if x.group(8):
+ format += '%z'
+ return datetime.datetime.strptime(igit_proof, format)
+
+
+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.py b/contrib/python/podman/libs/containers.py
new file mode 100644
index 000000000..cbbef23b1
--- /dev/null
+++ b/contrib/python/podman/libs/containers.py
@@ -0,0 +1,211 @@
+"""Models for manipulating containers and storage."""
+import collections
+import functools
+import json
+import signal
+
+
+class Container(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
+
+ self._refresh(data)
+ 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 + apply aliases."""
+ if key == 'running':
+ key = 'containerrunning'
+ return super().__getitem__(key)
+
+ def _refresh(self, data):
+ super().update(data)
+ for k, v in data.items():
+ setattr(self, k, v)
+ setattr(self, 'running', data['containerrunning'])
+
+ def refresh(self):
+ """Refresh status fields for this container."""
+ ctnr = Containers(self._client).get(self.id)
+ self._refresh(ctnr)
+
+ 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 arguements
+ podman.AttachToContainer()
+
+ def processes(self):
+ """Show processes running in container."""
+ with self._client() as podman:
+ results = podman.ListContainerProcesses(self.id)
+ for p in results['container']:
+ yield p
+
+ 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):
+ """Send signal to container, return id if successful.
+
+ default signal is signal.SIGTERM.
+ """
+ with self._client() as podman:
+ results = podman.KillContainer(self.id, signal)
+ return results['container']
+
+ 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 start(self):
+ """Start container, return id on success."""
+ with self._client() as podman:
+ results = podman.StartContainer(self.id)
+ return results['container']
+
+ def stop(self, timeout=25):
+ """Stop container, return id on success."""
+ with self._client() as podman:
+ results = podman.StopContainer(self.id, timeout)
+ return results['container']
+
+ 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:
+ results = podman.RestartContainer(self.id, timeout)
+ return results['container']
+
+ def rename(self, target):
+ """Rename container, return id on success."""
+ with self._client() as podman:
+ # TODO: Need arguements
+ 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(), arguements
+ podman.ResizeContainerTty()
+
+ def pause(self):
+ """Pause container, return id on success."""
+ with self._client() as podman:
+ results = podman.PauseContainer(self.id)
+ return results['container']
+
+ def unpause(self):
+ """Unpause container, return id on success."""
+ with self._client() as podman:
+ results = podman.UnpauseContainer(self.id)
+ return results['container']
+
+ def update_container(self, *args, **kwargs):
+ """TODO: Update container..., return id on success."""
+ with self._client() as podman:
+ results = podman.UpdateContainer()
+ self.refresh()
+ return results['container']
+
+ 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)
+ for line in results:
+ yield line
+
+
+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 create(self, *args, **kwargs):
+ """Create container layer over the specified image.
+
+ See podman-create.1.md for kwargs details.
+ """
+ with self._client() as podman:
+ results = podman.CreateContainer()
+ return results['id']
+
+ 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
new file mode 100644
index 000000000..c28afd940
--- /dev/null
+++ b/contrib/python/podman/libs/errors.py
@@ -0,0 +1,58 @@
+"""Error classes and wrappers for VarlinkError."""
+from varlink import VarlinkError
+
+
+class VarlinkErrorProxy(VarlinkError):
+ """Class to Proxy VarlinkError methods."""
+
+ def __init__(self, obj):
+ """Construct proxy from Exception."""
+ self._obj = obj
+ self.__module__ = 'libpod'
+
+ def __getattr__(self, item):
+ """Return item from proxied Exception."""
+ return getattr(self._obj, item)
+
+
+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
new file mode 100644
index 000000000..1d40984a7
--- /dev/null
+++ b/contrib/python/podman/libs/images.py
@@ -0,0 +1,137 @@
+"""Models for manipulating images in/to/from storage."""
+import collections
+import functools
+import json
+
+
+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 export(self, dest, compressed=False):
+ """Write image to dest, return True 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)
+
+ 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 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 create(self, *args, **kwargs):
+ """Create image from configuration."""
+ with self._client() as podman:
+ results = podman.CreateImage()
+ return results['image']
+
+ def create_from(self, *args, **kwargs):
+ """Create image from container."""
+ # TODO: Should this be on container?
+ with self._client() as podman:
+ results = podman.CreateFromContainer()
+ return results['image']
+
+ def build(self, *args, **kwargs):
+ """Build container from image.
+
+ See podman-build.1.md for kwargs details.
+ """
+ with self._client() as podman:
+ # TODO: Need arguments
+ podman.BuildImage()
+
+ 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)
+ for img in results['images']:
+ yield img
diff --git a/contrib/python/podman/libs/system.py b/contrib/python/podman/libs/system.py
new file mode 100644
index 000000000..c59867760
--- /dev/null
+++ b/contrib/python/podman/libs/system.py
@@ -0,0 +1,40 @@
+"""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/requirements.txt b/contrib/python/requirements.txt
new file mode 100644
index 000000000..f5d264ddb
--- /dev/null
+++ b/contrib/python/requirements.txt
@@ -0,0 +1,2 @@
+varlink==25
+setuptools
diff --git a/contrib/python/setup.py b/contrib/python/setup.py
new file mode 100644
index 000000000..d1533f968
--- /dev/null
+++ b/contrib/python/setup.py
@@ -0,0 +1,37 @@
+import os
+
+from setuptools import find_packages, setup
+
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, 'README.md')) as me:
+ readme = me.read()
+
+with open(os.path.join(root, 'requirements.txt')) as r:
+ requirements = r.read().splitlines()
+
+setup(
+ name='podman',
+ version='0.1.0',
+ description='A client for communicating with a Podman server',
+ long_description=readme,
+ author='Jhon Honce',
+ author_email='jhonce@redhat.com',
+ url='http://github.com/projectatomic/libpod',
+ license='Apache Software License',
+ python_requires='>=3',
+ include_package_data=True,
+ install_requires=requirements,
+ packages=find_packages(exclude=['test']),
+ zip_safe=True,
+ keywords='varlink libpod podman',
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'Topic :: Software Development',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Programming Language :: Python :: 3.6',
+ ])
+# Not supported
+# long_description_content_type='text/markdown',
diff --git a/contrib/python/test/__init__.py b/contrib/python/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/contrib/python/test/__init__.py
diff --git a/contrib/python/test/podman_testcase.py b/contrib/python/test/podman_testcase.py
new file mode 100644
index 000000000..fc99f26ce
--- /dev/null
+++ b/contrib/python/test/podman_testcase.py
@@ -0,0 +1,106 @@
+import contextlib
+import functools
+import itertools
+import os
+import subprocess
+import time
+import unittest
+
+from varlink import VarlinkError
+
+MethodNotImplemented = 'org.varlink.service.MethodNotImplemented'
+
+
+class PodmanTestCase(unittest.TestCase):
+ """Hide the sausage making of initializing storage."""
+
+ @classmethod
+ def setUpClass(cls):
+ if hasattr(PodmanTestCase, 'alpine_process'):
+ PodmanTestCase.tearDownClass()
+
+ def run_cmd(*args):
+ cmd = list(itertools.chain(*args))
+ try:
+ pid = subprocess.Popen(
+ cmd,
+ close_fds=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = pid.communicate()
+ except OSError as e:
+ print('{}: {}({})'.format(cmd, e.strerror, e.returncode))
+ except ValueError as e:
+ print('{}: {}'.format(cmd, e.message))
+ raise
+ else:
+ return out.strip()
+
+ tmpdir = os.environ.get('TMPDIR', '/tmp')
+ podman_args = [
+ '--storage-driver=vfs',
+ '--root={}/crio'.format(tmpdir),
+ '--runroot={}/crio-run'.format(tmpdir),
+ '--cni-config-dir={}/cni/net.d'.format(tmpdir),
+ ]
+
+ run_podman = functools.partial(run_cmd, ['podman'], podman_args)
+
+ id = run_podman(['pull', 'alpine'])
+ setattr(PodmanTestCase, 'alpine_id', id)
+
+ run_podman(['pull', 'busybox'])
+ run_podman(['images'])
+
+ run_cmd(['rm', '-f', '{}/alpine_gold.tar'.format(tmpdir)])
+ run_podman([
+ 'save', '--output', '{}/alpine_gold.tar'.format(tmpdir), 'alpine'
+ ])
+
+ PodmanTestCase.alpine_log = open(
+ os.path.join('/tmp/', 'alpine.log'), 'w')
+
+ cmd = ['podman']
+ cmd.extend(podman_args)
+ cmd.extend(['run', '-d', 'alpine', 'sleep', '500'])
+ PodmanTestCase.alpine_process = subprocess.Popen(
+ cmd,
+ stdout=PodmanTestCase.alpine_log,
+ stderr=subprocess.STDOUT,
+ )
+
+ PodmanTestCase.busybox_log = open(
+ os.path.join('/tmp/', 'busybox.log'), 'w')
+
+ cmd = ['podman']
+ cmd.extend(podman_args)
+ cmd.extend(['create', 'busybox'])
+ PodmanTestCase.busybox_process = subprocess.Popen(
+ cmd,
+ stdout=PodmanTestCase.busybox_log,
+ stderr=subprocess.STDOUT,
+ )
+ # give podman time to start ctnr
+ time.sleep(2)
+
+ # Close our handle of file
+ PodmanTestCase.alpine_log.close()
+ PodmanTestCase.busybox_log.close()
+
+ @classmethod
+ def tearDownClass(cls):
+ try:
+ PodmanTestCase.alpine_process.kill()
+ assert 0 == PodmanTestCase.alpine_process.wait(500)
+ delattr(PodmanTestCase, 'alpine_process')
+
+ PodmanTestCase.busybox_process.kill()
+ assert 0 == PodmanTestCase.busybox_process.wait(500)
+ except Exception as e:
+ print('Exception: {}'.format(e))
+ raise
+
+ @contextlib.contextmanager
+ def assertRaisesNotImplemented(self):
+ with self.assertRaisesRegex(VarlinkError, MethodNotImplemented):
+ yield
diff --git a/contrib/python/test/test_containers.py b/contrib/python/test/test_containers.py
new file mode 100644
index 000000000..9f6123e05
--- /dev/null
+++ b/contrib/python/test/test_containers.py
@@ -0,0 +1,186 @@
+import os
+import time
+import unittest
+from test.podman_testcase import PodmanTestCase
+
+import podman
+from podman import datetime_parse
+
+
+class TestContainers(PodmanTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def setUp(self):
+ self.tmpdir = os.environ['TMPDIR']
+ self.host = os.environ['PODMAN_HOST']
+
+ self.pclient = podman.Client(self.host)
+ self.ctns = self.loadCache()
+ # TODO: Change to start() when Implemented
+ self.alpine_ctnr.restart()
+
+ def tearDown(self):
+ pass
+
+ def loadCache(self):
+ with podman.Client(self.host) as pclient:
+ self.ctns = list(pclient.containers.list())
+
+ self.alpine_ctnr = next(
+ iter([c for c in self.ctns if 'alpine' in c['image']] or []), None)
+ return self.ctns
+
+ def test_list(self):
+ actual = self.loadCache()
+ self.assertGreaterEqual(len(actual), 2)
+ self.assertIsNotNone(self.alpine_ctnr)
+ self.assertIn('alpine', self.alpine_ctnr.image)
+
+ def test_delete_stopped(self):
+ before = self.loadCache()
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.stop())
+ actual = self.pclient.containers.delete_stopped()
+ self.assertIn(self.alpine_ctnr.id, actual)
+ after = self.loadCache()
+
+ self.assertLess(len(after), len(before))
+ TestContainers.setUpClass()
+ self.loadCache()
+
+ def test_create(self):
+ with self.assertRaisesNotImplemented():
+ self.pclient.containers.create()
+
+ def test_get(self):
+ actual = self.pclient.containers.get(self.alpine_ctnr.id)
+ for k in ['id', 'status', 'ports']:
+ self.assertEqual(actual[k], self.alpine_ctnr[k])
+
+ with self.assertRaises(podman.ContainerNotFound):
+ self.pclient.containers.get("bozo")
+
+ def test_attach(self):
+ with self.assertRaisesNotImplemented():
+ self.alpine_ctnr.attach()
+
+ def test_processes(self):
+ actual = list(self.alpine_ctnr.processes())
+ self.assertGreaterEqual(len(actual), 2)
+
+ def test_start_stop_wait(self):
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.stop())
+ self.alpine_ctnr.refresh()
+ self.assertFalse(self.alpine_ctnr['running'])
+
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.restart())
+ self.alpine_ctnr.refresh()
+ self.assertTrue(self.alpine_ctnr.running)
+
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.stop())
+ self.alpine_ctnr.refresh()
+ self.assertFalse(self.alpine_ctnr['containerrunning'])
+
+ actual = self.alpine_ctnr.wait()
+ self.assertEqual(0, actual)
+
+ def test_changes(self):
+ actual = self.alpine_ctnr.changes()
+
+ self.assertListEqual(
+ sorted(['changed', 'added', 'deleted']), sorted(
+ list(actual.keys())))
+
+ # TODO: brittle, depends on knowing history of ctnr
+ self.assertGreaterEqual(len(actual['changed']), 2)
+ self.assertGreaterEqual(len(actual['added']), 3)
+ self.assertEqual(len(actual['deleted']), 0)
+
+ def test_kill(self):
+ self.assertTrue(self.alpine_ctnr.running)
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.kill(9))
+ time.sleep(2)
+
+ self.alpine_ctnr.refresh()
+ self.assertFalse(self.alpine_ctnr.running)
+
+ def test_inspect(self):
+ actual = self.alpine_ctnr.inspect()
+ self.assertEqual(actual.id, self.alpine_ctnr.id)
+ self.assertEqual(
+ datetime_parse(actual.created),
+ datetime_parse(self.alpine_ctnr.createdat))
+
+ def test_export(self):
+ target = os.path.join(self.tmpdir, 'alpine_export_ctnr.tar')
+
+ actual = self.alpine_ctnr.export(target)
+ self.assertEqual(actual, target)
+ self.assertTrue(os.path.isfile(target))
+ self.assertGreater(os.path.getsize(target), 0)
+
+ def test_remove(self):
+ before = self.loadCache()
+
+ with self.assertRaises(podman.ErrorOccurred):
+ self.alpine_ctnr.remove()
+
+ self.assertEqual(
+ self.alpine_ctnr.id, self.alpine_ctnr.remove(force=True))
+ after = self.loadCache()
+
+ self.assertLess(len(after), len(before))
+ TestContainers.setUpClass()
+ self.loadCache()
+
+ def test_restart(self):
+ self.assertTrue(self.alpine_ctnr.running)
+ before = self.alpine_ctnr.runningfor
+
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.restart())
+
+ self.alpine_ctnr.refresh()
+ after = self.alpine_ctnr.runningfor
+ self.assertTrue(self.alpine_ctnr.running)
+
+ # TODO: restore check when restart zeros counter
+ # self.assertLess(after, before)
+
+ def test_rename(self):
+ with self.assertRaisesNotImplemented():
+ self.alpine_ctnr.rename('new_alpine')
+
+ def test_resize_tty(self):
+ with self.assertRaisesNotImplemented():
+ self.alpine_ctnr.resize_tty(132, 43)
+
+ def test_pause_unpause(self):
+ self.assertTrue(self.alpine_ctnr.running)
+
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.pause())
+ self.alpine_ctnr.refresh()
+ self.assertFalse(self.alpine_ctnr.running)
+
+ self.assertEqual(self.alpine_ctnr.id, self.alpine_ctnr.unpause())
+ self.alpine_ctnr.refresh()
+ self.assertTrue(self.alpine_ctnr.running)
+
+ def test_stats(self):
+ self.alpine_ctnr.restart()
+ actual = self.alpine_ctnr.stats()
+ self.assertEqual(self.alpine_ctnr.id, actual.id)
+ self.assertEqual(self.alpine_ctnr.names, actual.name)
+
+ def test_logs(self):
+ self.alpine_ctnr.restart()
+ actual = list(self.alpine_ctnr.logs())
+ self.assertIsNotNone(actual)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/contrib/python/test/test_images.py b/contrib/python/test/test_images.py
new file mode 100644
index 000000000..7195c06d5
--- /dev/null
+++ b/contrib/python/test/test_images.py
@@ -0,0 +1,151 @@
+import itertools
+import os
+import unittest
+from test.podman_testcase import PodmanTestCase
+
+import podman
+
+
+class TestImages(PodmanTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def setUp(self):
+ self.tmpdir = os.environ['TMPDIR']
+ self.host = os.environ['PODMAN_HOST']
+
+ self.pclient = podman.Client(self.host)
+ self.images = self.loadCache()
+
+ def tearDown(self):
+ pass
+
+ def loadCache(self):
+ with podman.Client(self.host) as pclient:
+ self.images = list(pclient.images.list())
+
+ self.alpine_image = next(
+ iter([
+ i for i in self.images
+ if 'docker.io/library/alpine:latest' in i['repoTags']
+ ] or []), None)
+ return self.images
+
+ def test_list(self):
+ actual = self.loadCache()
+ self.assertGreaterEqual(len(actual), 2)
+ self.assertIsNotNone(self.alpine_image)
+
+ def test_build(self):
+ with self.assertRaisesNotImplemented():
+ self.pclient.images.build()
+
+ def test_create(self):
+ with self.assertRaisesNotImplemented():
+ self.pclient.images.create()
+
+ def test_create_from(self):
+ with self.assertRaisesNotImplemented():
+ self.pclient.images.create_from()
+
+ def test_export(self):
+ path = os.path.join(self.tmpdir, 'alpine_export.tar')
+ target = 'oci-archive:{}:latest'.format(path)
+
+ actual = self.alpine_image.export(target, False)
+ self.assertTrue(actual)
+ self.assertTrue(os.path.isfile(path))
+
+ def test_history(self):
+ count = 0
+ for record in self.alpine_image.history():
+ count += 1
+ self.assertEqual(record.id, self.alpine_image.id)
+ self.assertGreater(count, 0)
+
+ def test_inspect(self):
+ actual = self.alpine_image.inspect()
+ self.assertEqual(actual.id, self.alpine_image.id)
+
+ def test_push(self):
+ path = '{}/alpine_push'.format(self.tmpdir)
+ target = 'dir:{}'.format(path)
+ self.alpine_image.push(target)
+
+ self.assertTrue(os.path.isfile(os.path.join(path, 'manifest.json')))
+ self.assertTrue(os.path.isfile(os.path.join(path, 'version')))
+
+ def test_tag(self):
+ self.assertEqual(self.alpine_image.id,
+ self.alpine_image.tag('alpine:fubar'))
+ self.loadCache()
+ self.assertIn('alpine:fubar', self.alpine_image.repoTags)
+
+ def test_remove(self):
+ before = self.loadCache()
+
+ # assertRaises doesn't follow the import name :(
+ with self.assertRaises(podman.ErrorOccurred):
+ self.alpine_image.remove()
+
+ # TODO: remove this block once force=True works
+ with podman.Client(self.host) as pclient:
+ for ctnr in pclient.containers.list():
+ if 'alpine' in ctnr.image:
+ ctnr.stop()
+ ctnr.remove()
+
+ actual = self.alpine_image.remove(force=True)
+ self.assertEqual(self.alpine_image.id, actual)
+ after = self.loadCache()
+
+ self.assertLess(len(after), len(before))
+ TestImages.setUpClass()
+ self.loadCache()
+
+ def test_import_delete_unused(self):
+ before = self.loadCache()
+ # create unused image, so we have something to delete
+ source = os.path.join(self.tmpdir, 'alpine_gold.tar')
+ new_img = self.pclient.images.import_image(source, 'alpine2:latest',
+ 'unittest.test_import')
+ after = self.loadCache()
+
+ self.assertEqual(len(before) + 1, len(after))
+ self.assertIsNotNone(
+ next(iter([i for i in after if new_img in i['id']] or []), None))
+
+ actual = self.pclient.images.delete_unused()
+ self.assertIn(new_img, actual)
+
+ after = self.loadCache()
+ self.assertEqual(len(before), len(after))
+
+ TestImages.setUpClass()
+ self.loadCache()
+
+ def test_pull(self):
+ before = self.loadCache()
+ actual = self.pclient.images.pull('prom/busybox:latest')
+ after = self.loadCache()
+
+ self.assertEqual(len(before) + 1, len(after))
+ self.assertIsNotNone(
+ next(iter([i for i in after if actual in i['id']] or []), None))
+
+ def test_search(self):
+ actual = self.pclient.images.search('alpine', 25)
+ names, lengths = itertools.tee(actual)
+
+ for img in names:
+ self.assertIn('alpine', img['name'])
+ self.assertTrue(0 < len(list(lengths)) <= 25)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/contrib/python/test/test_libs.py b/contrib/python/test/test_libs.py
new file mode 100644
index 000000000..e2160fc30
--- /dev/null
+++ b/contrib/python/test/test_libs.py
@@ -0,0 +1,46 @@
+import datetime
+import unittest
+
+import podman
+
+
+class TestLibs(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_parse(self):
+ expected = datetime.datetime.strptime(
+ '2018-05-08T14:12:53.797795-0700', '%Y-%m-%dT%H:%M:%S.%f%z')
+ for v in [
+ '2018-05-08T14:12:53.797795191-07:00',
+ '2018-05-08T14:12:53.797795-07:00',
+ '2018-05-08T14:12:53.797795-0700',
+ '2018-05-08 14:12:53.797795191 -0700 MST'
+ ]:
+ actual = podman.datetime_parse(v)
+ self.assertEqual(actual, expected)
+
+ podman.datetime_parse(datetime.datetime.now().isoformat())
+
+ def test_parse_fail(self):
+ # chronologist humor: '1752-09-05T12:00:00.000000-0000' also not
+ # handled correctly by python for my locale.
+ for v in [
+ '1752-9-5',
+ '1752-09-05',
+ ]:
+ with self.assertRaises(ValueError):
+ podman.datetime_parse(v)
+
+ def test_format(self):
+ expected = '2018-05-08T18:24:52.753227-07:00'
+ dt = podman.datetime_parse(expected)
+ actual = podman.datetime_format(dt)
+ self.assertEqual(actual, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/contrib/python/test/test_runner.sh b/contrib/python/test/test_runner.sh
new file mode 100755
index 000000000..3e6dee110
--- /dev/null
+++ b/contrib/python/test/test_runner.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+
+# podman needs to play some games with resources
+if [[ $(id -u) != 0 ]]; then
+ echo >&2 $0 must be run as root.
+ exit 2
+fi
+
+while getopts "vh" arg; do
+ case $arg in
+ v ) VERBOSE='-v' ;;
+ h ) echo >2 $0 [-v] [-h] [test.TestCase|test.TestCase.step] ; exit 2 ;;
+ esac
+done
+shift $((OPTIND-1))
+
+# Create temporary directory for storage
+export TMPDIR=`mktemp -d /tmp/podman.XXXXXXXXXX`
+
+function umount {
+ # xargs -r always ran once, so write any mount points to file first
+ mount |awk "/$1/"' { print $3 }' >${TMPDIR}/mounts
+ if [[ -s ${TMPDIR}/mounts ]]; then
+ xargs <${TMPDIR}/mounts -t umount
+ fi
+}
+
+function cleanup {
+ umount '^(shm|nsfs)'
+ umount '\/run\/netns'
+ rm -fr ${TMPDIR}
+}
+trap cleanup EXIT
+
+# setup path to find new binaries _NOT_ system binaries
+if [[ ! -x ../../bin/podman ]]; then
+ echo 1>&2 Cannot find podman binary from libpod root directory, Or, run \"make binaries\"
+ exit 1
+fi
+export PATH=../../bin:$PATH
+
+function showlog {
+ [ -s "$1" ] && (echo $1 =====; cat "$1")
+}
+
+# Need a location to store the podman socket
+mkdir -p ${TMPDIR}/{podman,crio,crio-run,cni/net.d}
+
+# Cannot be done in python unittest fixtures. EnvVar not picked up.
+export REGISTRIES_CONFIG_PATH=${TMPDIR}/registry.conf
+cat >$REGISTRIES_CONFIG_PATH <<-EOT
+ [registries.search]
+ registries = ['docker.io']
+ [registries.insecure]
+ registries = []
+ [registries.block]
+ registries = []
+EOT
+
+export CNI_CONFIG_PATH=${TMPDIR}/cni/net.d
+cat >$CNI_CONFIG_PATH/87-podman-bridge.conflist <<-EOT
+{
+ "cniVersion": "0.3.0",
+ "name": "podman",
+ "plugins": [{
+ "type": "bridge",
+ "bridge": "cni0",
+ "isGateway": true,
+ "ipMasq": true,
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.88.0.0/16",
+ "routes": [{
+ "dst": "0.0.0.0/0"
+ }]
+ }
+ },
+ {
+ "type": "portmap",
+ "capabilities": {
+ "portMappings": true
+ }
+ }
+ ]
+}
+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="podman $PODMAN_ARGS"
+
+# document what we're about to do...
+$PODMAN --version
+
+set -x
+# Run podman in background without systemd for test purposes
+$PODMAN varlink ${PODMAN_HOST} >/tmp/test_runner.output 2>&1 &
+
+if [[ -z $1 ]]; then
+ export PYTHONPATH=.
+ python3 -m unittest discover -s . $VERBOSE
+else
+ export PYTHONPATH=.:./test
+ python3 -m unittest $1 $VERBOSE
+fi
+
+set +x
+pkill podman
+pkill -9 conmon
+
+showlog /tmp/alpine.log
+showlog /tmp/busybox.log
diff --git a/contrib/python/test/test_system.py b/contrib/python/test/test_system.py
new file mode 100644
index 000000000..c0d30acd7
--- /dev/null
+++ b/contrib/python/test/test_system.py
@@ -0,0 +1,49 @@
+import os
+import unittest
+
+import varlink
+
+import podman
+
+
+class TestSystem(unittest.TestCase):
+ def setUp(self):
+ self.host = os.environ['PODMAN_HOST']
+
+ def tearDown(self):
+ pass
+
+ def test_bad_address(self):
+ with self.assertRaisesRegex(varlink.client.ConnectionError,
+ "Invalid address 'bad address'"):
+ podman.Client('bad address')
+
+ def test_ping(self):
+ with podman.Client(self.host) as pclient:
+ self.assertTrue(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
+ self.assertListEqual(
+ sorted([
+ 'built', 'client_version', 'git_commit', 'go_version',
+ 'os_arch', 'version'
+ ]), sorted(list(pclient.system.versions._fields)))
+ pclient.system.versions
+ self.assertIsNot(podman.__version__, '0.0.0')
+
+ def test_info(self):
+ with podman.Client(self.host) as pclient:
+ actual = pclient.system.info()
+ # Values change too much to do exhaustive testing
+ self.assertIsNotNone(actual.podman['go_version'])
+ self.assertListEqual(
+ sorted([
+ 'host', 'insecure_registries', 'podman', 'registries',
+ 'store'
+ ]), sorted(list(actual._fields)))
+
+
+if __name__ == '__main__':
+ unittest.main()