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

View File

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

View File

@ -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'])
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']
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
return self
def refresh(self):
"""Refresh status fields for this container."""

View File

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

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

View File

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

View File

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

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.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',
]

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

View File

@ -1,413 +1,11 @@
"""Remote client command for creating container from image."""
import argparse
import sys
from builtins import vars
import podman
from pypodman.lib import AbstractActionBase
class UnitAction(argparse.Action):
"""Validate number given is positive integer, with optional suffix."""
def __call__(self, parser, namespace, values, option_string=None):
"""Validate input."""
if isinstance(values, str):
if not values[:-1].isdigit():
msg = 'unit must be a positive integer, with optional suffix'
raise argparse.ArgumentError(self, msg)
if not values[-1] in ('b', 'k', 'm', 'g'):
msg = 'unit only supports suffices of: b, k, m, g'
raise argparse.ArgumentError(self, msg)
elif values <= 0:
msg = 'number must be a positive integer.'
raise argparse.ArgumentError(self, msg)
setattr(namespace, self.dest, values)
def add_options(parser):
"""Add options for Create command."""
parser.add_argument(
'--add-host',
action='append',
metavar='HOST',
help=('Add a line to /etc/hosts. The format is hostname:ip.'
' The option can be set multiple times.'),
)
parser.add_argument(
'--attach',
'-a',
action='append',
metavar='FD',
help=('Attach to STDIN, STDOUT or STDERR. The option can be set'
' for each of stdin, stdout, and stderr.'))
parser.add_argument(
'--annotation',
action='append',
help=('Add an annotation to the container. The format is'
' key=value. The option can be set multiple times.'))
parser.add_argument(
'--blkio-weight',
choices=range(10, 1000),
metavar='[10-1000]',
help=('Block IO weight (relative weight) accepts a'
' weight value between 10 and 1000.'))
parser.add_argument(
'--blkio-weight-device',
action='append',
metavar='WEIGHT',
help=('Block IO weight (relative device weight,'
' format: DEVICE_NAME:WEIGHT).'))
parser.add_argument(
'--cap-add',
action='append',
metavar='CAP',
help=('Add Linux capabilities'
'The option can be set multiple times.'))
parser.add_argument(
'--cap-drop',
action='append',
metavar='CAP',
help=('Drop Linux capabilities'
'The option can be set multiple times.'))
parser.add_argument(
'--cgroup-parent',
metavar='PATH',
help=('Path to cgroups under which the cgroup for the'
' container will be created. If the path is not'
' absolute, the path is considered to be relative'
' to the cgroups path of the init process. Cgroups'
' will be created if they do not already exist.'))
parser.add_argument(
'--cidfile',
metavar='PATH',
help='Write the container ID to the file, on the remote host.')
parser.add_argument(
'--conmon-pidfile',
metavar='PATH',
help=('Write the pid of the conmon process to a file,'
' on the remote host.'))
parser.add_argument(
'--cpu-count',
type=int,
metavar='COUNT',
help=('Limit the number of CPUs available'
' for execution by the container.'))
parser.add_argument(
'--cpu-period',
type=int,
metavar='PERIOD',
help=('Limit the CPU CFS (Completely Fair Scheduler) period.'))
parser.add_argument(
'--cpu-quota',
type=int,
metavar='QUOTA',
help=('Limit the CPU CFS (Completely Fair Scheduler) quota.'))
parser.add_argument(
'--cpu-rt-period',
type=int,
metavar='PERIOD',
help=('Limit the CPU real-time period in microseconds.'))
parser.add_argument(
'--cpu-rt-runtime',
type=int,
metavar='LIMIT',
help=('Limit the CPU real-time runtime in microseconds.'))
parser.add_argument(
'--cpu-shares',
type=int,
metavar='SHARES',
help=('CPU shares (relative weight)'))
parser.add_argument(
'--cpus',
type=int,
help=('Number of CPUs. The default is 0 which means no limit'))
parser.add_argument(
'--cpuset-cpus',
metavar='LIST',
help=('CPUs in which to allow execution (0-3, 0,1)'))
parser.add_argument(
'--cpuset-mems',
metavar='NODES',
help=('Memory nodes (MEMs) in which to allow execution (0-3, 0,1).'
' Only effective on NUMA systems'))
parser.add_argument(
'--detach',
'-d',
choices=['True', 'False'],
help=('Detached mode: run the container in the background and'
' print the new container ID. The default is false.'))
parser.add_argument(
'--detach-keys',
metavar='KEY(s)',
help=('Override the key sequence for detaching a container.'
' Format is a single character [a-Z] or ctrl-<value> where'
' <value> is one of: a-z, @, ^, [, , or _.'))
parser.add_argument(
'--device',
action='append',
help=('Add a host device to the container'
'The option can be set multiple times.'),
)
parser.add_argument(
'--device-read-bps',
action='append',
metavar='LIMIT',
help=('Limit read rate (bytes per second) from a device'
' (e.g. --device-read-bps=/dev/sda:1mb)'
'The option can be set multiple times.'),
)
parser.add_argument(
'--device-read-iops',
action='append',
metavar='LIMIT',
help=('Limit read rate (IO per second) from a device'
' (e.g. --device-read-iops=/dev/sda:1000)'
'The option can be set multiple times.'),
)
parser.add_argument(
'--device-write-bps',
action='append',
metavar='LIMIT',
help=('Limit write rate (bytes per second) to a device'
' (e.g. --device-write-bps=/dev/sda:1mb)'
'The option can be set multiple times.'),
)
parser.add_argument(
'--device-write-iops',
action='append',
metavar='LIMIT',
help=('Limit write rate (IO per second) to a device'
' (e.g. --device-write-iops=/dev/sda:1000)'
'The option can be set multiple times.'),
)
parser.add_argument(
'--dns',
action='append',
metavar='SERVER',
help=('Set custom DNS servers.'
'The option can be set multiple times.'),
)
parser.add_argument(
'--dns-option',
action='append',
metavar='OPT',
help=('Set custom DNS options.'
'The option can be set multiple times.'),
)
parser.add_argument(
'--dns-search',
action='append',
metavar='DOMAIN',
help=('Set custom DNS search domains.'
'The option can be set multiple times.'),
)
parser.add_argument(
'--entrypoint',
help=('Overwrite the default ENTRYPOINT of the image.'),
)
parser.add_argument(
'--env',
'-e',
action='append',
help=('Set environment variables.'),
)
parser.add_argument(
'--env-file',
help=('Read in a line delimited file of environment variables,'
' on the remote host.'),
)
parser.add_argument(
'--expose',
metavar='PORT(s)',
help=('Expose a port, or a range of ports'
' (e.g. --expose=3300-3310) to set up port redirection.'),
)
parser.add_argument(
'--gidmap',
metavar='MAP',
help=('GID map for the user namespace'),
)
parser.add_argument(
'--group-add',
action='append',
metavar='GROUP',
help=('Add additional groups to run as'))
parser.add_argument('--hostname', help='Container host name')
volume_group = parser.add_mutually_exclusive_group()
volume_group.add_argument(
'--image-volume',
choices=['bind', 'tmpfs', 'ignore'],
metavar='MODE',
help='Tells podman how to handle the builtin image volumes')
volume_group.add_argument(
'--builtin-volume',
choices=['bind', 'tmpfs', 'ignore'],
metavar='MODE',
help='Tells podman how to handle the builtin image volumes')
parser.add_argument(
'--interactive',
'-i',
choices=['True', 'False'],
help='Keep STDIN open even if not attached. The default is false')
parser.add_argument('--ipc', help='Create namespace')
parser.add_argument(
'--kernel-memory',
action=UnitAction,
metavar='UNIT',
help=('Kernel memory limit (format: <number>[<unit>],'
' where unit = b, k, m or g)'))
parser.add_argument(
'--label',
'-l',
help=('Add metadata to a container'
' (e.g., --label com.example.key=value)'))
parser.add_argument(
'--label-file', help='Read in a line delimited file of labels')
parser.add_argument(
'--log-driver',
choices=['json-file', 'journald'],
help='Logging driver for the container.')
parser.add_argument(
'--log-opt', action='append', help='Logging driver specific options')
parser.add_argument(
'--mac-address', help='Container MAC address (e.g. 92:d0:c6:0a:29:33)')
parser.add_argument(
'--memory',
'-m',
action=UnitAction,
metavar='UNIT',
help='Memory limit (format: [], where unit = b, k, m or g)')
parser.add_argument(
'--memory-reservation',
action=UnitAction,
metavar='UNIT',
help='Memory soft limit (format: [], where unit = b, k, m or g)')
parser.add_argument(
'--memory-swap',
action=UnitAction,
metavar='UNIT',
help=('A limit value equal to memory plus swap.'
'Must be used with the --memory flag'))
parser.add_argument(
'--memory-swappiness',
choices=range(0, 100),
metavar='[0-100]',
help="Tune a container's memory swappiness behavior")
parser.add_argument('--name', help='Assign a name to the container')
parser.add_argument(
'--network',
metavar='BRIDGE',
help=('Set the Network mode for the container.'))
parser.add_argument(
'--oom-kill-disable',
choices=['True', 'False'],
help='Whether to disable OOM Killer for the container or not')
parser.add_argument(
'--oom-score-adj',
choices=range(-1000, 1000),
metavar='[-1000-1000]',
help="Tune the host's OOM preferences for containers")
parser.add_argument('--pid', help='Set the PID mode for the container')
parser.add_argument(
'--pids-limit',
type=int,
metavar='LIMIT',
help=("Tune the container's pids limit."
" Set -1 to have unlimited pids for the container."))
parser.add_argument('--pod', help='Run container in an existing pod')
parser.add_argument(
'--privileged',
choices=['True', 'False'],
help='Give extended privileges to this container.')
parser.add_argument(
'--publish',
'-p',
metavar='PORT(s)',
help="Publish a container's port, or range of ports, to the host")
parser.add_argument(
'--publish-all',
'-P',
action='store_true',
help=("Publish all exposed ports to random"
" ports on the host interfaces"))
parser.add_argument(
'--quiet',
'-q',
action='store_true',
help='Suppress output information when pulling images')
parser.add_argument(
'--read-only',
choices=['True', 'False'],
help="Mount the container's root filesystem as read only.")
parser.add_argument(
'--rm',
choices=['True', 'False'],
help='Automatically remove the container when it exits.')
parser.add_argument(
'--rootfs',
action='store_true',
help=('If specified, the first argument refers to an'
' exploded container on the file system of remote host.'))
parser.add_argument(
'--security-opt',
action='append',
metavar='OPT',
help='Set security options.')
parser.add_argument(
'--shm-size',
action=UnitAction,
metavar='UNIT',
help='Size of /dev/shm')
parser.add_argument(
'--stop-signal', metavar='SIGTERM', help='Signal to stop a container')
parser.add_argument(
'--stop-timeout',
metavar='TIMEOUT',
help='Seconds to wait on stopping container.')
parser.add_argument(
'--subgidname',
metavar='MAP',
help='Name for GID map from the /etc/subgid file')
parser.add_argument(
'--subuidname',
metavar='MAP',
help='Name for UID map from the /etc/subuid file')
parser.add_argument(
'--sysctl',
action='append',
help='Configure namespaced kernel parameters at runtime')
parser.add_argument('--tmpfs', help='Create a tmpfs mount')
parser.add_argument(
'--tty',
'-t',
choices=['True', 'False'],
help='Allocate a pseudo-TTY for standard input of container.')
parser.add_argument(
'--uidmap', metavar='MAP', help='UID map for the user namespace')
parser.add_argument('--ulimit', metavar='OPT', help='Ulimit options')
parser.add_argument(
'--user',
'-u',
help=('Sets the username or UID used and optionally'
' the groupname or GID for the specified command.'))
parser.add_argument(
'--userns',
choices=['host', 'ns'],
help='Set the usernamespace mode for the container')
parser.add_argument(
'--uts',
choices=['host', 'ns'],
help='Set the UTS mode for the container')
parser.add_argument('--volume', '-v', help='Create a bind mount.')
parser.add_argument(
'--volumes-from',
action='append',
help='Mount volumes from the specified container(s).')
parser.add_argument(
'--workdir', '-w', help='Working directory inside the container')
from ._create_args import CreateArguments
class Create(AbstractActionBase):
@ -419,40 +17,40 @@ class Create(AbstractActionBase):
parser = parent.add_parser(
'create', help='create container from image')
add_options(parser)
CreateArguments.add_arguments(parser)
parser.add_argument('image', nargs='*', help='source image id.')
parser.add_argument('image', nargs=1, help='source image id')
parser.add_argument(
'command',
nargs='*',
help='command and args to run.',
)
parser.set_defaults(class_=cls, method='create')
def __init__(self, args):
"""Construct Create class."""
super().__init__(args)
if not args.image:
raise ValueError('You must supply at least one image id'
' or name to be retrieved.')
# image id used only on client
del self.opts['image']
def create(self):
"""Create container."""
# Dump all unset arguments before transmitting to service
opts = {k: v for k, v in vars(self._args).items() if v is not None}
# image id(s) used only on client
del opts['image']
for ident in self._args.image:
try:
img = self.client.images.get(ident)
img.container(**opts)
print(ident)
except podman.ImageNotFound as e:
sys.stdout.flush()
print(
'Image {} not found.'.format(e.name),
file=sys.stderr,
flush=True)
except podman.ErrorOccurred as e:
sys.stdout.flush()
print(
'{}'.format(e.reason).capitalize(),
file=sys.stderr,
flush=True)
try:
for ident in self._args.image:
try:
img = self.client.images.get(ident)
img.container(**self.opts)
print(ident)
except podman.ImageNotFound as e:
sys.stdout.flush()
print(
'Image {} not found.'.format(e.name),
file=sys.stderr,
flush=True)
except podman.ErrorOccurred as e:
sys.stdout.flush()
print(
'{}'.format(e.reason).capitalize(),
file=sys.stderr,
flush=True)

View File

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

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':
humanize.naturaldate(podman.datetime_parse(image.created)),
'size':
humanize.naturalsize(int(image.size)),
humanize.naturalsize(int(image.size), binary=True),
'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):
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

View File

@ -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='+',

View File

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

View File

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

View File

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

View File

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

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).'
' (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)."""

View File

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

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

View File

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

View File

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