From e6074eb9acbd3964ee0c1ea6a9b5ec68fa309687 Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Thu, 6 Sep 2018 14:32:14 -0700 Subject: Implement new subcommands * Refactor create subparser to share arguments with run subparser * Add argparse.*Action subclasses to reduce duplicate code in parsers * Using BooleanAction now accept True/False value as expected * .pylintrc added to loosen variable name policing * Update AbstractBaseAction to remove unset arguments before transmitting to podman service * Align logging messages to podman output * Renamed global argument from --user to --username, to avoid conflict with create/run podman commands * Add new subcommands: run, create, history, import, info, push, restart and search Signed-off-by: Jhon Honce Closes: #1519 Approved by: rhatdan --- contrib/python/podman/Makefile | 6 + .../python/podman/podman/libs/_containers_start.py | 1 + contrib/python/podman/podman/libs/containers.py | 32 +- contrib/python/podman/test/test_runner.sh | 3 +- contrib/python/pypodman/.pylintrc | 4 + contrib/python/pypodman/Makefile | 6 + contrib/python/pypodman/pypodman/lib/__init__.py | 12 +- .../python/pypodman/pypodman/lib/action_base.py | 5 + .../pypodman/pypodman/lib/actions/__init__.py | 14 + .../pypodman/pypodman/lib/actions/_create_args.py | 394 ++++++++++++++++++ .../pypodman/pypodman/lib/actions/commit_action.py | 14 +- .../pypodman/pypodman/lib/actions/create_action.py | 460 ++------------------- .../pypodman/pypodman/lib/actions/export_action.py | 2 +- .../pypodman/lib/actions/history_action.py | 83 ++++ .../pypodman/pypodman/lib/actions/images_action.py | 2 +- .../pypodman/pypodman/lib/actions/import_action.py | 60 +++ .../pypodman/pypodman/lib/actions/info_action.py | 49 +++ .../pypodman/lib/actions/inspect_action.py | 4 +- .../pypodman/pypodman/lib/actions/kill_action.py | 2 +- .../pypodman/pypodman/lib/actions/logs_action.py | 16 +- .../pypodman/pypodman/lib/actions/port_action.py | 2 +- .../pypodman/pypodman/lib/actions/ps_action.py | 6 +- .../pypodman/pypodman/lib/actions/pull_action.py | 5 +- .../pypodman/pypodman/lib/actions/push_action.py | 56 +++ .../pypodman/lib/actions/restart_action.py | 50 +++ .../pypodman/pypodman/lib/actions/rm_action.py | 5 +- .../pypodman/pypodman/lib/actions/rmi_action.py | 5 +- .../pypodman/pypodman/lib/actions/run_action.py | 73 ++++ .../pypodman/pypodman/lib/actions/search_action.py | 160 +++++++ contrib/python/pypodman/pypodman/lib/config.py | 252 ----------- .../python/pypodman/pypodman/lib/parser_actions.py | 185 +++++++++ .../python/pypodman/pypodman/lib/podman_parser.py | 232 +++++++++++ contrib/python/pypodman/pypodman/lib/report.py | 2 +- contrib/python/pypodman/requirements.txt | 1 + 34 files changed, 1464 insertions(+), 739 deletions(-) create mode 100644 contrib/python/pypodman/.pylintrc create mode 100644 contrib/python/pypodman/pypodman/lib/actions/_create_args.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/history_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/import_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/info_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/push_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/restart_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/run_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/search_action.py delete mode 100644 contrib/python/pypodman/pypodman/lib/config.py create mode 100644 contrib/python/pypodman/pypodman/lib/parser_actions.py create mode 100644 contrib/python/pypodman/pypodman/lib/podman_parser.py (limited to 'contrib') diff --git a/contrib/python/podman/Makefile b/contrib/python/podman/Makefile index e7e365a9c..f691092b7 100644 --- a/contrib/python/podman/Makefile +++ b/contrib/python/podman/Makefile @@ -1,5 +1,6 @@ PYTHON ?= /usr/bin/python3 DESTDIR ?= / +PODMAN_VERSION ?= '0.0.4' .PHONY: python-podman python-podman: @@ -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/podman/podman/libs/_containers_start.py b/contrib/python/podman/podman/libs/_containers_start.py index 8e705771a..20130f5d6 100644 --- a/contrib/python/podman/podman/libs/_containers_start.py +++ b/contrib/python/podman/podman/libs/_containers_start.py @@ -20,6 +20,7 @@ class Mixin: Will block if container has been detached. """ with self._client() as podman: + logging.debug('Starting Container "%s"', self._id) results = podman.StartContainer(self._id) logging.debug('Started Container "%s"', results['container']) diff --git a/contrib/python/podman/podman/libs/containers.py b/contrib/python/podman/podman/libs/containers.py index f56137e12..e211a284e 100644 --- a/contrib/python/podman/podman/libs/containers.py +++ b/contrib/python/podman/podman/libs/containers.py @@ -3,6 +3,7 @@ import collections import functools import getpass import json +import logging import signal import time @@ -32,17 +33,26 @@ class Container(AttachMixin, StartMixin, collections.UserDict): """Get items from parent dict.""" return super().__getitem__(key) - def _refresh(self, podman): - ctnr = podman.GetContainer(self._id) - super().update(ctnr['container']) - - for k, v in self.data.items(): - setattr(self, k, v) - if 'containerrunning' in self.data: - setattr(self, 'running', self.data['containerrunning']) - self.data['running'] = self.data['containerrunning'] - - return self + def _refresh(self, podman, tries=1): + try: + ctnr = podman.GetContainer(self._id) + except BrokenPipeError: + logging.debug('Failed GetContainer(%s) try %d/3', self._id, tries) + if tries > 3: + raise + else: + with self._client() as pman: + self._refresh(pman, tries + 1) + else: + super().update(ctnr['container']) + + for k, v in self.data.items(): + setattr(self, k, v) + 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.""" diff --git a/contrib/python/podman/test/test_runner.sh b/contrib/python/podman/test/test_runner.sh index c37d9cf6e..76432cf47 100755 --- a/contrib/python/podman/test/test_runner.sh +++ b/contrib/python/podman/test/test_runner.sh @@ -97,11 +97,12 @@ EOT cat >$TMPDIR/ctnr/hello.sh <<-EOT echo 'Hello, World' +exit 0 EOT cat >$TMPDIR/ctnr/Dockerfile <<-EOT FROM alpine:latest -COPY ./hello.sh /tmp/hello.sh +COPY ./hello.sh /tmp/ RUN chmod 755 /tmp/hello.sh ENTRYPOINT ["/tmp/hello.sh"] EOT 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- where' + ' 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- where' - ' 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: [],' - ' 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/config.py b/contrib/python/pypodman/pypodman/lib/config.py deleted file mode 100644 index 2f0cbf8ae..000000000 --- a/contrib/python/pypodman/pypodman/lib/config.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Parse configuration while building subcommands.""" -import argparse -import curses -import getpass -import inspect -import logging -import os -import sys -from contextlib import suppress - -import pkg_resources -import pytoml - -# TODO: setup.py and obtain __version__ from rpm.spec -try: - __version__ = pkg_resources.get_distribution('pypodman').version -except Exception: # pylint: disable=broad-except - __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: - _, width = curses.initscr().getmaxyx() - kwargs['width'] = width - finally: - curses.endwin() - 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['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', - '-l', - 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( - '--port', - '-p', - type=int, - action=PortAction, - help='port for ssh tunnel to remote host. (default: 22)') - self.add_argument( - '--remote-socket-path', - metavar='PATH', - help=('path of podman socket on remote host' - ' (default: /run/podman/io.podman)')) - self.add_argument( - '--identity-file', - '-i', - metavar='PATH', - action=PathAction, - 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')) - - actions_parser = self.add_subparsers( - dest='subparser_name', help='actions') - - # import buried here to prevent import loops - import pypodman.lib.actions # pylint: disable=cyclic-import - assert pypodman.lib.actions - - # pull in plugin(s) code for each subcommand - for name, obj in inspect.getmembers( - sys.modules['pypodman.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 "%s"', 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 - with suppress(OSError): - with open(os.path.join(dir_, - 'pypodman/pypodman.conf')) as stream: - config.update(pytoml.load(stream)) - - def reqattr(name, value): - if value: - setattr(args, name, value) - return value - return self.error( - 'Required argument "{}" is not configured.'.format(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 os.environ.get('LOGNAME') - 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.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) - - if args.host: - components = ['ssh://', args.user, '@', args.host] - if args.port: - components.extend((':', str(args.port))) - components.append(args.remote_socket_path) - - args.remote_uri = ''.join(components) - 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('%s: %s', self.prog, message) - logging.error("Try '%s --help' for more information.", self.prog) - super().exit(2) 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: [], 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/podman_parser.py b/contrib/python/pypodman/pypodman/lib/podman_parser.py new file mode 100644 index 000000000..4150e5d50 --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/podman_parser.py @@ -0,0 +1,232 @@ +"""Parse configuration while building subcommands.""" +import argparse +import curses +import getpass +import inspect +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 +except Exception: # pylint: disable=broad-except + __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: + _, 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['description'] = ('Portable and simple management' + ' tool for containers and images') + 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( + '--username', + '-l', + 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( + '--port', + '-p', + action=PositiveIntAction, + help='port for ssh tunnel to remote host. (default: 22)') + self.add_argument( + '--remote-socket-path', + metavar='PATH', + help=('path of podman socket on remote host' + ' (default: /run/podman/io.podman)')) + self.add_argument( + '--identity-file', + '-i', + action=PathAction, + 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)')) + + actions_parser = self.add_subparsers( + dest='subparser_name', help='commands') + + # import buried here to prevent import loops + import pypodman.lib.actions # pylint: disable=cyclic-import + assert pypodman.lib.actions + + # pull in plugin(s) code for each subcommand + for name, obj in inspect.getmembers( + sys.modules['pypodman.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 "%s"', 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 + with suppress(OSError): + 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 + return self.error( + 'Required argument "{}" is not configured.'.format(name)) + + reqattr( + 'run_dir', + getattr(args, 'run_dir') + or os.environ.get('RUN_DIR') + or config['default'].get('run_dir') + or Path(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( + 'username', + getattr(args, 'username') + or os.environ.get('USER') + or os.environ.get('LOGNAME') + or config['default'].get('username') + 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.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.username)) + ) # yapf:disable + + if not os.path.isfile(args.identity_file): + args.identity_file = None + + if args.host: + 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) + + if args.host: + components = ['ssh://', args.username, '@', args.host] + if args.port: + components.extend((':', str(args.port))) + components.append(args.remote_socket_path) + + args.remote_uri = ''.join(components) + 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('%s: %s', self.prog, message) + logging.error("Try '%s --help' for more information.", self.prog) + super().exit(2) 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 -- cgit v1.2.3-54-g00ecf