Refactor libpod python varlink bindings

- More pythonic
- Leverage context managers to help with socket leaks
- Add system unittest's
- Add image unittest's
- Add container unittest's
- Add models for system, containers and images, and their collections
- Add helper functions for datetime parsing/formatting
- GetInfo() implemented
- Add support for setuptools
- Update documentation
- Support for Python 3.4-3.6

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

Closes: #748
Approved by: baude
This commit is contained in:
Jhon Honce
2018-05-14 18:01:08 -07:00
committed by Atomic Bot
parent c7bc7580a6
commit 1aaf8df5be
29 changed files with 1401 additions and 89 deletions

View File

@ -9,7 +9,7 @@ services:
before_install:
- sudo apt-get -qq update
- sudo apt-get -qq install btrfs-tools libdevmapper-dev libgpgme11-dev libapparmor-dev
- sudo apt-get -qq install autoconf automake bison e2fslibs-dev libfuse-dev libtool liblzma-dev gettext
- sudo apt-get -qq install autoconf automake bison e2fslibs-dev libfuse-dev libtool liblzma-dev gettext python3-setuptools
- sudo make install.libseccomp.sudo
install:

View File

@ -90,6 +90,9 @@ test/checkseccomp/checkseccomp: .gopathok $(wildcard test/checkseccomp/*.go)
podman: .gopathok $(shell hack/find-godeps.sh $(GOPKGDIR) cmd/podman $(PROJECT)) varlink_generate varlink_api_generate
$(GO) build -i $(LDFLAGS_PODMAN) -tags "$(BUILDTAGS)" -o bin/$@ $(PROJECT)/cmd/podman
python-podman:
$(MAKE) -C contrib/python python-podman
clean:
ifneq ($(GOPATH),)
rm -f "$(GOPATH)/.gopathok"
@ -105,6 +108,8 @@ endif
rm -f test/copyimg/copyimg
rm -f test/checkseccomp/checkseccomp
rm -fr build/
$(MAKE) -C contrib/python clean
libpodimage:
docker build -t ${LIBPOD_IMAGE} .
@ -136,16 +141,16 @@ localunit: varlink_generate
ginkgo:
ginkgo -v test/e2e/
localintegration: varlink_generate test-binaries
localintegration: varlink_generate test-binaries clientintegration
ginkgo -v -cover -flakeAttempts 3 -progress -trace -noColor test/e2e/.
# Temporarily disabling these tests due to varlink issues
# in our CI environment
# bash test/varlink/run_varlink_tests.sh
clientintegration:
$(MAKE) -C contrib/python integration
vagrant-check:
BOX=$(BOX) sh ./vagrant.sh
binaries: varlink_generate podman
binaries: varlink_generate podman python-podman
test-binaries: test/bin2img/bin2img test/copyimg/copyimg test/checkseccomp/checkseccomp
@ -282,4 +287,6 @@ validate: gofmt .gitvalidation
shell \
changelog \
validate \
install.libseccomp.sudo
install.libseccomp.sudo \
python-podman \
clientintegration

View File

@ -1,4 +0,0 @@
#__version__ = version
__title__ = 'libpod'

View File

@ -1,45 +0,0 @@
from varlink import Client
from libpodpy.images import Images
from libpodpy.system import System
from libpodpy.containers import Containers
class LibpodClient(object):
"""
A client for communicating with a Docker server.
Example:
>>> from libpodpy import client
>>> c = client.LibpodClient("unix:/run/podman/io.projectatomic.podman")
Args:
Requires the varlink URI for libpod
"""
def __init__(self, varlink_uri):
c = Client(address=varlink_uri)
self.conn = c.open("io.projectatomic.podman")
@property
def images(self):
"""
An object for managing images through libpod
"""
return Images(self.conn)
@property
def system(self):
"""
An object for system related calls through libpod
"""
return System(self.conn)
@property
def containers(self):
"""
An object for managing containers through libpod
"""
return Containers(self.conn)

View File

@ -1,8 +0,0 @@
class Containers(object):
def __init__(self, client):
self.client = client
def List(self):
pass

View File

@ -1,15 +0,0 @@
class Images(object):
"""
The Images class deals with image related functions for libpod.
"""
def __init__(self, client):
self.client = client
def List(self):
"""
Lists all images in the libpod image store
return: a list of ImageList objects
"""
return self.client.ListImages()

View File

@ -1,10 +0,0 @@
class System(object):
def __init__(self, client):
self.client = client
def Ping(self):
return self.client.Ping()
def Version(self):
return self.client.GetVersion()

3
contrib/python/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
dist
*.egg-info

View File

@ -0,0 +1 @@
v0.1.0, 2018-05-11 -- Initial release.

View File

@ -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.

View File

@ -0,0 +1,2 @@
prune test/
include README.md

15
contrib/python/Makefile Normal file
View File

@ -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 {} \;

15
contrib/python/README.md Normal file
View File

@ -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
```

View File

@ -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',
]

View File

@ -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)

View File

@ -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)))

View File

@ -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'])

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -0,0 +1,2 @@
varlink==25
setuptools

37
contrib/python/setup.py Normal file
View File

@ -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',

View File

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()