summaryrefslogtreecommitdiff
path: root/contrib/python/pypodman/lib
diff options
context:
space:
mode:
authorJhon Honce <jhonce@redhat.com>2018-07-12 19:26:14 -0700
committerJhon Honce <jhonce@redhat.com>2018-07-13 12:50:12 -0700
commit74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15 (patch)
tree75ba256d70545d79aa61d7c57c20df886be1555f /contrib/python/pypodman/lib
parent44b523c946c88e540b50d7ba59f441b5f8e0bad0 (diff)
downloadpodman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.tar.gz
podman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.tar.bz2
podman-74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15.zip
Update python directories to better support setup.py
Signed-off-by: Jhon Honce <jhonce@redhat.com>
Diffstat (limited to 'contrib/python/pypodman/lib')
-rw-r--r--contrib/python/pypodman/lib/__init__.py11
-rw-r--r--contrib/python/pypodman/lib/action_base.py84
-rw-r--r--contrib/python/pypodman/lib/actions/__init__.py7
-rw-r--r--contrib/python/pypodman/lib/actions/images_action.py88
-rw-r--r--contrib/python/pypodman/lib/actions/ps_action.py76
-rw-r--r--contrib/python/pypodman/lib/actions/rm_action.py51
-rw-r--r--contrib/python/pypodman/lib/actions/rmi_action.py50
-rw-r--r--contrib/python/pypodman/lib/config.py212
-rw-r--r--contrib/python/pypodman/lib/future_abstract.py29
-rwxr-xr-xcontrib/python/pypodman/lib/pypodman.py76
-rw-r--r--contrib/python/pypodman/lib/report.py67
11 files changed, 751 insertions, 0 deletions
diff --git a/contrib/python/pypodman/lib/__init__.py b/contrib/python/pypodman/lib/__init__.py
new file mode 100644
index 000000000..5a8303668
--- /dev/null
+++ b/contrib/python/pypodman/lib/__init__.py
@@ -0,0 +1,11 @@
+"""Remote podman client support library."""
+from .action_base import AbstractActionBase
+from .config import PodmanArgumentParser
+from .report import Report, ReportColumn
+
+__all__ = [
+ 'AbstractActionBase',
+ 'PodmanArgumentParser',
+ 'Report',
+ 'ReportColumn',
+]
diff --git a/contrib/python/pypodman/lib/action_base.py b/contrib/python/pypodman/lib/action_base.py
new file mode 100644
index 000000000..ff2922262
--- /dev/null
+++ b/contrib/python/pypodman/lib/action_base.py
@@ -0,0 +1,84 @@
+"""Base class for all actions of remote client."""
+import abc
+from functools import lru_cache
+
+import podman
+
+
+class AbstractActionBase(abc.ABC):
+ """Base class for all actions of remote client."""
+
+ @classmethod
+ @abc.abstractmethod
+ def subparser(cls, parser):
+ """Define parser for this action. Subclasses must implement.
+
+ API:
+ Use set_defaults() to set attributes "class_" and "method". These will
+ be invoked as class_(parsed_args).method()
+ """
+ parser.add_argument(
+ '--all',
+ action='store_true',
+ help=('list all items.'
+ ' (default: no-op, included for compatibility.)'))
+ parser.add_argument(
+ '--no-trunc',
+ '--notruncate',
+ action='store_false',
+ dest='truncate',
+ default=True,
+ help='Display extended information. (default: False)')
+ parser.add_argument(
+ '--noheading',
+ action='store_false',
+ dest='heading',
+ default=True,
+ help=('Omit the table headings from the output.'
+ ' (default: False)'))
+ parser.add_argument(
+ '--quiet',
+ action='store_true',
+ help='List only the IDs. (default: %(default)s)')
+
+ def __init__(self, args):
+ """Construct class."""
+ self._args = args
+
+ @property
+ def remote_uri(self):
+ """URI for remote side of connection."""
+ return self._args.remote_uri
+
+ @property
+ def local_uri(self):
+ """URI for local side of connection."""
+ return self._args.local_uri
+
+ @property
+ def identity_file(self):
+ """Key for authenication."""
+ return self._args.identity_file
+
+ @property
+ @lru_cache(maxsize=1)
+ def client(self):
+ """Podman remote client for communicating."""
+ if self._args.host is None:
+ return podman.Client(
+ uri=self.local_uri)
+ else:
+ return podman.Client(
+ uri=self.local_uri,
+ remote_uri=self.remote_uri,
+ identity_file=self.identity_file)
+
+ def __repr__(self):
+ """Compute the “official” string representation of object."""
+ return ("{}(local_uri='{}', remote_uri='{}',"
+ " identity_file='{}')").format(
+ self.__class__,
+ self.local_uri,
+ self.remote_uri,
+ self.identity_file,
+ )
diff --git a/contrib/python/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/lib/actions/__init__.py
new file mode 100644
index 000000000..cdc58b6ab
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/__init__.py
@@ -0,0 +1,7 @@
+"""Module to export all the podman subcommands."""
+from .images_action import Images
+from .ps_action import Ps
+from .rm_action import Rm
+from .rmi_action import Rmi
+
+__all__ = ['Images', 'Ps', 'Rm', 'Rmi']
diff --git a/contrib/python/pypodman/lib/actions/images_action.py b/contrib/python/pypodman/lib/actions/images_action.py
new file mode 100644
index 000000000..f6a7497e5
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/images_action.py
@@ -0,0 +1,88 @@
+"""Remote client commands dealing with images."""
+import operator
+from collections import OrderedDict
+
+import humanize
+import podman
+
+from .. import AbstractActionBase, Report, ReportColumn
+
+
+class Images(AbstractActionBase):
+ """Class for Image manipulation."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Images commands to parent parser."""
+ parser = parent.add_parser('images', help='list images')
+ super().subparser(parser)
+ parser.add_argument(
+ '--sort',
+ choices=['created', 'id', 'repository', 'size', 'tag'],
+ default='created',
+ type=str.lower,
+ help=('Change sort ordered of displayed images.'
+ ' (default: %(default)s)'))
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ '--digests',
+ action='store_true',
+ help='Include digests with images. (default: %(default)s)')
+ parser.set_defaults(class_=cls, method='list')
+
+ def __init__(self, args):
+ """Construct Images class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'name':
+ ReportColumn('name', 'REPOSITORY', 40),
+ 'tag':
+ ReportColumn('tag', 'TAG', 10),
+ 'id':
+ ReportColumn('id', 'IMAGE ID', 12),
+ 'created':
+ ReportColumn('created', 'CREATED', 12),
+ 'size':
+ ReportColumn('size', 'SIZE', 8),
+ 'repoDigests':
+ ReportColumn('repoDigests', 'DIGESTS', 35),
+ })
+
+ def list(self):
+ """List images."""
+ images = sorted(
+ self.client.images.list(),
+ key=operator.attrgetter(self._args.sort))
+ if len(images) == 0:
+ return 0
+
+ rows = list()
+ for image in images:
+ fields = dict(image)
+ fields.update({
+ 'created':
+ humanize.naturaldate(podman.datetime_parse(image.created)),
+ 'size':
+ humanize.naturalsize(int(image.size)),
+ 'repoDigests':
+ ' '.join(image.repoDigests),
+ })
+
+ for r in image.repoTags:
+ name, tag = r.split(':', 1)
+ fields.update({
+ 'name': name,
+ 'tag': tag,
+ })
+ rows.append(fields)
+
+ if not self._args.digests:
+ del self.columns['repoDigests']
+
+ with Report(self.columns, heading=self._args.heading) as report:
+ report.layout(
+ rows, self.columns.keys(), truncate=self._args.truncate)
+ for row in rows:
+ report.row(**row)
diff --git a/contrib/python/pypodman/lib/actions/ps_action.py b/contrib/python/pypodman/lib/actions/ps_action.py
new file mode 100644
index 000000000..4bbec5578
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/ps_action.py
@@ -0,0 +1,76 @@
+"""Remote client commands dealing with containers."""
+import operator
+from collections import OrderedDict
+
+import humanize
+import podman
+
+from .. import AbstractActionBase, Report, ReportColumn
+
+
+class Ps(AbstractActionBase):
+ """Class for Container manipulation."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Images command to parent parser."""
+ parser = parent.add_parser('ps', help='list containers')
+ super().subparser(parser)
+ parser.add_argument(
+ '--sort',
+ choices=[
+ 'createdat', 'id', 'image', 'names', 'runningfor', 'size',
+ 'status'
+ ],
+ default='createdat',
+ type=str.lower,
+ help=('Change sort ordered of displayed containers.'
+ ' (default: %(default)s)'))
+ parser.set_defaults(class_=cls, method='list')
+
+ def __init__(self, args):
+ """Construct Ps class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'id':
+ ReportColumn('id', 'CONTAINER ID', 14),
+ 'image':
+ ReportColumn('image', 'IMAGE', 30),
+ 'command':
+ ReportColumn('column', 'COMMAND', 20),
+ 'createdat':
+ ReportColumn('createdat', 'CREATED', 12),
+ 'status':
+ ReportColumn('status', 'STATUS', 10),
+ 'ports':
+ ReportColumn('ports', 'PORTS', 28),
+ 'names':
+ ReportColumn('names', 'NAMES', 18)
+ })
+
+ def list(self):
+ """List containers."""
+ # TODO: Verify sorting on dates and size
+ ctnrs = sorted(
+ self.client.containers.list(),
+ key=operator.attrgetter(self._args.sort))
+ if len(ctnrs) == 0:
+ return 0
+
+ rows = list()
+ for ctnr in ctnrs:
+ fields = dict(ctnr)
+ fields.update({
+ 'command':
+ ' '.join(ctnr.command),
+ 'createdat':
+ humanize.naturaldate(podman.datetime_parse(ctnr.createdat)),
+ })
+ rows.append(fields)
+
+ with Report(self.columns, heading=self._args.heading) as report:
+ report.layout(
+ rows, self.columns.keys(), truncate=self._args.truncate)
+ for row in rows:
+ report.row(**row)
diff --git a/contrib/python/pypodman/lib/actions/rm_action.py b/contrib/python/pypodman/lib/actions/rm_action.py
new file mode 100644
index 000000000..bd8950bd6
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/rm_action.py
@@ -0,0 +1,51 @@
+"""Remote client command for deleting containers."""
+import sys
+
+import podman
+
+from .. import AbstractActionBase
+
+
+class Rm(AbstractActionBase):
+ """Class for removing containers from storage."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Rm command to parent parser."""
+ parser = parent.add_parser('rm', help='delete container(s)')
+ parser.add_argument(
+ '-f',
+ '--force',
+ action='store_true',
+ help=('force delete of running container(s).'
+ ' (default: %(default)s)'))
+ parser.add_argument(
+ 'targets', nargs='*', help='container id(s) to delete')
+ parser.set_defaults(class_=cls, method='remove')
+
+ def __init__(self, args):
+ """Construct Rm class."""
+ super().__init__(args)
+ if len(args.targets) < 1:
+ raise ValueError('You must supply at least one container id'
+ ' or name to be deleted.')
+
+ def remove(self):
+ """Remove container(s)."""
+ for id in self._args.targets:
+ try:
+ ctnr = self.client.containers.get(id)
+ ctnr.remove(self._args.force)
+ print(id)
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/lib/actions/rmi_action.py b/contrib/python/pypodman/lib/actions/rmi_action.py
new file mode 100644
index 000000000..91f0deeaf
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/rmi_action.py
@@ -0,0 +1,50 @@
+"""Remote client command for deleting images."""
+import sys
+
+import podman
+
+from .. import AbstractActionBase
+
+
+class Rmi(AbstractActionBase):
+ """Clas for removing images from storage."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Rmi command to parent parser."""
+ parser = parent.add_parser('rmi', help='delete image(s)')
+ parser.add_argument(
+ '-f',
+ '--force',
+ action='store_true',
+ help=('force delete of image(s) and associated containers.'
+ ' (default: %(default)s)'))
+ parser.add_argument('targets', nargs='*', help='image id(s) to delete')
+ parser.set_defaults(class_=cls, method='remove')
+
+ def __init__(self, args):
+ """Construct Rmi class."""
+ super().__init__(args)
+ if len(args.targets) < 1:
+ raise ValueError('You must supply at least one image id'
+ ' or name to be deleted.')
+
+ def remove(self):
+ """Remove image(s)."""
+ for id in self._args.targets:
+ try:
+ img = self.client.images.get(id)
+ img.remove(self._args.force)
+ print(id)
+ except podman.ImageNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Image {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/lib/config.py b/contrib/python/pypodman/lib/config.py
new file mode 100644
index 000000000..e687697ef
--- /dev/null
+++ b/contrib/python/pypodman/lib/config.py
@@ -0,0 +1,212 @@
+import argparse
+import curses
+import getpass
+import inspect
+import logging
+import os
+import sys
+
+import pkg_resources
+
+import pytoml
+
+# TODO: setup.py and obtain __version__ from rpm.spec
+try:
+ __version__ = pkg_resources.get_distribution('pypodman').version
+except Exception:
+ __version__ = '0.0.0'
+
+
+class HelpFormatter(argparse.RawDescriptionHelpFormatter):
+ """Set help width to screen size."""
+
+ def __init__(self, *args, **kwargs):
+ """Construct HelpFormatter using screen width."""
+ if 'width' not in kwargs:
+ kwargs['width'] = 80
+ try:
+ height, width = curses.initscr().getmaxyx()
+ kwargs['width'] = width
+ finally:
+ curses.endwin()
+ super().__init__(*args, **kwargs)
+
+
+class PodmanArgumentParser(argparse.ArgumentParser):
+ """Default remote podman configuration."""
+
+ def __init__(self, **kwargs):
+ """Construct the parser."""
+ kwargs['add_help'] = True
+ kwargs['allow_abbrev'] = True
+ kwargs['description'] = __doc__
+ kwargs['formatter_class'] = HelpFormatter
+
+ super().__init__(**kwargs)
+
+ def initialize_parser(self):
+ """Initialize parser without causing recursion meltdown."""
+ self.add_argument(
+ '--version',
+ action='version',
+ version='%(prog)s v. ' + __version__)
+ self.add_argument(
+ '--log-level',
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
+ default='WARNING',
+ type=str.upper,
+ help='set logging level for events. (default: %(default)s)',
+ )
+ self.add_argument(
+ '--run-dir',
+ metavar='DIRECTORY',
+ help=('directory to place local socket bindings.'
+ ' (default: XDG_RUNTIME_DIR/pypodman'))
+ self.add_argument(
+ '--user',
+ default=getpass.getuser(),
+ help='Authenicating user on remote host. (default: %(default)s)')
+ self.add_argument(
+ '--host', help='name of remote host. (default: None)')
+ self.add_argument(
+ '--remote-socket-path',
+ metavar='PATH',
+ help=('path of podman socket on remote host'
+ ' (default: /run/podman/io.projectatomic.podman)'))
+ self.add_argument(
+ '--identity-file',
+ metavar='PATH',
+ help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
+ self.add_argument(
+ '--config-home',
+ metavar='DIRECTORY',
+ help=('home of configuration "pypodman.conf".'
+ ' (default: XDG_CONFIG_HOME/pypodman'))
+
+ actions_parser = self.add_subparsers(
+ dest='subparser_name', help='actions')
+
+ # pull in plugin(s) code for each subcommand
+ for name, obj in inspect.getmembers(
+ sys.modules['lib.actions'],
+ lambda member: inspect.isclass(member)):
+ if hasattr(obj, 'subparser'):
+ try:
+ obj.subparser(actions_parser)
+ except NameError as e:
+ logging.critical(e)
+ logging.warning(
+ 'See subparser configuration for Class "{}"'.format(
+ name))
+ sys.exit(3)
+
+ def parse_args(self, args=None, namespace=None):
+ """Parse command line arguments, backed by env var and config_file."""
+ self.initialize_parser()
+ cooked = super().parse_args(args, namespace)
+ return self.resolve_configuration(cooked)
+
+ def resolve_configuration(self, args):
+ """Find and fill in any arguments not passed on command line."""
+ args.xdg_runtime_dir = os.environ.get('XDG_RUNTIME_DIR', '/tmp')
+ args.xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
+ os.path.expanduser('~/.config'))
+ args.xdg_config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg')
+
+ # Configuration file(s) are optional,
+ # required arguments may be provided elsewhere
+ config = {'default': {}}
+ dirs = args.xdg_config_dirs.split(':')
+ dirs.extend((args.xdg_config_home, args.config_home))
+ for dir_ in dirs:
+ if dir_ is None:
+ continue
+ try:
+ with open(os.path.join(dir_, 'pypodman/pypodman.conf'),
+ 'r') as stream:
+ config.update(pytoml.load(stream))
+ except OSError:
+ pass
+
+ def reqattr(name, value):
+ if value:
+ setattr(args, name, value)
+ return value
+ self.error('Required argument "%s" is not configured.' % name)
+
+ reqattr(
+ 'run_dir',
+ getattr(args, 'run_dir')
+ or os.environ.get('RUN_DIR')
+ or config['default'].get('run_dir')
+ or os.path.join(args.xdg_runtime_dir, 'pypodman')
+ ) # yapf: disable
+
+ setattr(
+ args,
+ 'host',
+ getattr(args, 'host')
+ or os.environ.get('HOST')
+ or config['default'].get('host')
+ ) # yapf:disable
+
+ reqattr(
+ 'user',
+ getattr(args, 'user')
+ or os.environ.get('USER')
+ or config['default'].get('user')
+ or getpass.getuser()
+ ) # yapf:disable
+
+ reqattr(
+ 'remote_socket_path',
+ getattr(args, 'remote_socket_path')
+ or os.environ.get('REMOTE_SOCKET_PATH')
+ or config['default'].get('remote_socket_path')
+ or '/run/podman/io.projectatomic.podman'
+ ) # yapf:disable
+
+ reqattr(
+ 'log_level',
+ getattr(args, 'log_level')
+ or os.environ.get('LOG_LEVEL')
+ or config['default'].get('log_level')
+ or logging.WARNING
+ ) # yapf:disable
+
+ setattr(
+ args,
+ 'identity_file',
+ getattr(args, 'identity_file')
+ or os.environ.get('IDENTITY_FILE')
+ or config['default'].get('identity_file')
+ or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.user))
+ ) # yapf:disable
+
+ if not os.path.isfile(args.identity_file):
+ args.identity_file = None
+
+ if args.host:
+ args.local_socket_path = os.path.join(args.run_dir,
+ "podman.socket")
+ else:
+ args.local_socket_path = args.remote_socket_path
+
+ args.local_uri = "unix:{}".format(args.local_socket_path)
+ args.remote_uri = "ssh://{}@{}{}".format(args.user, args.host,
+ args.remote_socket_path)
+ return args
+
+ def exit(self, status=0, message=None):
+ """Capture message and route to logger."""
+ if message:
+ log = logging.info if status == 0 else logging.error
+ log(message)
+ super().exit(status)
+
+ def error(self, message):
+ """Capture message and route to logger."""
+ logging.error('{}: {}'.format(self.prog, message))
+ logging.error("Try '{} --help' for more information.".format(
+ self.prog))
+ super().exit(2)
diff --git a/contrib/python/pypodman/lib/future_abstract.py b/contrib/python/pypodman/lib/future_abstract.py
new file mode 100644
index 000000000..75a1d42db
--- /dev/null
+++ b/contrib/python/pypodman/lib/future_abstract.py
@@ -0,0 +1,29 @@
+"""Utilities for with-statement contexts. See PEP 343."""
+
+import abc
+
+import _collections_abc
+
+try:
+ from contextlib import AbstractContextManager
+except ImportError:
+ # Copied from python3.7 library as "backport"
+ class AbstractContextManager(abc.ABC):
+ """An abstract base class for context managers."""
+
+ def __enter__(self):
+ """Return `self` upon entering the runtime context."""
+ return self
+
+ @abc.abstractmethod
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Raise any exception triggered within the runtime context."""
+ return None
+
+ @classmethod
+ def __subclasshook__(cls, C):
+ """Check whether subclass is considered a subclass of this ABC."""
+ if cls is AbstractContextManager:
+ return _collections_abc._check_methods(C, "__enter__",
+ "__exit__")
+ return NotImplemented
diff --git a/contrib/python/pypodman/lib/pypodman.py b/contrib/python/pypodman/lib/pypodman.py
new file mode 100755
index 000000000..4bc71a9cc
--- /dev/null
+++ b/contrib/python/pypodman/lib/pypodman.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Remote podman client."""
+
+import logging
+import os
+import sys
+
+import lib.actions
+from lib import PodmanArgumentParser
+
+assert lib.actions # silence pyflakes
+
+
+def main():
+ """Entry point."""
+ # Setup logging so we use stderr and can change logging level later
+ # Do it now before there is any chance of a default setup hardcoding crap.
+ log = logging.getLogger()
+ fmt = logging.Formatter('%(asctime)s | %(levelname)-8s | %(message)s',
+ '%Y-%m-%d %H:%M:%S %Z')
+ stderr = logging.StreamHandler(stream=sys.stderr)
+ stderr.setFormatter(fmt)
+ log.addHandler(stderr)
+ log.setLevel(logging.WARNING)
+
+ parser = PodmanArgumentParser()
+ args = parser.parse_args()
+
+ log.setLevel(args.log_level)
+ logging.debug('Logging initialized at level {}'.format(
+ logging.getLevelName(logging.getLogger().getEffectiveLevel())))
+
+ def want_tb():
+ """Add traceback when logging events."""
+ return log.getEffectiveLevel() == logging.DEBUG
+
+ try:
+ if not os.path.exists(args.run_dir):
+ os.makedirs(args.run_dir)
+ except PermissionError as e:
+ logging.critical(e, exc_info=want_tb())
+ sys.exit(6)
+
+ # class_(args).method() are set by the sub-command's parser
+ returncode = None
+ try:
+ obj = args.class_(args)
+ except Exception as e:
+ logging.critical(repr(e), exc_info=want_tb())
+ logging.warning('See subparser "{}" configuration.'.format(
+ args.subparser_name))
+ sys.exit(5)
+
+ try:
+ returncode = getattr(obj, args.method)()
+ except AttributeError as e:
+ logging.critical(e, exc_info=want_tb())
+ logging.warning('See subparser "{}" configuration.'.format(
+ args.subparser_name))
+ returncode = 3
+ except KeyboardInterrupt:
+ pass
+ except (
+ ConnectionRefusedError,
+ ConnectionResetError,
+ TimeoutError,
+ ) as e:
+ logging.critical(e, exc_info=want_tb())
+ logging.info('Review connection arguments for correctness.')
+ returncode = 4
+
+ return 0 if returncode is None else returncode
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/contrib/python/pypodman/lib/report.py b/contrib/python/pypodman/lib/report.py
new file mode 100644
index 000000000..25fe2ae0d
--- /dev/null
+++ b/contrib/python/pypodman/lib/report.py
@@ -0,0 +1,67 @@
+"""Report Manager."""
+import sys
+from collections import namedtuple
+
+from .future_abstract import AbstractContextManager
+
+
+class ReportColumn(namedtuple('ReportColumn', 'key display width default')):
+ """Hold attributes of output column."""
+
+ __slots__ = ()
+
+ def __new__(cls, key, display, width, default=None):
+ """Add defaults for attributes."""
+ return super(ReportColumn, cls).__new__(cls, key, display, width,
+ default)
+
+
+class Report(AbstractContextManager):
+ """Report Manager."""
+
+ def __init__(self, columns, heading=True, epilog=None, file=sys.stdout):
+ """Construct Report.
+
+ columns is a mapping for named fields to column headings.
+ headers True prints headers on table.
+ epilog will be printed when the report context is closed.
+ """
+ self._columns = columns
+ self._file = file
+ self._heading = heading
+ self.epilog = epilog
+ self._format = None
+
+ def row(self, **fields):
+ """Print row for report."""
+ if self._heading:
+ hdrs = {k: v.display for (k, v) in self._columns.items()}
+ print(self._format.format(**hdrs), flush=True, file=self._file)
+ self._heading = False
+ fields = {k: str(v) for k, v in fields.items()}
+ print(self._format.format(**fields))
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Leave Report context and print epilog if provided."""
+ if self.epilog:
+ print(self.epilog, flush=True, file=self._file)
+
+ def layout(self, iterable, keys, truncate=True):
+ """Use data and headings build format for table to fit."""
+ format = []
+
+ for key in keys:
+ value = max(map(lambda x: len(str(x.get(key, ''))), iterable))
+ # print('key', key, 'value', value)
+
+ if truncate:
+ row = self._columns.get(
+ key, ReportColumn(key, key.upper(), len(key)))
+ if value < row.width:
+ step = row.width if value == 0 else value
+ value = max(len(key), step)
+ elif value > row.width:
+ value = row.width if row.width != 0 else value
+
+ format.append('{{{0}:{1}.{1}}}'.format(key, value))
+ self._format = ' '.join(format)