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 <jhonce@redhat.com>

Closes: #1519
Approved by: rhatdan
This commit is contained in:
Jhon Honce
2018-09-06 14:32:14 -07:00
committed by Atomic Bot
parent 09f506930c
commit e6074eb9ac
33 changed files with 1252 additions and 527 deletions

View File

@ -1,5 +1,6 @@
PYTHON ?= /usr/bin/python3 PYTHON ?= /usr/bin/python3
DESTDIR ?= / DESTDIR ?= /
PODMAN_VERSION ?= '0.0.4'
.PHONY: python-podman .PHONY: python-podman
python-podman: python-podman:
@ -17,6 +18,11 @@ integration:
install: install:
$(PYTHON) setup.py install --root ${DESTDIR} $(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 .PHONY: clobber
clobber: uninstall clean clobber: uninstall clean

View File

@ -20,6 +20,7 @@ class Mixin:
Will block if container has been detached. Will block if container has been detached.
""" """
with self._client() as podman: with self._client() as podman:
logging.debug('Starting Container "%s"', self._id)
results = podman.StartContainer(self._id) results = podman.StartContainer(self._id)
logging.debug('Started Container "%s"', results['container']) logging.debug('Started Container "%s"', results['container'])

View File

@ -3,6 +3,7 @@ import collections
import functools import functools
import getpass import getpass
import json import json
import logging
import signal import signal
import time import time
@ -32,8 +33,17 @@ class Container(AttachMixin, StartMixin, collections.UserDict):
"""Get items from parent dict.""" """Get items from parent dict."""
return super().__getitem__(key) return super().__getitem__(key)
def _refresh(self, podman): def _refresh(self, podman, tries=1):
try:
ctnr = podman.GetContainer(self._id) 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']) super().update(ctnr['container'])
for k, v in self.data.items(): for k, v in self.data.items():

View File

@ -97,11 +97,12 @@ EOT
cat >$TMPDIR/ctnr/hello.sh <<-EOT cat >$TMPDIR/ctnr/hello.sh <<-EOT
echo 'Hello, World' echo 'Hello, World'
exit 0
EOT EOT
cat >$TMPDIR/ctnr/Dockerfile <<-EOT cat >$TMPDIR/ctnr/Dockerfile <<-EOT
FROM alpine:latest FROM alpine:latest
COPY ./hello.sh /tmp/hello.sh COPY ./hello.sh /tmp/
RUN chmod 755 /tmp/hello.sh RUN chmod 755 /tmp/hello.sh
ENTRYPOINT ["/tmp/hello.sh"] ENTRYPOINT ["/tmp/hello.sh"]
EOT EOT

View File

@ -0,0 +1,4 @@
[VARIABLES]
# Enforce only pep8 variable names
variable-rgx=[a-z0-9_]{1,30}$

View File

@ -1,5 +1,6 @@
PYTHON ?= /usr/bin/python3 PYTHON ?= /usr/bin/python3
DESTDIR := / DESTDIR := /
PODMAN_VERSION ?= '0.0.4'
.PHONY: python-pypodman .PHONY: python-pypodman
python-pypodman: python-pypodman:
@ -17,6 +18,11 @@ integration:
install: install:
$(PYTHON) setup.py install --root ${DESTDIR} $(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 .PHONY: clobber
clobber: uninstall clean clobber: uninstall clean

View File

@ -1,8 +1,18 @@
"""Remote podman client support library.""" """Remote podman client support library."""
from pypodman.lib.action_base import AbstractActionBase 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 from pypodman.lib.report import Report, ReportColumn
# Silence pylint overlording...
assert BooleanAction
assert BooleanValidate
assert PathAction
assert PositiveIntAction
assert UnitAction
__all__ = [ __all__ = [
'AbstractActionBase', 'AbstractActionBase',
'PodmanArgumentParser', 'PodmanArgumentParser',

View File

@ -43,7 +43,12 @@ class AbstractActionBase(abc.ABC):
def __init__(self, args): def __init__(self, args):
"""Construct class.""" """Construct class."""
# Dump all unset arguments before transmitting to service
self._args = args self._args = args
self.opts = {
k: v
for k, v in vars(self._args).items() if v is not None
}
@property @property
def remote_uri(self): def remote_uri(self):

View File

@ -3,7 +3,10 @@ from pypodman.lib.actions.attach_action import Attach
from pypodman.lib.actions.commit_action import Commit from pypodman.lib.actions.commit_action import Commit
from pypodman.lib.actions.create_action import Create from pypodman.lib.actions.create_action import Create
from pypodman.lib.actions.export_action import Export 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.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.inspect_action import Inspect
from pypodman.lib.actions.kill_action import Kill from pypodman.lib.actions.kill_action import Kill
from pypodman.lib.actions.logs_action import Logs 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.port_action import Port
from pypodman.lib.actions.ps_action import Ps from pypodman.lib.actions.ps_action import Ps
from pypodman.lib.actions.pull_action import Pull 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.rm_action import Rm
from pypodman.lib.actions.rmi_action import Rmi 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__ = [ __all__ = [
'Attach', 'Attach',
'Commit', 'Commit',
'Create', 'Create',
'Export', 'Export',
'History',
'Images', 'Images',
'Import',
'Info',
'Inspect', 'Inspect',
'Kill', 'Kill',
'Logs', 'Logs',
@ -29,6 +39,10 @@ __all__ = [
'Port', 'Port',
'Ps', 'Ps',
'Pull', 'Pull',
'Push',
'Restart',
'Rm', 'Rm',
'Rmi', 'Rmi',
'Run',
'Search',
] ]

View File

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

View File

@ -2,7 +2,7 @@
import sys import sys
import podman import podman
from pypodman.lib import AbstractActionBase from pypodman.lib import AbstractActionBase, BooleanAction
class Commit(AbstractActionBase): class Commit(AbstractActionBase):
@ -47,14 +47,14 @@ class Commit(AbstractActionBase):
parser.add_argument( parser.add_argument(
'--pause', '--pause',
'-p', '-p',
choices=('True', 'False'), action=BooleanAction,
default=True, default=True,
type=bool,
help='Pause the container when creating an image', help='Pause the container when creating an image',
) )
parser.add_argument( parser.add_argument(
'--quiet', '--quiet',
'-q', '-q',
action='store_true',
help='Suppress output', help='Suppress output',
) )
parser.add_argument( parser.add_argument(
@ -71,20 +71,24 @@ class Commit(AbstractActionBase):
def __init__(self, args): def __init__(self, args):
"""Construct Commit class.""" """Construct Commit class."""
super().__init__(args)
if not args.container: if not args.container:
raise ValueError('You must supply one container id' raise ValueError('You must supply one container id'
' or name to be used as source.') ' or name to be used as source.')
if not args.image: if not args.image:
raise ValueError('You must supply one image id' raise ValueError('You must supply one image id'
' or name to be created.') ' or name to be created.')
super().__init__(args)
# used only on client
del self.opts['image']
del self.opts['container']
def commit(self): def commit(self):
"""Create image from container.""" """Create image from container."""
try: try:
try: try:
ctnr = self.client.containers.get(self._args.container[0]) ctnr = self.client.containers.get(self._args.container[0])
ident = ctnr.commit(**self._args) ident = ctnr.commit(**self.opts)
print(ident) print(ident)
except podman.ContainerNotFound as e: except podman.ContainerNotFound as e:
sys.stdout.flush() sys.stdout.flush()

View File

@ -1,413 +1,11 @@
"""Remote client command for creating container from image.""" """Remote client command for creating container from image."""
import argparse
import sys import sys
from builtins import vars from builtins import vars
import podman import podman
from pypodman.lib import AbstractActionBase from pypodman.lib import AbstractActionBase
from ._create_args import CreateArguments
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')
class Create(AbstractActionBase): class Create(AbstractActionBase):
@ -419,30 +17,30 @@ class Create(AbstractActionBase):
parser = parent.add_parser( parser = parent.add_parser(
'create', help='create container from image') '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') parser.set_defaults(class_=cls, method='create')
def __init__(self, args): def __init__(self, args):
"""Construct Create class.""" """Construct Create class."""
super().__init__(args) super().__init__(args)
if not args.image:
raise ValueError('You must supply at least one image id' # image id used only on client
' or name to be retrieved.') del self.opts['image']
def create(self): def create(self):
"""Create container.""" """Create container."""
# Dump all unset arguments before transmitting to service try:
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: for ident in self._args.image:
try: try:
img = self.client.images.get(ident) img = self.client.images.get(ident)
img.container(**opts) img.container(**self.opts)
print(ident) print(ident)
except podman.ImageNotFound as e: except podman.ImageNotFound as e:
sys.stdout.flush() sys.stdout.flush()

View File

@ -29,7 +29,6 @@ class Export(AbstractActionBase):
def __init__(self, args): def __init__(self, args):
"""Construct Export class.""" """Construct Export class."""
super().__init__(args)
if not args.container: if not args.container:
raise ValueError('You must supply one container id' raise ValueError('You must supply one container id'
' or name to be used as source.') ' or name to be used as source.')
@ -37,6 +36,7 @@ class Export(AbstractActionBase):
if not args.output: if not args.output:
raise ValueError('You must supply one filename' raise ValueError('You must supply one filename'
' to be created as tarball using --output.') ' to be created as tarball using --output.')
super().__init__(args)
def export(self): def export(self):
"""Create tarball from container filesystem.""" """Create tarball from container filesystem."""

View File

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

View File

@ -65,7 +65,7 @@ class Images(AbstractActionBase):
'created': 'created':
humanize.naturaldate(podman.datetime_parse(image.created)), humanize.naturaldate(podman.datetime_parse(image.created)),
'size': 'size':
humanize.naturalsize(int(image.size)), humanize.naturalsize(int(image.size), binary=True),
'repoDigests': 'repoDigests':
' '.join(image.repoDigests), ' '.join(image.repoDigests),
}) })

View File

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

View File

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

View File

@ -41,7 +41,7 @@ class Inspect(AbstractActionBase):
def _get_container(self, ident): def _get_container(self, ident):
try: try:
logging.debug("Get container %s", ident) logging.debug("Getting container %s", ident)
ctnr = self.client.containers.get(ident) ctnr = self.client.containers.get(ident)
except podman.ContainerNotFound: except podman.ContainerNotFound:
pass pass
@ -50,7 +50,7 @@ class Inspect(AbstractActionBase):
def _get_image(self, ident): def _get_image(self, ident):
try: try:
logging.debug("Get image %s", ident) logging.debug("Getting image %s", ident)
img = self.client.images.get(ident) img = self.client.images.get(ident)
except podman.ImageNotFound: except podman.ImageNotFound:
pass pass

View File

@ -19,7 +19,7 @@ class Kill(AbstractActionBase):
choices=range(1, signal.NSIG), choices=range(1, signal.NSIG),
metavar='[1,{}]'.format(signal.NSIG), metavar='[1,{}]'.format(signal.NSIG),
default=9, default=9,
help='Signal to send to the container. (Default: 9)') help='Signal to send to the container. (default: 9)')
parser.add_argument( parser.add_argument(
'containers', 'containers',
nargs='+', nargs='+',

View File

@ -5,20 +5,7 @@ import sys
from collections import deque from collections import deque
import podman import podman
from pypodman.lib import AbstractActionBase from pypodman.lib import AbstractActionBase, PositiveIntAction
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)
class Logs(AbstractActionBase): class Logs(AbstractActionBase):
@ -32,7 +19,6 @@ class Logs(AbstractActionBase):
'--tail', '--tail',
metavar='LINES', metavar='LINES',
action=PositiveIntAction, action=PositiveIntAction,
type=int,
help='Output the specified number of LINES at the end of the logs') help='Output the specified number of LINES at the end of the logs')
parser.add_argument( parser.add_argument(
'container', 'container',

View File

@ -29,10 +29,10 @@ class Port(AbstractActionBase):
def __init__(self, args): def __init__(self, args):
"""Construct Port class.""" """Construct Port class."""
super().__init__(args)
if not args.all and not args.containers: if not args.all and not args.containers:
ValueError('You must supply at least one' ValueError('You must supply at least one'
' container id or name, or --all.') ' container id or name, or --all.')
super().__init__(args)
def port(self): def port(self):
"""Retrieve ports from containers.""" """Retrieve ports from containers."""

View File

@ -18,10 +18,8 @@ class Ps(AbstractActionBase):
super().subparser(parser) super().subparser(parser)
parser.add_argument( parser.add_argument(
'--sort', '--sort',
choices=[ choices=('createdat', 'id', 'image', 'names', 'runningfor', 'size',
'createdat', 'id', 'image', 'names', 'runningfor', 'size', 'status'),
'status'
],
default='createdat', default='createdat',
type=str.lower, type=str.lower,
help=('Change sort ordered of displayed containers.' help=('Change sort ordered of displayed containers.'

View File

@ -17,7 +17,7 @@ class Pull(AbstractActionBase):
) )
parser.add_argument( parser.add_argument(
'targets', 'targets',
nargs='*', nargs='+',
help='image id(s) to retrieve.', help='image id(s) to retrieve.',
) )
parser.set_defaults(class_=cls, method='pull') parser.set_defaults(class_=cls, method='pull')
@ -25,9 +25,6 @@ class Pull(AbstractActionBase):
def __init__(self, args): def __init__(self, args):
"""Construct Pull class.""" """Construct Pull class."""
super().__init__(args) 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): def pull(self):
"""Retrieve image.""" """Retrieve image."""

View File

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

View File

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

View File

@ -19,15 +19,12 @@ class Rm(AbstractActionBase):
help=('force delete of running container(s).' help=('force delete of running container(s).'
' (default: %(default)s)')) ' (default: %(default)s)'))
parser.add_argument( 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') parser.set_defaults(class_=cls, method='remove')
def __init__(self, args): def __init__(self, args):
"""Construct Rm class.""" """Construct Rm class."""
super().__init__(args) 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): def remove(self):
"""Remove container(s).""" """Remove container(s)."""

View File

@ -18,15 +18,12 @@ class Rmi(AbstractActionBase):
action='store_true', action='store_true',
help=('force delete of image(s) and associated containers.' help=('force delete of image(s) and associated containers.'
' (default: %(default)s)')) ' (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') parser.set_defaults(class_=cls, method='remove')
def __init__(self, args): def __init__(self, args):
"""Construct Rmi class.""" """Construct Rmi class."""
super().__init__(args) 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): def remove(self):
"""Remove image(s).""" """Remove image(s)."""

