From 0a4ade1c175d3188ad55d22d751a86c96e060a44 Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Thu, 24 May 2018 13:44:04 -0700 Subject: Implement python podman create and start - Added alias 'container()' to image model for CreateContainer() - Fixed return in containers_create.go to wrap error in varlink exception - Added a wait time to container.kill(), number of seconds to wait for the container to change state - Refactored cached_property() to use system libraries - Refactored tests to speed up performance Signed-off-by: Jhon Honce Closes: #821 Approved by: rhatdan --- contrib/python/podman/libs/__init__.py | 53 ++++++++---------- contrib/python/podman/libs/containers.py | 93 ++++++++++++++++---------------- contrib/python/podman/libs/images.py | 53 ++++++++++++++---- 3 files changed, 112 insertions(+), 87 deletions(-) (limited to 'contrib/python/podman/libs') diff --git a/contrib/python/podman/libs/__init__.py b/contrib/python/podman/libs/__init__.py index 7285921d7..9db5dacf5 100644 --- a/contrib/python/podman/libs/__init__.py +++ b/contrib/python/podman/libs/__init__.py @@ -1,8 +1,8 @@ """Support files for podman API implementation.""" import datetime -import threading -from dateutil.parser import parse as dateutil_parse +import functools +from dateutil.parser import parse as dateutil_parse __all__ = [ 'cached_property', @@ -11,37 +11,28 @@ __all__ = [ ] -class cached_property(object): - """cached_property() - computed once per instance, cached as attribute. +def cached_property(fn): + """Cache return to save future expense.""" + return property(functools.lru_cache(maxsize=8)(fn)) - 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] +# Cannot subclass collections.UserDict, breaks varlink +class Config(dict): + """Silently ignore None values, only take key once.""" + + def __init__(self, **kwargs): + """Construct dictionary.""" + super(Config, self).__init__(kwargs) + + def __setitem__(self, key, value): + """Store unique, not None values.""" + if value is None: + return + + if super().__contains__(key): + return + + super().__setitem__(key, value) def datetime_parse(string): diff --git a/contrib/python/podman/libs/containers.py b/contrib/python/podman/libs/containers.py index 04ea76180..96ec6be37 100644 --- a/contrib/python/podman/libs/containers.py +++ b/contrib/python/podman/libs/containers.py @@ -4,6 +4,7 @@ import functools import getpass import json import signal +import time class Container(collections.UserDict): @@ -16,28 +17,34 @@ class Container(collections.UserDict): self._client = client self._id = id - self._refresh(data) + with client() as podman: + self._refresh(podman) + assert self._id == self.data['id'],\ 'Requested container id({}) does not match store id({})'.format( self._id, self.id ) def __getitem__(self, key): - """Get items from parent dict + apply aliases.""" - if key == 'running': - key = 'containerrunning' + """Get items from parent dict.""" return super().__getitem__(key) - def _refresh(self, data): - super().update(data) - for k, v in data.items(): + def _refresh(self, podman): + ctnr = podman.GetContainer(self._id) + super().update(ctnr['container']) + + for k, v in self.data.items(): setattr(self, k, v) - setattr(self, 'running', data['containerrunning']) + if 'containerrunning' in self.data: + setattr(self, 'running', self.data['containerrunning']) + self.data['running'] = self.data['containerrunning'] + + return self def refresh(self): """Refresh status fields for this container.""" - ctnr = Containers(self._client).get(self.id) - self._refresh(ctnr) + with self._client() as podman: + return self._refresh(podman) def attach(self, detach_key=None, no_stdin=False, sig_proxy=True): """Attach to running container.""" @@ -49,8 +56,7 @@ class Container(collections.UserDict): """Show processes running in container.""" with self._client() as podman: results = podman.ListContainerProcesses(self.id) - for p in results['container']: - yield p + yield from results['container'] def changes(self): """Retrieve container changes.""" @@ -58,14 +64,24 @@ class Container(collections.UserDict): results = podman.ListContainerChanges(self.id) return results['container'] - def kill(self, signal=signal.SIGTERM): - """Send signal to container, return id if successful. + def kill(self, signal=signal.SIGTERM, wait=25): + """Send signal to container. default signal is signal.SIGTERM. + wait n of seconds, 0 waits forever. """ with self._client() as podman: - results = podman.KillContainer(self.id, signal) - return results['container'] + podman.KillContainer(self.id, signal) + timeout = time.time() + wait + while True: + self._refresh(podman) + if self.status != 'running': + return self + + if wait and timeout < time.time(): + raise TimeoutError() + + time.sleep(0.5) def _lower_hook(self): """Convert all keys to lowercase.""" @@ -80,10 +96,8 @@ class Container(collections.UserDict): """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) + 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. @@ -134,16 +148,16 @@ class Container(collections.UserDict): return results['image'] def start(self): - """Start container, return id on success.""" + """Start container, return container on success.""" with self._client() as podman: - results = podman.StartContainer(self.id) - return results['container'] + podman.StartContainer(self.id) + return self._refresh(podman) 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'] + podman.StopContainer(self.id, timeout) + return self._refresh(podman) def remove(self, force=False): """Remove container, return id on success. @@ -157,8 +171,8 @@ class Container(collections.UserDict): 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'] + podman.RestartContainer(self.id, timeout) + return self._refresh(podman) def rename(self, target): """Rename container, return id on success.""" @@ -177,21 +191,20 @@ class Container(collections.UserDict): def pause(self): """Pause container, return id on success.""" with self._client() as podman: - results = podman.PauseContainer(self.id) - return results['container'] + podman.PauseContainer(self.id) + return self._refresh(podman) def unpause(self): """Unpause container, return id on success.""" with self._client() as podman: - results = podman.UnpauseContainer(self.id) - return results['container'] + podman.UnpauseContainer(self.id) + return self._refresh(podman) def update_container(self, *args, **kwargs): """TODO: Update container..., return id on success.""" with self._client() as podman: - results = podman.UpdateContainer() - self.refresh() - return results['container'] + podman.UpdateContainer() + return self._refresh(podman) def wait(self): """Wait for container to finish, return 'returncode'.""" @@ -210,8 +223,7 @@ class Container(collections.UserDict): """Retrieve container logs.""" with self._client() as podman: results = podman.GetContainerLogs(self.id) - for line in results: - yield line + yield from results class Containers(object): @@ -234,15 +246,6 @@ class Containers(object): 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: diff --git a/contrib/python/podman/libs/images.py b/contrib/python/podman/libs/images.py index d6fd7946d..d617a766b 100644 --- a/contrib/python/podman/libs/images.py +++ b/contrib/python/podman/libs/images.py @@ -3,6 +3,9 @@ import collections import functools import json +from . import Config +from .containers import Container + class Image(collections.UserDict): """Model for an Image.""" @@ -25,8 +28,42 @@ class Image(collections.UserDict): """Get items from parent dict.""" return super().__getitem__(key) + def _split_token(self, values=None, sep='='): + mapped = {} + if values: + for var in values: + k, v = var.split(sep, 1) + mapped[k] = v + return mapped + + def create(self, *args, **kwargs): + """Create container from image. + + Pulls defaults from image.inspect() + """ + # Inialize config from parameters + with self._client() as podman: + details = self.inspect() + + # TODO: remove network settings once defaults implemented on service side + config = Config(image_id=self.id, **kwargs) + config['command'] = details.containerconfig['cmd'] + config['env'] = self._split_token(details.containerconfig['env']) + config['image'] = details.repotags[0] + config['labels'] = self._split_token(details.labels) + config['net_mode'] = 'bridge' + config['network'] = 'bridge' + config['work_dir'] = '/tmp' + + with self._client() as podman: + id = podman.CreateContainer(config)['container'] + cntr = podman.GetContainer(id) + return Container(self._client, id, cntr['container']) + + container = create + def export(self, dest, compressed=False): - """Write image to dest, return True on success.""" + """Write image to dest, return id on success.""" with self._client() as podman: results = podman.ExportImage(self.id, dest, compressed) return results['image'] @@ -50,8 +87,8 @@ class Image(collections.UserDict): """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) + 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.""" @@ -89,12 +126,6 @@ class Images(object): 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 build(self, *args, **kwargs): """Build container from image. @@ -125,9 +156,9 @@ class Images(object): def search(self, id, limit=25): """Search registries for id.""" with self._client() as podman: - results = podman.SearchImage(id) + results = podman.SearchImage(id, limit) for img in results['images']: - yield img + yield collections.namedtuple('ImageSearch', img.keys())(**img) def get(self, id): """Get Image from id.""" -- cgit v1.2.3-54-g00ecf