mirror of
https://github.com/containers/podman.git
synced 2025-07-03 09:17:15 +08:00
Move python code from contrib to it's own repo python-podman
Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
12
.papr.sh
12
.papr.sh
@ -25,14 +25,12 @@ pwd
|
||||
# -t integration test
|
||||
# -u unit test
|
||||
# -v validate
|
||||
# -p run python tests
|
||||
|
||||
build=0
|
||||
install=0
|
||||
integrationtest=0
|
||||
unittest=0
|
||||
validate=0
|
||||
runpython=0
|
||||
options=0
|
||||
install_tools_made=0
|
||||
|
||||
@ -44,9 +42,6 @@ while getopts "biptuv" opt; do
|
||||
i) install=1
|
||||
options=1
|
||||
;;
|
||||
p) runpython=1
|
||||
options=1
|
||||
;;
|
||||
t) integrationtest=1
|
||||
options=1
|
||||
;;
|
||||
@ -124,18 +119,11 @@ if [ $install -eq 1 ]; then
|
||||
make TAGS="${TAGS}" install.man PREFIX=/usr ETCDIR=/etc
|
||||
make TAGS="${TAGS}" install.cni PREFIX=/usr ETCDIR=/etc
|
||||
make TAGS="${TAGS}" install.systemd PREFIX=/usr ETCDIR=/etc
|
||||
if [ $runpython -eq 1 ]; then
|
||||
make TAGS="${TAGS}" install.python PREFIX=/usr ETCDIR=/etc
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# Run integration tests
|
||||
if [ $integrationtest -eq 1 ]; then
|
||||
make TAGS="${TAGS}" test-binaries
|
||||
make varlink_generate GOPATH=/go
|
||||
if [ $runpython -eq 1 ]; then
|
||||
make clientintegration GOPATH=/go
|
||||
fi
|
||||
make ginkgo GOPATH=/go $INTEGRATION_TEST_ENVS
|
||||
fi
|
||||
|
@ -4,10 +4,8 @@ set -xeuo pipefail
|
||||
DIST=${DIST:=Fedora}
|
||||
CONTAINER_RUNTIME=${CONTAINER_RUNTIME:=docker}
|
||||
IMAGE=fedorapodmanbuild
|
||||
PYTHON=python3
|
||||
if [[ ${DIST} != "Fedora" ]]; then
|
||||
IMAGE=centospodmanbuild
|
||||
PYTHON=python
|
||||
fi
|
||||
|
||||
# Since CRIU 3.11 has been pushed to Fedora 28 the checkpoint/restore
|
||||
@ -21,4 +19,4 @@ modprobe iptable_nat || :
|
||||
${CONTAINER_RUNTIME} build -t ${IMAGE} -f Dockerfile.${DIST} . 2>build.log
|
||||
|
||||
# Run the tests
|
||||
${CONTAINER_RUNTIME} run --rm --privileged --net=host -v $PWD:/go/src/github.com/containers/libpod:Z --workdir /go/src/github.com/containers/libpod -e CGROUP_MANAGER=cgroupfs -e PYTHON=$PYTHON -e STORAGE_OPTIONS="--storage-driver=vfs" -e CRIO_ROOT="/go/src/github.com/containers/libpod" -e PODMAN_BINARY="/usr/bin/podman" -e CONMON_BINARY="/usr/libexec/podman/conmon" -e DIST=$DIST -e CONTAINER_RUNTIME=$CONTAINER_RUNTIME $IMAGE sh ./.papr.sh -b -i -t
|
||||
${CONTAINER_RUNTIME} run --rm --privileged --net=host -v $PWD:/go/src/github.com/containers/libpod:Z --workdir /go/src/github.com/containers/libpod -e CGROUP_MANAGER=cgroupfs -e STORAGE_OPTIONS="--storage-driver=vfs" -e CRIO_ROOT="/go/src/github.com/containers/libpod" -e PODMAN_BINARY="/usr/bin/podman" -e CONMON_BINARY="/usr/libexec/podman/conmon" -e DIST=$DIST -e CONTAINER_RUNTIME=$CONTAINER_RUNTIME $IMAGE sh ./.papr.sh -b -i -t
|
||||
|
@ -27,7 +27,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libudev-dev \
|
||||
protobuf-c-compiler \
|
||||
protobuf-compiler \
|
||||
python-minimal \
|
||||
libglib2.0-dev \
|
||||
libapparmor-dev \
|
||||
btrfs-tools \
|
||||
@ -37,11 +36,6 @@ RUN apt-get update && apt-get install -y \
|
||||
liblzma-dev \
|
||||
netcat \
|
||||
socat \
|
||||
python3-pip \
|
||||
python3-dateutil \
|
||||
python3-setuptools \
|
||||
python3-psutil \
|
||||
python3-pytoml \
|
||||
lsof \
|
||||
xz-utils \
|
||||
--no-install-recommends \
|
||||
@ -129,9 +123,6 @@ COPY cni/87-podman-bridge.conflist /etc/cni/net.d/87-podman-bridge.conflist
|
||||
# Make sure we have some policy for pulling images
|
||||
RUN mkdir -p /etc/containers && curl https://raw.githubusercontent.com/projectatomic/registries/master/registries.fedora -o /etc/containers/registries.conf
|
||||
|
||||
# Install python3 varlink module from pypi
|
||||
RUN pip3 install varlink
|
||||
|
||||
COPY test/policy.json /etc/containers/policy.json
|
||||
COPY test/redhat_sigstore.yaml /etc/containers/registries.d/registry.access.redhat.com.yaml
|
||||
|
||||
|
@ -19,10 +19,6 @@ RUN yum -y install btrfs-progs-devel \
|
||||
runc \
|
||||
make \
|
||||
ostree-devel \
|
||||
python \
|
||||
python3-dateutil \
|
||||
python3-psutil \
|
||||
python3-pytoml \
|
||||
lsof \
|
||||
which\
|
||||
golang-github-cpuguy83-go-md2man \
|
||||
|
@ -20,10 +20,6 @@ RUN dnf -y install btrfs-progs-devel \
|
||||
runc \
|
||||
make \
|
||||
ostree-devel \
|
||||
python \
|
||||
python3-dateutil \
|
||||
python3-psutil \
|
||||
python3-pytoml \
|
||||
lsof \
|
||||
which\
|
||||
golang-github-cpuguy83-go-md2man \
|
||||
|
29
Makefile
29
Makefile
@ -26,8 +26,6 @@ ifneq (,$(findstring varlink,$(BUILDTAGS)))
|
||||
endif
|
||||
CONTAINER_RUNTIME := $(shell command -v podman 2> /dev/null || echo docker)
|
||||
|
||||
HAS_PYTHON3 := $(shell command -v python3 2>/dev/null)
|
||||
|
||||
BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
|
||||
OCIUMOUNTINSTALLDIR=$(PREFIX)/share/oci-umount/oci-umount.d
|
||||
|
||||
@ -88,9 +86,6 @@ endif
|
||||
lint: .gopathok varlink_generate
|
||||
@echo "checking lint"
|
||||
@./.tool/lint
|
||||
# Not ready
|
||||
# @$(MAKE) -C contrib/python/podman lint
|
||||
# @$(MAKE) -C contrib/python/pypodman lint
|
||||
|
||||
gofmt:
|
||||
find . -name '*.go' ! -path './vendor/*' -exec gofmt -s -w {} \+
|
||||
@ -122,12 +117,6 @@ bin/podman.cross.%: .gopathok
|
||||
GOARCH="$${TARGET##*.}" \
|
||||
$(GO) build -ldflags '$(LDFLAGS_PODMAN)' -tags '$(BUILDTAGS_CROSS)' -o "$@" $(PROJECT)/cmd/podman
|
||||
|
||||
python:
|
||||
ifdef HAS_PYTHON3
|
||||
$(MAKE) -C contrib/python/podman python-podman
|
||||
$(MAKE) -C contrib/python/pypodman python-pypodman
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -rf \
|
||||
.gopathok \
|
||||
@ -147,8 +136,6 @@ clean:
|
||||
$(MANPAGES) ||:
|
||||
find . -name \*~ -delete
|
||||
find . -name \#\* -delete
|
||||
$(MAKE) -C contrib/python/podman clean
|
||||
$(MAKE) -C contrib/python/pypodman clean
|
||||
|
||||
libpodimage:
|
||||
${CONTAINER_RUNTIME} build -t ${LIBPOD_IMAGE} .
|
||||
@ -181,7 +168,7 @@ localunit: test/goecho/goecho varlink_generate
|
||||
ginkgo:
|
||||
ginkgo -v -tags "$(BUILDTAGS)" -cover -flakeAttempts 3 -progress -trace -noColor test/e2e/.
|
||||
|
||||
localintegration: varlink_generate test-binaries clientintegration ginkgo
|
||||
localintegration: varlink_generate test-binaries ginkgo
|
||||
|
||||
localsystem: .install.ginkgo .install.gomega
|
||||
ginkgo -v -noColor test/system/
|
||||
@ -189,10 +176,6 @@ localsystem: .install.ginkgo .install.gomega
|
||||
system.test-binary: .install.ginkgo .install.gomega
|
||||
$(GO) test -c ./test/system
|
||||
|
||||
clientintegration:
|
||||
$(MAKE) -C contrib/python/podman integration
|
||||
$(MAKE) -C contrib/python/pypodman integration
|
||||
|
||||
perftest:
|
||||
$ cd contrib/perftest;go build
|
||||
|
||||
@ -230,7 +213,7 @@ changelog:
|
||||
$(shell cat $(TMPFILE) >> changelog.txt)
|
||||
$(shell rm $(TMPFILE))
|
||||
|
||||
install: .gopathok install.bin install.man install.cni install.systemd install.python
|
||||
install: .gopathok install.bin install.man install.cni install.systemd
|
||||
|
||||
install.bin:
|
||||
install ${SELINUXOPT} -d -m 755 $(BINDIR)
|
||||
@ -268,10 +251,6 @@ install.systemd:
|
||||
install ${SELINUXOPT} -m 644 contrib/varlink/io.podman.service ${SYSTEMDDIR}/io.podman.service
|
||||
install ${SELINUXOPT} -m 644 contrib/varlink/podman.conf ${TMPFILESDIR}/podman.conf
|
||||
|
||||
install.python:
|
||||
$(MAKE) DESTDIR=${DESTDIR} -C contrib/python/podman install
|
||||
$(MAKE) DESTDIR=${DESTDIR} -C contrib/python/pypodman install
|
||||
|
||||
uninstall:
|
||||
for i in $(filter %.1,$(MANPAGES)); do \
|
||||
rm -f $(MANDIR)/man1/$$(basename $${i}); \
|
||||
@ -279,8 +258,6 @@ uninstall:
|
||||
for i in $(filter %.5,$(MANPAGES)); do \
|
||||
rm -f $(MANDIR)/man5/$$(basename $${i}); \
|
||||
done
|
||||
$(MAKE) -C contrib/python/pypodman uninstall
|
||||
$(MAKE) -C contrib/python/podman uninstall
|
||||
|
||||
.PHONY: .gitvalidation
|
||||
.gitvalidation: .gopathok
|
||||
@ -382,5 +359,3 @@ build-all-new-commits:
|
||||
changelog \
|
||||
validate \
|
||||
install.libseccomp.sudo \
|
||||
python \
|
||||
clientintegration
|
||||
|
5
contrib/python/.gitignore
vendored
5
contrib/python/.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
build
|
||||
dist
|
||||
*.egg-info
|
||||
*.pyc
|
||||
__pycache__
|
@ -1,564 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=0
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
locally-enabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
#argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
argument-rgx=[a-z_][a-z0-9_]{1,30}$
|
||||
argument-name-hint=[a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=c,
|
||||
e,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
r,
|
||||
v,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
#variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Support argparse.Action constructor API
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=12
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=10
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception".
|
||||
overgeneral-exceptions=Exception
|
@ -1 +0,0 @@
|
||||
v0.1.0, 2018-05-11 -- Initial release.
|
@ -1,13 +0,0 @@
|
||||
Copyright 2018 Red Hat, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,3 +0,0 @@
|
||||
prune test/
|
||||
include README.md
|
||||
include requirements.txt
|
@ -1,40 +0,0 @@
|
||||
PYTHON ?= $(shell command -v python3 2>/dev/null || command -v python)
|
||||
DESTDIR ?= /
|
||||
PODMAN_VERSION ?= '0.11.1.1'
|
||||
|
||||
.PHONY: python-podman
|
||||
python-podman:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) \
|
||||
$(PYTHON) setup.py sdist bdist
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(PYTHON) -m pylint podman
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
test/test_runner.sh -v
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) \
|
||||
$(PYTHON) setup.py install --root ${DESTDIR}
|
||||
|
||||
.PHONY: upload
|
||||
upload:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) $(PYTHON) setup.py sdist bdist_wheel
|
||||
twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
|
||||
.PHONY: clobber
|
||||
clobber: uninstall clean
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
$(PYTHON) -m pip uninstall --yes podman ||:
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf podman.egg-info dist
|
||||
find . -depth -name __pycache__ -exec rm -rf {} \;
|
||||
find . -depth -name \*.pyc -exec rm -f {} \;
|
||||
$(PYTHON) ./setup.py clean --all
|
@ -1,44 +0,0 @@
|
||||
# podman - pythonic library for working with varlink interface to Podman
|
||||
|
||||
## Status: Active Development
|
||||
|
||||
See [libpod](https://github.com/containers/libpod)
|
||||
|
||||
## Releases
|
||||
|
||||
To build the podman egg and install as user:
|
||||
|
||||
```sh
|
||||
cd ~/libpod/contrib/python/podman
|
||||
python3 setup.py clean -a && python3 setup.py sdist bdist
|
||||
python3 setup.py install --user
|
||||
```
|
||||
|
||||
## Code snippets/examples:
|
||||
|
||||
### Show images in storage
|
||||
|
||||
```python
|
||||
import podman
|
||||
|
||||
with podman.Client() as client:
|
||||
list(map(print, client.images.list()))
|
||||
```
|
||||
|
||||
### Show containers created since midnight
|
||||
|
||||
```python
|
||||
from datetime import datetime, time, timezone
|
||||
|
||||
import podman
|
||||
|
||||
midnight = datetime.combine(datetime.today(), time.min, tzinfo=timezone.utc)
|
||||
|
||||
with podman.Client() as client:
|
||||
for c in client.containers.list():
|
||||
created_at = podman.datetime_parse(c.createdat)
|
||||
|
||||
if created_at > midnight:
|
||||
print('Container {}: image: {} created at: {}'.format(
|
||||
c.id[:12], c.image[:32], podman.datetime_format(created_at)))
|
||||
```
|
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Run top on Alpine container."""
|
||||
|
||||
import podman
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
with podman.Client() as client:
|
||||
id = client.images.pull('alpine:latest')
|
||||
img = client.images.get(id)
|
||||
cntr = img.create(detach=True, tty=True, command=['/usr/bin/top'])
|
||||
cntr.attach(eot=4)
|
||||
|
||||
try:
|
||||
cntr.start()
|
||||
print()
|
||||
except (BrokenPipeError, KeyboardInterrupt):
|
||||
print('\nContainer disconnected.')
|
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Show containers grouped by image id."""
|
||||
|
||||
from itertools import groupby
|
||||
|
||||
import podman
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
with podman.Client() as client:
|
||||
ctnrs = sorted(client.containers.list(), key=lambda k: k.imageid)
|
||||
for key, grp in groupby(ctnrs, key=lambda k: k.imageid):
|
||||
print('Image: {}'.format(key))
|
||||
for c in grp:
|
||||
print(' : container: {} created at: {}'.format(
|
||||
c.id[:12], podman.datetime_format(c.createdat)))
|
@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Show all images on system."""
|
||||
|
||||
import podman
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
with podman.Client() as client:
|
||||
for img in client.images.list():
|
||||
print(img)
|
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Pull Fedora and inspect image and container."""
|
||||
|
||||
import podman
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
with podman.Client() as client:
|
||||
id = client.images.pull('registry.fedoraproject.org/fedora:28')
|
||||
img = client.images.get(id)
|
||||
print(img.inspect())
|
||||
|
||||
cntr = img.create()
|
||||
print(cntr.inspect())
|
||||
|
||||
cntr.remove()
|
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Show all containers created since midnight."""
|
||||
|
||||
from datetime import datetime, time, timezone
|
||||
|
||||
import podman
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
|
||||
midnight = datetime.combine(datetime.today(), time.min, tzinfo=timezone.utc)
|
||||
|
||||
with podman.Client() as client:
|
||||
for c in client.containers.list():
|
||||
created_at = podman.datetime_parse(c.createdat)
|
||||
|
||||
if created_at > midnight:
|
||||
print('{}: image: {} createdAt: {}'.format(
|
||||
c.id[:12], c.image[:32], podman.datetime_format(created_at)))
|
@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Create new image from container."""
|
||||
|
||||
import sys
|
||||
|
||||
import podman
|
||||
|
||||
|
||||
def print_history(details):
|
||||
"""Format history data from an image, in a table."""
|
||||
for i, r in enumerate(details):
|
||||
print(
|
||||
'{}: {} {} {}'.format(i, r.id[:12],
|
||||
podman.datetime_format(r.created), r.tags),
|
||||
sep='\n')
|
||||
print("-" * 25)
|
||||
|
||||
|
||||
print('{}\n'.format(__doc__))
|
||||
|
||||
with podman.Client() as client:
|
||||
ctnr = next(
|
||||
(c for c in client.containers.list() if 'alpine' in c['image']), None)
|
||||
|
||||
if ctnr:
|
||||
print_history(client.images.get(ctnr.imageid).history())
|
||||
|
||||
# Make changes as we save the container to a new image
|
||||
id = ctnr.commit('alpine-ash', changes=['CMD=/bin/ash'])
|
||||
print_history(client.images.get(id).history())
|
||||
else:
|
||||
print('Unable to find "alpine" container.', file=sys.stderr)
|
@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PYTHONPATH=..
|
||||
|
||||
function examples {
|
||||
for file in $@; do
|
||||
python3 -c "import ast; f=open('"${file}"'); t=ast.parse(f.read()); print(ast.get_docstring(t) + ' -- "${file}"')"
|
||||
done
|
||||
}
|
||||
|
||||
while getopts "lh" arg; do
|
||||
case $arg in
|
||||
l ) examples $(ls eg_*.py); exit 0 ;;
|
||||
h ) echo 1>&2 $0 [-l] [-h] filename ; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# podman needs to play some games with resources
|
||||
if [[ $(id -u) != 0 ]]; then
|
||||
echo 1>&2 $0 must be run as root.
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! systemctl --quiet is-active io.podman.socket; then
|
||||
echo 1>&2 'podman is not running. systemctl enable --now io.podman.socket'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function cleanup {
|
||||
podman rm $1 >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Setup storage with an image and container
|
||||
podman pull alpine:latest >/tmp/podman.output 2>&1
|
||||
CTNR=$(podman create alpine)
|
||||
trap "cleanup $CTNR" EXIT
|
||||
|
||||
if [[ -f $1 ]]; then
|
||||
python3 $1
|
||||
else
|
||||
python3 $1.py
|
||||
fi
|
@ -1,29 +0,0 @@
|
||||
"""A client for communicating with a Podman server."""
|
||||
import pkg_resources
|
||||
|
||||
from .client import Client
|
||||
from .libs import FoldedString, datetime_format, datetime_parse
|
||||
from .libs.errors import (ContainerNotFound, ErrorOccurred, ImageNotFound,
|
||||
NoContainerRunning, NoContainersInPod,
|
||||
PodContainerError, PodmanError, PodNotFound)
|
||||
|
||||
assert FoldedString
|
||||
|
||||
try:
|
||||
__version__ = pkg_resources.get_distribution('podman').version
|
||||
except Exception: # pylint: disable=broad-except
|
||||
__version__ = '0.0.0'
|
||||
|
||||
__all__ = [
|
||||
'Client',
|
||||
'ContainerNotFound',
|
||||
'datetime_format',
|
||||
'datetime_parse',
|
||||
'ErrorOccurred',
|
||||
'ImageNotFound',
|
||||
'NoContainerRunning',
|
||||
'NoContainersInPod',
|
||||
'PodContainerError',
|
||||
'PodmanError',
|
||||
'PodNotFound',
|
||||
]
|
@ -1,212 +0,0 @@
|
||||
"""A client for communicating with a Podman varlink service."""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from varlink import Client as VarlinkClient
|
||||
from varlink import VarlinkError
|
||||
|
||||
from .libs import cached_property
|
||||
from .libs.containers import Containers
|
||||
from .libs.errors import error_factory
|
||||
from .libs.images import Images
|
||||
from .libs.system import System
|
||||
from .libs.tunnel import Context, Portal, Tunnel
|
||||
from .libs.pods import Pods
|
||||
|
||||
|
||||
class BaseClient():
|
||||
"""Context manager for API workers to access varlink."""
|
||||
|
||||
def __init__(self, context):
|
||||
"""Construct Client."""
|
||||
self._client = None
|
||||
self._iface = None
|
||||
self._context = context
|
||||
|
||||
def __call__(self):
|
||||
"""Support being called for old API."""
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def factory(cls,
|
||||
uri=None,
|
||||
interface='io.podman',
|
||||
*args,
|
||||
**kwargs):
|
||||
"""Construct a Client based on input."""
|
||||
log_level = os.environ.get('LOG_LEVEL')
|
||||
if log_level is not None:
|
||||
logging.basicConfig(level=logging.getLevelName(log_level.upper()))
|
||||
|
||||
if uri is None:
|
||||
raise ValueError('uri is required and cannot be None')
|
||||
if interface is None:
|
||||
raise ValueError('interface is required and cannot be None')
|
||||
|
||||
unsupported = set(kwargs.keys()).difference(
|
||||
('uri', 'interface', 'remote_uri', 'identity_file'))
|
||||
if unsupported:
|
||||
raise ValueError('Unknown keyword arguments: {}'.format(
|
||||
', '.join(unsupported)))
|
||||
|
||||
local_path = urlparse(uri).path
|
||||
if local_path == '':
|
||||
raise ValueError('path is required for uri,'
|
||||
' expected format "unix://path_to_socket"')
|
||||
|
||||
if kwargs.get('remote_uri') is None:
|
||||
return LocalClient(Context(uri, interface))
|
||||
|
||||
required = ('{} is required, expected format'
|
||||
' "ssh://user@hostname[:port]/path_to_socket".')
|
||||
|
||||
# Remote access requires the full tuple of information
|
||||
if kwargs.get('remote_uri') is None:
|
||||
raise ValueError(required.format('remote_uri'))
|
||||
|
||||
remote = urlparse(kwargs['remote_uri'])
|
||||
if remote.username is None:
|
||||
raise ValueError(required.format('username'))
|
||||
if remote.path == '':
|
||||
raise ValueError(required.format('path'))
|
||||
if remote.hostname is None:
|
||||
raise ValueError(required.format('hostname'))
|
||||
|
||||
return RemoteClient(
|
||||
Context(
|
||||
uri,
|
||||
interface,
|
||||
local_path,
|
||||
remote.path,
|
||||
remote.username,
|
||||
remote.hostname,
|
||||
remote.port,
|
||||
kwargs.get('identity_file'),
|
||||
))
|
||||
|
||||
|
||||
class LocalClient(BaseClient):
|
||||
"""Context manager for API workers to access varlink."""
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter context for LocalClient."""
|
||||
self._client = VarlinkClient(address=self._context.uri)
|
||||
self._iface = self._client.open(self._context.interface)
|
||||
return self._iface
|
||||
|
||||
def __exit__(self, e_type, e, e_traceback):
|
||||
"""Cleanup context for LocalClient."""
|
||||
if hasattr(self._client, 'close'):
|
||||
self._client.close()
|
||||
self._iface.close()
|
||||
|
||||
if isinstance(e, VarlinkError):
|
||||
raise error_factory(e)
|
||||
|
||||
|
||||
class RemoteClient(BaseClient):
|
||||
"""Context manager for API workers to access remote varlink."""
|
||||
|
||||
def __init__(self, context):
|
||||
"""Construct RemoteCLient."""
|
||||
super().__init__(context)
|
||||
self._portal = Portal()
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager for API workers to access varlink."""
|
||||
tunnel = self._portal.get(self._context.uri)
|
||||
if tunnel is None:
|
||||
tunnel = Tunnel(self._context).bore()
|
||||
self._portal[self._context.uri] = tunnel
|
||||
|
||||
try:
|
||||
self._client = VarlinkClient(address=self._context.uri)
|
||||
self._iface = self._client.open(self._context.interface)
|
||||
return self._iface
|
||||
except Exception:
|
||||
tunnel.close()
|
||||
raise
|
||||
|
||||
def __exit__(self, e_type, e, e_traceback):
|
||||
"""Cleanup context for RemoteClient."""
|
||||
if hasattr(self._client, 'close'):
|
||||
self._client.close()
|
||||
self._iface.close()
|
||||
|
||||
# set timer to shutdown ssh tunnel
|
||||
# self._portal.get(self._context.uri).close()
|
||||
if isinstance(e, VarlinkError):
|
||||
raise error_factory(e)
|
||||
|
||||
|
||||
class Client():
|
||||
"""A client for communicating with a Podman varlink service.
|
||||
|
||||
Example:
|
||||
|
||||
>>> import podman
|
||||
>>> c = podman.Client()
|
||||
>>> c.system.versions
|
||||
|
||||
Example remote podman:
|
||||
|
||||
>>> import podman
|
||||
>>> c = podman.Client(uri='unix:/tmp/podman.sock',
|
||||
remote_uri='ssh://user@host/run/podman/io.podman',
|
||||
identity_file='~/.ssh/id_rsa')
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
uri='unix:/run/podman/io.podman',
|
||||
interface='io.podman',
|
||||
**kwargs):
|
||||
"""Construct a podman varlink Client.
|
||||
|
||||
uri from default systemd unit file.
|
||||
interface from io.podman.varlink, do not change unless
|
||||
you are a varlink guru.
|
||||
"""
|
||||
self._client = BaseClient.factory(uri, interface, **kwargs)
|
||||
|
||||
address = "{}-{}".format(uri, interface)
|
||||
# Quick validation of connection data provided
|
||||
try:
|
||||
if not System(self._client).ping():
|
||||
raise ConnectionRefusedError(
|
||||
errno.ECONNREFUSED,
|
||||
('Failed varlink connection "{}"').format(address))
|
||||
except FileNotFoundError:
|
||||
raise ConnectionError(
|
||||
errno.ECONNREFUSED,
|
||||
('Failed varlink connection "{}".'
|
||||
' Is podman socket or service running?').format(address))
|
||||
|
||||
def __enter__(self):
|
||||
"""Return `self` upon entering the runtime context."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Raise any exception triggered within the runtime context."""
|
||||
pass
|
||||
|
||||
@cached_property
|
||||
def system(self):
|
||||
"""Manage system model for podman."""
|
||||
return System(self._client)
|
||||
|
||||
@cached_property
|
||||
def images(self):
|
||||
"""Manage images model for libpod."""
|
||||
return Images(self._client)
|
||||
|
||||
@cached_property
|
||||
def containers(self):
|
||||
"""Manage containers model for libpod."""
|
||||
return Containers(self._client)
|
||||
|
||||
@cached_property
|
||||
def pods(self):
|
||||
"""Manage pods model for libpod."""
|
||||
return Pods(self._client)
|
@ -1,75 +0,0 @@
|
||||
"""Support files for podman API implementation."""
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
|
||||
from dateutil.parser import parse as dateutil_parse
|
||||
|
||||
__all__ = [
|
||||
'cached_property',
|
||||
'datetime_format',
|
||||
'datetime_parse',
|
||||
'fold_keys',
|
||||
]
|
||||
|
||||
|
||||
def cached_property(fn):
|
||||
"""Decorate property to cache return value."""
|
||||
return property(functools.lru_cache(maxsize=8)(fn))
|
||||
|
||||
|
||||
class ConfigDict(collections.UserDict):
|
||||
"""Silently ignore None values, only take key once."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Construct dictionary."""
|
||||
super().__init__(kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Store unique, not None values."""
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if super().__contains__(key):
|
||||
return
|
||||
|
||||
super().__setitem__(key, value)
|
||||
|
||||
|
||||
class FoldedString(collections.UserString):
|
||||
"""Foldcase sequences value."""
|
||||
|
||||
def __init__(self, seq):
|
||||
super().__init__(seq)
|
||||
self.data.casefold()
|
||||
|
||||
|
||||
def fold_keys(): # noqa: D202
|
||||
"""Fold case of dictionary keys."""
|
||||
|
||||
@functools.wraps(fold_keys)
|
||||
def wrapped(mapping):
|
||||
"""Fold case of dictionary keys."""
|
||||
return {k.casefold(): v for (k, v) in mapping.items()}
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def datetime_parse(string):
|
||||
"""Convert timestamps to datetime.
|
||||
|
||||
tzinfo aware, if provided.
|
||||
"""
|
||||
return dateutil_parse(string.upper(), fuzzy=True)
|
||||
|
||||
|
||||
def datetime_format(dt):
|
||||
"""Format datetime in consistent style."""
|
||||
if isinstance(dt, str):
|
||||
return datetime_parse(dt).isoformat()
|
||||
|
||||
if isinstance(dt, datetime.datetime):
|
||||
return dt.isoformat()
|
||||
|
||||
raise ValueError('Unable to format {}. Type {} not supported.'.format(
|
||||
dt, type(dt)))
|
@ -1,79 +0,0 @@
|
||||
"""Exported method Container.attach()."""
|
||||
|
||||
import collections
|
||||
import fcntl
|
||||
import logging
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
|
||||
|
||||
class Mixin:
|
||||
"""Publish attach() for inclusion in Container class."""
|
||||
|
||||
def attach(self, eot=4, stdin=None, stdout=None):
|
||||
"""Attach to container's PID1 stdin and stdout.
|
||||
|
||||
stderr is ignored.
|
||||
PseudoTTY work is done in start().
|
||||
"""
|
||||
if stdin is None:
|
||||
stdin = sys.stdin.fileno()
|
||||
elif hasattr(stdin, 'fileno'):
|
||||
stdin = stdin.fileno()
|
||||
|
||||
if stdout is None:
|
||||
stdout = sys.stdout.fileno()
|
||||
elif hasattr(stdout, 'fileno'):
|
||||
stdout = stdout.fileno()
|
||||
|
||||
with self._client() as podman:
|
||||
attach = podman.GetAttachSockets(self._id)
|
||||
|
||||
# This is the UDS where all the IO goes
|
||||
io_socket = attach['sockets']['io_socket']
|
||||
assert len(io_socket) <= 107,\
|
||||
'Path length for sockets too long. {} > 107'.format(
|
||||
len(io_socket)
|
||||
)
|
||||
|
||||
# This is the control socket where resizing events are sent to conmon
|
||||
# attach['sockets']['control_socket']
|
||||
self.pseudo_tty = collections.namedtuple(
|
||||
'PseudoTTY',
|
||||
['stdin', 'stdout', 'io_socket', 'control_socket', 'eot'])(
|
||||
stdin,
|
||||
stdout,
|
||||
attach['sockets']['io_socket'],
|
||||
attach['sockets']['control_socket'],
|
||||
eot,
|
||||
)
|
||||
|
||||
@property
|
||||
def resize_handler(self):
|
||||
"""Send the new window size to conmon."""
|
||||
|
||||
def wrapped(signum, frame): # pylint: disable=unused-argument
|
||||
packed = fcntl.ioctl(self.pseudo_tty.stdout, termios.TIOCGWINSZ,
|
||||
struct.pack('HHHH', 0, 0, 0, 0))
|
||||
rows, cols, _, _ = struct.unpack('HHHH', packed)
|
||||
logging.debug('Resize window(%dx%d) using %s', rows, cols,
|
||||
self.pseudo_tty.control_socket)
|
||||
|
||||
# TODO: Need some kind of timeout in case pipe is blocked
|
||||
with open(self.pseudo_tty.control_socket, 'w') as skt:
|
||||
# send conmon window resize message
|
||||
skt.write('1 {} {}\n'.format(rows, cols))
|
||||
|
||||
return wrapped
|
||||
|
||||
@property
|
||||
def log_handler(self):
|
||||
"""Send command to reopen log to conmon."""
|
||||
|
||||
def wrapped(signum, frame): # pylint: disable=unused-argument
|
||||
with open(self.pseudo_tty.control_socket, 'w') as skt:
|
||||
# send conmon reopen log message
|
||||
skt.write('2\n')
|
||||
|
||||
return wrapped
|
@ -1,82 +0,0 @@
|
||||
"""Exported method Container.start()."""
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
|
||||
CONMON_BUFSZ = 8192
|
||||
|
||||
|
||||
class Mixin:
|
||||
"""Publish start() for inclusion in Container class."""
|
||||
|
||||
def start(self):
|
||||
"""Start container, return container on success.
|
||||
|
||||
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'])
|
||||
|
||||
if not hasattr(self, 'pseudo_tty') or self.pseudo_tty is None:
|
||||
return self._refresh(podman)
|
||||
|
||||
logging.debug('Setting up PseudoTTY for Container "%s"',
|
||||
results['container'])
|
||||
|
||||
try:
|
||||
# save off the old settings for terminal
|
||||
tcoldattr = termios.tcgetattr(self.pseudo_tty.stdin)
|
||||
tty.setraw(self.pseudo_tty.stdin)
|
||||
|
||||
# initialize container's window size
|
||||
self.resize_handler(None, sys._getframe(0))
|
||||
|
||||
# catch any resizing events and send the resize info
|
||||
# to the control fifo "socket"
|
||||
signal.signal(signal.SIGWINCH, self.resize_handler)
|
||||
|
||||
except termios.error:
|
||||
tcoldattr = None
|
||||
|
||||
try:
|
||||
# TODO: Is socket.SOCK_SEQPACKET supported in Windows?
|
||||
with socket.socket(socket.AF_UNIX,
|
||||
socket.SOCK_SEQPACKET) as skt:
|
||||
# Prepare socket for use with conmon/container
|
||||
skt.connect(self.pseudo_tty.io_socket)
|
||||
|
||||
sources = [skt, self.pseudo_tty.stdin]
|
||||
while sources:
|
||||
logging.debug('Waiting on sources: %s', sources)
|
||||
readable, _, _ = select.select(sources, [], [])
|
||||
|
||||
if skt in readable:
|
||||
data = skt.recv(CONMON_BUFSZ)
|
||||
if data:
|
||||
# Remove source marker when writing
|
||||
os.write(self.pseudo_tty.stdout, data[1:])
|
||||
else:
|
||||
sources.remove(skt)
|
||||
|
||||
if self.pseudo_tty.stdin in readable:
|
||||
data = os.read(self.pseudo_tty.stdin, CONMON_BUFSZ)
|
||||
if data:
|
||||
skt.sendall(data)
|
||||
|
||||
if self.pseudo_tty.eot in data:
|
||||
sources.clear()
|
||||
else:
|
||||
sources.remove(self.pseudo_tty.stdin)
|
||||
finally:
|
||||
if tcoldattr:
|
||||
termios.tcsetattr(self.pseudo_tty.stdin, termios.TCSADRAIN,
|
||||
tcoldattr)
|
||||
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||
return self._refresh(podman)
|
@ -1,248 +0,0 @@
|
||||
"""Models for manipulating containers and storage."""
|
||||
import collections
|
||||
import getpass
|
||||
import json
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
|
||||
from . import fold_keys
|
||||
from ._containers_attach import Mixin as AttachMixin
|
||||
from ._containers_start import Mixin as StartMixin
|
||||
|
||||
|
||||
class Container(AttachMixin, StartMixin, collections.UserDict):
|
||||
"""Model for a container."""
|
||||
|
||||
def __init__(self, client, ident, data, refresh=True):
|
||||
"""Construct Container Model."""
|
||||
super(Container, self).__init__(data)
|
||||
self._client = client
|
||||
self._id = ident
|
||||
|
||||
if refresh:
|
||||
with client() as podman:
|
||||
self._refresh(podman)
|
||||
else:
|
||||
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']
|
||||
|
||||
assert self._id == data['id'],\
|
||||
'Requested container id({}) does not match store id({})'.format(
|
||||
self._id, data['id']
|
||||
)
|
||||
|
||||
def _refresh(self, podman, tries=1):
|
||||
try:
|
||||
ctnr = podman.GetContainer(self._id)
|
||||
except BrokenPipeError:
|
||||
logging.debug('Failed GetContainer(%s) try %d/3', self._id, tries)
|
||||
if tries > 3:
|
||||
raise
|
||||
else:
|
||||
with self._client() as pman:
|
||||
self._refresh(pman, tries + 1)
|
||||
else:
|
||||
super().update(ctnr['container'])
|
||||
|
||||
for k, v in self.data.items():
|
||||
setattr(self, k, v)
|
||||
if 'containerrunning' in self.data:
|
||||
setattr(self, 'running', self.data['containerrunning'])
|
||||
self.data['running'] = self.data['containerrunning']
|
||||
|
||||
return self
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh status fields for this container."""
|
||||
with self._client() as podman:
|
||||
return self._refresh(podman)
|
||||
|
||||
def processes(self):
|
||||
"""Show processes running in container."""
|
||||
with self._client() as podman:
|
||||
results = podman.ListContainerProcesses(self._id)
|
||||
yield from results['container']
|
||||
|
||||
def changes(self):
|
||||
"""Retrieve container changes."""
|
||||
with self._client() as podman:
|
||||
results = podman.ListContainerChanges(self._id)
|
||||
return results['container']
|
||||
|
||||
def kill(self, sig=signal.SIGTERM, wait=25):
|
||||
"""Send signal to container.
|
||||
|
||||
default signal is signal.SIGTERM.
|
||||
wait n of seconds, 0 waits forever.
|
||||
"""
|
||||
with self._client() as podman:
|
||||
podman.KillContainer(self._id, sig)
|
||||
timeout = time.time() + wait
|
||||
while True:
|
||||
self._refresh(podman)
|
||||
if self.status != 'running': # pylint: disable=no-member
|
||||
return self
|
||||
|
||||
if wait and timeout < time.time():
|
||||
raise TimeoutError()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
def inspect(self):
|
||||
"""Retrieve details about containers."""
|
||||
with self._client() as podman:
|
||||
results = podman.InspectContainer(self._id)
|
||||
obj = json.loads(results['container'], object_hook=fold_keys())
|
||||
return collections.namedtuple('ContainerInspect', obj.keys())(**obj)
|
||||
|
||||
def export(self, target):
|
||||
"""Export container from store to tarball.
|
||||
|
||||
TODO: should there be a compress option, like images?
|
||||
"""
|
||||
with self._client() as podman:
|
||||
results = podman.ExportContainer(self._id, target)
|
||||
return results['tarfile']
|
||||
|
||||
def commit(self, image_name, **kwargs):
|
||||
"""Create image from container.
|
||||
|
||||
Keyword arguments:
|
||||
author -- change image's author
|
||||
message -- change image's message, docker format only.
|
||||
pause -- pause container during commit
|
||||
change -- Additional properties to change
|
||||
|
||||
Change examples:
|
||||
CMD=/usr/bin/zsh
|
||||
ENTRYPOINT=/bin/sh date
|
||||
ENV=TEST=test_containers.TestContainers.test_commit
|
||||
EXPOSE=8888/tcp
|
||||
LABEL=unittest=test_commit
|
||||
USER=bozo:circus
|
||||
VOLUME=/data
|
||||
WORKDIR=/data/application
|
||||
|
||||
All changes overwrite existing values.
|
||||
See inspect() to obtain current settings.
|
||||
"""
|
||||
author = kwargs.get('author', None) or getpass.getuser()
|
||||
change = kwargs.get('change', None) or []
|
||||
message = kwargs.get('message', None) or ''
|
||||
pause = kwargs.get('pause', None) or True
|
||||
|
||||
for c in change:
|
||||
if c.startswith('LABEL=') and c.count('=') < 2:
|
||||
raise ValueError(
|
||||
'LABEL should have the format: LABEL=label=value, not {}'.
|
||||
format(c))
|
||||
|
||||
with self._client() as podman:
|
||||
results = podman.Commit(self._id, image_name, change, author,
|
||||
message, pause)
|
||||
return results['image']
|
||||
|
||||
def stop(self, timeout=25):
|
||||
"""Stop container, return id on success."""
|
||||
with self._client() as podman:
|
||||
podman.StopContainer(self._id, timeout)
|
||||
return self._refresh(podman)
|
||||
|
||||
def remove(self, force=False):
|
||||
"""Remove container, return id on success.
|
||||
|
||||
force=True, stop running container.
|
||||
"""
|
||||
with self._client() as podman:
|
||||
results = podman.RemoveContainer(self._id, force)
|
||||
return results['container']
|
||||
|
||||
def restart(self, timeout=25):
|
||||
"""Restart container with timeout, return id on success."""
|
||||
with self._client() as podman:
|
||||
podman.RestartContainer(self._id, timeout)
|
||||
return self._refresh(podman)
|
||||
|
||||
def rename(self, target): # pylint: disable=unused-argument
|
||||
"""Rename container, return id on success."""
|
||||
with self._client() as podman:
|
||||
# TODO: Need arguments
|
||||
results = podman.RenameContainer()
|
||||
# TODO: fixup objects cached information
|
||||
return results['container']
|
||||
|
||||
def resize_tty(self, width, height): # pylint: disable=unused-argument
|
||||
"""Resize container tty."""
|
||||
with self._client() as podman:
|
||||
# TODO: magic re: attach(), arguments
|
||||
podman.ResizeContainerTty()
|
||||
|
||||
def pause(self):
|
||||
"""Pause container, return id on success."""
|
||||
with self._client() as podman:
|
||||
podman.PauseContainer(self._id)
|
||||
return self._refresh(podman)
|
||||
|
||||
def unpause(self):
|
||||
"""Unpause container, return id on success."""
|
||||
with self._client() as podman:
|
||||
podman.UnpauseContainer(self._id)
|
||||
return self._refresh(podman)
|
||||
|
||||
def update_container(self, *args, **kwargs): \
|
||||
# pylint: disable=unused-argument
|
||||
"""TODO: Update container..., return id on success."""
|
||||
with self._client() as podman:
|
||||
podman.UpdateContainer()
|
||||
return self._refresh(podman)
|
||||
|
||||
def wait(self):
|
||||
"""Wait for container to finish, return 'returncode'."""
|
||||
with self._client() as podman:
|
||||
results = podman.WaitContainer(self._id)
|
||||
return int(results['exitcode'])
|
||||
|
||||
def stats(self):
|
||||
"""Retrieve resource stats from the container."""
|
||||
with self._client() as podman:
|
||||
results = podman.GetContainerStats(self._id)
|
||||
obj = results['container']
|
||||
return collections.namedtuple('StatDetail', obj.keys())(**obj)
|
||||
|
||||
def logs(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""Retrieve container logs."""
|
||||
with self._client() as podman:
|
||||
results = podman.GetContainerLogs(self._id)
|
||||
yield from results['container']
|
||||
|
||||
|
||||
class Containers():
|
||||
"""Model for Containers collection."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Construct model for Containers collection."""
|
||||
self._client = client
|
||||
|
||||
def list(self):
|
||||
"""List of containers in the container store."""
|
||||
with self._client() as podman:
|
||||
results = podman.ListContainers()
|
||||
for cntr in results['containers']:
|
||||
yield Container(self._client, cntr['id'], cntr, refresh=False)
|
||||
|
||||
def delete_stopped(self):
|
||||
"""Delete all stopped containers."""
|
||||
with self._client() as podman:
|
||||
results = podman.DeleteStoppedContainers()
|
||||
return results['containers']
|
||||
|
||||
def get(self, id_):
|
||||
"""Retrieve container details from store."""
|
||||
with self._client() as podman:
|
||||
cntr = podman.GetContainer(id_)
|
||||
return Container(self._client, cntr['container']['id'],
|
||||
cntr['container'])
|
@ -1,77 +0,0 @@
|
||||
"""Error classes and wrappers for VarlinkError."""
|
||||
from varlink import VarlinkError
|
||||
|
||||
|
||||
class VarlinkErrorProxy(VarlinkError):
|
||||
"""Class to Proxy VarlinkError methods."""
|
||||
|
||||
def __init__(self, message, namespaced=False):
|
||||
"""Construct proxy from Exception."""
|
||||
super().__init__(message.as_dict(), namespaced)
|
||||
self._message = message
|
||||
self.__module__ = 'libpod'
|
||||
|
||||
def __getattr__(self, method):
|
||||
"""Return attribute from proxied Exception."""
|
||||
if hasattr(self._message, method):
|
||||
return getattr(self._message, method)
|
||||
|
||||
try:
|
||||
return self._message.parameters()[method]
|
||||
except KeyError:
|
||||
raise AttributeError('No such attribute: {}'.format(method))
|
||||
|
||||
|
||||
class ContainerNotFound(VarlinkErrorProxy):
|
||||
"""Raised when Client cannot find requested container."""
|
||||
|
||||
|
||||
class ImageNotFound(VarlinkErrorProxy):
|
||||
"""Raised when Client cannot find requested image."""
|
||||
|
||||
|
||||
class PodNotFound(VarlinkErrorProxy):
|
||||
"""Raised when Client cannot find requested image."""
|
||||
|
||||
|
||||
class PodContainerError(VarlinkErrorProxy):
|
||||
"""Raised when a container fails requested pod operation."""
|
||||
|
||||
|
||||
class NoContainerRunning(VarlinkErrorProxy):
|
||||
"""Raised when no container is running in pod."""
|
||||
|
||||
|
||||
class NoContainersInPod(VarlinkErrorProxy):
|
||||
"""Raised when Client fails to connect to runtime."""
|
||||
|
||||
|
||||
class ErrorOccurred(VarlinkErrorProxy):
|
||||
"""Raised when an error occurs during the execution.
|
||||
|
||||
See error() to see actual error text.
|
||||
"""
|
||||
|
||||
|
||||
class PodmanError(VarlinkErrorProxy):
|
||||
"""Raised when Client fails to connect to runtime."""
|
||||
|
||||
|
||||
ERROR_MAP = {
|
||||
'io.podman.ContainerNotFound': ContainerNotFound,
|
||||
'io.podman.ErrorOccurred': ErrorOccurred,
|
||||
'io.podman.ImageNotFound': ImageNotFound,
|
||||
'io.podman.NoContainerRunning': NoContainerRunning,
|
||||
'io.podman.NoContainersInPod': NoContainersInPod,
|
||||
'io.podman.PodContainerError': PodContainerError,
|
||||
'io.podman.PodNotFound': PodNotFound,
|
||||
'io.podman.RuntimeError': PodmanError,
|
||||
}
|
||||
|
||||
|
||||
def error_factory(exception):
|
||||
"""Map Exceptions to a discrete type."""
|
||||
try:
|
||||
return ERROR_MAP[exception.error()](exception)
|
||||
except KeyError:
|
||||
return exception
|
@ -1,164 +0,0 @@
|
||||
"""Models for manipulating images in/to/from storage."""
|
||||
import collections
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
from . import ConfigDict, fold_keys
|
||||
from .containers import Container
|
||||
|
||||
|
||||
class Image(collections.UserDict):
|
||||
"""Model for an Image."""
|
||||
|
||||
def __init__(self, client, id, data):
|
||||
"""Construct Image Model."""
|
||||
super().__init__(data)
|
||||
for k, v in data.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
self._id = id
|
||||
self._client = client
|
||||
|
||||
assert self._id == data['id'],\
|
||||
'Requested image id({}) does not match store id({})'.format(
|
||||
self._id, data['id']
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _split_token(values=None, sep='='):
|
||||
if not values:
|
||||
return {}
|
||||
return {
|
||||
k: v1 for k, v1 in (v0.split(sep, 1) for v0 in values)
|
||||
}
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
"""Create container from image.
|
||||
|
||||
Pulls defaults from image.inspect()
|
||||
"""
|
||||
details = self.inspect()
|
||||
|
||||
config = ConfigDict(image_id=self._id, **kwargs)
|
||||
config['command'] = details.config.get('cmd')
|
||||
config['env'] = self._split_token(details.config.get('env'))
|
||||
config['image'] = copy.deepcopy(details.repotags[0])
|
||||
config['labels'] = copy.deepcopy(details.labels)
|
||||
config['net_mode'] = 'bridge'
|
||||
config['network'] = 'bridge'
|
||||
|
||||
logging.debug('Image %s: create config: %s', self._id, config)
|
||||
with self._client() as podman:
|
||||
id_ = podman.CreateContainer(config)['container']
|
||||
cntr = podman.GetContainer(id_)
|
||||
return Container(self._client, id_, cntr['container'])
|
||||
|
||||
container = create
|
||||
|
||||
def export(self, dest, compressed=False):
|
||||
"""Write image to dest, return id on success."""
|
||||
with self._client() as podman:
|
||||
results = podman.ExportImage(self._id, dest, compressed)
|
||||
return results['image']
|
||||
|
||||
def history(self):
|
||||
"""Retrieve image history."""
|
||||
with self._client() as podman:
|
||||
for r in podman.HistoryImage(self._id)['history']:
|
||||
yield collections.namedtuple('HistoryDetail', r.keys())(**r)
|
||||
|
||||
def inspect(self):
|
||||
"""Retrieve details about image."""
|
||||
with self._client() as podman:
|
||||
results = podman.InspectImage(self._id)
|
||||
obj = json.loads(results['image'], object_hook=fold_keys())
|
||||
return collections.namedtuple('ImageInspect', obj.keys())(**obj)
|
||||
|
||||
def push(self, target, tlsverify=True):
|
||||
"""Copy image to target, return id on success."""
|
||||
with self._client() as podman:
|
||||
results = podman.PushImage(self._id, target, tlsverify)
|
||||
return results['image']
|
||||
|
||||
def remove(self, force=False):
|
||||
"""Delete image, return id on success.
|
||||
|
||||
force=True, stop any running containers using image.
|
||||
"""
|
||||
with self._client() as podman:
|
||||
results = podman.RemoveImage(self._id, force)
|
||||
return results['image']
|
||||
|
||||
def tag(self, tag):
|
||||
"""Tag image."""
|
||||
with self._client() as podman:
|
||||
results = podman.TagImage(self._id, tag)
|
||||
return results['image']
|
||||
|
||||
|
||||
class Images():
|
||||
"""Model for Images collection."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Construct model for Images collection."""
|
||||
self._client = client
|
||||
|
||||
def list(self):
|
||||
"""List all images in the libpod image store."""
|
||||
with self._client() as podman:
|
||||
results = podman.ListImages()
|
||||
for img in results['images']:
|
||||
yield Image(self._client, img['id'], img)
|
||||
|
||||
def build(self, dockerfile=None, tags=None, **kwargs):
|
||||
"""Build container from image.
|
||||
|
||||
See podman-build.1.md for kwargs details.
|
||||
"""
|
||||
if dockerfile is None:
|
||||
raise ValueError('"dockerfile" is a required argument.')
|
||||
elif not hasattr(dockerfile, '__iter__'):
|
||||
raise ValueError('"dockerfile" is required to be an iter.')
|
||||
|
||||
if tags is None:
|
||||
raise ValueError('"tags" is a required argument.')
|
||||
elif not hasattr(tags, '__iter__'):
|
||||
raise ValueError('"tags" is required to be an iter.')
|
||||
|
||||
config = ConfigDict(dockerfile=dockerfile, tags=tags, **kwargs)
|
||||
with self._client() as podman:
|
||||
result = podman.BuildImage(config)
|
||||
return self.get(result['image']['id']), \
|
||||
(line for line in result['image']['logs'])
|
||||
|
||||
def delete_unused(self):
|
||||
"""Delete Images not associated with a container."""
|
||||
with self._client() as podman:
|
||||
results = podman.DeleteUnusedImages()
|
||||
return results['images']
|
||||
|
||||
def import_image(self, source, reference, message='', changes=None):
|
||||
"""Read image tarball from source and save in image store."""
|
||||
with self._client() as podman:
|
||||
results = podman.ImportImage(source, reference, message, changes)
|
||||
return results['image']
|
||||
|
||||
def pull(self, source):
|
||||
"""Copy image from registry to image store."""
|
||||
with self._client() as podman:
|
||||
results = podman.PullImage(source)
|
||||
return results['id']
|
||||
|
||||
def search(self, id_, limit=25):
|
||||
"""Search registries for id."""
|
||||
with self._client() as podman:
|
||||
results = podman.SearchImage(id_, limit)
|
||||
for img in results['images']:
|
||||
yield collections.namedtuple('ImageSearch', img.keys())(**img)
|
||||
|
||||
def get(self, id_):
|
||||
"""Get Image from id."""
|
||||
with self._client() as podman:
|
||||
result = podman.GetImage(id_)
|
||||
return Image(self._client, result['image']['id'], result['image'])
|
@ -1,163 +0,0 @@
|
||||
"""Model for accessing details of Pods from podman service."""
|
||||
import collections
|
||||
import json
|
||||
import signal
|
||||
import time
|
||||
|
||||
from . import ConfigDict, FoldedString, fold_keys
|
||||
|
||||
|
||||
class Pod(collections.UserDict):
|
||||
"""Model for a Pod."""
|
||||
|
||||
def __init__(self, client, ident, data):
|
||||
"""Construct Pod model."""
|
||||
super().__init__(data)
|
||||
|
||||
self._ident = ident
|
||||
self._client = client
|
||||
|
||||
with client() as podman:
|
||||
self._refresh(podman)
|
||||
|
||||
def _refresh(self, podman):
|
||||
pod = podman.GetPod(self._ident)
|
||||
super().update(pod['pod'])
|
||||
|
||||
for k, v in self.data.items():
|
||||
setattr(self, k, v)
|
||||
return self
|
||||
|
||||
def inspect(self):
|
||||
"""Retrieve details about pod."""
|
||||
with self._client() as podman:
|
||||
results = podman.InspectPod(self._ident)
|
||||
obj = json.loads(results['pod'], object_hook=fold_keys())
|
||||
obj['id'] = obj['config']['id']
|
||||
return collections.namedtuple('PodInspect', obj.keys())(**obj)
|
||||
|
||||
def kill(self, signal_=signal.SIGTERM, wait=25):
|
||||
"""Send signal to all containers in pod.
|
||||
|
||||
default signal is signal.SIGTERM.
|
||||
wait n of seconds, 0 waits forever.
|
||||
"""
|
||||
with self._client() as podman:
|
||||
podman.KillPod(self._ident, signal_)
|
||||
timeout = time.time() + wait
|
||||
while True:
|
||||
# pylint: disable=maybe-no-member
|
||||
self._refresh(podman)
|
||||
running = FoldedString(self.status)
|
||||
if running != 'running':
|
||||
break
|
||||
|
||||
if wait and timeout < time.time():
|
||||
raise TimeoutError()
|
||||
|
||||
time.sleep(0.5)
|
||||
return self
|
||||
|
||||
def pause(self):
|
||||
"""Pause all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
podman.PausePod(self._ident)
|
||||
return self._refresh(podman)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh status fields for this pod."""
|
||||
with self._client() as podman:
|
||||
return self._refresh(podman)
|
||||
|
||||
def remove(self, force=False):
|
||||
"""Remove pod and its containers returning pod ident.
|
||||
|
||||
force=True, stop any running container.
|
||||
"""
|
||||
with self._client() as podman:
|
||||
results = podman.RemovePod(self._ident, force)
|
||||
return results['pod']
|
||||
|
||||
def restart(self):
|
||||
"""Restart all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
podman.RestartPod(self._ident)
|
||||
return self._refresh(podman)
|
||||
|
||||
def stats(self):
|
||||
"""Stats on all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
results = podman.GetPodStats(self._ident)
|
||||
for obj in results['containers']:
|
||||
yield collections.namedtuple('ContainerStats', obj.keys())(**obj)
|
||||
|
||||
def start(self):
|
||||
"""Start all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
podman.StartPod(self._ident)
|
||||
return self._refresh(podman)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
podman.StopPod(self._ident)
|
||||
return self._refresh(podman)
|
||||
|
||||
def top(self):
|
||||
"""Display stats for all containers."""
|
||||
with self._client() as podman:
|
||||
results = podman.TopPod(self._ident)
|
||||
return results['pod']
|
||||
|
||||
def unpause(self):
|
||||
"""Unpause all containers in the pod."""
|
||||
with self._client() as podman:
|
||||
podman.UnpausePod(self._ident)
|
||||
return self._refresh(podman)
|
||||
|
||||
def wait(self):
|
||||
"""Wait for all containers to exit."""
|
||||
with self._client() as podman:
|
||||
results = podman.WaitPod(self._ident)
|
||||
return results['pod']
|
||||
|
||||
|
||||
class Pods():
|
||||
"""Model for accessing pods."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Construct pod model."""
|
||||
self._client = client
|
||||
|
||||
def create(self,
|
||||
ident=None,
|
||||
cgroupparent=None,
|
||||
labels=None,
|
||||
share=None,
|
||||
infra=False):
|
||||
"""Create a new empty pod."""
|
||||
config = ConfigDict(
|
||||
name=ident,
|
||||
cgroupParent=cgroupparent,
|
||||
labels=labels,
|
||||
share=share,
|
||||
infra=infra,
|
||||
)
|
||||
|
||||
with self._client() as podman:
|
||||
result = podman.CreatePod(config)
|
||||
details = podman.GetPod(result['pod'])
|
||||
return Pod(self._client, result['pod'], details['pod'])
|
||||
|
||||
def get(self, ident):
|
||||
"""Get Pod from ident."""
|
||||
with self._client() as podman:
|
||||
result = podman.GetPod(ident)
|
||||
return Pod(self._client, result['pod']['id'], result['pod'])
|
||||
|
||||
def list(self):
|
||||
"""List all pods."""
|
||||
with self._client() as podman:
|
||||
results = podman.ListPods()
|
||||
for pod in results['pods']:
|
||||
yield Pod(self._client, pod['id'], pod)
|
@ -1,40 +0,0 @@
|
||||
"""Models for accessing details from varlink server."""
|
||||
import collections
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from . import cached_property
|
||||
|
||||
|
||||
class System():
|
||||
"""Model for accessing system resources."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Construct system model."""
|
||||
self._client = client
|
||||
|
||||
@cached_property
|
||||
def versions(self):
|
||||
"""Access versions."""
|
||||
with self._client() as podman:
|
||||
vers = podman.GetVersion()['version']
|
||||
|
||||
client = '0.0.0'
|
||||
try:
|
||||
client = pkg_resources.get_distribution('podman').version
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
vers['client_version'] = client
|
||||
return collections.namedtuple('Version', vers.keys())(**vers)
|
||||
|
||||
def info(self):
|
||||
"""Return podman info."""
|
||||
with self._client() as podman:
|
||||
info = podman.GetInfo()['info']
|
||||
return collections.namedtuple('Info', info.keys())(**info)
|
||||
|
||||
def ping(self):
|
||||
"""Return True if server awake."""
|
||||
with self._client() as podman:
|
||||
response = podman.Ping()
|
||||
return response['ping']['message'] == 'OK'
|
@ -1,190 +0,0 @@
|
||||
"""Cache for SSH tunnels."""
|
||||
import collections
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import weakref
|
||||
from contextlib import suppress
|
||||
|
||||
import psutil
|
||||
|
||||
Context = collections.namedtuple('Context', (
|
||||
'uri',
|
||||
'interface',
|
||||
'local_socket',
|
||||
'remote_socket',
|
||||
'username',
|
||||
'hostname',
|
||||
'port',
|
||||
'identity_file',
|
||||
))
|
||||
Context.__new__.__defaults__ = (None, ) * len(Context._fields)
|
||||
|
||||
|
||||
class Portal(collections.MutableMapping):
|
||||
"""Expiring container for tunnels."""
|
||||
|
||||
def __init__(self, sweap=25):
|
||||
"""Construct portal, reap tunnels every sweap seconds."""
|
||||
self.data = collections.OrderedDict()
|
||||
self.sweap = sweap
|
||||
self.ttl = sweap * 2
|
||||
self.lock = threading.RLock()
|
||||
self._schedule_reaper()
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Given uri return tunnel and update TTL."""
|
||||
with self.lock:
|
||||
value, _ = self.data[key]
|
||||
self.data[key] = (value, time.time() + self.ttl)
|
||||
self.data.move_to_end(key)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Store given tunnel keyed with uri."""
|
||||
if not isinstance(value, Tunnel):
|
||||
raise ValueError('Portals only support Tunnels.')
|
||||
|
||||
with self.lock:
|
||||
self.data[key] = (value, time.time() + self.ttl)
|
||||
self.data.move_to_end(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Remove and close tunnel from portal."""
|
||||
with self.lock:
|
||||
value, _ = self.data[key]
|
||||
del self.data[key]
|
||||
value.close()
|
||||
del value
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate tunnels."""
|
||||
with self.lock:
|
||||
values = self.data.values()
|
||||
|
||||
for tunnel, _ in values:
|
||||
yield tunnel
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of tunnels in portal."""
|
||||
with self.lock:
|
||||
return len(self.data)
|
||||
|
||||
def _schedule_reaper(self):
|
||||
timer = threading.Timer(interval=self.sweap, function=self.reap)
|
||||
timer.setName('PortalReaper')
|
||||
timer.setDaemon(True)
|
||||
timer.start()
|
||||
|
||||
def reap(self):
|
||||
"""Remove tunnels who's TTL has expired."""
|
||||
now = time.time()
|
||||
with self.lock:
|
||||
reaped_data = self.data.copy()
|
||||
for entry in reaped_data.items():
|
||||
if entry[1][1] < now:
|
||||
del self.data[entry[0]]
|
||||
else:
|
||||
# StopIteration as soon as possible
|
||||
break
|
||||
self._schedule_reaper()
|
||||
|
||||
|
||||
class Tunnel():
|
||||
"""SSH tunnel."""
|
||||
|
||||
def __init__(self, context):
|
||||
"""Construct Tunnel."""
|
||||
self.context = context
|
||||
self._closed = True
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
"""Is tunnel closed."""
|
||||
return self._closed
|
||||
|
||||
def bore(self):
|
||||
"""Create SSH tunnel from given context."""
|
||||
cmd = ['ssh', '-fNT']
|
||||
|
||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
cmd.append('-v')
|
||||
else:
|
||||
cmd.append('-q')
|
||||
|
||||
if self.context.port:
|
||||
cmd.extend(('-p', str(self.context.port)))
|
||||
|
||||
cmd.extend(('-L', '{}:{}'.format(self.context.local_socket,
|
||||
self.context.remote_socket)))
|
||||
if self.context.identity_file:
|
||||
cmd.extend(('-i', self.context.identity_file))
|
||||
|
||||
cmd.append('{}@{}'.format(self.context.username,
|
||||
self.context.hostname))
|
||||
|
||||
logging.debug('Opening tunnel "%s", cmd "%s"', self.context.uri,
|
||||
' '.join(cmd))
|
||||
|
||||
tunnel = subprocess.Popen(cmd, close_fds=True)
|
||||
# The return value of Popen() has no long term value as that process
|
||||
# has already exited by the time control is returned here. This is a
|
||||
# side effect of the -f option. wait() will be called to clean up
|
||||
# resources.
|
||||
for _ in range(300):
|
||||
# TODO: Make timeout configurable
|
||||
if os.path.exists(self.context.local_socket) \
|
||||
or tunnel.returncode is not None:
|
||||
break
|
||||
with suppress(subprocess.TimeoutExpired):
|
||||
# waiting for either socket to be created
|
||||
# or first child to exit
|
||||
tunnel.wait(0.5)
|
||||
else:
|
||||
raise TimeoutError(
|
||||
'Failed to create tunnel "{}", using: "{}"'.format(
|
||||
self.context.uri, ' '.join(cmd)))
|
||||
if tunnel.returncode is not None and tunnel.returncode != 0:
|
||||
raise subprocess.CalledProcessError(tunnel.returncode,
|
||||
' '.join(cmd))
|
||||
tunnel.wait()
|
||||
|
||||
self._closed = False
|
||||
weakref.finalize(self, self.close)
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
"""Close SSH tunnel."""
|
||||
logging.debug('Closing tunnel "%s"', self.context.uri)
|
||||
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
# Find all ssh instances for user with uri tunnel the hard way...
|
||||
targets = [
|
||||
p
|
||||
for p in psutil.process_iter(attrs=['name', 'username', 'cmdline'])
|
||||
if p.info['username'] == getpass.getuser()
|
||||
and p.info['name'] == 'ssh'
|
||||
and self.context.local_socket in ' '.join(p.info['cmdline'])
|
||||
] # yapf: disable
|
||||
|
||||
# ask nicely for ssh to quit, reap results
|
||||
for proc in targets:
|
||||
proc.terminate()
|
||||
_, alive = psutil.wait_procs(targets, timeout=300)
|
||||
|
||||
# kill off the uncooperative, then report any stragglers
|
||||
for proc in alive:
|
||||
proc.kill()
|
||||
_, alive = psutil.wait_procs(targets, timeout=300)
|
||||
|
||||
for proc in alive:
|
||||
logging.info('process %d survived SIGKILL, giving up.', proc.pid)
|
||||
|
||||
with suppress(OSError):
|
||||
os.remove(self.context.local_socket)
|
||||
self._closed = True
|
@ -1,4 +0,0 @@
|
||||
psutil
|
||||
python-dateutil
|
||||
setuptools>=39
|
||||
varlink
|
@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
root = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
with open(os.path.join(root, 'README.md')) as me:
|
||||
readme = me.read()
|
||||
|
||||
with open(os.path.join(root, 'requirements.txt')) as r:
|
||||
requirements = r.read().splitlines()
|
||||
|
||||
|
||||
setup(
|
||||
name='podman',
|
||||
version=os.environ.get('PODMAN_VERSION', '0.0.0'),
|
||||
description='A library for communicating with a Podman server',
|
||||
author='Jhon Honce',
|
||||
author_email='jhonce@redhat.com',
|
||||
license='Apache Software License',
|
||||
long_description=readme,
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
packages=find_packages(exclude=['test']),
|
||||
python_requires='>=3',
|
||||
zip_safe=True,
|
||||
url='http://github.com/containers/libpod',
|
||||
keywords='varlink libpod podman',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Topic :: Software Development',
|
||||
])
|
@ -1,112 +0,0 @@
|
||||
"""Base for podman tests."""
|
||||
import contextlib
|
||||
import functools
|
||||
import itertools
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from varlink import VarlinkError
|
||||
|
||||
MethodNotImplemented = 'org.varlink.service.MethodNotImplemented'
|
||||
|
||||
|
||||
class PodmanTestCase(unittest.TestCase):
|
||||
"""Hide the sausage making of initializing storage."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Fixture to setup podman test case."""
|
||||
if hasattr(PodmanTestCase, 'alpine_process'):
|
||||
PodmanTestCase.tearDownClass()
|
||||
|
||||
def run_cmd(*args):
|
||||
cmd = list(itertools.chain(*args))
|
||||
try:
|
||||
pid = subprocess.Popen(
|
||||
cmd,
|
||||
close_fds=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out, err = pid.communicate()
|
||||
except OSError as e:
|
||||
print('{}: {}({})'.format(cmd, e.strerror, e.returncode))
|
||||
except ValueError as e:
|
||||
print('{}: {}'.format(cmd, e.message))
|
||||
raise
|
||||
else:
|
||||
return out.strip()
|
||||
|
||||
tmpdir = os.environ.get('TMPDIR', '/tmp')
|
||||
podman_args = [
|
||||
'--storage-driver=vfs',
|
||||
'--cgroup-manager=cgroupfs',
|
||||
'--root={}/crio'.format(tmpdir),
|
||||
'--runroot={}/crio-run'.format(tmpdir),
|
||||
'--cni-config-dir={}/cni/net.d'.format(tmpdir),
|
||||
]
|
||||
|
||||
run_podman = functools.partial(run_cmd, ['podman'], podman_args)
|
||||
|
||||
id = run_podman(['pull', 'alpine'])
|
||||
setattr(PodmanTestCase, 'alpine_id', id)
|
||||
|
||||
run_podman(['pull', 'busybox'])
|
||||
run_podman(['images'])
|
||||
|
||||
run_cmd(['rm', '-f', '{}/alpine_gold.tar'.format(tmpdir)])
|
||||
run_podman([
|
||||
'save', '--output', '{}/alpine_gold.tar'.format(tmpdir), 'alpine'
|
||||
])
|
||||
|
||||
PodmanTestCase.alpine_log = open(
|
||||
os.path.join('/tmp/', 'alpine.log'), 'w')
|
||||
|
||||
cmd = ['podman']
|
||||
cmd.extend(podman_args)
|
||||
# cmd.extend(['run', '-d', 'alpine', 'sleep', '500'])
|
||||
cmd.extend(['run', '-dt', 'alpine', '/bin/sh'])
|
||||
PodmanTestCase.alpine_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=PodmanTestCase.alpine_log,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
PodmanTestCase.busybox_log = open(
|
||||
os.path.join('/tmp/', 'busybox.log'), 'w')
|
||||
|
||||
cmd = ['podman']
|
||||
cmd.extend(podman_args)
|
||||
cmd.extend(['create', 'busybox'])
|
||||
PodmanTestCase.busybox_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=PodmanTestCase.busybox_log,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
# give podman time to start ctnr
|
||||
time.sleep(2)
|
||||
|
||||
# Close our handle of file
|
||||
PodmanTestCase.alpine_log.close()
|
||||
PodmanTestCase.busybox_log.close()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Fixture to clean up after podman unittest."""
|
||||
try:
|
||||
PodmanTestCase.alpine_process.kill()
|
||||
assert 0 == PodmanTestCase.alpine_process.wait(500)
|
||||
delattr(PodmanTestCase, 'alpine_process')
|
||||
|
||||
PodmanTestCase.busybox_process.kill()
|
||||
assert 0 == PodmanTestCase.busybox_process.wait(500)
|
||||
except Exception as e:
|
||||
print('Exception: {}'.format(e))
|
||||
raise
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertRaisesNotImplemented(self):
|
||||
"""Sugar for unimplemented varlink methods."""
|
||||
with self.assertRaisesRegex(VarlinkError, MethodNotImplemented):
|
||||
yield
|
@ -1,43 +0,0 @@
|
||||
"""Decorator to retry failed method."""
|
||||
import functools
|
||||
import time
|
||||
|
||||
|
||||
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, print_=None):
|
||||
"""Retry calling the decorated function using an exponential backoff.
|
||||
|
||||
Specialized for our unittests
|
||||
from:
|
||||
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
|
||||
original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
|
||||
|
||||
:param ExceptionToCheck: the exception to check. may be a tuple of
|
||||
exceptions to check
|
||||
:type ExceptionToCheck: Exception or tuple
|
||||
:param tries: number of times to try (not retry) before giving up
|
||||
:type tries: int
|
||||
:param delay: initial delay between retries in seconds
|
||||
:type delay: int
|
||||
:param backoff: backoff multiplier e.g. value of 2 will double the delay
|
||||
each retry
|
||||
:type backoff: int
|
||||
"""
|
||||
def deco_retry(f):
|
||||
@functools.wraps(f)
|
||||
def f_retry(*args, **kwargs):
|
||||
mtries, mdelay = tries, delay
|
||||
while mtries > 1:
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ExceptionToCheck as e:
|
||||
if print_:
|
||||
print_('{}, Retrying in {} seconds...'.format(
|
||||
str(e), mdelay))
|
||||
time.sleep(mdelay)
|
||||
mtries -= 1
|
||||
mdelay *= backoff
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return f_retry # true decorator
|
||||
|
||||
return deco_retry
|
@ -1,35 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import podman
|
||||
from podman.client import BaseClient, Client, LocalClient, RemoteClient
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@patch('podman.libs.system.System.ping', return_value=True)
|
||||
def test_local(self, mock_ping):
|
||||
p = Client(
|
||||
uri='unix:/run/podman',
|
||||
interface='io.podman',
|
||||
)
|
||||
|
||||
self.assertIsInstance(p._client, LocalClient)
|
||||
self.assertIsInstance(p._client, BaseClient)
|
||||
|
||||
mock_ping.assert_called_once_with()
|
||||
|
||||
@patch('podman.libs.system.System.ping', return_value=True)
|
||||
def test_remote(self, mock_ping):
|
||||
p = Client(
|
||||
uri='unix:/run/podman',
|
||||
interface='io.podman',
|
||||
remote_uri='ssh://user@hostname/run/podman/podman',
|
||||
identity_file='~/.ssh/id_rsa')
|
||||
|
||||
self.assertIsInstance(p._client, BaseClient)
|
||||
mock_ping.assert_called_once_with()
|
@ -1,244 +0,0 @@
|
||||
import os
|
||||
import signal
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from test.podman_testcase import PodmanTestCase
|
||||
from test.retry_decorator import retry
|
||||
|
||||
import podman
|
||||
|
||||
|
||||
class TestContainers(PodmanTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = os.environ['TMPDIR']
|
||||
self.host = os.environ['PODMAN_HOST']
|
||||
|
||||
self.pclient = podman.Client(self.host)
|
||||
self.loadCache()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def loadCache(self):
|
||||
self.containers = list(self.pclient.containers.list())
|
||||
|
||||
self.alpine_ctnr = next(
|
||||
iter([c for c in self.containers if 'alpine' in c['image']] or []),
|
||||
None)
|
||||
|
||||
if self.alpine_ctnr and self.alpine_ctnr.status != 'running':
|
||||
self.alpine_ctnr.start()
|
||||
|
||||
def test_list(self):
|
||||
self.assertGreaterEqual(len(self.containers), 2)
|
||||
self.assertIsNotNone(self.alpine_ctnr)
|
||||
self.assertIn('alpine', self.alpine_ctnr.image)
|
||||
|
||||
def test_delete_stopped(self):
|
||||
before = len(self.containers)
|
||||
|
||||
self.alpine_ctnr.stop()
|
||||
target = self.alpine_ctnr.id
|
||||
actual = self.pclient.containers.delete_stopped()
|
||||
self.assertIn(target, actual)
|
||||
|
||||
self.loadCache()
|
||||
after = len(self.containers)
|
||||
|
||||
self.assertLess(after, before)
|
||||
TestContainers.setUpClass()
|
||||
|
||||
def test_get(self):
|
||||
actual = self.pclient.containers.get(self.alpine_ctnr.id)
|
||||
for k in ['id', 'status', 'ports']:
|
||||
self.assertEqual(actual[k], self.alpine_ctnr[k])
|
||||
|
||||
with self.assertRaises(podman.ContainerNotFound):
|
||||
self.pclient.containers.get("bozo")
|
||||
|
||||
def test_attach(self):
|
||||
# StringIO does not support fileno() so we had to go old school
|
||||
input = os.path.join(self.tmpdir, 'test_attach.stdin')
|
||||
output = os.path.join(self.tmpdir, 'test_attach.stdout')
|
||||
|
||||
with open(input, 'w+') as mock_in, open(output, 'w+') as mock_out:
|
||||
# double quote is indeed in the expected place
|
||||
mock_in.write('echo H"ello, World"; exit\n')
|
||||
mock_in.seek(0, 0)
|
||||
|
||||
ctnr = self.pclient.images.get(self.alpine_ctnr.image).container(
|
||||
detach=True, tty=True)
|
||||
ctnr.attach(stdin=mock_in.fileno(), stdout=mock_out.fileno())
|
||||
ctnr.start()
|
||||
|
||||
mock_out.flush()
|
||||
mock_out.seek(0, 0)
|
||||
output = mock_out.read()
|
||||
self.assertIn('Hello', output)
|
||||
|
||||
ctnr.remove(force=True)
|
||||
|
||||
def test_processes(self):
|
||||
actual = list(self.alpine_ctnr.processes())
|
||||
self.assertGreaterEqual(len(actual), 2)
|
||||
|
||||
def test_start_stop_wait(self):
|
||||
ctnr = self.alpine_ctnr.stop()
|
||||
self.assertFalse(ctnr['running'])
|
||||
|
||||
ctnr.start()
|
||||
self.assertTrue(ctnr.running)
|
||||
|
||||
ctnr.stop()
|
||||
self.assertFalse(ctnr['containerrunning'])
|
||||
|
||||
actual = ctnr.wait()
|
||||
self.assertGreaterEqual(actual, 0)
|
||||
|
||||
def test_changes(self):
|
||||
actual = self.alpine_ctnr.changes()
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(['changed', 'added', 'deleted']), sorted(
|
||||
list(actual.keys())))
|
||||
|
||||
# TODO: brittle, depends on knowing history of ctnr
|
||||
self.assertGreaterEqual(len(actual['changed']), 0)
|
||||
self.assertGreaterEqual(len(actual['added']), 0)
|
||||
self.assertEqual(len(actual['deleted']), 0)
|
||||
|
||||
def test_kill(self):
|
||||
self.assertTrue(self.alpine_ctnr.running)
|
||||
ctnr = self.alpine_ctnr.kill(signal.SIGKILL)
|
||||
self.assertFalse(ctnr.running)
|
||||
|
||||
def test_inspect(self):
|
||||
actual = self.alpine_ctnr.inspect()
|
||||
self.assertEqual(actual.id, self.alpine_ctnr.id)
|
||||
# TODO: Datetime values from inspect missing offset in CI instance
|
||||
# self.assertEqual(
|
||||
# datetime_parse(actual.created),
|
||||
# datetime_parse(self.alpine_ctnr.createdat))
|
||||
|
||||
def test_export(self):
|
||||
target = os.path.join(self.tmpdir, 'alpine_export_ctnr.tar')
|
||||
|
||||
actual = self.alpine_ctnr.export(target)
|
||||
self.assertEqual(actual, target)
|
||||
self.assertTrue(os.path.isfile(target))
|
||||
self.assertGreater(os.path.getsize(target), 0)
|
||||
|
||||
def test_commit(self):
|
||||
# TODO: Test for STOPSIGNAL when supported by OCI
|
||||
# TODO: Test for message when supported by OCI
|
||||
details = self.pclient.images.get(self.alpine_ctnr.image).inspect()
|
||||
changes = ['ENV=' + i for i in details.config['env']]
|
||||
changes.append('CMD=/usr/bin/zsh')
|
||||
changes.append('ENTRYPOINT=/bin/sh date')
|
||||
changes.append('ENV=TEST=test_containers.TestContainers.test_commit')
|
||||
changes.append('EXPOSE=80')
|
||||
changes.append('EXPOSE=8888')
|
||||
changes.append('LABEL=unittest=test_commit')
|
||||
changes.append('USER=bozo:circus')
|
||||
changes.append('VOLUME=/data')
|
||||
changes.append('WORKDIR=/data/application')
|
||||
|
||||
id = self.alpine_ctnr.commit(
|
||||
'alpine3', author='Bozo the clown', change=changes, pause=True)
|
||||
img = self.pclient.images.get(id)
|
||||
self.assertIsNotNone(img)
|
||||
|
||||
details = img.inspect()
|
||||
self.assertEqual(details.author, 'Bozo the clown')
|
||||
self.assertListEqual(['/usr/bin/zsh'], details.config['cmd'])
|
||||
self.assertListEqual(['/bin/sh date'],
|
||||
details.config['entrypoint'])
|
||||
self.assertIn('TEST=test_containers.TestContainers.test_commit',
|
||||
details.config['env'])
|
||||
self.assertTrue(
|
||||
[e for e in details.config['env'] if 'PATH=' in e])
|
||||
self.assertDictEqual({
|
||||
'80': {},
|
||||
'8888': {},
|
||||
}, details.config['exposedports'])
|
||||
self.assertDictEqual({'unittest': 'test_commit'}, details.labels)
|
||||
self.assertEqual('bozo:circus', details.config['user'])
|
||||
self.assertEqual({'/data': {}}, details.config['volumes'])
|
||||
self.assertEqual('/data/application',
|
||||
details.config['workingdir'])
|
||||
|
||||
def test_remove(self):
|
||||
before = len(self.containers)
|
||||
|
||||
with self.assertRaises(podman.ErrorOccurred):
|
||||
self.alpine_ctnr.remove()
|
||||
|
||||
self.assertEqual(
|
||||
self.alpine_ctnr.id, self.alpine_ctnr.remove(force=True))
|
||||
self.loadCache()
|
||||
after = len(self.containers)
|
||||
|
||||
self.assertLess(after, before)
|
||||
TestContainers.setUpClass()
|
||||
|
||||
def test_restart(self):
|
||||
self.assertTrue(self.alpine_ctnr.running)
|
||||
before = self.alpine_ctnr.runningfor
|
||||
|
||||
ctnr = self.alpine_ctnr.restart()
|
||||
self.assertTrue(ctnr.running)
|
||||
|
||||
after = self.alpine_ctnr.runningfor
|
||||
|
||||
# TODO: restore check when restart zeros counter
|
||||
# self.assertLess(after, before)
|
||||
|
||||
def test_rename(self):
|
||||
with self.assertRaisesNotImplemented():
|
||||
self.alpine_ctnr.rename('new_alpine')
|
||||
|
||||
def test_resize_tty(self):
|
||||
with self.assertRaisesNotImplemented():
|
||||
self.alpine_ctnr.resize_tty(132, 43)
|
||||
|
||||
def test_pause_unpause(self):
|
||||
self.assertTrue(self.alpine_ctnr.running)
|
||||
|
||||
ctnr = self.alpine_ctnr.pause()
|
||||
self.assertEqual(ctnr.status, 'paused')
|
||||
|
||||
ctnr = self.alpine_ctnr.unpause()
|
||||
self.assertTrue(ctnr.running)
|
||||
self.assertTrue(ctnr.status, 'running')
|
||||
|
||||
# creating cgoups can be flakey
|
||||
@retry(podman.libs.errors.ErrorOccurred, tries=4, delay=2, print_=print)
|
||||
def test_stats(self):
|
||||
try:
|
||||
self.assertTrue(self.alpine_ctnr.running)
|
||||
|
||||
actual = self.alpine_ctnr.stats()
|
||||
self.assertEqual(self.alpine_ctnr.id, actual.id)
|
||||
self.assertEqual(self.alpine_ctnr.names, actual.name)
|
||||
except Exception:
|
||||
info = Path('/proc/self/mountinfo')
|
||||
with info.open() as fd:
|
||||
print('{} {}'.format(self.alpine_ctnr.id, info))
|
||||
print(fd.read())
|
||||
|
||||
def test_logs(self):
|
||||
self.assertTrue(self.alpine_ctnr.running)
|
||||
actual = list(self.alpine_ctnr.logs())
|
||||
self.assertIsNotNone(actual)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,174 +0,0 @@
|
||||
import itertools
|
||||
import os
|
||||
import unittest
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from test.podman_testcase import PodmanTestCase
|
||||
|
||||
import podman
|
||||
from podman import FoldedString
|
||||
|
||||
|
||||
class TestImages(PodmanTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = os.environ['TMPDIR']
|
||||
self.host = os.environ['PODMAN_HOST']
|
||||
|
||||
self.pclient = podman.Client(self.host)
|
||||
self.images = self.loadCache()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def loadCache(self):
|
||||
with podman.Client(self.host) as pclient:
|
||||
self.images = list(pclient.images.list())
|
||||
|
||||
self.alpine_image = next(
|
||||
iter([
|
||||
i for i in self.images
|
||||
if 'docker.io/library/alpine:latest' in i['repoTags']
|
||||
] or []), None)
|
||||
|
||||
return self.images
|
||||
|
||||
def test_list(self):
|
||||
actual = self.loadCache()
|
||||
self.assertGreaterEqual(len(actual), 2)
|
||||
self.assertIsNotNone(self.alpine_image)
|
||||
|
||||
@unittest.skip('TODO: missing buildah json file')
|
||||
def test_build(self):
|
||||
path = os.path.join(self.tmpdir, 'ctnr', 'Dockerfile')
|
||||
img, logs = self.pclient.images.build(
|
||||
dockerfile=[path],
|
||||
tags=['alpine-unittest'],
|
||||
)
|
||||
self.assertIsNotNone(img)
|
||||
self.assertIn('localhost/alpine-unittest:latest', img.repoTags)
|
||||
self.assertLess(
|
||||
podman.datetime_parse(img.created), datetime.now(timezone.utc))
|
||||
self.assertTrue(logs)
|
||||
|
||||
def test_create(self):
|
||||
img_details = self.alpine_image.inspect()
|
||||
|
||||
actual = self.alpine_image.container(command=['sleep', '1h'])
|
||||
self.assertIsNotNone(actual)
|
||||
self.assertEqual(FoldedString(actual.status), 'configured')
|
||||
|
||||
ctnr = actual.start()
|
||||
self.assertEqual(FoldedString(ctnr.status), 'running')
|
||||
|
||||
ctnr_details = ctnr.inspect()
|
||||
for e in img_details.config['env']:
|
||||
self.assertIn(e, ctnr_details.config['env'])
|
||||
|
||||
def test_export(self):
|
||||
path = os.path.join(self.tmpdir, 'alpine_export.tar')
|
||||
target = 'oci-archive:{}:latest'.format(path)
|
||||
|
||||
actual = self.alpine_image.export(target, False)
|
||||
self.assertTrue(actual)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
def test_get(self):
|
||||
actual = self.pclient.images.get(self.alpine_image.id)
|
||||
self.assertEqual(actual, self.alpine_image)
|
||||
|
||||
def test_history(self):
|
||||
records = []
|
||||
bucket = Counter()
|
||||
for record in self.alpine_image.history():
|
||||
self.assertIn(record.id, (self.alpine_image.id, '<missing>'))
|
||||
bucket[record.id] += 1
|
||||
records.append(record)
|
||||
|
||||
self.assertGreater(bucket[self.alpine_image.id], 0)
|
||||
self.assertEqual(sum(bucket.values()), len(records))
|
||||
|
||||
def test_inspect(self):
|
||||
actual = self.alpine_image.inspect()
|
||||
self.assertEqual(actual.id, self.alpine_image.id)
|
||||
|
||||
def test_push(self):
|
||||
path = '{}/alpine_push'.format(self.tmpdir)
|
||||
target = 'dir:{}'.format(path)
|
||||
self.alpine_image.push(target, tlsverify=False)
|
||||
|
||||
self.assertTrue(os.path.isfile(os.path.join(path, 'manifest.json')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(path, 'version')))
|
||||
|
||||
def test_tag(self):
|
||||
self.assertEqual(self.alpine_image.id,
|
||||
self.alpine_image.tag('alpine:fubar'))
|
||||
self.loadCache()
|
||||
self.assertIn('localhost/alpine:fubar', self.alpine_image.repoTags)
|
||||
|
||||
def test_remove(self):
|
||||
before = self.loadCache()
|
||||
|
||||
# assertRaises doesn't follow the import name :(
|
||||
with self.assertRaises(podman.ErrorOccurred):
|
||||
self.alpine_image.remove()
|
||||
|
||||
actual = self.alpine_image.remove(force=True)
|
||||
self.assertEqual(self.alpine_image.id, actual)
|
||||
after = self.loadCache()
|
||||
|
||||
self.assertLess(len(after), len(before))
|
||||
TestImages.setUpClass()
|
||||
self.loadCache()
|
||||
|
||||
def test_import_delete_unused(self):
|
||||
before = self.loadCache()
|
||||
# create unused image, so we have something to delete
|
||||
source = os.path.join(self.tmpdir, 'alpine_gold.tar')
|
||||
new_img = self.pclient.images.import_image(
|
||||
source,
|
||||
'alpine2:latest',
|
||||
'unittest.test_import',
|
||||
)
|
||||
after = self.loadCache()
|
||||
|
||||
self.assertEqual(len(before) + 1, len(after))
|
||||
self.assertIsNotNone(
|
||||
next(iter([i for i in after if new_img in i['id']] or []), None))
|
||||
|
||||
actual = self.pclient.images.delete_unused()
|
||||
self.assertIn(new_img, actual)
|
||||
|
||||
after = self.loadCache()
|
||||
self.assertGreaterEqual(len(before), len(after))
|
||||
|
||||
TestImages.setUpClass()
|
||||
self.loadCache()
|
||||
|
||||
def test_pull(self):
|
||||
before = self.loadCache()
|
||||
actual = self.pclient.images.pull('prom/busybox:latest')
|
||||
after = self.loadCache()
|
||||
|
||||
self.assertEqual(len(before) + 1, len(after))
|
||||
self.assertIsNotNone(
|
||||
next(iter([i for i in after if actual in i['id']] or []), None))
|
||||
|
||||
def test_search(self):
|
||||
actual = self.pclient.images.search('alpine', 25)
|
||||
names, length = itertools.tee(actual)
|
||||
|
||||
for img in names:
|
||||
self.assertIn('alpine', img.name)
|
||||
self.assertTrue(0 < len(list(length)) <= 25)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,53 +0,0 @@
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import podman
|
||||
|
||||
|
||||
class TestLibs(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_parse(self):
|
||||
expected = datetime.datetime.strptime(
|
||||
'2018-05-08T14:12:53.797795-0700', '%Y-%m-%dT%H:%M:%S.%f%z')
|
||||
for v in [
|
||||
'2018-05-08T14:12:53.797795191-07:00',
|
||||
'2018-05-08T14:12:53.797795-07:00',
|
||||
'2018-05-08T14:12:53.797795-0700',
|
||||
'2018-05-08 14:12:53.797795191 -0700 MST',
|
||||
]:
|
||||
actual = podman.datetime_parse(v)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
expected = datetime.datetime.strptime(
|
||||
'2018-05-08T14:12:53.797795-0000', '%Y-%m-%dT%H:%M:%S.%f%z')
|
||||
for v in [
|
||||
'2018-05-08T14:12:53.797795191Z',
|
||||
'2018-05-08T14:12:53.797795191z',
|
||||
]:
|
||||
actual = podman.datetime_parse(v)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
actual = podman.datetime_parse(datetime.datetime.now().isoformat())
|
||||
self.assertIsNotNone(actual)
|
||||
|
||||
def test_parse_fail(self):
|
||||
for v in [
|
||||
'There is no time here.',
|
||||
]:
|
||||
with self.assertRaises(ValueError):
|
||||
podman.datetime_parse(v)
|
||||
|
||||
def test_format(self):
|
||||
expected = '2018-05-08T18:24:52.753227-07:00'
|
||||
dt = podman.datetime_parse(expected)
|
||||
actual = podman.datetime_format(dt)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,66 +0,0 @@
|
||||
import os
|
||||
from test.podman_testcase import PodmanTestCase
|
||||
|
||||
import podman
|
||||
from podman import FoldedString
|
||||
|
||||
pod = None
|
||||
|
||||
|
||||
class TestPodsCtnrs(PodmanTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Populate storage
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = os.environ['TMPDIR']
|
||||
self.host = os.environ['PODMAN_HOST']
|
||||
|
||||
self.pclient = podman.Client(self.host)
|
||||
|
||||
def test_010_populate(self):
|
||||
global pod
|
||||
|
||||
pod = self.pclient.pods.create('pod1')
|
||||
self.assertEqual('pod1', pod.name)
|
||||
|
||||
img = self.pclient.images.get('docker.io/library/alpine:latest')
|
||||
ctnr = img.container(pod=pod.id)
|
||||
|
||||
pod.refresh()
|
||||
self.assertEqual('1', pod.numberofcontainers)
|
||||
self.assertEqual(ctnr.id, pod.containersinfo[0]['id'])
|
||||
|
||||
def test_015_one_shot(self):
|
||||
global pod
|
||||
|
||||
details = pod.inspect()
|
||||
state = FoldedString(details.containers[0]['state'])
|
||||
self.assertEqual(state, 'configured')
|
||||
|
||||
pod = pod.start()
|
||||
status = FoldedString(pod.containersinfo[0]['status'])
|
||||
# Race on whether container is still running or finished
|
||||
self.assertIn(status, ('stopped', 'exited', 'running'))
|
||||
|
||||
pod = pod.restart()
|
||||
status = FoldedString(pod.containersinfo[0]['status'])
|
||||
self.assertIn(status, ('stopped', 'exited', 'running'))
|
||||
|
||||
# Pod kill is broken, so use stop for now
|
||||
killed = pod.stop()
|
||||
self.assertEqual(pod, killed)
|
||||
|
||||
def test_999_remove(self):
|
||||
global pod
|
||||
|
||||
ident = pod.remove(force=True)
|
||||
self.assertEqual(ident, pod.id)
|
||||
|
||||
with self.assertRaises(StopIteration):
|
||||
next(self.pclient.pods.list())
|
@ -1,94 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import podman
|
||||
import varlink
|
||||
|
||||
ident = None
|
||||
pod = None
|
||||
|
||||
|
||||
class TestPodsNoCtnrs(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmpdir = os.environ['TMPDIR']
|
||||
self.host = os.environ['PODMAN_HOST']
|
||||
|
||||
self.pclient = podman.Client(self.host)
|
||||
|
||||
def test_010_create(self):
|
||||
global ident
|
||||
|
||||
actual = self.pclient.pods.create('pod0')
|
||||
self.assertIsNotNone(actual)
|
||||
ident = actual.id
|
||||
|
||||
def test_015_list(self):
|
||||
global ident, pod
|
||||
|
||||
actual = next(self.pclient.pods.list())
|
||||
self.assertEqual('pod0', actual.name)
|
||||
self.assertEqual(ident, actual.id)
|
||||
self.assertEqual('Created', actual.status)
|
||||
self.assertEqual('0', actual.numberofcontainers)
|
||||
self.assertFalse(actual.containersinfo)
|
||||
pod = actual
|
||||
|
||||
def test_020_get(self):
|
||||
global ident, pod
|
||||
|
||||
actual = self.pclient.pods.get(pod.id)
|
||||
self.assertEqual('pod0', actual.name)
|
||||
self.assertEqual(ident, actual.id)
|
||||
self.assertEqual('Created', actual.status)
|
||||
self.assertEqual('0', actual.numberofcontainers)
|
||||
self.assertFalse(actual.containersinfo)
|
||||
|
||||
def test_025_inspect(self):
|
||||
global ident, pod
|
||||
|
||||
details = pod.inspect()
|
||||
self.assertEqual(ident, details.id)
|
||||
self.assertEqual('pod0', details.config['name'])
|
||||
self.assertIsNone(details.containers)
|
||||
|
||||
def test_030_ident_no_ctnrs(self):
|
||||
global ident, pod
|
||||
|
||||
actual = pod.kill()
|
||||
self.assertEqual(pod, actual)
|
||||
|
||||
actual = pod.pause()
|
||||
self.assertEqual(pod, actual)
|
||||
|
||||
actual = pod.unpause()
|
||||
self.assertEqual(pod, actual)
|
||||
|
||||
actual = pod.stop()
|
||||
self.assertEqual(pod, actual)
|
||||
|
||||
def test_045_raises_no_ctnrs(self):
|
||||
global ident, pod
|
||||
|
||||
with self.assertRaises(podman.NoContainersInPod):
|
||||
pod.start()
|
||||
|
||||
with self.assertRaises(podman.NoContainersInPod):
|
||||
pod.restart()
|
||||
|
||||
with self.assertRaises(podman.NoContainerRunning):
|
||||
next(pod.stats())
|
||||
|
||||
with self.assertRaises(varlink.error.MethodNotImplemented):
|
||||
pod.top()
|
||||
|
||||
with self.assertRaises(varlink.error.MethodNotImplemented):
|
||||
pod.wait()
|
||||
|
||||
def test_999_remove(self):
|
||||
global ident, pod
|
||||
|
||||
actual = pod.remove()
|
||||
self.assertEqual(ident, actual)
|
||||
|
||||
with self.assertRaises(StopIteration):
|
||||
next(self.pclient.pods.list())
|
@ -1,156 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# podman needs to play some games with resources
|
||||
if [[ $(id -u) != 0 ]]; then
|
||||
echo >&2 $0 must be run as root.
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# setup path to find new binaries _NOT_ system binaries
|
||||
if [[ ! -x ../../../bin/podman ]]; then
|
||||
echo 1>&2 Cannot find podman binary from libpod root directory. Run \"make binaries\"
|
||||
exit 1
|
||||
fi
|
||||
export PATH=../../../bin:$PATH
|
||||
|
||||
function usage {
|
||||
echo 1>&2 $0 '[-v] [-h] [test.<TestCase>|test.<TestCase>.<step>]'
|
||||
}
|
||||
|
||||
while getopts "vh" arg; do
|
||||
case $arg in
|
||||
v ) VERBOSE='-v'; export LOG_LEVEL=debug ;;
|
||||
h ) usage ; exit 0 ;;
|
||||
\? ) usage ; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND -1))
|
||||
|
||||
function cleanup {
|
||||
set +xeuo pipefail
|
||||
# aggressive cleanup as tests may crash leaving crap around
|
||||
umount '^(shm|nsfs)'
|
||||
umount '\/run\/netns'
|
||||
if [[ $RETURNCODE -eq 0 ]]; then
|
||||
rm -r "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create temporary directory for storage
|
||||
export TMPDIR=`mktemp -d /tmp/podman.XXXXXXXXXX`
|
||||
trap "cleanup $TMPDIR" EXIT
|
||||
|
||||
function umount {
|
||||
set +xeuo pipefail
|
||||
# xargs -r always ran once, so write any mount points to file first
|
||||
mount |awk "/$1/"' { print $3 }' >${TMPDIR}/mounts
|
||||
if [[ -s ${TMPDIR}/mounts ]]; then
|
||||
xargs <${TMPDIR}/mounts -t umount
|
||||
fi
|
||||
}
|
||||
|
||||
function showlog {
|
||||
[[ -s $1 ]] && cat <<-EOT
|
||||
$1 =====
|
||||
$(cat "$1")
|
||||
|
||||
EOT
|
||||
}
|
||||
|
||||
# Need locations to store stuff
|
||||
mkdir -p ${TMPDIR}/{podman,crio,crio-run,cni/net.d,ctnr,tunnel}
|
||||
|
||||
# Cannot be done in python unittest fixtures. EnvVar not picked up.
|
||||
export REGISTRIES_CONFIG_PATH=${TMPDIR}/registry.conf
|
||||
cat >$REGISTRIES_CONFIG_PATH <<-EOT
|
||||
[registries.search]
|
||||
registries = ['docker.io']
|
||||
[registries.insecure]
|
||||
registries = []
|
||||
[registries.block]
|
||||
registries = []
|
||||
EOT
|
||||
|
||||
export CNI_CONFIG_PATH=${TMPDIR}/cni/net.d
|
||||
cat >$CNI_CONFIG_PATH/87-podman-bridge.conflist <<-EOT
|
||||
{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "podman",
|
||||
"plugins": [{
|
||||
"type": "bridge",
|
||||
"bridge": "cni0",
|
||||
"isGateway": true,
|
||||
"ipMasq": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.88.0.0/16",
|
||||
"routes": [{
|
||||
"dst": "0.0.0.0/0"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "portmap",
|
||||
"capabilities": {
|
||||
"portMappings": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
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/
|
||||
RUN chmod 755 /tmp/hello.sh
|
||||
ENTRYPOINT ["/tmp/hello.sh"]
|
||||
EOT
|
||||
|
||||
export PODMAN_HOST="unix:${TMPDIR}/podman/io.podman"
|
||||
PODMAN_ARGS="--storage-driver=vfs \
|
||||
--root=${TMPDIR}/crio \
|
||||
--runroot=${TMPDIR}/crio-run \
|
||||
--cni-config-dir=$CNI_CONFIG_PATH \
|
||||
--cgroup-manager=cgroupfs \
|
||||
"
|
||||
if [[ -n $VERBOSE ]]; then
|
||||
PODMAN_ARGS="$PODMAN_ARGS --log-level=$LOG_LEVEL"
|
||||
fi
|
||||
PODMAN="podman $PODMAN_ARGS"
|
||||
|
||||
cat <<-EOT |tee /tmp/test_podman.output
|
||||
$($PODMAN --version)
|
||||
$PODMAN varlink --timeout=0 ${PODMAN_HOST}
|
||||
==========================================
|
||||
EOT
|
||||
|
||||
# Run podman in background without systemd for test purposes
|
||||
$PODMAN varlink --timeout=0 ${PODMAN_HOST} >>/tmp/test_podman.output 2>&1 &
|
||||
if [[ $? != 0 ]]; then
|
||||
echo 1>&2 Failed to start podman
|
||||
showlog /tmp/test_podman.output
|
||||
fi
|
||||
|
||||
if [[ -z $1 ]]; then
|
||||
export PYTHONPATH=.
|
||||
python3 -m unittest discover -s . $VERBOSE
|
||||
RETURNCODE=$?
|
||||
else
|
||||
export PYTHONPATH=.:./test
|
||||
python3 -m unittest $1 $VERBOSE
|
||||
RETURNCODE=$?
|
||||
fi
|
||||
|
||||
pkill -9 podman
|
||||
pkill -9 conmon
|
||||
|
||||
showlog /tmp/test_podman.output
|
||||
showlog /tmp/alpine.log
|
||||
showlog /tmp/busybox.log
|
||||
|
||||
exit $RETURNCODE
|
@ -1,63 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import podman
|
||||
import varlink
|
||||
|
||||
|
||||
class TestSystem(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.host = os.environ['PODMAN_HOST']
|
||||
self.tmpdir = os.environ['TMPDIR']
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_bad_address(self):
|
||||
with self.assertRaisesRegex(varlink.client.ConnectionError,
|
||||
"Invalid address 'bad address'"):
|
||||
podman.Client('bad address')
|
||||
|
||||
def test_ping(self):
|
||||
with podman.Client(self.host) as pclient:
|
||||
self.assertTrue(pclient.system.ping())
|
||||
|
||||
@unittest.skip("TODO: Need to setup ssh under Travis")
|
||||
def test_remote_ping(self):
|
||||
host = urlparse(self.host)
|
||||
remote_uri = 'ssh://root@localhost{}'.format(host.path)
|
||||
|
||||
local_uri = 'unix:{}/tunnel/podman.sock'.format(self.tmpdir)
|
||||
with podman.Client(
|
||||
uri=local_uri,
|
||||
remote_uri=remote_uri,
|
||||
identity_file=os.path.expanduser('~/.ssh/id_rsa'),
|
||||
) as remote_client:
|
||||
self.assertTrue(remote_client.system.ping())
|
||||
|
||||
def test_versions(self):
|
||||
with podman.Client(self.host) as pclient:
|
||||
# Values change with each build so we cannot test too much
|
||||
self.assertListEqual(
|
||||
sorted([
|
||||
'built', 'client_version', 'git_commit', 'go_version',
|
||||
'os_arch', 'version'
|
||||
]), sorted(list(pclient.system.versions._fields)))
|
||||
pclient.system.versions
|
||||
self.assertIsNot(podman.__version__, '0.0.0')
|
||||
|
||||
def test_info(self):
|
||||
with podman.Client(self.host) as pclient:
|
||||
actual = pclient.system.info()
|
||||
# Values change too much to do exhaustive testing
|
||||
self.assertIsNotNone(actual.podman['go_version'])
|
||||
self.assertListEqual(
|
||||
sorted([
|
||||
'host', 'insecure_registries', 'podman', 'registries',
|
||||
'store'
|
||||
]), sorted(list(actual._fields)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,86 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from podman.libs.tunnel import Context, Portal, Tunnel
|
||||
|
||||
|
||||
class TestTunnel(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tunnel_01 = MagicMock(spec=Tunnel)
|
||||
self.tunnel_02 = MagicMock(spec=Tunnel)
|
||||
|
||||
def test_portal_ops(self):
|
||||
portal = Portal(sweap=500)
|
||||
portal['unix:/01'] = self.tunnel_01
|
||||
portal['unix:/02'] = self.tunnel_02
|
||||
|
||||
self.assertEqual(portal.get('unix:/01'), self.tunnel_01)
|
||||
self.assertEqual(portal.get('unix:/02'), self.tunnel_02)
|
||||
|
||||
del portal['unix:/02']
|
||||
with self.assertRaises(KeyError):
|
||||
portal['unix:/02']
|
||||
self.assertEqual(len(portal), 1)
|
||||
|
||||
def test_portal_reaping(self):
|
||||
portal = Portal(sweap=0.5)
|
||||
portal['unix:/01'] = self.tunnel_01
|
||||
portal['unix:/02'] = self.tunnel_02
|
||||
|
||||
self.assertEqual(len(portal), 2)
|
||||
for entry in portal:
|
||||
self.assertIn(entry, (self.tunnel_01, self.tunnel_02))
|
||||
|
||||
time.sleep(1)
|
||||
portal.reap()
|
||||
self.assertEqual(len(portal), 0)
|
||||
|
||||
def test_portal_no_reaping(self):
|
||||
portal = Portal(sweap=500)
|
||||
portal['unix:/01'] = self.tunnel_01
|
||||
portal['unix:/02'] = self.tunnel_02
|
||||
|
||||
portal.reap()
|
||||
self.assertEqual(len(portal), 2)
|
||||
for entry in portal:
|
||||
self.assertIn(entry, (self.tunnel_01, self.tunnel_02))
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('weakref.finalize')
|
||||
def test_tunnel(self, mock_finalize, mock_exists, mock_Popen):
|
||||
mock_Popen.return_value.returncode = 0
|
||||
|
||||
context = Context(
|
||||
'unix:/01',
|
||||
'io.podman',
|
||||
'/tmp/user/socket',
|
||||
'/run/podman/socket',
|
||||
'user',
|
||||
'hostname',
|
||||
None,
|
||||
'~/.ssh/id_rsa',
|
||||
)
|
||||
tunnel = Tunnel(context).bore()
|
||||
|
||||
cmd = ['ssh', '-fNT']
|
||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
cmd.append('-v')
|
||||
else:
|
||||
cmd.append('-q')
|
||||
|
||||
cmd.extend((
|
||||
'-L',
|
||||
'{}:{}'.format(context.local_socket, context.remote_socket),
|
||||
'-i',
|
||||
context.identity_file,
|
||||
'{}@{}'.format(context.username, context.hostname),
|
||||
))
|
||||
|
||||
mock_finalize.assert_called_once_with(tunnel, tunnel.close)
|
||||
mock_exists.assert_called_once_with(context.local_socket)
|
||||
mock_Popen.assert_called_once_with(cmd, close_fds=True)
|
@ -1,8 +0,0 @@
|
||||
[tox]
|
||||
envlist = py34,py35,py36
|
||||
skipdist = True
|
||||
|
||||
[testenv]
|
||||
deps=-rrequirements.txt
|
||||
whitelist_externals = bash
|
||||
commands=bash test/test_runner.sh
|
@ -1,564 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=0
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
locally-enabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
#argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
argument-rgx=[a-z_][a-z0-9_]{1,30}$
|
||||
argument-name-hint=[a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=c,
|
||||
e,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
r,
|
||||
v,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
#variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Support argparse.Action constructor API
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=12
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=10
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception".
|
||||
overgeneral-exceptions=Exception
|
@ -1,2 +0,0 @@
|
||||
prune test/
|
||||
include README.md
|
@ -1,39 +0,0 @@
|
||||
PYTHON ?= $(shell command -v python3 2>/dev/null || command -v python)
|
||||
DESTDIR := /
|
||||
PODMAN_VERSION ?= '0.11.1.1'
|
||||
|
||||
.PHONY: python-pypodman
|
||||
python-pypodman:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) \
|
||||
$(PYTHON) setup.py sdist bdist
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(PYTHON) -m pylint pypodman
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
true
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) \
|
||||
$(PYTHON) setup.py install --root ${DESTDIR}
|
||||
|
||||
.PHONY: upload
|
||||
upload:
|
||||
PODMAN_VERSION=$(PODMAN_VERSION) $(PYTHON) setup.py sdist bdist_wheel
|
||||
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
|
||||
.PHONY: clobber
|
||||
clobber: uninstall clean
|
||||
|
||||
.PHONY: uninstall
|
||||
$(PYTHON) -m pip uninstall --yes pypodman ||:
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf pypodman.egg-info dist
|
||||
find . -depth -name __pycache__ -exec rm -rf {} \;
|
||||
find . -depth -name \*.pyc -exec rm -f {} \;
|
||||
$(PYTHON) ./setup.py clean --all
|
@ -1,34 +0,0 @@
|
||||
# pypodman - CLI for podman written in python
|
||||
|
||||
## Status: Active Development
|
||||
|
||||
See [libpod](https://github.com/containers/libpod/contrib/python/pypodman)
|
||||
|
||||
## Releases
|
||||
|
||||
To build the pypodman egg and install as user:
|
||||
|
||||
```sh
|
||||
cd ~/libpod/contrib/python/pypodman
|
||||
python3 setup.py clean -a && python3 setup.py sdist bdist
|
||||
python3 setup.py install --user
|
||||
```
|
||||
Add `~/.local/bin` to your `PATH` to run pypodman command.
|
||||
|
||||
## Running command:
|
||||
|
||||
### Against local podman service
|
||||
```sh
|
||||
$ pypodman images
|
||||
```
|
||||
### Against remote podman service
|
||||
```sh
|
||||
$ pypodman --host node001.example.org images
|
||||
```
|
||||
### Full help system available
|
||||
```sh
|
||||
$ pypodman -h
|
||||
```
|
||||
```sh
|
||||
$ pypodman images -h
|
||||
```
|
@ -1,101 +0,0 @@
|
||||
.TH pypodman 1 2018-07-20 0.7.3
|
||||
.SH NAME
|
||||
pypodman \- CLI management tool for containers and images
|
||||
.SH SYNOPSIS
|
||||
\f[B]pypodman\f[] [\f[I]global options\f[]] \f[I]command\f[] [\f[I]options\f[]]
|
||||
.SH DESCRIPTION
|
||||
pypodman is a simple client only tool to help with debugging issues when daemons
|
||||
such as CRI runtime and the kubelet are not responding or failing.
|
||||
.P
|
||||
pypodman uses a VarLink API to commicate with a podman service running on either
|
||||
the local or remote machine. pypodman uses ssh to create secure tunnels when
|
||||
communicating with a remote service.
|
||||
.SH GLOBAL OPTIONS
|
||||
.PP
|
||||
\f[B]\[en]help, \-h\f[]
|
||||
.PP
|
||||
Print usage statement.
|
||||
.PP
|
||||
\f[B]\[en]version\f[]
|
||||
.PP
|
||||
Print program version number and exit.
|
||||
.PP
|
||||
\f[B]\[en]config\-home\f[]
|
||||
.PP
|
||||
Directory that will be namespaced with \f[C]pypodman\f[] to hold
|
||||
\f[C]pypodman.conf\f[].
|
||||
See FILES below for more details.
|
||||
.PP
|
||||
\f[B]\[en]log\-level\f[]
|
||||
.PP
|
||||
Log events above specified level: DEBUG, INFO, WARNING (default), ERROR,
|
||||
or CRITICAL.
|
||||
.PP
|
||||
\f[B]\[en]run\-dir\f[]
|
||||
.PP
|
||||
Directory that will be namespaced with \f[C]pypodman\f[] to hold local socket
|
||||
bindings. The default is `\f[C]$XDG_RUNTIME_DIR\\\f[].
|
||||
.PP
|
||||
\f[B]\[en]user\f[]
|
||||
.PP
|
||||
Authenicating user on remote host. \f[C]pypodman\f[] defaults to the logged in
|
||||
user.
|
||||
.PP
|
||||
\f[B]\[en]host\f[]
|
||||
.PP
|
||||
Name of remote host. There is no default, if not given \f[C]pypodman\f[]
|
||||
attempts to connect to \f[C]\-\-remote\-socket\-path\f[] on local host.
|
||||
.PP
|
||||
\f[B]\[en]port\f[]
|
||||
.PP
|
||||
The optional port for \f[C]ssh\f[] to connect tunnel to on remote host.
|
||||
Default is None and will allow \f[C]ssh\f[] to follow it's default configuration.
|
||||
.PP
|
||||
\f[B]\[en]remote\-socket\-path\f[]
|
||||
.PP
|
||||
Path on remote host for podman service's \f[C]AF_UNIX\f[] socket. The default is
|
||||
\f[C]/run/podman/io.podman\f[].
|
||||
.PP
|
||||
\f[B]\[en]identity\-file\f[]
|
||||
.PP
|
||||
The optional \f[C]ssh\f[] identity file to authenicate when tunnelling to remote
|
||||
host. Default is None and will allow \f[C]ssh\f[] to follow it's default methods
|
||||
for resolving the identity and private key using the logged in user.
|
||||
.SH COMMANDS
|
||||
.PP
|
||||
See podman(1) (podman.1.md)
|
||||
.SH FILES
|
||||
.PP
|
||||
\f[B]pypodman/pypodman.conf\f[]
|
||||
(\f[C]Any\ element\ of\ XDG_CONFIG_DIRS\f[] and/or
|
||||
\f[C]XDG_CONFIG_HOME\f[] and/or \f[B]\[en]config\-home\f[])
|
||||
.PP
|
||||
pypodman.conf is one or more configuration files for running the pypodman
|
||||
command. pypodman.conf is a TOML file with the stanza \f[C][default]\f[], with a
|
||||
map of \f[C]option: value\f[].
|
||||
.PP
|
||||
pypodman follows the XDG (freedesktop.org) conventions for resolving it's
|
||||
configuration. The list below are read from top to bottom with later items
|
||||
overwriting earlier. Any missing items are ignored.
|
||||
.IP \[bu] 2
|
||||
\f[C]pypodman/pypodman.conf\f[] from any path element in
|
||||
\f[C]XDG_CONFIG_DIRS\f[] or \f[C]\\etc\\xdg\f[]
|
||||
.IP \[bu] 2
|
||||
\f[C]XDG_CONFIG_HOME\f[] or $HOME/.config + \f[C]pypodman/pypodman.conf\f[]
|
||||
.IP \[bu] 2
|
||||
From \f[C]\-\-config\-home\f[] command line option + \f[C]pypodman/pypodman.conf\f[]
|
||||
.IP \[bu] 2
|
||||
From environment variable prefixed with PODMAN_, for example: PODMAN_RUN_DIR
|
||||
.IP \[bu] 2
|
||||
From command line option, for example: \[en]run\-dir
|
||||
.PP
|
||||
This should provide Operators the ability to setup basic configurations
|
||||
and allow users to customize them.
|
||||
.PP
|
||||
\f[B]XDG_RUNTIME_DIR\f[] (\f[C]XDG_RUNTIME_DIR/io.podman\f[])
|
||||
.PP
|
||||
Directory where pypodman stores non\-essential runtime files and other file
|
||||
objects (such as sockets, named pipes, \&...).
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\f[C]podman(1)\f[], \f[C]libpod(8)\f[]
|
@ -1,43 +0,0 @@
|
||||
"""Remote podman client support library."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib.action_base import AbstractActionBase
|
||||
from pypodman.lib.parser_actions import (ChangeAction, PathAction,
|
||||
PositiveIntAction, SignalAction,
|
||||
UnitAction)
|
||||
from pypodman.lib.podman_parser import PodmanArgumentParser
|
||||
from pypodman.lib.report import Report, ReportColumn
|
||||
|
||||
# Silence pylint overlording...
|
||||
assert ChangeAction
|
||||
assert PathAction
|
||||
assert PositiveIntAction
|
||||
assert SignalAction
|
||||
assert UnitAction
|
||||
|
||||
__all__ = [
|
||||
'AbstractActionBase',
|
||||
'PodmanArgumentParser',
|
||||
'Report',
|
||||
'ReportColumn',
|
||||
]
|
||||
|
||||
|
||||
def query_model(model, identifiers=None):
|
||||
"""Retrieve all (default) or given model(s)."""
|
||||
objs = []
|
||||
if identifiers is None:
|
||||
objs.extend(model.list())
|
||||
else:
|
||||
try:
|
||||
for ident in identifiers:
|
||||
objs.append(model.get(ident))
|
||||
except (
|
||||
podman.PodNotFound,
|
||||
podman.ImageNotFound,
|
||||
podman.ContainerNotFound,
|
||||
) as ex:
|
||||
print(
|
||||
'"{}" not found'.format(ex.name), file=sys.stderr, flush=True)
|
||||
return objs
|
@ -1,79 +0,0 @@
|
||||
"""Base class for all actions of remote client."""
|
||||
import abc
|
||||
from functools import lru_cache
|
||||
|
||||
import podman
|
||||
|
||||
|
||||
class AbstractActionBase(abc.ABC):
|
||||
"""Base class for all actions of remote client."""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def subparser(cls, parent):
|
||||
"""Define parser for this action. Subclasses must implement.
|
||||
|
||||
API:
|
||||
Use set_defaults() to set attributes "class_" and "method". These will
|
||||
be invoked as class_(parsed_args).method()
|
||||
"""
|
||||
parent.add_flag(
|
||||
'--all',
|
||||
help='list all items.')
|
||||
parent.add_flag(
|
||||
'--truncate',
|
||||
'--trunc',
|
||||
default=True,
|
||||
help="Truncate id's and other long fields.")
|
||||
parent.add_flag(
|
||||
'--heading',
|
||||
default=True,
|
||||
help='Include table headings in the output.')
|
||||
parent.add_flag(
|
||||
'--quiet',
|
||||
help='List only the IDs.')
|
||||
|
||||
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):
|
||||
"""URI for remote side of connection."""
|
||||
return self._args.remote_uri
|
||||
|
||||
@property
|
||||
def local_uri(self):
|
||||
"""URI for local side of connection."""
|
||||
return self._args.local_uri
|
||||
|
||||
@property
|
||||
def identity_file(self):
|
||||
"""Key for authenication."""
|
||||
return self._args.identity_file
|
||||
|
||||
@property
|
||||
@lru_cache(maxsize=1)
|
||||
def client(self):
|
||||
"""Podman remote client for communicating."""
|
||||
if self._args.host is None:
|
||||
return podman.Client(uri=self.local_uri)
|
||||
return podman.Client(
|
||||
uri=self.local_uri,
|
||||
remote_uri=self.remote_uri,
|
||||
identity_file=self.identity_file)
|
||||
|
||||
def __repr__(self):
|
||||
"""Compute the “official” string representation of object."""
|
||||
return ("{}(local_uri='{}', remote_uri='{}',"
|
||||
" identity_file='{}')").format(
|
||||
self.__class__,
|
||||
self.local_uri,
|
||||
self.remote_uri,
|
||||
self.identity_file,
|
||||
)
|
@ -1,54 +0,0 @@
|
||||
"""Module to export all the podman subcommands."""
|
||||
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
|
||||
from pypodman.lib.actions.mount_action import Mount
|
||||
from pypodman.lib.actions.pause_action import Pause
|
||||
from pypodman.lib.actions.pod_action import Pod
|
||||
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
|
||||
from pypodman.lib.actions.start_action import Start
|
||||
from pypodman.lib.actions.version_action import Version
|
||||
|
||||
__all__ = [
|
||||
'Attach',
|
||||
'Commit',
|
||||
'Create',
|
||||
'Export',
|
||||
'History',
|
||||
'Images',
|
||||
'Import',
|
||||
'Info',
|
||||
'Inspect',
|
||||
'Kill',
|
||||
'Logs',
|
||||
'Mount',
|
||||
'Pause',
|
||||
'Pod',
|
||||
'Port',
|
||||
'Ps',
|
||||
'Pull',
|
||||
'Push',
|
||||
'Restart',
|
||||
'Rm',
|
||||
'Rmi',
|
||||
'Run',
|
||||
'Search',
|
||||
'Start',
|
||||
'Version',
|
||||
]
|
@ -1,401 +0,0 @@
|
||||
"""Implement common create container arguments together."""
|
||||
|
||||
from pypodman.lib import SignalAction, 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_flag(
|
||||
'--detach',
|
||||
'-d',
|
||||
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',
|
||||
action='append',
|
||||
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',
|
||||
action='append',
|
||||
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')
|
||||
|
||||
# only way for argparse to handle these options.
|
||||
vol_args = {
|
||||
'choices': ('bind', 'tmpfs', 'ignore'),
|
||||
'metavar': 'MODE',
|
||||
'type': str.lower,
|
||||
'help': 'Tells podman how to handle the builtin image volumes',
|
||||
}
|
||||
|
||||
volume_group = parser.add_mutually_exclusive_group()
|
||||
volume_group.add_argument('--image-volume', **vol_args)
|
||||
volume_group.add_argument('--builtin-volume', **vol_args)
|
||||
|
||||
parser.add_flag(
|
||||
'--interactive',
|
||||
'-i',
|
||||
help='Keep STDIN open even if not attached.')
|
||||
parser.add_argument('--ipc', help='Create namespace')
|
||||
parser.add_argument(
|
||||
'--kernel-memory', action=UnitAction, help='Kernel memory limit')
|
||||
parser.add_argument(
|
||||
'--label',
|
||||
'-l',
|
||||
action='append',
|
||||
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',
|
||||
metavar='json-file',
|
||||
default='json-file',
|
||||
help='Logging driver for the container. (default: %(default)s)')
|
||||
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',
|
||||
'--net',
|
||||
metavar='BRIDGE',
|
||||
help='Set the Network mode for the container.'
|
||||
' (format: bridge, host, container:UUID, ns:PATH, none)')
|
||||
parser.add_flag(
|
||||
'--oom-kill-disable',
|
||||
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 Namespace mode for the container.'
|
||||
'(format: host, container:UUID, ns:PATH)')
|
||||
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_flag(
|
||||
'--privileged',
|
||||
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_flag(
|
||||
'--publish-all',
|
||||
'-P',
|
||||
help='Publish all exposed ports to random'
|
||||
' ports on the host interfaces.')
|
||||
parser.add_flag(
|
||||
'--quiet',
|
||||
'-q',
|
||||
help='Suppress output information when pulling images')
|
||||
parser.add_flag(
|
||||
'--read-only',
|
||||
help="Mount the container's root filesystem as read only.")
|
||||
parser.add_flag(
|
||||
'--rm',
|
||||
help='Automatically remove the container when it exits.')
|
||||
parser.add_argument(
|
||||
'--rootfs',
|
||||
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_flag(
|
||||
'--sig-proxy',
|
||||
help='Proxy signals sent to the podman run'
|
||||
' command to the container process')
|
||||
parser.add_argument(
|
||||
'--stop-signal',
|
||||
action=SignalAction,
|
||||
default='TERM',
|
||||
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',
|
||||
action='append',
|
||||
metavar='MOUNT',
|
||||
help='Create a tmpfs mount.'
|
||||
' (default: rw,noexec,nosuid,nodev,size=65536k.)')
|
||||
parser.add_flag(
|
||||
'--tty',
|
||||
'-t',
|
||||
help='Allocate a pseudo-TTY for standard input of container.')
|
||||
parser.add_argument(
|
||||
'--uidmap',
|
||||
action='append',
|
||||
metavar='MAP',
|
||||
help='UID map for the user namespace')
|
||||
parser.add_argument(
|
||||
'--ulimit',
|
||||
action='append',
|
||||
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',
|
||||
metavar='NAMESPACE',
|
||||
help='Set the user namespace mode for the container')
|
||||
parser.add_argument(
|
||||
'--uts',
|
||||
choices=('host', 'ns'),
|
||||
type=str.lower,
|
||||
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')
|
@ -1,68 +0,0 @@
|
||||
"""Remote client command for attaching to a container."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Attach(AbstractActionBase):
|
||||
"""Class for attaching to a running container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Attach command to parent parser."""
|
||||
parser = parent.add_parser('attach', help='attach to container')
|
||||
parser.add_argument(
|
||||
'--image',
|
||||
help='image to instantiate and attach to',
|
||||
)
|
||||
parser.add_argument(
|
||||
'command',
|
||||
nargs='*',
|
||||
help='image to instantiate and attach to',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='attach')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Attach class."""
|
||||
super().__init__(args)
|
||||
if not args.image:
|
||||
raise ValueError('You must supply one image id'
|
||||
' or name to be attached.')
|
||||
|
||||
def attach(self):
|
||||
"""Attach to instantiated image."""
|
||||
args = {
|
||||
'detach': True,
|
||||
'tty': True,
|
||||
}
|
||||
if self._args.command:
|
||||
args['command'] = self._args.command
|
||||
|
||||
try:
|
||||
try:
|
||||
ident = self.client.images.pull(self._args.image)
|
||||
img = self.client.images.get(ident)
|
||||
except podman.ImageNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Image {} not found.'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
|
||||
ctnr = img.create(**args)
|
||||
ctnr.attach(eot=4)
|
||||
|
||||
try:
|
||||
ctnr.start()
|
||||
print()
|
||||
except (BrokenPipeError, KeyboardInterrupt):
|
||||
print('\nContainer disconnected.')
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
@ -1,99 +0,0 @@
|
||||
"""Remote client command for creating image from container."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, ChangeAction
|
||||
|
||||
|
||||
class Commit(AbstractActionBase):
|
||||
"""Class for creating image from container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Commit command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'commit',
|
||||
help='create image from container',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--author',
|
||||
help='Set the author for the committed image',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--change',
|
||||
'-c',
|
||||
action=ChangeAction,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
'-f',
|
||||
choices=('oci', 'docker'),
|
||||
default='oci',
|
||||
type=str.lower,
|
||||
help='Set the format of the image manifest and metadata.'
|
||||
' (Ignored.)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--iidfile',
|
||||
metavar='PATH',
|
||||
help='Write the image ID to the file',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--message',
|
||||
'-m',
|
||||
help='Set commit message for committed image'
|
||||
' (Only on docker images.)',
|
||||
)
|
||||
parser.add_flag(
|
||||
'--pause',
|
||||
'-p',
|
||||
help='Pause the container when creating an image',
|
||||
)
|
||||
parser.add_flag(
|
||||
'--quiet',
|
||||
'-q',
|
||||
help='Suppress output',
|
||||
)
|
||||
parser.add_argument(
|
||||
'container',
|
||||
nargs=1,
|
||||
help='container to use as source',
|
||||
)
|
||||
parser.add_argument(
|
||||
'image',
|
||||
nargs=1,
|
||||
help='image name to create',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='commit')
|
||||
|
||||
def commit(self):
|
||||
"""Create image from container."""
|
||||
try:
|
||||
try:
|
||||
ctnr = self.client.containers.get(self._args.container[0])
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container {} not found.'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
else:
|
||||
ident = ctnr.commit(
|
||||
self.opts['image'][0],
|
||||
change=self.opts.get('change', None),
|
||||
message=self.opts.get('message', None),
|
||||
pause=self.opts['pause'],
|
||||
author=self.opts.get('author', None),
|
||||
)
|
||||
|
||||
if not self.opts['quiet']:
|
||||
print(ident)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,55 +0,0 @@
|
||||
"""Remote client command for creating container from image."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
from ._create_args import CreateArguments
|
||||
|
||||
|
||||
class Create(AbstractActionBase):
|
||||
"""Class for creating container from image."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Create command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'create', help='create container from image')
|
||||
|
||||
CreateArguments.add_arguments(parser)
|
||||
|
||||
parser.add_argument('image', nargs=1, help='source image id')
|
||||
parser.add_argument(
|
||||
'command',
|
||||
nargs=parent.REMAINDER,
|
||||
help='command and args to run.',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='create')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Create class."""
|
||||
super().__init__(args)
|
||||
|
||||
# image id used only on client
|
||||
del self.opts['image']
|
||||
|
||||
def create(self):
|
||||
"""Create container."""
|
||||
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)
|
@ -1,54 +0,0 @@
|
||||
"""Remote client command for export container filesystem to tarball."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Export(AbstractActionBase):
|
||||
"""Class for exporting container filesystem to tarball."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Export command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'export',
|
||||
help='export container to tarball',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
'-o',
|
||||
metavar='PATH',
|
||||
nargs=1,
|
||||
required=True,
|
||||
help='Write to this file on host',
|
||||
)
|
||||
parser.add_argument(
|
||||
'container',
|
||||
nargs=1,
|
||||
help='container to use as source',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='export')
|
||||
|
||||
def export(self):
|
||||
"""Create tarball from container filesystem."""
|
||||
try:
|
||||
try:
|
||||
ctnr = self.client.containers.get(self._args.container[0])
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container {} not found.'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
else:
|
||||
ctnr.export(self._args.output[0])
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,79 +0,0 @@
|
||||
"""Remote client for reporting image history."""
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
import humanize
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, 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_flag(
|
||||
'--human',
|
||||
'-H',
|
||||
help='Display sizes and dates in human readable format.')
|
||||
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),
|
||||
'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)
|
@ -1,86 +0,0 @@
|
||||
"""Remote client commands dealing with images."""
|
||||
import operator
|
||||
from collections import OrderedDict
|
||||
|
||||
import humanize
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, Report, ReportColumn
|
||||
|
||||
|
||||
class Images(AbstractActionBase):
|
||||
"""Class for Image manipulation."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Images commands to parent parser."""
|
||||
parser = parent.add_parser('images', help='list images')
|
||||
super().subparser(parser)
|
||||
parser.add_argument(
|
||||
'--sort',
|
||||
choices=['created', 'id', 'repository', 'size', 'tag'],
|
||||
default='created',
|
||||
type=str.lower,
|
||||
help=('Change sort ordered of displayed images.'
|
||||
' (default: %(default)s)'))
|
||||
|
||||
parser.add_flag(
|
||||
'--digests',
|
||||
help='Include digests with images.')
|
||||
parser.set_defaults(class_=cls, method='list')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Images class."""
|
||||
super().__init__(args)
|
||||
|
||||
self.columns = OrderedDict({
|
||||
'name':
|
||||
ReportColumn('name', 'REPOSITORY', 0),
|
||||
'tag':
|
||||
ReportColumn('tag', 'TAG', 10),
|
||||
'id':
|
||||
ReportColumn('id', 'IMAGE ID', 12),
|
||||
'created':
|
||||
ReportColumn('created', 'CREATED', 12),
|
||||
'size':
|
||||
ReportColumn('size', 'SIZE', 8),
|
||||
'repoDigests':
|
||||
ReportColumn('repoDigests', 'DIGESTS', 35),
|
||||
})
|
||||
|
||||
def list(self):
|
||||
"""List images."""
|
||||
images = sorted(
|
||||
self.client.images.list(),
|
||||
key=operator.attrgetter(self._args.sort))
|
||||
if not images:
|
||||
return
|
||||
|
||||
rows = list()
|
||||
for image in images:
|
||||
fields = dict(image)
|
||||
fields.update({
|
||||
'created':
|
||||
humanize.naturaldate(podman.datetime_parse(image.created)),
|
||||
'size':
|
||||
humanize.naturalsize(int(image.size)),
|
||||
'repoDigests':
|
||||
' '.join(image.repoDigests),
|
||||
})
|
||||
|
||||
for r in image.repoTags:
|
||||
name, tag = r.rsplit(':', 1)
|
||||
fields.update({
|
||||
'name': name,
|
||||
'tag': tag,
|
||||
})
|
||||
rows.append(fields)
|
||||
|
||||
if not self._args.digests:
|
||||
del self.columns['repoDigests']
|
||||
|
||||
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)
|
@ -1,69 +0,0 @@
|
||||
"""Remote client command to import tarball as image filesystem."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, ChangeAction
|
||||
|
||||
|
||||
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=ChangeAction,
|
||||
)
|
||||
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 import_(self):
|
||||
"""Import tarball as image filesystem."""
|
||||
# ImportImage() validates it's parameters therefore we need to create
|
||||
# pristine dict() for keywords
|
||||
options = {}
|
||||
if 'message' in self.opts:
|
||||
options['message'] = self.opts['message']
|
||||
if 'change' in self.opts and self.opts['change']:
|
||||
options['changes'] = self.opts['change']
|
||||
|
||||
reference = self.opts['reference'][0] if 'reference' in self.opts\
|
||||
else None
|
||||
|
||||
try:
|
||||
ident = self.client.images.import_image(
|
||||
self.opts['source'][0],
|
||||
reference,
|
||||
**options,
|
||||
)
|
||||
print(ident)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,45 +0,0 @@
|
||||
"""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 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)
|
@ -1,89 +0,0 @@
|
||||
"""Remote client command for inspecting podman objects."""
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Inspect(AbstractActionBase):
|
||||
"""Class for inspecting podman objects."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Inspect command to parent parser."""
|
||||
parser = parent.add_parser('inspect', help='inspect objects')
|
||||
parser.add_argument(
|
||||
'--type',
|
||||
'-t',
|
||||
choices=('all', 'container', 'image'),
|
||||
default='all',
|
||||
type=str.lower,
|
||||
help='Type of object to inspect',
|
||||
)
|
||||
parser.add_flag(
|
||||
'--size',
|
||||
help='Display the total file size if the type is a container.')
|
||||
parser.add_argument(
|
||||
'objects',
|
||||
nargs='+',
|
||||
help='objects to inspect',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='inspect')
|
||||
|
||||
def _get_container(self, ident):
|
||||
try:
|
||||
logging.debug("Getting container %s", ident)
|
||||
ctnr = self.client.containers.get(ident)
|
||||
except podman.ContainerNotFound:
|
||||
pass
|
||||
else:
|
||||
return ctnr.inspect()
|
||||
|
||||
def _get_image(self, ident):
|
||||
try:
|
||||
logging.debug("Getting image %s", ident)
|
||||
img = self.client.images.get(ident)
|
||||
except podman.ImageNotFound:
|
||||
pass
|
||||
else:
|
||||
return img.inspect()
|
||||
|
||||
def inspect(self):
|
||||
"""Inspect provided podman objects."""
|
||||
output = []
|
||||
try:
|
||||
for ident in self._args.objects:
|
||||
obj = None
|
||||
|
||||
if self._args.type in ('all', 'container'):
|
||||
obj = self._get_container(ident)
|
||||
if obj is None and self._args.type in ('all', 'image'):
|
||||
obj = self._get_image(ident)
|
||||
|
||||
if obj is None:
|
||||
if self._args.type == 'container':
|
||||
msg = 'Container "{}" not found'.format(ident)
|
||||
elif self._args.type == 'image':
|
||||
msg = 'Image "{}" not found'.format(ident)
|
||||
else:
|
||||
msg = 'Object "{}" not found'.format(ident)
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
else:
|
||||
fields = obj._asdict()
|
||||
if not self._args.size:
|
||||
try:
|
||||
del fields['sizerootfs']
|
||||
except KeyError:
|
||||
pass
|
||||
output.append(fields)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
else:
|
||||
print(json.dumps(output, indent=2))
|
@ -1,49 +0,0 @@
|
||||
"""Remote client command for signaling podman containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, SignalAction
|
||||
|
||||
|
||||
class Kill(AbstractActionBase):
|
||||
"""Class for sending signal to main process in container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Kill command to parent parser."""
|
||||
parser = parent.add_parser('kill', help='signal container')
|
||||
parser.add_argument(
|
||||
'--signal',
|
||||
'-s',
|
||||
action=SignalAction,
|
||||
default=9,
|
||||
help='Signal to send to the container. (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'containers',
|
||||
nargs='+',
|
||||
help='containers to signal',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='kill')
|
||||
|
||||
def kill(self):
|
||||
"""Signal provided containers."""
|
||||
try:
|
||||
for ident in self._args.containers:
|
||||
try:
|
||||
ctnr = self.client.containers.get(ident)
|
||||
ctnr.kill(self._args.signal)
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container "{}" not found'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
else:
|
||||
print(ident)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
@ -1,61 +0,0 @@
|
||||
"""Remote client command for retrieving container logs."""
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, PositiveIntAction
|
||||
|
||||
|
||||
class Logs(AbstractActionBase):
|
||||
"""Class for retrieving logs from container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Logs command to parent parser."""
|
||||
parser = parent.add_parser('logs', help='retrieve logs from container')
|
||||
parser.add_argument(
|
||||
'--tail',
|
||||
metavar='LINES',
|
||||
action=PositiveIntAction,
|
||||
help='Output the specified number of LINES at the end of the logs')
|
||||
parser.add_argument(
|
||||
'container',
|
||||
nargs=1,
|
||||
help='retrieve container logs',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='logs')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Logs class."""
|
||||
super().__init__(args)
|
||||
|
||||
def logs(self):
|
||||
"""Retrieve logs from containers."""
|
||||
try:
|
||||
ident = self._args.container[0]
|
||||
try:
|
||||
logging.debug('Get container "%s" logs', ident)
|
||||
ctnr = self.client.containers.get(ident)
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container "{}" not found'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
else:
|
||||
if self._args.tail:
|
||||
logs = iter(deque(ctnr.logs(), maxlen=self._args.tail))
|
||||
else:
|
||||
logs = ctnr.logs()
|
||||
|
||||
for line in logs:
|
||||
sys.stdout.write(line)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
@ -1,78 +0,0 @@
|
||||
"""Remote client command for retrieving mounts from containers."""
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, Report, ReportColumn
|
||||
|
||||
|
||||
class Mount(AbstractActionBase):
|
||||
"""Class for retrieving mounts from container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add mount command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'mount', help='retrieve mounts from containers.')
|
||||
super().subparser(parser)
|
||||
parser.add_argument(
|
||||
'containers',
|
||||
nargs='*',
|
||||
help='containers to list ports',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='mount')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Mount class."""
|
||||
super().__init__(args)
|
||||
|
||||
self.columns = OrderedDict({
|
||||
'id':
|
||||
ReportColumn('id', 'CONTAINER ID', 14),
|
||||
'destination':
|
||||
ReportColumn('destination', 'DESTINATION', 0)
|
||||
})
|
||||
|
||||
def mount(self):
|
||||
"""Retrieve mounts from containers."""
|
||||
try:
|
||||
ctnrs = []
|
||||
if not self._args.containers:
|
||||
ctnrs = self.client.containers.list()
|
||||
else:
|
||||
for ident in self._args.containers:
|
||||
try:
|
||||
ctnrs.append(self.client.containers.get(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)
|
||||
return 1
|
||||
|
||||
if not ctnrs:
|
||||
print(
|
||||
'Unable to find any containers.', file=sys.stderr, flush=True)
|
||||
return 1
|
||||
|
||||
rows = list()
|
||||
for ctnr in ctnrs:
|
||||
details = ctnr.inspect()
|
||||
rows.append({
|
||||
'id': ctnr.id,
|
||||
'destination': details.graphdriver['data']['mergeddir']
|
||||
})
|
||||
|
||||
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)
|
@ -1,43 +0,0 @@
|
||||
"""Remote client command for pausing processes in containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Pause(AbstractActionBase):
|
||||
"""Class for pausing processes in container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pause command to parent parser."""
|
||||
parser = parent.add_parser('pause', help='pause container processes')
|
||||
parser.add_argument(
|
||||
'containers',
|
||||
nargs='+',
|
||||
help='containers to pause',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='pause')
|
||||
|
||||
def pause(self):
|
||||
"""Pause provided containers."""
|
||||
try:
|
||||
for ident in self._args.containers:
|
||||
try:
|
||||
ctnr = self.client.containers.get(ident)
|
||||
ctnr.pause()
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container "{}" not found'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
else:
|
||||
print(ident)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
@ -1,24 +0,0 @@
|
||||
"""Provide subparsers for pod commands."""
|
||||
from pypodman.lib.actions.pod.create_parser import CreatePod
|
||||
from pypodman.lib.actions.pod.inspect_parser import InspectPod
|
||||
from pypodman.lib.actions.pod.kill_parser import KillPod
|
||||
from pypodman.lib.actions.pod.pause_parser import PausePod
|
||||
from pypodman.lib.actions.pod.processes_parser import ProcessesPod
|
||||
from pypodman.lib.actions.pod.remove_parser import RemovePod
|
||||
from pypodman.lib.actions.pod.start_parser import StartPod
|
||||
from pypodman.lib.actions.pod.stop_parser import StopPod
|
||||
from pypodman.lib.actions.pod.top_parser import TopPod
|
||||
from pypodman.lib.actions.pod.unpause_parser import UnpausePod
|
||||
|
||||
__all__ = [
|
||||
'CreatePod',
|
||||
'InspectPod',
|
||||
'KillPod',
|
||||
'PausePod',
|
||||
'ProcessesPod',
|
||||
'RemovePod',
|
||||
'StartPod',
|
||||
'StopPod',
|
||||
'TopPod',
|
||||
'UnpausePod',
|
||||
]
|
@ -1,76 +0,0 @@
|
||||
"""Remote client command for creating pod."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class CreatePod(AbstractActionBase):
|
||||
"""Implement Create Pod command."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Create command to parent parser."""
|
||||
parser = parent.add_parser('create', help='create pod')
|
||||
super().subparser(parser)
|
||||
|
||||
parser.add_argument(
|
||||
'--cgroup-parent',
|
||||
dest='cgroupparent',
|
||||
type=str,
|
||||
help='Path to cgroups under which the'
|
||||
' cgroup for the pod will be created.')
|
||||
parser.add_flag(
|
||||
'--infra',
|
||||
help='Create an infra container and associate it with the pod.')
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--label',
|
||||
dest='labels',
|
||||
action='append',
|
||||
type=str,
|
||||
help='Add metadata to a pod (e.g., --label=com.example.key=value)')
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--name',
|
||||
dest='ident',
|
||||
type=str,
|
||||
help='Assign name to the pod')
|
||||
parser.add_argument(
|
||||
'--share',
|
||||
choices=('ipc', 'net', 'pid', 'user', 'uts'),
|
||||
help='Comma deliminated list of kernel namespaces to share')
|
||||
|
||||
parser.set_defaults(class_=cls, method='create')
|
||||
|
||||
# TODO: Add golang CLI arguments not included in API.
|
||||
# parser.add_argument(
|
||||
# '--infra-command',
|
||||
# default='/pause',
|
||||
# help='Command to run to start the infra container.'
|
||||
# '(default: %(default)s)')
|
||||
# parser.add_argument(
|
||||
# '--infra-image',
|
||||
# default='k8s.gcr.io/pause:3.1',
|
||||
# help='Image to create for the infra container.'
|
||||
# '(default: %(default)s)')
|
||||
# parser.add_argument(
|
||||
# '--podidfile',
|
||||
# help='Write the pod ID to given file name on remote host')
|
||||
|
||||
def create(self):
|
||||
"""Create Pod from given options."""
|
||||
config = {}
|
||||
for key in ('ident', 'cgroupparent', 'infra', 'labels', 'share'):
|
||||
config[key] = self.opts.get(key)
|
||||
|
||||
try:
|
||||
pod = self.client.pods.create(**config)
|
||||
except podman.ErrorOccurred as ex:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
else:
|
||||
print(pod.id)
|
@ -1,43 +0,0 @@
|
||||
"""Remote client command for inspecting pods."""
|
||||
import json
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class InspectPod(AbstractActionBase):
|
||||
"""Class for reporting on pods and their containers."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Inspect command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'inspect',
|
||||
help='configuration and state information about a given pod')
|
||||
parser.add_argument('pod', nargs='+', help='pod(s) to inspect')
|
||||
parser.set_defaults(class_=cls, method='inspect')
|
||||
|
||||
def inspect(self):
|
||||
"""Report on provided pods."""
|
||||
output = {}
|
||||
try:
|
||||
for ident in self._args.pod:
|
||||
try:
|
||||
pod = self.client.pods.get(ident)
|
||||
except podman.PodNotFound:
|
||||
print(
|
||||
'Pod "{}" not found.'.format(ident),
|
||||
file=sys.stdout,
|
||||
flush=True)
|
||||
output.update(pod.inspect()._asdict())
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
else:
|
||||
print(json.dumps(output, indent=2))
|
||||
return 0
|
@ -1,57 +0,0 @@
|
||||
"""Remote client command for signaling pods and their containers."""
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, SignalAction
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class KillPod(AbstractActionBase):
|
||||
"""Class for sending signal to processes in pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Kill command to parent parser."""
|
||||
parser = parent.add_parser('kill', help='signal containers in pod')
|
||||
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Sends signal to all pods.')
|
||||
parser.add_argument(
|
||||
'-s',
|
||||
'--signal',
|
||||
action=SignalAction,
|
||||
default=9,
|
||||
help='Signal to send to the pod. (default: %(default)s)')
|
||||
parser.add_argument('pod', nargs='*', help='pod(s) to signal')
|
||||
parser.set_defaults(class_=cls, method='kill')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Pod Kill object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, but not both')
|
||||
super().__init__(args)
|
||||
|
||||
def kill(self):
|
||||
"""Signal provided pods."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.kill(self._args.signal)
|
||||
print(pod.id)
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found.'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as e:
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,49 +0,0 @@
|
||||
"""Remote client command for pausing processes in pod."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class PausePod(AbstractActionBase):
|
||||
"""Class for pausing containers in pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Pause command to parent parser."""
|
||||
parser = parent.add_parser('pause', help='pause containers in pod')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Pause all pods.')
|
||||
parser.add_argument('pod', nargs='*', help='pod(s) to pause.')
|
||||
parser.set_defaults(class_=cls, method='pause')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Pod Pause object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, but not both')
|
||||
super().__init__(args)
|
||||
|
||||
def pause(self):
|
||||
"""Pause containers in provided Pod."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.pause()
|
||||
print(pod.id)
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,94 +0,0 @@
|
||||
"""Report on pod's containers' processes."""
|
||||
import operator
|
||||
from collections import OrderedDict
|
||||
|
||||
from pypodman.lib import AbstractActionBase, Report, ReportColumn
|
||||
|
||||
|
||||
class ProcessesPod(AbstractActionBase):
|
||||
"""Report on Pod's processes."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Ps command to parent parser."""
|
||||
parser = parent.add_parser('ps', help='list processes of pod')
|
||||
super().subparser(parser)
|
||||
|
||||
parser.add_flag(
|
||||
'--ctr-names',
|
||||
help='Include container name in the info field.')
|
||||
parser.add_flag(
|
||||
'--ctr-ids',
|
||||
help='Include container ID in the info field.')
|
||||
parser.add_flag(
|
||||
'--ctr-status',
|
||||
help='Include container status in the info field.')
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=('json'),
|
||||
help='Pretty-print containers to JSON')
|
||||
parser.add_argument(
|
||||
'--sort',
|
||||
choices=('created', 'id', 'name', 'status', 'count'),
|
||||
default='created',
|
||||
type=str.lower,
|
||||
help='Sort on given field. (default: %(default)s)')
|
||||
parser.add_argument('--filter', help='Not Implemented')
|
||||
parser.set_defaults(class_=cls, method='processes')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct ProcessesPod class."""
|
||||
if args.sort == 'created':
|
||||
args.sort = 'createdat'
|
||||
elif args.sort == 'count':
|
||||
args.sort = 'numberofcontainers'
|
||||
|
||||
super().__init__(args)
|
||||
|
||||
self.columns = OrderedDict({
|
||||
'id':
|
||||
ReportColumn('id', 'POD ID', 14),
|
||||
'name':
|
||||
ReportColumn('name', 'NAME', 30),
|
||||
'status':
|
||||
ReportColumn('status', 'STATUS', 8),
|
||||
'numberofcontainers':
|
||||
ReportColumn('numberofcontainers', 'NUMBER OF CONTAINERS', 0),
|
||||
'info':
|
||||
ReportColumn('info', 'CONTAINER INFO', 0),
|
||||
})
|
||||
|
||||
def processes(self):
|
||||
"""List pods."""
|
||||
pods = sorted(
|
||||
self.client.pods.list(), key=operator.attrgetter(self._args.sort))
|
||||
if not pods:
|
||||
return
|
||||
|
||||
rows = list()
|
||||
for pod in pods:
|
||||
fields = dict(pod)
|
||||
if self._args.ctr_ids \
|
||||
or self._args.ctr_names \
|
||||
or self._args.ctr_status:
|
||||
keys = ('id', 'name', 'status', 'info')
|
||||
info = []
|
||||
for ctnr in pod.containersinfo:
|
||||
ctnr_info = []
|
||||
if self._args.ctr_ids:
|
||||
ctnr_info.append(ctnr['id'])
|
||||
if self._args.ctr_names:
|
||||
ctnr_info.append(ctnr['name'])
|
||||
if self._args.ctr_status:
|
||||
ctnr_info.append(ctnr['status'])
|
||||
info.append("[ {} ]".format(" ".join(ctnr_info)))
|
||||
fields.update({'info': " ".join(info)})
|
||||
else:
|
||||
keys = ('id', 'name', 'status', 'numberofcontainers')
|
||||
|
||||
rows.append(fields)
|
||||
|
||||
with Report(self.columns, heading=self._args.heading) as report:
|
||||
report.layout(rows, keys, truncate=self._args.truncate)
|
||||
for row in rows:
|
||||
report.row(**row)
|
@ -1,54 +0,0 @@
|
||||
"""Remote client command for deleting pod and containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class RemovePod(AbstractActionBase):
|
||||
"""Class for removing pod and containers from storage."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Rm command to parent parser."""
|
||||
parser = parent.add_parser('rm', help='Delete pod and container(s)')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Remove all pods.')
|
||||
parser.add_flag(
|
||||
'--force',
|
||||
'-f',
|
||||
help='Stop and remove container(s) then delete pod.')
|
||||
parser.add_argument(
|
||||
'pod', nargs='*', help='Pod to remove. Or, use --all')
|
||||
parser.set_defaults(class_=cls, method='remove')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct RemovePod object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, but not both')
|
||||
super().__init__(args)
|
||||
|
||||
def remove(self):
|
||||
"""Remove pod and container(s)."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.remove(self._args.force)
|
||||
print(pod.id)
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found.'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize,
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,50 +0,0 @@
|
||||
"""Remote client command for restarting pod and container(s)."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class RestartPod(AbstractActionBase):
|
||||
"""Class for restarting containers in Pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Restart command to parent parser."""
|
||||
parser = parent.add_parser('restart', help='restart containers in pod')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Restart all pods.')
|
||||
parser.add_argument(
|
||||
'pod', nargs='*', help='Pod to restart. Or, use --all')
|
||||
parser.set_defaults(class_=cls, method='restart')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct RestartPod object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, not both')
|
||||
super().__init__(args)
|
||||
|
||||
def restart(self):
|
||||
"""Restart pod and container(s)."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.restart()
|
||||
print(pod.id)
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found.'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,45 +0,0 @@
|
||||
"""Remote client command for starting pod and container(s)."""
|
||||
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class StartPod(AbstractActionBase):
|
||||
"""Class for starting pod and container(s)."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Start command to parent parser."""
|
||||
parser = parent.add_parser('start', help='start pod')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Start all pods.')
|
||||
parser.add_argument(
|
||||
'pod', nargs='*', help='Pod to start. Or, use --all')
|
||||
parser.set_defaults(class_=cls, method='start')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct StartPod object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, but not both')
|
||||
super().__init__(args)
|
||||
|
||||
def start(self):
|
||||
"""Start pod and container(s)."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.start()
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,44 +0,0 @@
|
||||
"""Remote client command for stopping pod and container(s)."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class StopPod(AbstractActionBase):
|
||||
"""Class for stopping pod and container(s)."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Stop command to parent parser."""
|
||||
parser = parent.add_parser('stop', help='stop pod')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Stop all pods.')
|
||||
parser.add_argument(
|
||||
'pod', nargs='*', help='Pod to stop. Or, use --all')
|
||||
parser.set_defaults(class_=cls, method='stop')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Contruct StopPod object."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, not both')
|
||||
super().__init__(args)
|
||||
|
||||
def stop(self):
|
||||
"""Stop pod and container(s)."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.stop()
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,35 +0,0 @@
|
||||
"""Remote client command for reporting on pod and container(s)."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class TopPod(AbstractActionBase):
|
||||
"""Report on containers in Pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Top command to parent parser."""
|
||||
parser = parent.add_parser('top', help='report on containers in pod')
|
||||
parser.add_argument('pod', nargs=1, help='Pod to report on.')
|
||||
parser.set_defaults(class_=cls, method='top')
|
||||
|
||||
def top(self):
|
||||
"""Report on pod and container(s)."""
|
||||
try:
|
||||
for ident in self._args.pod:
|
||||
pod = self.client.pods.get(ident)
|
||||
print(pod.top())
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found.'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,50 +0,0 @@
|
||||
"""Remote client command for unpausing processes in pod."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
from pypodman.lib import query_model as query_pods
|
||||
|
||||
|
||||
class UnpausePod(AbstractActionBase):
|
||||
"""Class for unpausing containers in pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Unpause command to parent parser."""
|
||||
parser = parent.add_parser('unpause', help='unpause pod')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='Unpause all pods.')
|
||||
parser.add_argument(
|
||||
'pod', nargs='*', help='Pod to unpause. Or, use --all')
|
||||
parser.set_defaults(class_=cls, method='unpause')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Pod Unpause class."""
|
||||
if args.all and args.pod:
|
||||
raise ValueError('You may give a pod or use --all, but not both')
|
||||
super().__init__(args)
|
||||
|
||||
def unpause(self):
|
||||
"""Unpause containers in provided Pod."""
|
||||
idents = None if self._args.all else self._args.pod
|
||||
pods = query_pods(self.client.pods, idents)
|
||||
|
||||
for pod in pods:
|
||||
try:
|
||||
pod.unpause()
|
||||
print(pod.id)
|
||||
except podman.PodNotFound as ex:
|
||||
print(
|
||||
'Pod "{}" not found'.format(ex.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
except podman.ErrorOccurred as ex:
|
||||
print(
|
||||
'{}'.format(ex.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,36 +0,0 @@
|
||||
"""Remote client command for pod subcommands."""
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
# pylint: disable=wildcard-import
|
||||
# pylint: disable=unused-wildcard-import
|
||||
from .pod import *
|
||||
|
||||
|
||||
class Pod(AbstractActionBase):
|
||||
"""Class for creating a pod."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pod Create command to parent parser."""
|
||||
pod_parser = parent.add_parser(
|
||||
'pod',
|
||||
help='pod commands.'
|
||||
' For subcommands, see: %(prog)s pod --help')
|
||||
subparser = pod_parser.add_subparsers()
|
||||
|
||||
# pull in plugin(s) code for each subcommand
|
||||
for name, obj in inspect.getmembers(
|
||||
sys.modules['pypodman.lib.actions.pod'],
|
||||
predicate=inspect.isclass):
|
||||
if hasattr(obj, 'subparser'):
|
||||
try:
|
||||
obj.subparser(subparser)
|
||||
except NameError as e:
|
||||
logging.critical(e)
|
||||
logging.warning(
|
||||
'See subparser configuration for Class "%s"', name)
|
||||
sys.exit(3)
|
@ -1,61 +0,0 @@
|
||||
"""Remote client command for retrieving ports from containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Port(AbstractActionBase):
|
||||
"""Class for retrieving ports from container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Port command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'port', help='retrieve ports from containers')
|
||||
parser.add_flag(
|
||||
'--all',
|
||||
'-a',
|
||||
help='List all known port mappings for running containers')
|
||||
parser.add_argument(
|
||||
'containers',
|
||||
nargs='*',
|
||||
help='containers to list ports',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='port')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Port class."""
|
||||
if not args.all and not args.containers:
|
||||
raise ValueError('You must supply at least one'
|
||||
' container id or name, or --all.')
|
||||
super().__init__(args)
|
||||
|
||||
def port(self):
|
||||
"""Retrieve ports from containers."""
|
||||
try:
|
||||
ctnrs = []
|
||||
if self._args.all:
|
||||
ctnrs = self.client.containers.list()
|
||||
else:
|
||||
for ident in self._args.containers:
|
||||
try:
|
||||
ctnrs.append(self.client.containers.get(ident))
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container "{}" not found'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
|
||||
for ctnr in ctnrs:
|
||||
print("{}\n{}".format(ctnr.id, ctnr.ports))
|
||||
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,80 +0,0 @@
|
||||
"""Remote client commands dealing with containers."""
|
||||
import operator
|
||||
from collections import OrderedDict
|
||||
|
||||
import humanize
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase, Report, ReportColumn
|
||||
|
||||
|
||||
class Ps(AbstractActionBase):
|
||||
"""Class for Container manipulation."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Images command to parent parser."""
|
||||
parser = parent.add_parser('ps', help='list containers')
|
||||
super().subparser(parser)
|
||||
|
||||
parser.add_argument(
|
||||
'--sort',
|
||||
choices=('createdat', 'id', 'image', 'names', 'runningfor', 'size',
|
||||
'status'),
|
||||
default='createdat',
|
||||
type=str.lower,
|
||||
help=('Change sort ordered of displayed containers.'
|
||||
' (default: %(default)s)'))
|
||||
parser.set_defaults(class_=cls, method='list')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Ps class."""
|
||||
super().__init__(args)
|
||||
|
||||
self.columns = OrderedDict({
|
||||
'id':
|
||||
ReportColumn('id', 'CONTAINER ID', 12),
|
||||
'image':
|
||||
ReportColumn('image', 'IMAGE', 31),
|
||||
'command':
|
||||
ReportColumn('column', 'COMMAND', 20),
|
||||
'createdat':
|
||||
ReportColumn('createdat', 'CREATED', 12),
|
||||
'status':
|
||||
ReportColumn('status', 'STATUS', 10),
|
||||
'ports':
|
||||
ReportColumn('ports', 'PORTS', 0),
|
||||
'names':
|
||||
ReportColumn('names', 'NAMES', 18)
|
||||
})
|
||||
|
||||
def list(self):
|
||||
"""List containers."""
|
||||
if self._args.all:
|
||||
ictnrs = self.client.containers.list()
|
||||
else:
|
||||
ictnrs = filter(
|
||||
lambda c: podman.FoldedString(c['status']) == 'running',
|
||||
self.client.containers.list())
|
||||
|
||||
# TODO: Verify sorting on dates and size
|
||||
ctnrs = sorted(ictnrs, key=operator.attrgetter(self._args.sort))
|
||||
if not ctnrs:
|
||||
return
|
||||
|
||||
rows = list()
|
||||
for ctnr in ctnrs:
|
||||
fields = dict(ctnr)
|
||||
fields.update({
|
||||
'command':
|
||||
' '.join(ctnr.command),
|
||||
'createdat':
|
||||
humanize.naturaldate(podman.datetime_parse(ctnr.createdat)),
|
||||
})
|
||||
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)
|
@ -1,46 +0,0 @@
|
||||
"""Remote client command for pulling images."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Pull(AbstractActionBase):
|
||||
"""Class for retrieving images from repository."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Pull command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'pull',
|
||||
help='retrieve image from repository',
|
||||
)
|
||||
parser.add_argument(
|
||||
'targets',
|
||||
nargs='+',
|
||||
help='image id(s) to retrieve.',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='pull')
|
||||
|
||||
def __init__(self, args):
|
||||
"""Construct Pull class."""
|
||||
super().__init__(args)
|
||||
|
||||
def pull(self):
|
||||
"""Retrieve image."""
|
||||
for ident in self._args.targets:
|
||||
try:
|
||||
self.client.images.pull(ident)
|
||||
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)
|
@ -1,50 +0,0 @@
|
||||
"""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_flag(
|
||||
'--tlsverify',
|
||||
help='Require HTTPS and verify certificates when'
|
||||
' contacting registries.')
|
||||
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 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)
|
@ -1,46 +0,0 @@
|
||||
"""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 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)
|
@ -1,41 +0,0 @@
|
||||
"""Remote client command for deleting containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Rm(AbstractActionBase):
|
||||
"""Class for removing containers from storage."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Rm command to parent parser."""
|
||||
parser = parent.add_parser('rm', help='delete container(s)')
|
||||
parser.add_flag(
|
||||
'--force',
|
||||
'-f',
|
||||
help='force delete of running container(s).')
|
||||
parser.add_argument(
|
||||
'targets', nargs='+', help='container id(s) to delete')
|
||||
parser.set_defaults(class_=cls, method='remove')
|
||||
|
||||
def remove(self):
|
||||
"""Remove container(s)."""
|
||||
for ident in self._args.targets:
|
||||
try:
|
||||
ctnr = self.client.containers.get(ident)
|
||||
ctnr.remove(self._args.force)
|
||||
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)
|
@ -1,40 +0,0 @@
|
||||
"""Remote client command for deleting images."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Rmi(AbstractActionBase):
|
||||
"""Class for removing images from storage."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Rmi command to parent parser."""
|
||||
parser = parent.add_parser('rmi', help='delete image(s)')
|
||||
parser.add_flag(
|
||||
'--force',
|
||||
'-f',
|
||||
help='force delete of image(s) and associated containers.')
|
||||
parser.add_argument('targets', nargs='+', help='image id(s) to delete')
|
||||
parser.set_defaults(class_=cls, method='remove')
|
||||
|
||||
def remove(self):
|
||||
"""Remove image(s)."""
|
||||
for ident in self._args.targets:
|
||||
try:
|
||||
img = self.client.images.get(ident)
|
||||
img.remove(self._args.force)
|
||||
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)
|
@ -1,73 +0,0 @@
|
||||
"""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=parent.REMAINDER,
|
||||
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)
|
@ -1,160 +0,0 @@
|
||||
"""Remote client command for searching registries for an image."""
|
||||
import argparse
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import podman
|
||||
from pypodman.lib import (AbstractActionBase, 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':
|
||||
if val.capitalize() in ('True', 'False'):
|
||||
val = bool(val)
|
||||
else:
|
||||
msg = ('{} option "is-automated"'
|
||||
' must be True or False.'.format(self.dest))
|
||||
parser.error(msg)
|
||||
elif opt == 'is-official':
|
||||
if val.capitalize() in ('True', 'False'):
|
||||
val = bool(val)
|
||||
else:
|
||||
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)
|
@ -1,71 +0,0 @@
|
||||
"""Remote client command for starting containers."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Start(AbstractActionBase):
|
||||
"""Class for starting container."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Start command to parent parser."""
|
||||
parser = parent.add_parser('start', help='start container')
|
||||
parser.add_flag(
|
||||
'--attach',
|
||||
'-a',
|
||||
help="Attach container's STDOUT and STDERR.")
|
||||
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 _) (default: ^D)')
|
||||
parser.add_flag(
|
||||
'--interactive',
|
||||
'-i',
|
||||
help="Attach container's STDIN.")
|
||||
# TODO: Implement sig-proxy
|
||||
parser.add_flag(
|
||||
'--sig-proxy',
|
||||
help="Proxy received signals to the process."
|
||||
)
|
||||
parser.add_argument(
|
||||
'containers',
|
||||
nargs='+',
|
||||
help='containers to start',
|
||||
)
|
||||
parser.set_defaults(class_=cls, method='start')
|
||||
|
||||
def start(self):
|
||||
"""Start provided containers."""
|
||||
stdin = sys.stdin if self.opts['interactive'] else None
|
||||
stdout = sys.stdout if self.opts['attach'] else None
|
||||
|
||||
try:
|
||||
for ident in self._args.containers:
|
||||
try:
|
||||
ctnr = self.client.containers.get(ident)
|
||||
ctnr.attach(
|
||||
eot=self.opts['detach_keys'],
|
||||
stdin=stdin,
|
||||
stdout=stdout)
|
||||
ctnr.start()
|
||||
except podman.ContainerNotFound as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'Container "{}" not found'.format(e.name),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
else:
|
||||
print(ident)
|
||||
except podman.ErrorOccurred as e:
|
||||
sys.stdout.flush()
|
||||
print(
|
||||
'{}'.format(e.reason).capitalize(),
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
return 1
|
||||
return 0
|
@ -1,35 +0,0 @@
|
||||
"""Remote client command for reporting on Podman service."""
|
||||
import sys
|
||||
|
||||
import podman
|
||||
from pypodman.lib import AbstractActionBase
|
||||
|
||||
|
||||
class Version(AbstractActionBase):
|
||||
"""Class for reporting on Podman Service."""
|
||||
|
||||
@classmethod
|
||||
def subparser(cls, parent):
|
||||
"""Add Version command to parent parser."""
|
||||
parser = parent.add_parser(
|
||||
'version', help='report version on podman service')
|
||||
parser.set_defaults(class_=cls, method='version')
|
||||
|
||||
def version(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:
|
||||
version = info._asdict()['podman']
|
||||
host = info._asdict()['host']
|
||||
print("Version {}".format(version['podman_version']))
|
||||
print("Go Version {}".format(version['go_version']))
|
||||
print("Git Commit {}".format(version['git_commit']))
|
||||
print("OS/Arch {}/{}".format(host["os"], host["arch"]))
|
@ -1,247 +0,0 @@
|
||||
"""
|
||||
Supplimental argparse.Action converters and validaters.
|
||||
|
||||
The constructors are very verbose but remain for IDE support.
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import signal
|
||||
|
||||
# API defined by argparse.Action therefore shut up pylint
|
||||
# pragma pylint: disable=redefined-builtin
|
||||
# pragma pylint: disable=too-few-public-methods
|
||||
# pragma pylint: disable=too-many-arguments
|
||||
|
||||
|
||||
class ChangeAction(argparse.Action):
|
||||
"""Convert and validate change argument."""
|
||||
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
nargs=None,
|
||||
const=None,
|
||||
default=None,
|
||||
type=None,
|
||||
choices=None,
|
||||
required=False,
|
||||
help=None,
|
||||
metavar='OPT=VALUE'):
|
||||
"""Create ChangeAction object."""
|
||||
help = (help or '') + ('Apply change(s) to the new image.'
|
||||
' May be given multiple times.')
|
||||
if default is None:
|
||||
default = []
|
||||
|
||||
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."""
|
||||
items = getattr(namespace, self.dest, None) or []
|
||||
items = copy.copy(items)
|
||||
|
||||
choices = ('CMD', 'ENTRYPOINT', 'ENV', 'EXPOSE', 'LABEL', 'ONBUILD',
|
||||
'STOPSIGNAL', 'USER', 'VOLUME', 'WORKDIR')
|
||||
|
||||
opt, _ = values.split('=', 1)
|
||||
if opt not in choices:
|
||||
parser.error('Option "{}" is not supported by argument "{}",'
|
||||
' valid options are: {}'.format(
|
||||
opt, option_string, ', '.join(choices)))
|
||||
items.append(values)
|
||||
setattr(namespace, self.dest, items)
|
||||
|
||||
|
||||
class SignalAction(argparse.Action):
|
||||
"""Validate input as a signal."""
|
||||
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
nargs=None,
|
||||
const=None,
|
||||
default=None,
|
||||
type=str,
|
||||
choices=None,
|
||||
required=False,
|
||||
help='The signal to send.'
|
||||
' It may be given as a name or a number.',
|
||||
metavar='SIGNAL'):
|
||||
"""Create SignalAction 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)
|
||||
|
||||
if hasattr(signal, "Signals"):
|
||||
|
||||
def _signal_number(signame):
|
||||
cooked = 'SIG{}'.format(signame)
|
||||
try:
|
||||
return signal.Signals[cooked].value
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
|
||||
def _signal_number(signame):
|
||||
cooked = 'SIG{}'.format(signame)
|
||||
for n, v in sorted(signal.__dict__.items()):
|
||||
if n != cooked:
|
||||
continue
|
||||
if n.startswith("SIG") and not n.startswith("SIG_"):
|
||||
return v
|
||||
|
||||
self._signal_number = _signal_number
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
"""Validate input is a signal for platform."""
|
||||
if values.isdigit():
|
||||
signum = int(values)
|
||||
if signal.SIGRTMIN <= signum >= signal.SIGRTMAX:
|
||||
raise ValueError('"{}" is not a valid signal. {}-{}'.format(
|
||||
values, signal.SIGRTMIN, signal.SIGRTMAX))
|
||||
else:
|
||||
signum = self._signal_number(values)
|
||||
if signum is None:
|
||||
parser.error(
|
||||
'"{}" is not a valid signal,'
|
||||
' see your platform documentation.'.format(values))
|
||||
setattr(namespace, self.dest, signum)
|
||||
|
||||
|
||||
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(option_string)
|
||||
parser.error(msg)
|
||||
if not values[-1] in ('b', 'k', 'm', 'g'):
|
||||
msg = '{} only supports suffices of: b, k, m, g'.format(
|
||||
option_string)
|
||||
parser.error(msg)
|
||||
else:
|
||||
if val <= 0:
|
||||
msg = '{} must be a positive integer'.format(option_string)
|
||||
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='Must be a positive integer.',
|
||||
metavar=None):
|
||||
"""Create PositiveIntAction 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):
|
||||
"""Validate input."""
|
||||
if values > 0:
|
||||
setattr(namespace, self.dest, values)
|
||||
return
|
||||
|
||||
msg = '{} must be a positive integer'.format(option_string)
|
||||
parser.error(msg)
|
||||
|
||||
|
||||
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)))
|
@ -1,255 +0,0 @@
|
||||
"""Parse configuration while building subcommands."""
|
||||
import argparse
|
||||
import getpass
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
import pkg_resources
|
||||
import pytoml
|
||||
|
||||
from .parser_actions import PathAction, PositiveIntAction
|
||||
|
||||
# TODO: setup.py and obtain __version__ from rpm.spec
|
||||
try:
|
||||
__version__ = pkg_resources.get_distribution('pypodman').version
|
||||
except Exception: # pylint: disable=broad-except
|
||||
__version__ = '0.0.0'
|
||||
|
||||
|
||||
class HelpFormatter(argparse.RawDescriptionHelpFormatter):
|
||||
"""Set help width to screen size."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Construct HelpFormatter using screen width."""
|
||||
if 'width' not in kwargs:
|
||||
try:
|
||||
size = shutil.get_terminal_size()
|
||||
kwargs['width'] = size.columns
|
||||
except Exception: # pylint: disable=broad-except
|
||||
kwargs['width'] = 80
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class PodmanArgumentParser(argparse.ArgumentParser):
|
||||
"""Default remote podman configuration."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Construct the parser."""
|
||||
kwargs['add_help'] = True
|
||||
kwargs['description'] = ('Portable and simple management'
|
||||
' tool for containers and images')
|
||||
kwargs['formatter_class'] = HelpFormatter
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_flag(self, *args, **kwargs):
|
||||
"""Add flag to parser."""
|
||||
flags = [a for a in args if a[0] in self.prefix_chars]
|
||||
dest = flags[0].lstrip(self.prefix_chars)
|
||||
no_flag = '{0}{0}no-{1}'.format(self.prefix_chars, dest)
|
||||
|
||||
group = self.add_mutually_exclusive_group(required=False)
|
||||
group.add_argument(*flags, action='store_true', dest=dest, **kwargs)
|
||||
group.add_argument(no_flag, action='store_false', dest=dest, **kwargs)
|
||||
default = kwargs.get('default', False)
|
||||
self.set_defaults(**{dest: default})
|
||||
|
||||
def initialize_parser(self):
|
||||
"""Initialize parser without causing recursion meltdown."""
|
||||
self.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version='%(prog)s v. ' + __version__)
|
||||
self.add_argument(
|
||||
'--log-level',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
default='WARNING',
|
||||
type=str.upper,
|
||||
help='set logging level for events. (default: %(default)s)',
|
||||
)
|
||||
self.add_argument(
|
||||
'--run-dir',
|
||||
metavar='DIRECTORY',
|
||||
help=('directory to place local socket bindings.'
|
||||
' (default: XDG_RUNTIME_DIR/pypodman)'))
|
||||
self.add_argument(
|
||||
'--username',
|
||||
'-l',
|
||||
help='Authenicating user on remote host. (default: {})'.format(
|
||||
getpass.getuser()))
|
||||
self.add_argument(
|
||||
'--host', help='name of remote host. (default: None)')
|
||||
self.add_argument(
|
||||
'--port',
|
||||
'-p',
|
||||
action=PositiveIntAction,
|
||||
help='port for ssh tunnel to remote host. (default: 22)')
|
||||
self.add_argument(
|
||||
'--remote-socket-path',
|
||||
metavar='PATH',
|
||||
help=('path of podman socket on remote host'
|
||||
' (default: /run/podman/io.podman)'))
|
||||
self.add_argument(
|
||||
'--identity-file',
|
||||
'-i',
|
||||
action=PathAction,
|
||||
help='path to ssh identity file. (default: ~user/.ssh/id_dsa)')
|
||||
self.add_argument(
|
||||
'--config-home',
|
||||
metavar='DIRECTORY',
|
||||
action=PathAction,
|
||||
help=('home of configuration "pypodman.conf".'
|
||||
' (default: XDG_CONFIG_HOME/pypodman)'))
|
||||
|
||||
actions_parser = self.add_subparsers(
|
||||
dest='subparser_name', help='commands')
|
||||
# For create/exec/run: don't process options intended for subcommand
|
||||
actions_parser.REMAINDER = argparse.REMAINDER
|
||||
|
||||
# import buried here to prevent import loops
|
||||
import pypodman.lib.actions # pylint: disable=cyclic-import
|
||||
assert pypodman.lib.actions
|
||||
|
||||
# pull in plugin(s) code for each subcommand
|
||||
for name, obj in inspect.getmembers(
|
||||
sys.modules['pypodman.lib.actions'],
|
||||
predicate=inspect.isclass):
|
||||
if hasattr(obj, 'subparser'):
|
||||
try:
|
||||
obj.subparser(actions_parser)
|
||||
except NameError as e:
|
||||
logging.critical(e)
|
||||
logging.warning(
|
||||
'See subparser configuration for Class "%s"', name)
|
||||
sys.exit(3)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""Parse command line arguments, backed by env var and config_file."""
|
||||
self.initialize_parser()
|
||||
cooked = super().parse_args(args, namespace)
|
||||
return self.resolve_configuration(cooked)
|
||||
|
||||
def resolve_configuration(self, args):
|
||||
"""Find and fill in any arguments not passed on command line."""
|
||||
args.xdg_runtime_dir = os.environ.get('XDG_RUNTIME_DIR', '/tmp')
|
||||
args.xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
|
||||
os.path.expanduser('~/.config'))
|
||||
args.xdg_config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||
|
||||
# Configuration file(s) are optional,
|
||||
# required arguments may be provided elsewhere
|
||||
config = {'default': {}}
|
||||
dirs = args.xdg_config_dirs.split(':')
|
||||
dirs.extend((args.xdg_config_home, args.config_home))
|
||||
for dir_ in dirs:
|
||||
if dir_ is None:
|
||||
continue
|
||||
with suppress(OSError):
|
||||
cnf = Path(dir_, 'pypodman', 'pypodman.conf')
|
||||
with cnf.open() as stream:
|
||||
config.update(pytoml.load(stream))
|
||||
|
||||
def reqattr(name, value):
|
||||
"""Raise an error if value is unset."""
|
||||
if value:
|
||||
setattr(args, name, value)
|
||||
return value
|
||||
return self.error(
|
||||
'Required argument "{}" is not configured.'.format(name))
|
||||
|
||||
reqattr(
|
||||
'run_dir',
|
||||
getattr(args, 'run_dir')
|
||||
or os.environ.get('PODMAN_RUN_DIR')
|
||||
or config['default'].get('run_dir')
|
||||
or str(Path(args.xdg_runtime_dir, 'pypodman'))
|
||||
) # yapf: disable
|
||||
|
||||
setattr(
|
||||
args,
|
||||
'host',
|
||||
getattr(args, 'host')
|
||||
or os.environ.get('PODMAN_HOST')
|
||||
or config['default'].get('host')
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'username',
|
||||
getattr(args, 'username')
|
||||
or os.environ.get('PODMAN_USER')
|
||||
or config['default'].get('username')
|
||||
or os.environ.get('USER')
|
||||
or os.environ.get('LOGNAME')
|
||||
or getpass.getuser()
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'port',
|
||||
getattr(args, 'port')
|
||||
or os.environ.get('PODMAN_PORT')
|
||||
or config['default'].get('port', None)
|
||||
or 22
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'remote_socket_path',
|
||||
getattr(args, 'remote_socket_path')
|
||||
or os.environ.get('PODMAN_REMOTE_SOCKET_PATH')
|
||||
or config['default'].get('remote_socket_path')
|
||||
or '/run/podman/io.podman'
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'log_level',
|
||||
getattr(args, 'log_level')
|
||||
or os.environ.get('PODMAN_LOG_LEVEL')
|
||||
or config['default'].get('log_level')
|
||||
or logging.WARNING
|
||||
) # yapf:disable
|
||||
|
||||
setattr(
|
||||
args,
|
||||
'identity_file',
|
||||
getattr(args, 'identity_file')
|
||||
or os.environ.get('PODMAN_IDENTITY_FILE')
|
||||
or config['default'].get('identity_file')
|
||||
or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.username))
|
||||
) # yapf:disable
|
||||
|
||||
if not os.path.isfile(args.identity_file):
|
||||
args.identity_file = None
|
||||
|
||||
if args.host:
|
||||
args.local_socket_path = str(Path(args.run_dir, 'podman.socket'))
|
||||
else:
|
||||
args.local_socket_path = args.remote_socket_path
|
||||
|
||||
args.local_uri = 'unix:{}'.format(args.local_socket_path)
|
||||
|
||||
if args.host:
|
||||
components = ['ssh://', args.username, '@', args.host]
|
||||
if args.port:
|
||||
components.extend((':', str(args.port)))
|
||||
components.append(args.remote_socket_path)
|
||||
|
||||
args.remote_uri = ''.join(components)
|
||||
return args
|
||||
|
||||
def exit(self, status=0, message=None):
|
||||
"""Capture message and route to logger."""
|
||||
if message:
|
||||
log = logging.info if status == 0 else logging.error
|
||||
log(message)
|
||||
super().exit(status)
|
||||
|
||||
def error(self, message):
|
||||
"""Capture message and route to logger."""
|
||||
logging.error('%s: %s', self.prog, message)
|
||||
logging.error("Try '%s --help' for more information.", self.prog)
|
||||
super().exit(2)
|
@ -1,87 +0,0 @@
|
||||
"""Report Manager."""
|
||||
import string
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class ReportFormatter(string.Formatter):
|
||||
"""Custom formatter to default missing keys to '<none>'."""
|
||||
|
||||
def get_value(self, key, args, kwargs):
|
||||
"""Map missing key to value '<none>'."""
|
||||
try:
|
||||
if isinstance(key, int):
|
||||
return args[key]
|
||||
else:
|
||||
return kwargs[key]
|
||||
except KeyError:
|
||||
return '<none>'
|
||||
|
||||
|
||||
class ReportColumn(namedtuple('ReportColumn', 'key display width default')):
|
||||
"""Hold attributes of output column."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, key, display, width, default=None):
|
||||
"""Add defaults for attributes."""
|
||||
return super(ReportColumn, cls).__new__(cls, key, display, width,
|
||||
default)
|
||||
|
||||
|
||||
class Report():
|
||||
"""Report Manager."""
|
||||
|
||||
def __init__(self, columns, heading=True, epilog=None, file=sys.stdout):
|
||||
"""Construct Report.
|
||||
|
||||
columns is a mapping for named fields to column headings.
|
||||
headers True prints headers on table.
|
||||
epilog will be printed when the report context is closed.
|
||||
"""
|
||||
self._columns = columns
|
||||
self._file = file
|
||||
self._format_string = None
|
||||
self._formatter = ReportFormatter()
|
||||
self._heading = heading
|
||||
self.epilog = epilog
|
||||
|
||||
def row(self, **fields):
|
||||
"""Print row for report."""
|
||||
if self._heading:
|
||||
hdrs = {k: v.display for (k, v) in self._columns.items()}
|
||||
print(
|
||||
self._formatter.format(self._format_string, **hdrs),
|
||||
flush=True,
|
||||
file=self._file,
|
||||
)
|
||||
self._heading = False
|
||||
|
||||
fields = {k: str(v) for k, v in fields.items()}
|
||||
print(self._formatter.format(self._format_string, **fields))
|
||||
|
||||
def __enter__(self):
|
||||
"""Return `self` upon entering the runtime context."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Leave Report context and print epilog if provided."""
|
||||
if self.epilog:
|
||||
print(self.epilog, flush=True, file=self._file)
|
||||
|
||||
def layout(self, iterable, keys, truncate=True):
|
||||
"""Use data and headings build format for table to fit."""
|
||||
fmt = []
|
||||
|
||||
for key in keys:
|
||||
slice_ = [str(i.get(key, '')) for i in iterable]
|
||||
data_len = len(max(slice_, key=len))
|
||||
|
||||
info = self._columns.get(key,
|
||||
ReportColumn(key, key.upper(), data_len))
|
||||
display_len = max(data_len, len(info.display))
|
||||
if truncate and info.width != 0:
|
||||
display_len = info.width
|
||||
|
||||
fmt.append('{{{0}:{1}.{1}}}'.format(key, display_len))
|
||||
self._format_string = ' '.join(fmt)
|
@ -1,81 +0,0 @@
|
||||
"""Remote podman client."""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from pypodman.lib import PodmanArgumentParser
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point."""
|
||||
# Setup logging so we use stderr and can change logging level later
|
||||
# Do it now before there is any chance of a default setup hardcoding crap.
|
||||
log = logging.getLogger()
|
||||
fmt = logging.Formatter('%(asctime)s | %(levelname)-8s | %(message)s',
|
||||
'%Y-%m-%d %H:%M:%S %Z')
|
||||
stderr = logging.StreamHandler(stream=sys.stderr)
|
||||
stderr.setFormatter(fmt)
|
||||
log.addHandler(stderr)
|
||||
log.setLevel(logging.WARNING)
|
||||
|
||||
parser = PodmanArgumentParser()
|
||||
args = parser.parse_args()
|
||||
|
||||
log.setLevel(args.log_level)
|
||||
logging.debug(
|
||||
'Logging initialized at level %s',
|
||||
logging.getLevelName(logging.getLogger().getEffectiveLevel()))
|
||||
|
||||
def want_tb():
|
||||
"""Add traceback when logging events."""
|
||||
return log.getEffectiveLevel() == logging.DEBUG
|
||||
|
||||
try:
|
||||
if not os.path.exists(args.run_dir):
|
||||
os.makedirs(args.run_dir)
|
||||
except PermissionError as e:
|
||||
logging.critical(e, exc_info=want_tb())
|
||||
sys.exit(6)
|
||||
|
||||
# class_(args).method() are set by the sub-command's parser
|
||||
returncode = None
|
||||
try:
|
||||
obj = args.class_(args)
|
||||
except AttributeError:
|
||||
parser.print_help(sys.stderr)
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr, flush=True)
|
||||
sys.exit(1)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logging.critical(repr(e), exc_info=want_tb())
|
||||
logging.warning('See subparser "%s" configuration.',
|
||||
args.subparser_name)
|
||||
sys.exit(5)
|
||||
|
||||
try:
|
||||
returncode = getattr(obj, args.method)()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except AttributeError as e:
|
||||
logging.critical(e, exc_info=want_tb())
|
||||
logging.warning('See subparser "%s" configuration.',
|
||||
args.subparser_name)
|
||||
returncode = 3
|
||||
except (
|
||||
CalledProcessError,
|
||||
ConnectionError,
|
||||
ConnectionRefusedError,
|
||||
ConnectionResetError,
|
||||
TimeoutError,
|
||||
) as e:
|
||||
logging.critical(e, exc_info=want_tb())
|
||||
logging.info('Review connection arguments for correctness.')
|
||||
returncode = 4
|
||||
|
||||
return 0 if returncode is None else returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
@ -1,23 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
|
||||
from report import Report, ReportColumn
|
||||
|
||||
|
||||
class TestReport(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_report_column(self):
|
||||
rc = ReportColumn('k', 'v', 3)
|
||||
self.assertEqual(rc.key, 'k')
|
||||
self.assertEqual(rc.display, 'v')
|
||||
self.assertEqual(rc.width, 3)
|
||||
self.assertIsNone(rc.default)
|
||||
|
||||
rc = ReportColumn('k', 'v', 3, 'd')
|
||||
self.assertEqual(rc.key, 'k')
|
||||
self.assertEqual(rc.display, 'v')
|
||||
self.assertEqual(rc.width, 3)
|
||||
self.assertEqual(rc.default, 'd')
|
@ -1,5 +0,0 @@
|
||||
humanize
|
||||
podman
|
||||
pytoml
|
||||
PyYAML
|
||||
setuptools>=39
|
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
root = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
with open(os.path.join(root, 'README.md')) as me:
|
||||
readme = me.read()
|
||||
|
||||
with open(os.path.join(root, 'requirements.txt')) as r:
|
||||
requirements = r.read().splitlines()
|
||||
|
||||
|
||||
setup(
|
||||
name='pypodman',
|
||||
version=os.environ.get('PODMAN_VERSION', '0.0.0'),
|
||||
description='A client for communicating with a Podman server',
|
||||
author_email='jhonce@redhat.com',
|
||||
author='Jhon Honce',
|
||||
license='Apache Software License',
|
||||
long_description=readme,
|
||||
entry_points={'console_scripts': [
|
||||
'pypodman = pypodman.main:main',
|
||||
]},
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
packages=find_packages(exclude=['test']),
|
||||
python_requires='>=3',
|
||||
zip_safe=True,
|
||||
url='http://github.com/containers/libpod',
|
||||
keywords='varlink libpod podman pypodman',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
])
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user