summaryrefslogtreecommitdiff
path: root/contrib/python/pypodman
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python/pypodman')
-rw-r--r--contrib/python/pypodman/.pylintrc4
-rw-r--r--contrib/python/pypodman/Makefile6
-rw-r--r--contrib/python/pypodman/pypodman/lib/__init__.py12
-rw-r--r--contrib/python/pypodman/pypodman/lib/action_base.py5
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/__init__.py14
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/_create_args.py394
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/commit_action.py14
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/create_action.py460
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/export_action.py2
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/history_action.py83
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/images_action.py2
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/import_action.py60
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/info_action.py49
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/inspect_action.py4
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/kill_action.py2
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/logs_action.py16
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/port_action.py2
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/ps_action.py6
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/pull_action.py5
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/push_action.py56
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/restart_action.py50
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/rm_action.py5
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/rmi_action.py5
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/run_action.py73
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/search_action.py160
-rw-r--r--contrib/python/pypodman/pypodman/lib/parser_actions.py185
-rw-r--r--contrib/python/pypodman/pypodman/lib/podman_parser.py (renamed from contrib/python/pypodman/pypodman/lib/config.py)64
-rw-r--r--contrib/python/pypodman/pypodman/lib/report.py2
-rw-r--r--contrib/python/pypodman/requirements.txt1
29 files changed, 1224 insertions, 517 deletions
diff --git a/contrib/python/pypodman/.pylintrc b/contrib/python/pypodman/.pylintrc
new file mode 100644
index 000000000..eed3ae65b
--- /dev/null
+++ b/contrib/python/pypodman/.pylintrc
@@ -0,0 +1,4 @@
+[VARIABLES]
+
+# Enforce only pep8 variable names
+variable-rgx=[a-z0-9_]{1,30}$
diff --git a/contrib/python/pypodman/Makefile b/contrib/python/pypodman/Makefile
index fb25776fa..8c9691996 100644
--- a/contrib/python/pypodman/Makefile
+++ b/contrib/python/pypodman/Makefile
@@ -1,5 +1,6 @@
PYTHON ?= /usr/bin/python3
DESTDIR := /
+PODMAN_VERSION ?= '0.0.4'
.PHONY: python-pypodman
python-pypodman:
@@ -17,6 +18,11 @@ integration:
install:
$(PYTHON) setup.py install --root ${DESTDIR}
+.PHONY: upload
+upload:
+ $(PODMAN_VERSION) $(PYTHON) setup.py sdist bdist_wheel
+ twine upload --repository-url https://test.pypi.org/legacy/ dist/*
+
.PHONY: clobber
clobber: uninstall clean
diff --git a/contrib/python/pypodman/pypodman/lib/__init__.py b/contrib/python/pypodman/pypodman/lib/__init__.py
index 80fa0e1e9..6208deefd 100644
--- a/contrib/python/pypodman/pypodman/lib/__init__.py
+++ b/contrib/python/pypodman/pypodman/lib/__init__.py
@@ -1,8 +1,18 @@
"""Remote podman client support library."""
from pypodman.lib.action_base import AbstractActionBase
-from pypodman.lib.config import PodmanArgumentParser
+from pypodman.lib.parser_actions import (BooleanAction, BooleanValidate,
+ PathAction, PositiveIntAction,
+ UnitAction)
+from pypodman.lib.podman_parser import PodmanArgumentParser
from pypodman.lib.report import Report, ReportColumn
+# Silence pylint overlording...
+assert BooleanAction
+assert BooleanValidate
+assert PathAction
+assert PositiveIntAction
+assert UnitAction
+
__all__ = [
'AbstractActionBase',
'PodmanArgumentParser',
diff --git a/contrib/python/pypodman/pypodman/lib/action_base.py b/contrib/python/pypodman/pypodman/lib/action_base.py
index 5cc4c22a9..f312a63e9 100644
--- a/contrib/python/pypodman/pypodman/lib/action_base.py
+++ b/contrib/python/pypodman/pypodman/lib/action_base.py
@@ -43,7 +43,12 @@ class AbstractActionBase(abc.ABC):
def __init__(self, args):
"""Construct class."""
+ # Dump all unset arguments before transmitting to service
self._args = args
+ self.opts = {
+ k: v
+ for k, v in vars(self._args).items() if v is not None
+ }
@property
def remote_uri(self):
diff --git a/contrib/python/pypodman/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/pypodman/lib/actions/__init__.py
index b0af3c589..a5eb35755 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/__init__.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/__init__.py
@@ -3,7 +3,10 @@ from pypodman.lib.actions.attach_action import Attach
from pypodman.lib.actions.commit_action import Commit
from pypodman.lib.actions.create_action import Create
from pypodman.lib.actions.export_action import Export
+from pypodman.lib.actions.history_action import History
from pypodman.lib.actions.images_action import Images
+from pypodman.lib.actions.import_action import Import
+from pypodman.lib.actions.info_action import Info
from pypodman.lib.actions.inspect_action import Inspect
from pypodman.lib.actions.kill_action import Kill
from pypodman.lib.actions.logs_action import Logs
@@ -12,15 +15,22 @@ from pypodman.lib.actions.pause_action import Pause
from pypodman.lib.actions.port_action import Port
from pypodman.lib.actions.ps_action import Ps
from pypodman.lib.actions.pull_action import Pull
+from pypodman.lib.actions.push_action import Push
+from pypodman.lib.actions.restart_action import Restart
from pypodman.lib.actions.rm_action import Rm
from pypodman.lib.actions.rmi_action import Rmi
+from pypodman.lib.actions.run_action import Run
+from pypodman.lib.actions.search_action import Search
__all__ = [
'Attach',
'Commit',
'Create',
'Export',
+ 'History',
'Images',
+ 'Import',
+ 'Info',
'Inspect',
'Kill',
'Logs',
@@ -29,6 +39,10 @@ __all__ = [
'Port',
'Ps',
'Pull',
+ 'Push',
+ 'Restart',
'Rm',
'Rmi',
+ 'Run',
+ 'Search',
]
diff --git a/contrib/python/pypodman/pypodman/lib/actions/_create_args.py b/contrib/python/pypodman/pypodman/lib/actions/_create_args.py
new file mode 100644
index 000000000..8601704f3
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/_create_args.py
@@ -0,0 +1,394 @@
+"""Implement common create container arguments together."""
+from pypodman.lib import BooleanAction, UnitAction
+
+
+class CreateArguments():
+ """Helper to add all the create flags to a command."""
+
+ @classmethod
+ def add_arguments(cls, parser):
+ """Add CreateArguments to parser."""
+ parser.add_argument(
+ '--add-host',
+ action='append',
+ metavar='HOST',
+ help='Add a line to /etc/hosts.'
+ ' The option can be set multiple times.'
+ ' (Format: hostname:ip)')
+ parser.add_argument(
+ '--annotation',
+ action='append',
+ help='Add an annotation to the container.'
+ 'The option can be set multiple times.'
+ '(Format: key=value)')
+ parser.add_argument(
+ '--attach',
+ '-a',
+ action='append',
+ metavar='FD',
+ help=('Attach to STDIN, STDOUT or STDERR. The option can be set'
+ ' for each of stdin, stdout, and stderr.'))
+
+ parser.add_argument(
+ '--blkio-weight',
+ choices=range(10, 1000),
+ metavar='[10-1000]',
+ help=('Block IO weight (relative weight) accepts a'
+ ' weight value between 10 and 1000.'))
+ parser.add_argument(
+ '--blkio-weight-device',
+ action='append',
+ metavar='WEIGHT',
+ help='Block IO weight, relative device weight.'
+ ' (Format: DEVICE_NAME:WEIGHT)')
+ parser.add_argument(
+ '--cap-add',
+ action='append',
+ metavar='CAP',
+ help=('Add Linux capabilities'
+ 'The option can be set multiple times.'))
+ parser.add_argument(
+ '--cap-drop',
+ action='append',
+ metavar='CAP',
+ help=('Drop Linux capabilities'
+ 'The option can be set multiple times.'))
+ parser.add_argument(
+ '--cgroup-parent',
+ metavar='PATH',
+ help='Path to cgroups under which the cgroup for the'
+ ' container will be created. If the path is not'
+ ' absolute, the path is considered to be relative'
+ ' to the cgroups path of the init process. Cgroups'
+ ' will be created if they do not already exist.')
+ parser.add_argument(
+ '--cidfile',
+ metavar='PATH',
+ help='Write the container ID to the file, on the remote host.')
+ parser.add_argument(
+ '--conmon-pidfile',
+ metavar='PATH',
+ help=('Write the pid of the conmon process to a file,'
+ ' on the remote host.'))
+ parser.add_argument(
+ '--cpu-period',
+ type=int,
+ metavar='PERIOD',
+ help=('Limit the CPU CFS (Completely Fair Scheduler) period.'))
+ parser.add_argument(
+ '--cpu-quota',
+ type=int,
+ metavar='QUOTA',
+ help=('Limit the CPU CFS (Completely Fair Scheduler) quota.'))
+ parser.add_argument(
+ '--cpu-rt-period',
+ type=int,
+ metavar='PERIOD',
+ help=('Limit the CPU real-time period in microseconds.'))
+ parser.add_argument(
+ '--cpu-rt-runtime',
+ type=int,
+ metavar='LIMIT',
+ help=('Limit the CPU real-time runtime in microseconds.'))
+ parser.add_argument(
+ '--cpu-shares',
+ type=int,
+ metavar='SHARES',
+ help=('CPU shares (relative weight)'))
+ parser.add_argument(
+ '--cpus',
+ type=float,
+ help=('Number of CPUs. The default is 0.0 which means no limit'))
+ parser.add_argument(
+ '--cpuset-cpus',
+ metavar='LIST',
+ help=('CPUs in which to allow execution (0-3, 0,1)'))
+ parser.add_argument(
+ '--cpuset-mems',
+ metavar='NODES',
+ help=('Memory nodes (MEMs) in which to allow execution (0-3, 0,1).'
+ ' Only effective on NUMA systems'))
+ parser.add_argument(
+ '--detach',
+ '-d',
+ action=BooleanAction,
+ default=False,
+ help='Detached mode: run the container in the background and'
+ ' print the new container ID. (Default: False)')
+ parser.add_argument(
+ '--detach-keys',
+ metavar='KEY(s)',
+ default=4,
+ help='Override the key sequence for detaching a container.'
+ ' (Format: a single character [a-Z] or ctrl-<value> where'
+ ' <value> is one of: a-z, @, ^, [, , or _)')
+ parser.add_argument(
+ '--device',
+ action='append',
+ help=('Add a host device to the container'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--device-read-bps',
+ action='append',
+ metavar='LIMIT',
+ help=('Limit read rate (bytes per second) from a device'
+ ' (e.g. --device-read-bps=/dev/sda:1mb)'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--device-read-iops',
+ action='append',
+ metavar='LIMIT',
+ help=('Limit read rate (IO per second) from a device'
+ ' (e.g. --device-read-iops=/dev/sda:1000)'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--device-write-bps',
+ action='append',
+ metavar='LIMIT',
+ help=('Limit write rate (bytes per second) to a device'
+ ' (e.g. --device-write-bps=/dev/sda:1mb)'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--device-write-iops',
+ action='append',
+ metavar='LIMIT',
+ help=('Limit write rate (IO per second) to a device'
+ ' (e.g. --device-write-iops=/dev/sda:1000)'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--dns',
+ action='append',
+ metavar='SERVER',
+ help=('Set custom DNS servers.'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--dns-option',
+ action='append',
+ metavar='OPT',
+ help=('Set custom DNS options.'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--dns-search',
+ action='append',
+ metavar='DOMAIN',
+ help=('Set custom DNS search domains.'
+ 'The option can be set multiple times.'),
+ )
+ parser.add_argument(
+ '--entrypoint',
+ help=('Overwrite the default ENTRYPOINT of the image.'),
+ )
+ parser.add_argument(
+ '--env',
+ '-e',
+ action='append',
+ help=('Set environment variables.'),
+ )
+ parser.add_argument(
+ '--env-file',
+ help=('Read in a line delimited file of environment variables,'
+ ' on the remote host.'),
+ )
+ parser.add_argument(
+ '--expose',
+ metavar='RANGE',
+ help=('Expose a port, or a range of ports'
+ ' (e.g. --expose=3300-3310) to set up port redirection.'),
+ )
+ parser.add_argument(
+ '--gidmap',
+ metavar='MAP',
+ help=('GID map for the user namespace'),
+ )
+ parser.add_argument(
+ '--group-add',
+ action='append',
+ metavar='GROUP',
+ help=('Add additional groups to run as'))
+ parser.add_argument('--hostname', help='Container host name')
+
+ volume_group = parser.add_mutually_exclusive_group()
+ volume_group.add_argument(
+ '--image-volume',
+ choices=['bind', 'tmpfs', 'ignore'],
+ metavar='MODE',
+ help='Tells podman how to handle the builtin image volumes')
+ volume_group.add_argument(
+ '--builtin-volume',
+ choices=['bind', 'tmpfs', 'ignore'],
+ metavar='MODE',
+ help='Tells podman how to handle the builtin image volumes')
+
+ parser.add_argument(
+ '--interactive',
+ '-i',
+ action=BooleanAction,
+ default=False,
+ help='Keep STDIN open even if not attached. (Default: False)')
+ parser.add_argument('--ipc', help='Create namespace')
+ parser.add_argument(
+ '--kernel-memory', action=UnitAction, help='Kernel memory limit')
+ parser.add_argument(
+ '--label',
+ '-l',
+ help=('Add metadata to a container'
+ ' (e.g., --label com.example.key=value)'))
+ parser.add_argument(
+ '--label-file', help='Read in a line delimited file of labels')
+ parser.add_argument(
+ '--log-driver',
+ choices=['json-file', 'journald'],
+ help='Logging driver for the container.')
+ parser.add_argument(
+ '--log-opt',
+ action='append',
+ help='Logging driver specific options')
+ parser.add_argument(
+ '--memory', '-m', action=UnitAction, help='Memory limit')
+ parser.add_argument(
+ '--memory-reservation',
+ action=UnitAction,
+ help='Memory soft limit')
+ parser.add_argument(
+ '--memory-swap',
+ action=UnitAction,
+ help=('A limit value equal to memory plus swap.'
+ 'Must be used with the --memory flag'))
+ parser.add_argument(
+ '--memory-swappiness',
+ choices=range(0, 100),
+ metavar='[0-100]',
+ help="Tune a container's memory swappiness behavior")
+ parser.add_argument('--name', help='Assign a name to the container')
+ parser.add_argument(
+ '--network',
+ metavar='BRIDGE',
+ help=('Set the Network mode for the container.'))
+ parser.add_argument(
+ '--oom-kill-disable',
+ action=BooleanAction,
+ help='Whether to disable OOM Killer for the container or not')
+ parser.add_argument(
+ '--oom-score-adj',
+ choices=range(-1000, 1000),
+ metavar='[-1000-1000]',
+ help="Tune the host's OOM preferences for containers")
+ parser.add_argument('--pid', help='Set the PID mode for the container')
+ parser.add_argument(
+ '--pids-limit',
+ type=int,
+ metavar='LIMIT',
+ help=("Tune the container's pids limit."
+ " Set -1 to have unlimited pids for the container."))
+ parser.add_argument('--pod', help='Run container in an existing pod')
+ parser.add_argument(
+ '--privileged',
+ action=BooleanAction,
+ help='Give extended privileges to this container.')
+ parser.add_argument(
+ '--publish',
+ '-p',
+ metavar='RANGE',
+ help="Publish a container's port, or range of ports, to the host")
+ parser.add_argument(
+ '--publish-all',
+ '-P',
+ action=BooleanAction,
+ help='Publish all exposed ports to random'
+ ' ports on the host interfaces'
+ '(Default: False)')
+ parser.add_argument(
+ '--quiet',
+ '-q',
+ action='store_true',
+ help='Suppress output information when pulling images')
+ parser.add_argument(
+ '--read-only',
+ action=BooleanAction,
+ help="Mount the container's root filesystem as read only.")
+ parser.add_argument(
+ '--rm',
+ action=BooleanAction,
+ default=False,
+ help='Automatically remove the container when it exits.')
+ parser.add_argument(
+ '--rootfs',
+ action='store_true',
+ help=('If specified, the first argument refers to an'
+ ' exploded container on the file system of remote host.'))
+ parser.add_argument(
+ '--security-opt',
+ action='append',
+ metavar='OPT',
+ help='Set security options.')
+ parser.add_argument(
+ '--shm-size', action=UnitAction, help='Size of /dev/shm')
+ parser.add_argument(
+ '--sig-proxy',
+ action=BooleanAction,
+ default=True,
+ help='Proxy signals sent to the podman run'
+ ' command to the container process')
+ parser.add_argument(
+ '--stop-signal',
+ metavar='SIGTERM',
+ help='Signal to stop a container')
+ parser.add_argument(
+ '--stop-timeout',
+ metavar='TIMEOUT',
+ type=int,
+ default=10,
+ help='Seconds to wait on stopping container.')
+ parser.add_argument(
+ '--subgidname',
+ metavar='MAP',
+ help='Name for GID map from the /etc/subgid file')
+ parser.add_argument(
+ '--subuidname',
+ metavar='MAP',
+ help='Name for UID map from the /etc/subuid file')
+ parser.add_argument(
+ '--sysctl',
+ action='append',
+ help='Configure namespaced kernel parameters at runtime')
+ parser.add_argument(
+ '--tmpfs', metavar='MOUNT', help='Create a tmpfs mount')
+ parser.add_argument(
+ '--tty',
+ '-t',
+ action=BooleanAction,
+ default=False,
+ help='Allocate a pseudo-TTY for standard input of container.')
+ parser.add_argument(
+ '--uidmap', metavar='MAP', help='UID map for the user namespace')
+ parser.add_argument('--ulimit', metavar='OPT', help='Ulimit options')
+ parser.add_argument(
+ '--user',
+ '-u',
+ help=('Sets the username or UID used and optionally'
+ ' the groupname or GID for the specified command.'))
+ parser.add_argument(
+ '--userns',
+ choices=['host', 'ns'],
+ help='Set the usernamespace mode for the container')
+ parser.add_argument(
+ '--uts',
+ choices=['host', 'ns'],
+ help='Set the UTS mode for the container')
+ parser.add_argument('--volume', '-v', help='Create a bind mount.')
+ parser.add_argument(
+ '--volumes-from',
+ action='append',
+ help='Mount volumes from the specified container(s).')
+ parser.add_argument(
+ '--workdir',
+ '-w',
+ metavar='PATH',
+ help='Working directory inside the container')
diff --git a/contrib/python/pypodman/pypodman/lib/actions/commit_action.py b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py
index 1e16550ad..0da6a2078 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/commit_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py
@@ -2,7 +2,7 @@
import sys
import podman
-from pypodman.lib import AbstractActionBase
+from pypodman.lib import AbstractActionBase, BooleanAction
class Commit(AbstractActionBase):
@@ -47,14 +47,14 @@ class Commit(AbstractActionBase):
parser.add_argument(
'--pause',
'-p',
- choices=('True', 'False'),
+ action=BooleanAction,
default=True,
- type=bool,
help='Pause the container when creating an image',
)
parser.add_argument(
'--quiet',
'-q',
+ action='store_true',
help='Suppress output',
)
parser.add_argument(
@@ -71,20 +71,24 @@ class Commit(AbstractActionBase):
def __init__(self, args):
"""Construct Commit class."""
- super().__init__(args)
if not args.container:
raise ValueError('You must supply one container id'
' or name to be used as source.')
if not args.image:
raise ValueError('You must supply one image id'
' or name to be created.')
+ super().__init__(args)
+
+ # used only on client
+ del self.opts['image']
+ del self.opts['container']
def commit(self):
"""Create image from container."""
try:
try:
ctnr = self.client.containers.get(self._args.container[0])
- ident = ctnr.commit(**self._args)
+ ident = ctnr.commit(**self.opts)
print(ident)
except podman.ContainerNotFound as e:
sys.stdout.flush()
diff --git a/contrib/python/pypodman/pypodman/lib/actions/create_action.py b/contrib/python/pypodman/pypodman/lib/actions/create_action.py
index 94dd33061..e0cb8c409 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/create_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/create_action.py
@@ -1,413 +1,11 @@
"""Remote client command for creating container from image."""
-import argparse
import sys
from builtins import vars
import podman
from pypodman.lib import AbstractActionBase
-
-class UnitAction(argparse.Action):
- """Validate number given is positive integer, with optional suffix."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- """Validate input."""
- if isinstance(values, str):
- if not values[:-1].isdigit():
- msg = 'unit must be a positive integer, with optional suffix'
- raise argparse.ArgumentError(self, msg)
- if not values[-1] in ('b', 'k', 'm', 'g'):
- msg = 'unit only supports suffices of: b, k, m, g'
- raise argparse.ArgumentError(self, msg)
- elif values <= 0:
- msg = 'number must be a positive integer.'
- raise argparse.ArgumentError(self, msg)
-
- setattr(namespace, self.dest, values)
-
-
-def add_options(parser):
- """Add options for Create command."""
- parser.add_argument(
- '--add-host',
- action='append',
- metavar='HOST',
- help=('Add a line to /etc/hosts. The format is hostname:ip.'
- ' The option can be set multiple times.'),
- )
- parser.add_argument(
- '--attach',
- '-a',
- action='append',
- metavar='FD',
- help=('Attach to STDIN, STDOUT or STDERR. The option can be set'
- ' for each of stdin, stdout, and stderr.'))
- parser.add_argument(
- '--annotation',
- action='append',
- help=('Add an annotation to the container. The format is'
- ' key=value. The option can be set multiple times.'))
- parser.add_argument(
- '--blkio-weight',
- choices=range(10, 1000),
- metavar='[10-1000]',
- help=('Block IO weight (relative weight) accepts a'
- ' weight value between 10 and 1000.'))
- parser.add_argument(
- '--blkio-weight-device',
- action='append',
- metavar='WEIGHT',
- help=('Block IO weight (relative device weight,'
- ' format: DEVICE_NAME:WEIGHT).'))
- parser.add_argument(
- '--cap-add',
- action='append',
- metavar='CAP',
- help=('Add Linux capabilities'
- 'The option can be set multiple times.'))
- parser.add_argument(
- '--cap-drop',
- action='append',
- metavar='CAP',
- help=('Drop Linux capabilities'
- 'The option can be set multiple times.'))
- parser.add_argument(
- '--cgroup-parent',
- metavar='PATH',
- help=('Path to cgroups under which the cgroup for the'
- ' container will be created. If the path is not'
- ' absolute, the path is considered to be relative'
- ' to the cgroups path of the init process. Cgroups'
- ' will be created if they do not already exist.'))
- parser.add_argument(
- '--cidfile',
- metavar='PATH',
- help='Write the container ID to the file, on the remote host.')
- parser.add_argument(
- '--conmon-pidfile',
- metavar='PATH',
- help=('Write the pid of the conmon process to a file,'
- ' on the remote host.'))
- parser.add_argument(
- '--cpu-count',
- type=int,
- metavar='COUNT',
- help=('Limit the number of CPUs available'
- ' for execution by the container.'))
- parser.add_argument(
- '--cpu-period',
- type=int,
- metavar='PERIOD',
- help=('Limit the CPU CFS (Completely Fair Scheduler) period.'))
- parser.add_argument(
- '--cpu-quota',
- type=int,
- metavar='QUOTA',
- help=('Limit the CPU CFS (Completely Fair Scheduler) quota.'))
- parser.add_argument(
- '--cpu-rt-period',
- type=int,
- metavar='PERIOD',
- help=('Limit the CPU real-time period in microseconds.'))
- parser.add_argument(
- '--cpu-rt-runtime',
- type=int,
- metavar='LIMIT',
- help=('Limit the CPU real-time runtime in microseconds.'))
- parser.add_argument(
- '--cpu-shares',
- type=int,
- metavar='SHARES',
- help=('CPU shares (relative weight)'))
- parser.add_argument(
- '--cpus',
- type=int,
- help=('Number of CPUs. The default is 0 which means no limit'))
- parser.add_argument(
- '--cpuset-cpus',
- metavar='LIST',
- help=('CPUs in which to allow execution (0-3, 0,1)'))
- parser.add_argument(
- '--cpuset-mems',
- metavar='NODES',
- help=('Memory nodes (MEMs) in which to allow execution (0-3, 0,1).'
- ' Only effective on NUMA systems'))
- parser.add_argument(
- '--detach',
- '-d',
- choices=['True', 'False'],
- help=('Detached mode: run the container in the background and'
- ' print the new container ID. The default is false.'))
- parser.add_argument(
- '--detach-keys',
- metavar='KEY(s)',
- help=('Override the key sequence for detaching a container.'
- ' Format is a single character [a-Z] or ctrl-<value> where'
- ' <value> is one of: a-z, @, ^, [, , or _.'))
- parser.add_argument(
- '--device',
- action='append',
- help=('Add a host device to the container'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--device-read-bps',
- action='append',
- metavar='LIMIT',
- help=('Limit read rate (bytes per second) from a device'
- ' (e.g. --device-read-bps=/dev/sda:1mb)'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--device-read-iops',
- action='append',
- metavar='LIMIT',
- help=('Limit read rate (IO per second) from a device'
- ' (e.g. --device-read-iops=/dev/sda:1000)'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--device-write-bps',
- action='append',
- metavar='LIMIT',
- help=('Limit write rate (bytes per second) to a device'
- ' (e.g. --device-write-bps=/dev/sda:1mb)'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--device-write-iops',
- action='append',
- metavar='LIMIT',
- help=('Limit write rate (IO per second) to a device'
- ' (e.g. --device-write-iops=/dev/sda:1000)'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--dns',
- action='append',
- metavar='SERVER',
- help=('Set custom DNS servers.'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--dns-option',
- action='append',
- metavar='OPT',
- help=('Set custom DNS options.'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--dns-search',
- action='append',
- metavar='DOMAIN',
- help=('Set custom DNS search domains.'
- 'The option can be set multiple times.'),
- )
- parser.add_argument(
- '--entrypoint',
- help=('Overwrite the default ENTRYPOINT of the image.'),
- )
- parser.add_argument(
- '--env',
- '-e',
- action='append',
- help=('Set environment variables.'),
- )
- parser.add_argument(
- '--env-file',
- help=('Read in a line delimited file of environment variables,'
- ' on the remote host.'),
- )
- parser.add_argument(
- '--expose',
- metavar='PORT(s)',
- help=('Expose a port, or a range of ports'
- ' (e.g. --expose=3300-3310) to set up port redirection.'),
- )
- parser.add_argument(
- '--gidmap',
- metavar='MAP',
- help=('GID map for the user namespace'),
- )
- parser.add_argument(
- '--group-add',
- action='append',
- metavar='GROUP',
- help=('Add additional groups to run as'))
- parser.add_argument('--hostname', help='Container host name')
-
- volume_group = parser.add_mutually_exclusive_group()
- volume_group.add_argument(
- '--image-volume',
- choices=['bind', 'tmpfs', 'ignore'],
- metavar='MODE',
- help='Tells podman how to handle the builtin image volumes')
- volume_group.add_argument(
- '--builtin-volume',
- choices=['bind', 'tmpfs', 'ignore'],
- metavar='MODE',
- help='Tells podman how to handle the builtin image volumes')
- parser.add_argument(
- '--interactive',
- '-i',
- choices=['True', 'False'],
- help='Keep STDIN open even if not attached. The default is false')
- parser.add_argument('--ipc', help='Create namespace')
- parser.add_argument(
- '--kernel-memory',
- action=UnitAction,
- metavar='UNIT',
- help=('Kernel memory limit (format: <number>[<unit>],'
- ' where unit = b, k, m or g)'))
- parser.add_argument(
- '--label',
- '-l',
- help=('Add metadata to a container'
- ' (e.g., --label com.example.key=value)'))
- parser.add_argument(
- '--label-file', help='Read in a line delimited file of labels')
- parser.add_argument(
- '--log-driver',
- choices=['json-file', 'journald'],
- help='Logging driver for the container.')
- parser.add_argument(
- '--log-opt', action='append', help='Logging driver specific options')
- parser.add_argument(
- '--mac-address', help='Container MAC address (e.g. 92:d0:c6:0a:29:33)')
- parser.add_argument(
- '--memory',
- '-m',
- action=UnitAction,
- metavar='UNIT',
- help='Memory limit (format: [], where unit = b, k, m or g)')
- parser.add_argument(
- '--memory-reservation',
- action=UnitAction,
- metavar='UNIT',
- help='Memory soft limit (format: [], where unit = b, k, m or g)')
- parser.add_argument(
- '--memory-swap',
- action=UnitAction,
- metavar='UNIT',
- help=('A limit value equal to memory plus swap.'
- 'Must be used with the --memory flag'))
- parser.add_argument(
- '--memory-swappiness',
- choices=range(0, 100),
- metavar='[0-100]',
- help="Tune a container's memory swappiness behavior")
- parser.add_argument('--name', help='Assign a name to the container')
- parser.add_argument(
- '--network',
- metavar='BRIDGE',
- help=('Set the Network mode for the container.'))
- parser.add_argument(
- '--oom-kill-disable',
- choices=['True', 'False'],
- help='Whether to disable OOM Killer for the container or not')
- parser.add_argument(
- '--oom-score-adj',
- choices=range(-1000, 1000),
- metavar='[-1000-1000]',
- help="Tune the host's OOM preferences for containers")
- parser.add_argument('--pid', help='Set the PID mode for the container')
- parser.add_argument(
- '--pids-limit',
- type=int,
- metavar='LIMIT',
- help=("Tune the container's pids limit."
- " Set -1 to have unlimited pids for the container."))
- parser.add_argument('--pod', help='Run container in an existing pod')
- parser.add_argument(
- '--privileged',
- choices=['True', 'False'],
- help='Give extended privileges to this container.')
- parser.add_argument(
- '--publish',
- '-p',
- metavar='PORT(s)',
- help="Publish a container's port, or range of ports, to the host")
- parser.add_argument(
- '--publish-all',
- '-P',
- action='store_true',
- help=("Publish all exposed ports to random"
- " ports on the host interfaces"))
- parser.add_argument(
- '--quiet',
- '-q',
- action='store_true',
- help='Suppress output information when pulling images')
- parser.add_argument(
- '--read-only',
- choices=['True', 'False'],
- help="Mount the container's root filesystem as read only.")
- parser.add_argument(
- '--rm',
- choices=['True', 'False'],
- help='Automatically remove the container when it exits.')
- parser.add_argument(
- '--rootfs',
- action='store_true',
- help=('If specified, the first argument refers to an'
- ' exploded container on the file system of remote host.'))
- parser.add_argument(
- '--security-opt',
- action='append',
- metavar='OPT',
- help='Set security options.')
- parser.add_argument(
- '--shm-size',
- action=UnitAction,
- metavar='UNIT',
- help='Size of /dev/shm')
- parser.add_argument(
- '--stop-signal', metavar='SIGTERM', help='Signal to stop a container')
- parser.add_argument(
- '--stop-timeout',
- metavar='TIMEOUT',
- help='Seconds to wait on stopping container.')
- parser.add_argument(
- '--subgidname',
- metavar='MAP',
- help='Name for GID map from the /etc/subgid file')
- parser.add_argument(
- '--subuidname',
- metavar='MAP',
- help='Name for UID map from the /etc/subuid file')
- parser.add_argument(
- '--sysctl',
- action='append',
- help='Configure namespaced kernel parameters at runtime')
- parser.add_argument('--tmpfs', help='Create a tmpfs mount')
- parser.add_argument(
- '--tty',
- '-t',
- choices=['True', 'False'],
- help='Allocate a pseudo-TTY for standard input of container.')
- parser.add_argument(
- '--uidmap', metavar='MAP', help='UID map for the user namespace')
- parser.add_argument('--ulimit', metavar='OPT', help='Ulimit options')
- parser.add_argument(
- '--user',
- '-u',
- help=('Sets the username or UID used and optionally'
- ' the groupname or GID for the specified command.'))
- parser.add_argument(
- '--userns',
- choices=['host', 'ns'],
- help='Set the usernamespace mode for the container')
- parser.add_argument(
- '--uts',
- choices=['host', 'ns'],
- help='Set the UTS mode for the container')
- parser.add_argument('--volume', '-v', help='Create a bind mount.')
- parser.add_argument(
- '--volumes-from',
- action='append',
- help='Mount volumes from the specified container(s).')
- parser.add_argument(
- '--workdir', '-w', help='Working directory inside the container')
+from ._create_args import CreateArguments
class Create(AbstractActionBase):
@@ -419,40 +17,40 @@ class Create(AbstractActionBase):
parser = parent.add_parser(
'create', help='create container from image')
- add_options(parser)
+ CreateArguments.add_arguments(parser)
- parser.add_argument('image', nargs='*', help='source image id.')
+ parser.add_argument('image', nargs=1, help='source image id')
+ parser.add_argument(
+ 'command',
+ nargs='*',
+ help='command and args to run.',
+ )
parser.set_defaults(class_=cls, method='create')
def __init__(self, args):
"""Construct Create class."""
super().__init__(args)
- if not args.image:
- raise ValueError('You must supply at least one image id'
- ' or name to be retrieved.')
+
+ # image id used only on client
+ del self.opts['image']
def create(self):
"""Create container."""
- # Dump all unset arguments before transmitting to service
- opts = {k: v for k, v in vars(self._args).items() if v is not None}
-
- # image id(s) used only on client
- del opts['image']
-
- for ident in self._args.image:
- try:
- img = self.client.images.get(ident)
- img.container(**opts)
- print(ident)
- 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)
+ try:
+ for ident in self._args.image:
+ try:
+ img = self.client.images.get(ident)
+ img.container(**self.opts)
+ print(ident)
+ 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/pypodman/lib/actions/export_action.py b/contrib/python/pypodman/pypodman/lib/actions/export_action.py
index 2a6c2a3cf..f62cd3535 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/export_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/export_action.py
@@ -29,7 +29,6 @@ class Export(AbstractActionBase):
def __init__(self, args):
"""Construct Export class."""
- super().__init__(args)
if not args.container:
raise ValueError('You must supply one container id'
' or name to be used as source.')
@@ -37,6 +36,7 @@ class Export(AbstractActionBase):
if not args.output:
raise ValueError('You must supply one filename'
' to be created as tarball using --output.')
+ super().__init__(args)
def export(self):
"""Create tarball from container filesystem."""
diff --git a/contrib/python/pypodman/pypodman/lib/actions/history_action.py b/contrib/python/pypodman/pypodman/lib/actions/history_action.py
new file mode 100644
index 000000000..3e3f539fc
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/history_action.py
@@ -0,0 +1,83 @@
+"""Remote client for reporting image history."""
+import json
+from collections import OrderedDict
+
+import humanize
+
+import podman
+from pypodman.lib import (AbstractActionBase, BooleanAction, Report,
+ ReportColumn)
+
+
+class History(AbstractActionBase):
+ """Class for reporting Image History."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add History command to parent parser."""
+ parser = parent.add_parser('history', help='report image history')
+ super().subparser(parser)
+ parser.add_argument(
+ '--human',
+ '-H',
+ action=BooleanAction,
+ default='True',
+ help='Display sizes and dates in human readable format.'
+ ' (default: %(default)s)')
+ parser.add_argument(
+ '--format',
+ choices=('json', 'table'),
+ help="Alter the output for a format like 'json' or 'table'."
+ " (default: table)")
+ parser.add_argument(
+ 'image', nargs='+', help='image for history report')
+ parser.set_defaults(class_=cls, method='history')
+
+ def __init__(self, args):
+ """Construct History class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'id':
+ ReportColumn('id', 'ID', 12),
+ 'created':
+ ReportColumn('created', 'CREATED', 11),
+ 'createdBy':
+ ReportColumn('createdBy', 'CREATED BY', 45),
+ 'size':
+ ReportColumn('size', 'SIZE', 8),
+ 'comment':
+ ReportColumn('comment', 'COMMENT', 0)
+ })
+
+ def history(self):
+ """Report image history."""
+ rows = list()
+ for ident in self._args.image:
+ for details in self.client.images.get(ident).history():
+ fields = dict(details._asdict())
+
+ if self._args.human:
+ fields.update({
+ 'size':
+ humanize.naturalsize(details.size, binary=True),
+ 'created':
+ humanize.naturaldate(
+ podman.datetime_parse(details.created)),
+ })
+ del fields['tags']
+
+ rows.append(fields)
+
+ if self._args.quiet:
+ for row in rows:
+ ident = row['id'][:12] if self._args.truncate else row['id']
+ print(ident)
+ elif self._args.format == 'json':
+ print(json.dumps(rows, indent=2), flush=True)
+ else:
+ 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/pypodman/lib/actions/images_action.py b/contrib/python/pypodman/pypodman/lib/actions/images_action.py
index b8f5ccc78..d28e32db9 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/images_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/images_action.py
@@ -65,7 +65,7 @@ class Images(AbstractActionBase):
'created':
humanize.naturaldate(podman.datetime_parse(image.created)),
'size':
- humanize.naturalsize(int(image.size)),
+ humanize.naturalsize(int(image.size), binary=True),
'repoDigests':
' '.join(image.repoDigests),
})
diff --git a/contrib/python/pypodman/pypodman/lib/actions/import_action.py b/contrib/python/pypodman/pypodman/lib/actions/import_action.py
new file mode 100644
index 000000000..49b8a5a57
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/import_action.py
@@ -0,0 +1,60 @@
+"""Remote client command to import tarball as image filesystem."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Import(AbstractActionBase):
+ """Class for importing tarball as image filesystem."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Import command to parent parser."""
+ parser = parent.add_parser(
+ 'import', help='import tarball as image filesystem')
+ parser.add_argument(
+ '--change',
+ '-c',
+ action='append',
+ choices=('CMD', 'ENTRYPOINT', 'ENV', 'EXPOSE', 'LABEL',
+ 'STOPSIGNAL', 'USER', 'VOLUME', 'WORKDIR'),
+ type=str.upper,
+ help='Apply the following possible instructions',
+ )
+ parser.add_argument(
+ '--message', '-m', help='Set commit message for imported image.')
+ parser.add_argument(
+ 'source',
+ metavar='PATH',
+ nargs=1,
+ help='tarball to use as source on remote system',
+ )
+ parser.add_argument(
+ 'reference',
+ metavar='TAG',
+ nargs='*',
+ help='Optional tag for image. (default: None)',
+ )
+ parser.set_defaults(class_=cls, method='import_')
+
+ def __init__(self, args):
+ """Construct Import class."""
+ super().__init__(args)
+
+ def import_(self):
+ """Import tarball as image filesystem."""
+ try:
+ ident = self.client.images.import_image(
+ self.opts.source,
+ self.opts.reference,
+ message=self.opts.message,
+ changes=self.opts.change)
+ print(ident)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/info_action.py b/contrib/python/pypodman/pypodman/lib/actions/info_action.py
new file mode 100644
index 000000000..988284541
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/info_action.py
@@ -0,0 +1,49 @@
+"""Remote client command for reporting on Podman service."""
+import json
+import sys
+
+import podman
+import yaml
+from pypodman.lib import AbstractActionBase
+
+
+class Info(AbstractActionBase):
+ """Class for reporting on Podman Service."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Info command to parent parser."""
+ parser = parent.add_parser(
+ 'info', help='report info on podman service')
+ parser.add_argument(
+ '--format',
+ choices=('json', 'yaml'),
+ help="Alter the output for a format like 'json' or 'yaml'."
+ " (default: yaml)")
+ parser.set_defaults(class_=cls, method='info')
+
+ def __init__(self, args):
+ """Construct Info class."""
+ super().__init__(args)
+
+ def info(self):
+ """Report on Podman Service."""
+ try:
+ info = self.client.system.info()
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
+ else:
+ if self._args.format == 'json':
+ print(json.dumps(info._asdict(), indent=2), flush=True)
+ else:
+ print(
+ yaml.dump(
+ dict(info._asdict()),
+ canonical=False,
+ default_flow_style=False),
+ flush=True)
diff --git a/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py
index 0559cd40a..514b4702a 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py
@@ -41,7 +41,7 @@ class Inspect(AbstractActionBase):
def _get_container(self, ident):
try:
- logging.debug("Get container %s", ident)
+ logging.debug("Getting container %s", ident)
ctnr = self.client.containers.get(ident)
except podman.ContainerNotFound:
pass
@@ -50,7 +50,7 @@ class Inspect(AbstractActionBase):
def _get_image(self, ident):
try:
- logging.debug("Get image %s", ident)
+ logging.debug("Getting image %s", ident)
img = self.client.images.get(ident)
except podman.ImageNotFound:
pass
diff --git a/contrib/python/pypodman/pypodman/lib/actions/kill_action.py b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py
index 3caa42cf0..cb3d3f035 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/kill_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py
@@ -19,7 +19,7 @@ class Kill(AbstractActionBase):
choices=range(1, signal.NSIG),
metavar='[1,{}]'.format(signal.NSIG),
default=9,
- help='Signal to send to the container. (Default: 9)')
+ help='Signal to send to the container. (default: 9)')
parser.add_argument(
'containers',
nargs='+',
diff --git a/contrib/python/pypodman/pypodman/lib/actions/logs_action.py b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py
index 764a4b9c7..91ff7bb08 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/logs_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py
@@ -5,20 +5,7 @@ import sys
from collections import deque
import podman
-from pypodman.lib import AbstractActionBase
-
-
-class PositiveIntAction(argparse.Action):
- """Validate number given is positive integer."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- """Validate input."""
- if values > 0:
- setattr(namespace, self.dest, values)
- return
-
- msg = 'Must be a positive integer.'
- raise argparse.ArgumentError(self, msg)
+from pypodman.lib import AbstractActionBase, PositiveIntAction
class Logs(AbstractActionBase):
@@ -32,7 +19,6 @@ class Logs(AbstractActionBase):
'--tail',
metavar='LINES',
action=PositiveIntAction,
- type=int,
help='Output the specified number of LINES at the end of the logs')
parser.add_argument(
'container',
diff --git a/contrib/python/pypodman/pypodman/lib/actions/port_action.py b/contrib/python/pypodman/pypodman/lib/actions/port_action.py
index 60bbe12c4..8961c1935 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/port_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/port_action.py
@@ -29,10 +29,10 @@ class Port(AbstractActionBase):
def __init__(self, args):
"""Construct Port class."""
- super().__init__(args)
if not args.all and not args.containers:
ValueError('You must supply at least one'
' container id or name, or --all.')
+ super().__init__(args)
def port(self):
"""Retrieve ports from containers."""
diff --git a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
index 8c0a739d7..cd7a7947d 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
@@ -18,10 +18,8 @@ class Ps(AbstractActionBase):
super().subparser(parser)
parser.add_argument(
'--sort',
- choices=[
- 'createdat', 'id', 'image', 'names', 'runningfor', 'size',
- 'status'
- ],
+ choices=('createdat', 'id', 'image', 'names', 'runningfor', 'size',
+ 'status'),
default='createdat',
type=str.lower,
help=('Change sort ordered of displayed containers.'
diff --git a/contrib/python/pypodman/pypodman/lib/actions/pull_action.py b/contrib/python/pypodman/pypodman/lib/actions/pull_action.py
index d609eac28..d8fbfc1f0 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/pull_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/pull_action.py
@@ -17,7 +17,7 @@ class Pull(AbstractActionBase):
)
parser.add_argument(
'targets',
- nargs='*',
+ nargs='+',
help='image id(s) to retrieve.',
)
parser.set_defaults(class_=cls, method='pull')
@@ -25,9 +25,6 @@ class Pull(AbstractActionBase):
def __init__(self, args):
"""Construct Pull class."""
super().__init__(args)
- if not args.targets:
- raise ValueError('You must supply at least one container id'
- ' or name to be retrieved.')
def pull(self):
"""Retrieve image."""
diff --git a/contrib/python/pypodman/pypodman/lib/actions/push_action.py b/contrib/python/pypodman/pypodman/lib/actions/push_action.py
new file mode 100644
index 000000000..0030cb5b9
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/push_action.py
@@ -0,0 +1,56 @@
+"""Remote client command for pushing image elsewhere."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Push(AbstractActionBase):
+ """Class for pushing images to repository."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Push command to parent parser."""
+ parser = parent.add_parser(
+ 'push',
+ help='push image elsewhere',
+ )
+ parser.add_argument(
+ '--tlsverify',
+ action='store_true',
+ default=True,
+ help='Require HTTPS and verify certificates when'
+ ' contacting registries (default: %(default)s)')
+ parser.add_argument(
+ 'image', nargs=1, help='name or id of image to push')
+ parser.add_argument(
+ 'tag',
+ nargs=1,
+ help='destination image id',
+ )
+ parser.set_defaults(class_=cls, method='push')
+
+ def __init__(self, args):
+ """Construct Push class."""
+ super().__init__(args)
+
+ def pull(self):
+ """Store image elsewhere."""
+ try:
+ try:
+ img = self.client.images.get(self._args.image[0])
+ except podman.ImageNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Image {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ else:
+ img.push(self._args.tag[0], tlsverify=self._args.tlsverify)
+ print(self._args.image[0])
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/pypodman/lib/actions/restart_action.py b/contrib/python/pypodman/pypodman/lib/actions/restart_action.py
new file mode 100644
index 000000000..d99d1ad65
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/restart_action.py
@@ -0,0 +1,50 @@
+"""Remote client command for restarting containers."""
+import logging
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase, PositiveIntAction
+
+
+class Restart(AbstractActionBase):
+ """Class for Restarting containers."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Restart command to parent parser."""
+ parser = parent.add_parser('restart', help='restart container(s)')
+ parser.add_argument(
+ '--timeout',
+ action=PositiveIntAction,
+ default=10,
+ help='Timeout to wait before forcibly stopping the container'
+ ' (default: %(default)s seconds)')
+ parser.add_argument(
+ 'targets', nargs='+', help='container id(s) to restart')
+ parser.set_defaults(class_=cls, method='restart')
+
+ def __init__(self, args):
+ """Construct Restart class."""
+ super().__init__(args)
+
+ def restart(self):
+ """Restart container(s)."""
+ try:
+ for ident in self._args.targets:
+ try:
+ ctnr = self.client.containers.get(ident)
+ logging.debug('Restarting Container %s', ctnr.id)
+ ctnr.restart(timeout=self._args.timeout)
+ print(ident)
+ 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/pypodman/lib/actions/rm_action.py b/contrib/python/pypodman/pypodman/lib/actions/rm_action.py
index 62c0b8599..e8074ef4e 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/rm_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/rm_action.py
@@ -19,15 +19,12 @@ class Rm(AbstractActionBase):
help=('force delete of running container(s).'
' (default: %(default)s)'))
parser.add_argument(
- 'targets', nargs='*', help='container id(s) to delete')
+ '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 not args.targets:
- raise ValueError('You must supply at least one container id'
- ' or name to be deleted.')
def remove(self):
"""Remove container(s)."""
diff --git a/contrib/python/pypodman/pypodman/lib/actions/rmi_action.py b/contrib/python/pypodman/pypodman/lib/actions/rmi_action.py
index 9ff533821..c6ba835cb 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/rmi_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/rmi_action.py
@@ -18,15 +18,12 @@ class Rmi(AbstractActionBase):
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.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 not args.targets:
- raise ValueError('You must supply at least one image id'
- ' or name to be deleted.')
def remove(self):
"""Remove image(s)."""
diff --git a/contrib/python/pypodman/pypodman/lib/actions/run_action.py b/contrib/python/pypodman/pypodman/lib/actions/run_action.py
new file mode 100644
index 000000000..a63eb7917
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/run_action.py
@@ -0,0 +1,73 @@
+"""Remote client command for run a command in a new container."""
+import logging
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+from ._create_args import CreateArguments
+
+
+class Run(AbstractActionBase):
+ """Class for running a command in a container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Run command to parent parser."""
+ parser = parent.add_parser('run', help='Run container from image')
+
+ CreateArguments.add_arguments(parser)
+
+ parser.add_argument('image', nargs=1, help='source image id.')
+ parser.add_argument(
+ 'command',
+ nargs='*',
+ help='command and args to run.',
+ )
+ parser.set_defaults(class_=cls, method='run')
+
+ def __init__(self, args):
+ """Construct Run class."""
+ super().__init__(args)
+ if args.detach and args.rm:
+ raise ValueError('Incompatible options: --detach and --rm')
+
+ # image id used only on client
+ del self.opts['image']
+
+ def run(self):
+ """Run container."""
+ for ident in self._args.image:
+ try:
+ try:
+ img = self.client.images.get(ident)
+ ctnr = img.container(**self.opts)
+ except podman.ImageNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Image {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ continue
+ else:
+ logging.debug('New container created "{}"'.format(ctnr.id))
+
+ if self._args.detach:
+ ctnr.start()
+ print(ctnr.id)
+ else:
+ ctnr.attach(eot=4)
+ ctnr.start()
+ print(ctnr.id)
+
+ if self._args.rm:
+ ctnr.remove(force=True)
+ except (BrokenPipeError, KeyboardInterrupt):
+ print('\nContainer "{}" disconnected.'.format(ctnr.id))
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ 'Run for container "{}" failed: {} {}'.format(
+ ctnr.id, repr(e), e.reason.capitalize()),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/pypodman/lib/actions/search_action.py b/contrib/python/pypodman/pypodman/lib/actions/search_action.py
new file mode 100644
index 000000000..d2a585d92
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/search_action.py
@@ -0,0 +1,160 @@
+"""Remote client command for searching registries for an image."""
+import argparse
+import sys
+from collections import OrderedDict
+
+import podman
+from pypodman.lib import (AbstractActionBase, BooleanValidate,
+ PositiveIntAction, Report, ReportColumn)
+
+
+class FilterAction(argparse.Action):
+ """Parse filter argument components."""
+
+ def __init__(self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=None,
+ choices=None,
+ required=False,
+ help=None,
+ metavar='FILTER'):
+ """Create FilterAction object."""
+ help = (help or '') + (' (format: stars=##'
+ ' or is-automated=[True|False]'
+ ' or is-official=[True|False])')
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=type,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """
+ Convert and Validate input.
+
+ Note: side effects
+ 1) self.dest value is set to subargument dest
+ 2) new attribute self.dest + '_value' is created with 2nd value.
+ """
+ opt, val = values.split('=', 1)
+ if opt == 'stars':
+ msg = ('{} option "stars" requires'
+ ' a positive integer').format(self.dest)
+ try:
+ val = int(val)
+ except ValueError:
+ parser.error(msg)
+
+ if val < 0:
+ parser.error(msg)
+ elif opt == 'is-automated':
+ try:
+ val = BooleanValidate()(val)
+ except ValueError:
+ msg = ('{} option "is-automated"'
+ ' must be True or False.'.format(self.dest))
+ parser.error(msg)
+ elif opt == 'is-official':
+ try:
+ val = BooleanValidate()(val)
+ except ValueError:
+ msg = ('{} option "is-official"'
+ ' must be True or False.'.format(self.dest))
+ parser.error(msg)
+ else:
+ msg = ('{} only supports one of the following options:\n'
+ ' stars, is-automated, or is-official').format(self.dest)
+ parser.error(msg)
+ setattr(namespace, self.dest, opt)
+ setattr(namespace, self.dest + '_value', val)
+
+
+class Search(AbstractActionBase):
+ """Class for searching registries for an image."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Search command to parent parser."""
+ parser = parent.add_parser('search', help='search for images')
+ super().subparser(parser)
+ parser.add_argument(
+ '--filter',
+ '-f',
+ action=FilterAction,
+ help='Filter output based on conditions provided.')
+ parser.add_argument(
+ '--limit',
+ action=PositiveIntAction,
+ default=25,
+ help='Limit the number of results.'
+ ' (default: %(default)s)')
+ parser.add_argument('term', nargs=1, help='search term for image')
+ parser.set_defaults(class_=cls, method='search')
+
+ def __init__(self, args):
+ """Construct Search class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'name':
+ ReportColumn('name', 'NAME', 44),
+ 'description':
+ ReportColumn('description', 'DESCRIPTION', 44),
+ 'star_count':
+ ReportColumn('star_count', 'STARS', 5),
+ 'is_official':
+ ReportColumn('is_official', 'OFFICIAL', 8),
+ 'is_automated':
+ ReportColumn('is_automated', 'AUTOMATED', 9),
+ })
+
+ def search(self):
+ """Search registries for image."""
+ try:
+ rows = list()
+ for entry in self.client.images.search(
+ self._args.term[0], limit=self._args.limit):
+
+ if self._args.filter == 'is-official':
+ if self._args.filter_value != entry.is_official:
+ continue
+ elif self._args.filter == 'is-automated':
+ if self._args.filter_value != entry.is_automated:
+ continue
+ elif self._args.filter == 'stars':
+ if self._args.filter_value > entry.star_count:
+ continue
+
+ fields = dict(entry._asdict())
+
+ status = '[OK]' if entry.is_official else ''
+ fields['is_official'] = status
+
+ status = '[OK]' if entry.is_automated else ''
+ fields['is_automated'] = status
+
+ if self._args.truncate:
+ fields.update({'name': entry.name[-44:]})
+ 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)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/pypodman/lib/parser_actions.py b/contrib/python/pypodman/pypodman/lib/parser_actions.py
new file mode 100644
index 000000000..2a5859e47
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/parser_actions.py
@@ -0,0 +1,185 @@
+"""
+Supplimental argparse.Action converters and validaters.
+
+The constructors are very verbose but remain for IDE support.
+"""
+import argparse
+import os
+
+# API defined by argparse.Action shut up pylint
+# pragma pylint: disable=redefined-builtin
+# pragma pylint: disable=too-few-public-methods
+# pragma pylint: disable=too-many-arguments
+
+
+class BooleanValidate():
+ """Validate value is boolean string."""
+
+ def __call__(self, value):
+ """Return True, False or raise ValueError."""
+ val = value.capitalize()
+ if val == 'False':
+ return False
+ elif val == 'True':
+ return True
+ else:
+ raise ValueError('"{}" is not True or False'.format(value))
+
+
+class BooleanAction(argparse.Action):
+ """Convert and validate bool argument."""
+
+ def __init__(self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=None,
+ choices=('True', 'False'),
+ required=False,
+ help=None,
+ metavar='{True,False}'):
+ """Create BooleanAction object."""
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=type,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Convert and Validate input."""
+ try:
+ val = BooleanValidate()(values)
+ except ValueError:
+ parser.error('{} must be True or False.'.format(self.dest))
+ else:
+ setattr(namespace, self.dest, val)
+
+
+class UnitAction(argparse.Action):
+ """Validate number given is positive integer, with optional suffix."""
+
+ def __init__(self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=None,
+ choices=None,
+ required=False,
+ help=None,
+ metavar='UNIT'):
+ """Create UnitAction object."""
+ help = (help or metavar or dest
+ ) + ' (format: <number>[<unit>], where unit = b, k, m or g)'
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=type,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Validate input as a UNIT."""
+ try:
+ val = int(values)
+ except ValueError:
+ if not values[:-1].isdigit():
+ msg = ('{} must be a positive integer,'
+ ' with optional suffix').format(self.dest)
+ parser.error(msg)
+ if not values[-1] in ('b', 'k', 'm', 'g'):
+ msg = '{} only supports suffices of: b, k, m, g'.format(
+ self.dest)
+ parser.error(msg)
+ else:
+ if val <= 0:
+ msg = '{} must be a positive integer'.format(self.dest)
+ parser.error(msg)
+
+ setattr(namespace, self.dest, values)
+
+
+class PositiveIntAction(argparse.Action):
+ """Validate number given is positive integer."""
+
+ def __init__(self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=int,
+ choices=None,
+ required=False,
+ help=None,
+ metavar=None):
+ """Create PositiveIntAction object."""
+ self.message = '{} must be a positive integer'.format(dest)
+ help = help or self.message
+
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=int,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Validate input."""
+ if values > 0:
+ setattr(namespace, self.dest, values)
+ return
+
+ parser.error(self.message)
+
+
+class PathAction(argparse.Action):
+ """Expand user- and relative-paths."""
+
+ def __init__(self,
+ option_strings,
+ dest,
+ nargs=None,
+ const=None,
+ default=None,
+ type=None,
+ choices=None,
+ required=False,
+ help=None,
+ metavar='PATH'):
+ """Create PathAction object."""
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=type,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Resolve full path value on local filesystem."""
+ setattr(namespace, self.dest,
+ os.path.abspath(os.path.expanduser(values)))
diff --git a/contrib/python/pypodman/pypodman/lib/config.py b/contrib/python/pypodman/pypodman/lib/podman_parser.py
index 2f0cbf8ae..4150e5d50 100644
--- a/contrib/python/pypodman/pypodman/lib/config.py
+++ b/contrib/python/pypodman/pypodman/lib/podman_parser.py
@@ -7,10 +7,13 @@ import logging
import os
import sys
from contextlib import suppress
+from pathlib import Path
import pkg_resources
import pytoml
+from .parser_actions import PathAction, PositiveIntAction
+
# TODO: setup.py and obtain __version__ from rpm.spec
try:
__version__ = pkg_resources.get_distribution('pypodman').version
@@ -33,35 +36,14 @@ class HelpFormatter(argparse.RawDescriptionHelpFormatter):
super().__init__(*args, **kwargs)
-class PortAction(argparse.Action):
- """Validate port number given is positive integer."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- """Validate input."""
- if values > 0:
- setattr(namespace, self.dest, values)
- return
-
- msg = 'port numbers must be a positive integer.'
- raise argparse.ArgumentError(self, msg)
-
-
-class PathAction(argparse.Action):
- """Expand user- and relative-paths."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- """Resolve full path value."""
- setattr(namespace, self.dest,
- os.path.abspath(os.path.expanduser(values)))
-
-
class PodmanArgumentParser(argparse.ArgumentParser):
"""Default remote podman configuration."""
def __init__(self, **kwargs):
"""Construct the parser."""
kwargs['add_help'] = True
- kwargs['description'] = __doc__
+ kwargs['description'] = ('Portable and simple management'
+ ' tool for containers and images')
kwargs['formatter_class'] = HelpFormatter
super().__init__(**kwargs)
@@ -83,9 +65,9 @@ class PodmanArgumentParser(argparse.ArgumentParser):
'--run-dir',
metavar='DIRECTORY',
help=('directory to place local socket bindings.'
- ' (default: XDG_RUNTIME_DIR/pypodman'))
+ ' (default: XDG_RUNTIME_DIR/pypodman)'))
self.add_argument(
- '--user',
+ '--username',
'-l',
default=getpass.getuser(),
help='Authenicating user on remote host. (default: %(default)s)')
@@ -94,8 +76,7 @@ class PodmanArgumentParser(argparse.ArgumentParser):
self.add_argument(
'--port',
'-p',
- type=int,
- action=PortAction,
+ action=PositiveIntAction,
help='port for ssh tunnel to remote host. (default: 22)')
self.add_argument(
'--remote-socket-path',
@@ -105,18 +86,17 @@ class PodmanArgumentParser(argparse.ArgumentParser):
self.add_argument(
'--identity-file',
'-i',
- metavar='PATH',
action=PathAction,
- help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
+ help='path to ssh identity file. (default: ~user/.ssh/id_dsa)')
self.add_argument(
'--config-home',
metavar='DIRECTORY',
action=PathAction,
help=('home of configuration "pypodman.conf".'
- ' (default: XDG_CONFIG_HOME/pypodman'))
+ ' (default: XDG_CONFIG_HOME/pypodman)'))
actions_parser = self.add_subparsers(
- dest='subparser_name', help='actions')
+ dest='subparser_name', help='commands')
# import buried here to prevent import loops
import pypodman.lib.actions # pylint: disable=cyclic-import
@@ -157,11 +137,12 @@ class PodmanArgumentParser(argparse.ArgumentParser):
if dir_ is None:
continue
with suppress(OSError):
- with open(os.path.join(dir_,
- 'pypodman/pypodman.conf')) as stream:
+ cnf = Path(dir_, 'pypodman', 'pypodman.conf')
+ with cnf.open() as stream:
config.update(pytoml.load(stream))
def reqattr(name, value):
+ """Raise an error if value is unset."""
if value:
setattr(args, name, value)
return value
@@ -173,7 +154,7 @@ class PodmanArgumentParser(argparse.ArgumentParser):
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')
+ or Path(args.xdg_runtime_dir, 'pypodman')
) # yapf: disable
setattr(
@@ -185,11 +166,11 @@ class PodmanArgumentParser(argparse.ArgumentParser):
) # yapf:disable
reqattr(
- 'user',
- getattr(args, 'user')
+ 'username',
+ getattr(args, 'username')
or os.environ.get('USER')
or os.environ.get('LOGNAME')
- or config['default'].get('user')
+ or config['default'].get('username')
or getpass.getuser()
) # yapf:disable
@@ -215,22 +196,21 @@ class PodmanArgumentParser(argparse.ArgumentParser):
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))
+ or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.username))
) # 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")
+ args.local_socket_path = Path(args.run_dir, 'podman.socket')
else:
args.local_socket_path = args.remote_socket_path
- args.local_uri = "unix:{}".format(args.local_socket_path)
+ args.local_uri = 'unix:{}'.format(args.local_socket_path)
if args.host:
- components = ['ssh://', args.user, '@', args.host]
+ components = ['ssh://', args.username, '@', args.host]
if args.port:
components.extend((':', str(args.port)))
components.append(args.remote_socket_path)
diff --git a/contrib/python/pypodman/pypodman/lib/report.py b/contrib/python/pypodman/pypodman/lib/report.py
index a06e6055e..1db4268da 100644
--- a/contrib/python/pypodman/pypodman/lib/report.py
+++ b/contrib/python/pypodman/pypodman/lib/report.py
@@ -53,7 +53,7 @@ class Report():
fmt = []
for key in keys:
- slice_ = [i.get(key, '') for i in iterable]
+ slice_ = [str(i.get(key, '')) for i in iterable]
data_len = len(max(slice_, key=len))
info = self._columns.get(key,
diff --git a/contrib/python/pypodman/requirements.txt b/contrib/python/pypodman/requirements.txt
index 69cf41761..ba01ed36e 100644
--- a/contrib/python/pypodman/requirements.txt
+++ b/contrib/python/pypodman/requirements.txt
@@ -1,4 +1,5 @@
humanize
podman
pytoml
+PyYAML
setuptools>=39