View File

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

View File

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

View File

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

View File

@ -7,10 +7,13 @@ import logging
import os import os
import sys import sys
from contextlib import suppress from contextlib import suppress
from pathlib import Path
import pkg_resources import pkg_resources
import pytoml import pytoml
from .parser_actions import PathAction, PositiveIntAction
# TODO: setup.py and obtain __version__ from rpm.spec # TODO: setup.py and obtain __version__ from rpm.spec
try: try:
__version__ = pkg_resources.get_distribution('pypodman').version __version__ = pkg_resources.get_distribution('pypodman').version
@ -33,35 +36,14 @@ class HelpFormatter(argparse.RawDescriptionHelpFormatter):
super().__init__(*args, **kwargs) 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): class PodmanArgumentParser(argparse.ArgumentParser):
"""Default remote podman configuration.""" """Default remote podman configuration."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Construct the parser.""" """Construct the parser."""
kwargs['add_help'] = True kwargs['add_help'] = True
kwargs['description'] = __doc__ kwargs['description'] = ('Portable and simple management'
' tool for containers and images')
kwargs['formatter_class'] = HelpFormatter kwargs['formatter_class'] = HelpFormatter
super().__init__(**kwargs) super().__init__(**kwargs)
@ -83,9 +65,9 @@ class PodmanArgumentParser(argparse.ArgumentParser):
'--run-dir', '--run-dir',
metavar='DIRECTORY', metavar='DIRECTORY',
help=('directory to place local socket bindings.' help=('directory to place local socket bindings.'
' (default: XDG_RUNTIME_DIR/pypodman')) ' (default: XDG_RUNTIME_DIR/pypodman)'))
self.add_argument( self.add_argument(
'--user', '--username',
'-l', '-l',
default=getpass.getuser(), default=getpass.getuser(),
help='Authenicating user on remote host. (default: %(default)s)') help='Authenicating user on remote host. (default: %(default)s)')
@ -94,8 +76,7 @@ class PodmanArgumentParser(argparse.ArgumentParser):
self.add_argument( self.add_argument(
'--port', '--port',
'-p', '-p',
type=int, action=PositiveIntAction,
action=PortAction,
help='port for ssh tunnel to remote host. (default: 22)') help='port for ssh tunnel to remote host. (default: 22)')
self.add_argument( self.add_argument(
'--remote-socket-path', '--remote-socket-path',
@ -105,18 +86,17 @@ class PodmanArgumentParser(argparse.ArgumentParser):
self.add_argument( self.add_argument(
'--identity-file', '--identity-file',
'-i', '-i',
metavar='PATH',
action=PathAction, 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( self.add_argument(
'--config-home', '--config-home',
metavar='DIRECTORY', metavar='DIRECTORY',
action=PathAction, action=PathAction,
help=('home of configuration "pypodman.conf".' help=('home of configuration "pypodman.conf".'
' (default: XDG_CONFIG_HOME/pypodman')) ' (default: XDG_CONFIG_HOME/pypodman)'))
actions_parser = self.add_subparsers( actions_parser = self.add_subparsers(
dest='subparser_name', help='actions') dest='subparser_name', help='commands')
# import buried here to prevent import loops # import buried here to prevent import loops
import pypodman.lib.actions # pylint: disable=cyclic-import import pypodman.lib.actions # pylint: disable=cyclic-import
@ -157,11 +137,12 @@ class PodmanArgumentParser(argparse.ArgumentParser):
if dir_ is None: if dir_ is None:
continue continue
with suppress(OSError): with suppress(OSError):
with open(os.path.join(dir_, cnf = Path(dir_, 'pypodman', 'pypodman.conf')
'pypodman/pypodman.conf')) as stream: with cnf.open() as stream:
config.update(pytoml.load(stream)) config.update(pytoml.load(stream))
def reqattr(name, value): def reqattr(name, value):
"""Raise an error if value is unset."""
if value: if value:
setattr(args, name, value) setattr(args, name, value)
return value return value
@ -173,7 +154,7 @@ class PodmanArgumentParser(argparse.ArgumentParser):
getattr(args, 'run_dir') getattr(args, 'run_dir')
or os.environ.get('RUN_DIR') or os.environ.get('RUN_DIR')
or config['default'].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 ) # yapf: disable
setattr( setattr(
@ -185,11 +166,11 @@ class PodmanArgumentParser(argparse.ArgumentParser):
) # yapf:disable ) # yapf:disable
reqattr( reqattr(
'user', 'username',
getattr(args, 'user') getattr(args, 'username')
or os.environ.get('USER') or os.environ.get('USER')
or os.environ.get('LOGNAME') or os.environ.get('LOGNAME')
or config['default'].get('user') or config['default'].get('username')
or getpass.getuser() or getpass.getuser()
) # yapf:disable ) # yapf:disable
@ -215,22 +196,21 @@ class PodmanArgumentParser(argparse.ArgumentParser):
getattr(args, 'identity_file') getattr(args, 'identity_file')
or os.environ.get('IDENTITY_FILE') or os.environ.get('IDENTITY_FILE')
or config['default'].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 ) # yapf:disable
if not os.path.isfile(args.identity_file): if not os.path.isfile(args.identity_file):
args.identity_file = None args.identity_file = None
if args.host: if args.host:
args.local_socket_path = os.path.join(args.run_dir, args.local_socket_path = Path(args.run_dir, 'podman.socket')
"podman.socket")
else: else:
args.local_socket_path = args.remote_socket_path 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: if args.host:
components = ['ssh://', args.user, '@', args.host] components = ['ssh://', args.username, '@', args.host]
if args.port: if args.port:
components.extend((':', str(args.port))) components.extend((':', str(args.port)))
components.append(args.remote_socket_path) components.append(args.remote_socket_path)

View File

@ -53,7 +53,7 @@ class Report():
fmt = [] fmt = []
for key in keys: 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)) data_len = len(max(slice_, key=len))
info = self._columns.get(key, info = self._columns.get(key,

View File

@ -1,4 +1,5 @@
humanize humanize
podman podman
pytoml pytoml
PyYAML
setuptools>=39 setuptools>=39