mirror of
https://github.com/containers/podman.git
synced 2025-06-19 08:09:12 +08:00
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:
@ -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
|
||||
|
||||
|
@ -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'])
|
||||
|
||||
|
@ -3,6 +3,7 @@ import collections
|
||||
import functools
|
||||
import getpass
|
||||
import json
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
|
||||
@ -32,8 +33,17 @@ class Container(AttachMixin, StartMixin, collections.UserDict):
|
||||
"""Get items from parent dict."""
|
||||
return super().__getitem__(key)
|
||||
|
||||
def _refresh(self, podman):
|
||||
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():
|
||||
|
@ -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
|
||||
|
4
contrib/python/pypodman/.pylintrc
Normal file
4
contrib/python/pypodman/.pylintrc
Normal file
@ -0,0 +1,4 @@
|
||||
[VARIABLES]
|
||||
|
||||
# Enforce only pep8 variable names
|
||||
variable-rgx=[a-z0-9_]{1,30}$
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
]
|
||||
|
394
contrib/python/pypodman/pypodman/lib/actions/_create_args.py
Normal file
394
contrib/python/pypodman/pypodman/lib/actions/_create_args.py
Normal 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')
|
@ -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()
|
||||
|
@ -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,30 +17,30 @@ 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']
|
||||
|
||||
try:
|
||||
for ident in self._args.image:
|
||||
try:
|
||||
img = self.client.images.get(ident)
|
||||
img.container(**opts)
|
||||
img.container(**self.opts)
|
||||
print(ident)
|
||||
except podman.ImageNotFound as e:
|
||||
sys.stdout.flush()
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
@ -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),
|
||||
})
|
||||
|
@ -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
|
49
contrib/python/pypodman/pypodman/lib/actions/info_action.py
Normal file
49
contrib/python/pypodman/pypodman/lib/actions/info_action.py
Normal 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)
|
@ -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
|
||||
|
@ -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='+',
|
||||
|
@ -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',
|
||||
|
@ -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."""
|
||||
|
@ -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.'
|
||||
|
@ -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."""
|
||||
|
56
contrib/python/pypodman/pypodman/lib/actions/push_action.py
Normal file
56
contrib/python/pypodman/pypodman/lib/actions/push_action.py
Normal 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)
|
@ -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)
|
@ -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)."""
|
||||
|
@ -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)."""
|
||||
|
73
contrib/python/pypodman/pypodman/lib/actions/run_action.py
Normal file
73
contrib/python/pypodman/pypodman/lib/actions/run_action.py
Normal 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)
|
160
contrib/python/pypodman/pypodman/lib/actions/search_action.py
Normal file
160
contrib/python/pypodman/pypodman/lib/actions/search_action.py
Normal 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)
|
185
contrib/python/pypodman/pypodman/lib/parser_actions.py
Normal file
185
contrib/python/pypodman/pypodman/lib/parser_actions.py
Normal 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)))
|
@ -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)
|
@ -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,
|
||||
|
@ -1,4 +1,5 @@
|
||||
humanize
|
||||
podman
|
||||
pytoml
|
||||
PyYAML
|
||||
setuptools>=39
|
||||
|
Reference in New Issue
Block a user