summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Heon <matthew.heon@gmail.com>2018-07-13 16:34:51 -0400
committerGitHub <noreply@github.com>2018-07-13 16:34:51 -0400
commita689639a6502bab3f49b853bc2983c1b44363b2f (patch)
tree75ba256d70545d79aa61d7c57c20df886be1555f
parent14a6d51a8432fc0c3324fec02e8729d3032f2af2 (diff)
parent74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15 (diff)
downloadpodman-a689639a6502bab3f49b853bc2983c1b44363b2f.tar.gz
podman-a689639a6502bab3f49b853bc2983c1b44363b2f.tar.bz2
podman-a689639a6502bab3f49b853bc2983c1b44363b2f.zip
Merge pull request #1081 from jwhonce/wip/client
remote python client for podman
-rw-r--r--Makefile18
-rw-r--r--contrib/python/cmd/images.py21
-rw-r--r--contrib/python/cmd/pman.py42
-rw-r--r--contrib/python/cmd/ps.py19
-rw-r--r--contrib/python/cmd/remote_client.py136
-rw-r--r--contrib/python/cmd/rm.py22
-rw-r--r--contrib/python/cmd/rmi.py25
-rw-r--r--contrib/python/cmd/utils.py32
-rw-r--r--contrib/python/podman/CHANGES.txt (renamed from contrib/python/CHANGES.txt)0
-rw-r--r--contrib/python/podman/LICENSE.txt (renamed from contrib/python/LICENSE.txt)0
-rw-r--r--contrib/python/podman/MANIFEST.in (renamed from contrib/python/MANIFEST.in)0
-rw-r--r--contrib/python/podman/Makefile (renamed from contrib/python/Makefile)5
-rw-r--r--contrib/python/podman/README.md (renamed from contrib/python/README.md)2
-rw-r--r--contrib/python/podman/examples/eg_attach.py (renamed from contrib/python/examples/eg_attach.py)0
-rw-r--r--contrib/python/podman/examples/eg_containers_by_image.py (renamed from contrib/python/examples/eg_containers_by_image.py)0
-rw-r--r--contrib/python/podman/examples/eg_image_list.py (renamed from contrib/python/examples/eg_image_list.py)0
-rw-r--r--contrib/python/podman/examples/eg_inspect_fedora.py (renamed from contrib/python/examples/eg_inspect_fedora.py)0
-rw-r--r--contrib/python/podman/examples/eg_latest_containers.py (renamed from contrib/python/examples/eg_latest_containers.py)0
-rw-r--r--contrib/python/podman/examples/eg_new_image.py (renamed from contrib/python/examples/eg_new_image.py)0
-rwxr-xr-xcontrib/python/podman/examples/run_example.sh (renamed from contrib/python/examples/run_example.sh)0
-rw-r--r--contrib/python/podman/podman/__init__.py (renamed from contrib/python/podman/__init__.py)0
-rw-r--r--contrib/python/podman/podman/client.py (renamed from contrib/python/podman/client.py)26
-rw-r--r--contrib/python/podman/podman/libs/__init__.py (renamed from contrib/python/podman/libs/__init__.py)0
-rw-r--r--contrib/python/podman/podman/libs/_containers_attach.py (renamed from contrib/python/podman/libs/_containers_attach.py)0
-rw-r--r--contrib/python/podman/podman/libs/_containers_start.py (renamed from contrib/python/podman/libs/_containers_start.py)0
-rw-r--r--contrib/python/podman/podman/libs/containers.py (renamed from contrib/python/podman/libs/containers.py)0
-rw-r--r--contrib/python/podman/podman/libs/errors.py (renamed from contrib/python/podman/libs/errors.py)17
-rw-r--r--contrib/python/podman/podman/libs/images.py (renamed from contrib/python/podman/libs/images.py)0
-rw-r--r--contrib/python/podman/podman/libs/system.py (renamed from contrib/python/podman/libs/system.py)0
-rw-r--r--contrib/python/podman/podman/libs/tunnel.py (renamed from contrib/python/podman/libs/tunnel.py)37
-rw-r--r--contrib/python/podman/requirements.txt (renamed from contrib/python/requirements.txt)0
-rw-r--r--contrib/python/podman/setup.py (renamed from contrib/python/setup.py)0
-rw-r--r--contrib/python/podman/test/__init__.py (renamed from contrib/python/test/__init__.py)0
-rw-r--r--contrib/python/podman/test/podman_testcase.py (renamed from contrib/python/test/podman_testcase.py)0
-rw-r--r--contrib/python/podman/test/test_client.py (renamed from contrib/python/test/test_client.py)8
-rw-r--r--contrib/python/podman/test/test_containers.py (renamed from contrib/python/test/test_containers.py)0
-rw-r--r--contrib/python/podman/test/test_images.py (renamed from contrib/python/test/test_images.py)0
-rw-r--r--contrib/python/podman/test/test_libs.py (renamed from contrib/python/test/test_libs.py)0
-rwxr-xr-xcontrib/python/podman/test/test_runner.sh (renamed from contrib/python/test/test_runner.sh)4
-rw-r--r--contrib/python/podman/test/test_system.py (renamed from contrib/python/test/test_system.py)0
-rw-r--r--contrib/python/podman/test/test_tunnel.py (renamed from contrib/python/test/test_tunnel.py)2
-rw-r--r--contrib/python/pypodman/MANIFEST.in1
-rw-r--r--contrib/python/pypodman/Makefile21
-rw-r--r--contrib/python/pypodman/README.md32
-rw-r--r--contrib/python/pypodman/docs/pypodman.1.md82
-rw-r--r--contrib/python/pypodman/lib/__init__.py11
-rw-r--r--contrib/python/pypodman/lib/action_base.py84
-rw-r--r--contrib/python/pypodman/lib/actions/__init__.py7
-rw-r--r--contrib/python/pypodman/lib/actions/images_action.py88
-rw-r--r--contrib/python/pypodman/lib/actions/ps_action.py76
-rw-r--r--contrib/python/pypodman/lib/actions/rm_action.py51
-rw-r--r--contrib/python/pypodman/lib/actions/rmi_action.py50
-rw-r--r--contrib/python/pypodman/lib/config.py212
-rw-r--r--contrib/python/pypodman/lib/future_abstract.py29
-rwxr-xr-xcontrib/python/pypodman/lib/pypodman.py76
-rw-r--r--contrib/python/pypodman/lib/report.py67
-rw-r--r--contrib/python/pypodman/requirements.txt4
-rw-r--r--contrib/python/pypodman/setup.py44
-rw-r--r--contrib/python/pypodman/test/test_report.py23
-rw-r--r--contrib/spec/podman.spec.in30
60 files changed, 1056 insertions, 348 deletions
diff --git a/Makefile b/Makefile
index 4a1a34316..d829df773 100644
--- a/Makefile
+++ b/Makefile
@@ -114,7 +114,12 @@ bin/podman.cross.%: .gopathok
python-podman:
ifdef HAS_PYTHON3
- $(MAKE) -C contrib/python python-podman
+ $(MAKE) -C contrib/python/podman python-podman
+endif
+
+python-pypodman:
+ifdef HAS_PYTHON3
+ $(MAKE) -C contrib/python/pypodman python-pypodman
endif
clean:
@@ -128,9 +133,10 @@ clean:
test/copyimg/copyimg \
test/testdata/redis-image \
cmd/podman/varlink/ioprojectatomicpodman.go \
- $(MANPAGES)
+ $(MANPAGES) ||:
ifdef HAS_PYTHON3
- $(MAKE) -C contrib/python clean
+ $(MAKE) -C contrib/python/podman clean
+ $(MAKE) -C contrib/python/pypodman clean
endif
find . -name \*~ -delete
find . -name \#\* -delete
@@ -169,12 +175,13 @@ localintegration: varlink_generate test-binaries clientintegration
ginkgo -v -cover -flakeAttempts 3 -progress -trace -noColor test/e2e/.
clientintegration:
- $(MAKE) -C contrib/python integration
+ $(MAKE) -C contrib/python/podman integration
+ $(MAKE) -C contrib/python/pypodman integration
vagrant-check:
BOX=$(BOX) sh ./vagrant.sh
-binaries: varlink_generate podman python-podman
+binaries: varlink_generate podman python-podman python-pypodman
test-binaries: test/bin2img/bin2img test/copyimg/copyimg test/checkseccomp/checkseccomp
@@ -313,4 +320,5 @@ validate: gofmt .gitvalidation
validate \
install.libseccomp.sudo \
python-podman \
+ python-pypodman \
clientintegration
diff --git a/contrib/python/cmd/images.py b/contrib/python/cmd/images.py
deleted file mode 100644
index 3e0dff626..000000000
--- a/contrib/python/cmd/images.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from pman import PodmanRemote
-from utils import write_out, convert_size, stringTimeToHuman
-
-def cli(subparser):
- imagesp = subparser.add_parser("images",
- help=("list images"))
- imagesp.add_argument("all", action="store_true", help="list all images")
- imagesp.set_defaults(_class=Images, func='display_all_image_info')
-
-
-class Images(PodmanRemote):
-
- def display_all_image_info(self):
- col_fmt = "{0:40}{1:12}{2:14}{3:18}{4:14}"
- write_out(col_fmt.format("REPOSITORY", "TAG", "IMAGE ID", "CREATED", "SIZE"))
- for i in self.client.images.list():
- for r in i["repoTags"]:
- rsplit = r.rindex(":")
- name = r[0:rsplit-1]
- tag = r[rsplit+1:]
- write_out(col_fmt.format(name, tag, i["id"][:12], stringTimeToHuman(i["created"]), convert_size(i["size"])))
diff --git a/contrib/python/cmd/pman.py b/contrib/python/cmd/pman.py
deleted file mode 100644
index c75c3d174..000000000
--- a/contrib/python/cmd/pman.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import podman as p
-
-
-class PodmanRemote(object):
- def __init__(self):
- self.args = None
- self._remote_uri= None
- self._local_uri= None
- self._identity_file= None
- self._client = None
-
- def set_args(self, args, local_uri, remote_uri, identity_file):
- self.args = args
- self._local_uri = local_uri
- self.remote_uri = remote_uri
- self._identity_file = identity_file
-
- @property
- def remote_uri(self):
- return self._remote_uri
-
- @property
- def local_uri(self):
- return self._local_uri
-
- @property
- def client(self):
- if self._client is None:
- self._client = p.Client(uri=self.local_uri, remote_uri=self.remote_uri, identity_file=self.identity_file)
- return self._client
-
- @remote_uri.setter
- def remote_uri(self, uri):
- self._remote_uri = uri
-
- @local_uri.setter
- def local_uri(self, uri):
- self._local_uri= uri
-
- @property
- def identity_file(self):
- return self._identity_file
diff --git a/contrib/python/cmd/ps.py b/contrib/python/cmd/ps.py
deleted file mode 100644
index 85db5489e..000000000
--- a/contrib/python/cmd/ps.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from pman import PodmanRemote
-from utils import write_out, convert_size, stringTimeToHuman
-
-def cli(subparser):
- imagesp = subparser.add_parser("ps",
- help=("list containers"))
- imagesp.add_argument("all", action="store_true", help="list all containers")
- imagesp.set_defaults(_class=Ps, func='display_all_containers')
-
-
-class Ps(PodmanRemote):
-
- def display_all_containers(self):
- col_fmt = "{0:15}{1:32}{2:22}{3:14}{4:12}{5:30}{6:20}"
- write_out(col_fmt.format("CONTAINER ID", "IMAGE", "COMMAND", "CREATED", "STATUS", "PORTS", "NAMES"))
-
- for i in self.client.containers.list():
- command = " ".join(i["command"])
- write_out(col_fmt.format(i["id"][0:12], i["image"][0:30], command[0:20], stringTimeToHuman(i["createdat"]), i["status"], "", i["names"][0:20]))
diff --git a/contrib/python/cmd/remote_client.py b/contrib/python/cmd/remote_client.py
deleted file mode 100644
index 9bb5a0d9a..000000000
--- a/contrib/python/cmd/remote_client.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import os
-import getpass
-import argparse
-import images
-import ps, rm, rmi
-import sys
-from utils import write_err
-import pytoml
-
-default_conf_path = "/etc/containers/podman_client.conf"
-
-class HelpByDefaultArgumentParser(argparse.ArgumentParser):
-
- def error(self, message):
- write_err('%s: %s' % (self.prog, message))
- write_err("Try '%s --help' for more information." % self.prog)
- sys.exit(2)
-
- def print_usage(self, message="too few arguments"): # pylint: disable=arguments-differ
- self.prog = " ".join(sys.argv)
- self.error(message)
-
-
-def create_parser(help_text):
- parser = HelpByDefaultArgumentParser(description=help_text)
- parser.add_argument('-v', '--version', action='version', version="0.0",
- help=("show rpodman version and exit"))
- parser.add_argument('--debug', default=False, action='store_true',
- help=("show debug messages"))
- parser.add_argument('--run_dir', dest="run_dir",
- help=("directory to place socket bindings"))
- parser.add_argument('--user', dest="user",
- help=("remote user"))
- parser.add_argument('--host', dest="host",
- help=("remote host"))
- parser.add_argument('--remote_socket_path', dest="remote_socket_path",
- help=("remote socket path"))
- parser.add_argument('--identity_file', dest="identity_file",
- help=("path to identity file"))
- subparser = parser.add_subparsers(help=("commands"))
- images.cli(subparser)
- ps.cli(subparser)
- rm.cli(subparser)
- rmi.cli(subparser)
-
- return parser
-
-def load_toml(path):
- # Lets load the configuration file
- with open(path) as stream:
- return pytoml.load(stream)
-
-if __name__ == '__main__':
-
- host = None
- remote_socket_path = None
- user = None
- run_dir = None
-
- aparser = create_parser("podman remote tool")
- args = aparser.parse_args()
- if not os.path.exists(default_conf_path):
- conf = {"default": {}}
- else:
- conf = load_toml("/etc/containers/podman_client.conf")
-
- # run_dir
- if "run_dir" in os.environ:
- run_dir = os.environ["run_dir"]
- elif "run_dir" in conf["default"] and conf["default"]["run_dir"] is not None:
- run_dir = conf["default"]["run_dir"]
- else:
- xdg = os.environ["XDG_RUNTIME_DIR"]
- run_dir = os.path.join(xdg, "podman")
-
- # make the run_dir if it doesnt exist
- if not os.path.exists(run_dir):
- os.makedirs(run_dir)
-
- local_socket_path = os.path.join(run_dir, "podman.socket")
-
- # remote host
- if "host" in os.environ:
- host = os.environ["host"]
- elif getattr(args, "host") is not None:
- host = getattr(args, "host")
- else:
- host = conf["default"]["host"] if "host" in conf["default"] else None
-
- # remote user
- if "user" in os.environ:
- user = os.environ["user"]
- elif getattr(args, "user") is not None:
- user = getattr(args, "user")
- elif "user" in conf["default"] and conf["default"]["user"] is not None:
- user = conf["default"]["user"]
- else:
- user = getpass.getuser()
-
- # remote path
- if "remote_socket_path" in os.environ:
- remote_socket_path = os.environ["remote_socket_path"]
- elif getattr(args, "remote_socket_path") is not None:
- remote_socket_path = getattr(args, "remote_socket_path")
- elif "remote_socket_path" in conf["default"] and conf["default"]["remote_socket_path"]:
- remote_socket_path = conf["default"]["remote_socket_path"]
- else:
- remote_socket_path = None
-
-
- # identity file
- if "identity_file" in os.environ:
- identity_file = os.environ["identity_file"]
- elif getattr(args, "identity_file") is not None:
- identity_file = getattr(args, "identity_file")
- elif "identity_file" in conf["default"] and conf["default"]["identity_file"] is not None:
- identity_file = conf["default"]["identity_file"]
- else:
- identity_file = None
-
- if None in [host, local_socket_path, user, remote_socket_path]:
- print("missing input for local_socket, user, host, or remote_socket_path")
- sys.exit(1)
-
- local_uri = "unix:{}".format(local_socket_path)
- remote_uri = "ssh://{}@{}{}".format(user, host, remote_socket_path)
-
- _class = args._class() # pylint: disable=protected-access
- _class.set_args(args, local_uri, remote_uri, identity_file)
-
- if "func" in args:
- _func = getattr(_class, args.func)
- sys.exit(_func())
- else:
- aparser.print_usage()
- sys.exit(1) \ No newline at end of file
diff --git a/contrib/python/cmd/rm.py b/contrib/python/cmd/rm.py
deleted file mode 100644
index c9dfaa688..000000000
--- a/contrib/python/cmd/rm.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from pman import PodmanRemote
-from utils import write_out, convert_size, stringTimeToHuman
-
-def cli(subparser):
- imagesp = subparser.add_parser("rm",
- help=("delete one or more containers"))
- imagesp.add_argument("--force", "-f", action="store_true", help="force delete", dest="force")
- imagesp.add_argument("delete_targets", nargs='*', help="container images to delete")
- imagesp.set_defaults(_class=Rm, func='remove_containers')
-
-
-class Rm(PodmanRemote):
-
- def remove_containers(self):
- delete_targets = getattr(self.args, "delete_targets")
- if len(delete_targets) < 1:
- raise ValueError("you must supply at least one container id or name to delete")
- force = getattr(self.args, "force")
- for d in delete_targets:
- con = self.client.containers.get(d)
- con.remove(force)
- write_out(con["id"])
diff --git a/contrib/python/cmd/rmi.py b/contrib/python/cmd/rmi.py
deleted file mode 100644
index 807c5c1e4..000000000
--- a/contrib/python/cmd/rmi.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from pman import PodmanRemote
-from utils import write_out, write_err
-
-def cli(subparser):
- imagesp = subparser.add_parser("rmi",
- help=("delete one or more images"))
- imagesp.add_argument("--force", "-f", action="store_true", help="force delete", dest="force")
- imagesp.add_argument("delete_targets", nargs='*', help="images to delete")
- imagesp.set_defaults(_class=Rmi, func='remove_images')
-
-
-class Rmi(PodmanRemote):
-
- def remove_images(self):
- delete_targets = getattr(self.args, "delete_targets")
- if len(delete_targets) < 1:
- raise ValueError("you must supply at least one image id or name to delete")
- force = getattr(self.args, "force")
- for d in delete_targets:
- image = self.client.images.get(d)
- if image["containers"] > 0 and not force:
- write_err("unable to delete {} because it has associated errors. retry with --force".format(d))
- continue
- image.remove(force)
- write_out(image["id"])
diff --git a/contrib/python/cmd/utils.py b/contrib/python/cmd/utils.py
deleted file mode 100644
index d4a14164d..000000000
--- a/contrib/python/cmd/utils.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import sys
-import math
-import datetime
-
-def write_out(output, lf="\n"):
- _output(sys.stdout, output, lf)
-
-
-def write_err(output, lf="\n"):
- _output(sys.stderr, output, lf)
-
-
-def _output(fd, output, lf):
- fd.flush()
- fd.write(output + str(lf))
-
-
-def convert_size(size):
- if size > 0:
- size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
- i = int(math.floor(math.log(size, 1000)))
- p = math.pow(1000, i)
- s = round(size/p, 2) # pylint: disable=round-builtin,old-division
- if s > 0:
- return '%s %s' % (s, size_name[i])
- return '0B'
-
-def stringTimeToHuman(t):
- #datetime.date(datetime.strptime("05/Feb/2016", '%d/%b/%Y'))
- #2018-04-30 13:55:45.019400581 +0000 UTC
- #d = datetime.date(datetime.strptime(t, "%Y-%m-%d"))
- return "sometime ago"
diff --git a/contrib/python/CHANGES.txt b/contrib/python/podman/CHANGES.txt
index 2bac1c867..2bac1c867 100644
--- a/contrib/python/CHANGES.txt
+++ b/contrib/python/podman/CHANGES.txt
diff --git a/contrib/python/LICENSE.txt b/contrib/python/podman/LICENSE.txt
index decfce56d..decfce56d 100644
--- a/contrib/python/LICENSE.txt
+++ b/contrib/python/podman/LICENSE.txt
diff --git a/contrib/python/MANIFEST.in b/contrib/python/podman/MANIFEST.in
index 72e638cb9..72e638cb9 100644
--- a/contrib/python/MANIFEST.in
+++ b/contrib/python/podman/MANIFEST.in
diff --git a/contrib/python/Makefile b/contrib/python/podman/Makefile
index 6cb63c403..ea40cccac 100644
--- a/contrib/python/Makefile
+++ b/contrib/python/podman/Makefile
@@ -8,9 +8,14 @@ python-podman:
integration:
test/test_runner.sh
+.PHONY: install
+install:
+ $(PYTHON) setup.py install --user
+
.PHONY: clean
clean:
$(PYTHON) setup.py clean --all
+ pip3 uninstall podman ||:
rm -rf podman.egg-info dist
find . -depth -name __pycache__ -exec rm -rf {} \;
find . -depth -name \*.pyc -exec rm -f {} \;
diff --git a/contrib/python/README.md b/contrib/python/podman/README.md
index dcf40a1a9..fad03fd27 100644
--- a/contrib/python/README.md
+++ b/contrib/python/podman/README.md
@@ -9,7 +9,7 @@ See [libpod](https://github.com/projectatomic/libpod)
To build the podman egg:
```sh
-cd ~/libpod/contrib/pypodman
+cd ~/libpod/contrib/python
python3 setup.py clean -a && python3 setup.py bdist
```
diff --git a/contrib/python/examples/eg_attach.py b/contrib/python/podman/examples/eg_attach.py
index f5070dc53..f5070dc53 100644
--- a/contrib/python/examples/eg_attach.py
+++ b/contrib/python/podman/examples/eg_attach.py
diff --git a/contrib/python/examples/eg_containers_by_image.py b/contrib/python/podman/examples/eg_containers_by_image.py
index bf4fdebf1..bf4fdebf1 100644
--- a/contrib/python/examples/eg_containers_by_image.py
+++ b/contrib/python/podman/examples/eg_containers_by_image.py
diff --git a/contrib/python/examples/eg_image_list.py b/contrib/python/podman/examples/eg_image_list.py
index ef31fd708..ef31fd708 100644
--- a/contrib/python/examples/eg_image_list.py
+++ b/contrib/python/podman/examples/eg_image_list.py
diff --git a/contrib/python/examples/eg_inspect_fedora.py b/contrib/python/podman/examples/eg_inspect_fedora.py
index b5bbba46d..b5bbba46d 100644
--- a/contrib/python/examples/eg_inspect_fedora.py
+++ b/contrib/python/podman/examples/eg_inspect_fedora.py
diff --git a/contrib/python/examples/eg_latest_containers.py b/contrib/python/podman/examples/eg_latest_containers.py
index 446f670dd..446f670dd 100644
--- a/contrib/python/examples/eg_latest_containers.py
+++ b/contrib/python/podman/examples/eg_latest_containers.py
diff --git a/contrib/python/examples/eg_new_image.py b/contrib/python/podman/examples/eg_new_image.py
index 21e076dcb..21e076dcb 100644
--- a/contrib/python/examples/eg_new_image.py
+++ b/contrib/python/podman/examples/eg_new_image.py
diff --git a/contrib/python/examples/run_example.sh b/contrib/python/podman/examples/run_example.sh
index 0f6575073..0f6575073 100755
--- a/contrib/python/examples/run_example.sh
+++ b/contrib/python/podman/examples/run_example.sh
diff --git a/contrib/python/podman/__init__.py b/contrib/python/podman/podman/__init__.py
index 5a0356311..5a0356311 100644
--- a/contrib/python/podman/__init__.py
+++ b/contrib/python/podman/podman/__init__.py
diff --git a/contrib/python/podman/client.py b/contrib/python/podman/podman/client.py
index ad166eb06..404b7d117 100644
--- a/contrib/python/podman/client.py
+++ b/contrib/python/podman/podman/client.py
@@ -44,11 +44,11 @@ class BaseClient(object):
raise ValueError('path is required for uri,'
' expected format "unix://path_to_socket"')
- if kwargs.get('remote_uri') or kwargs.get('identity_file'):
+ if kwargs.get('remote_uri'):
# Remote access requires the full tuple of information
if kwargs.get('remote_uri') is None:
raise ValueError(
- 'remote is required,'
+ 'remote_uri is required,'
' expected format "ssh://user@hostname/path_to_socket".')
remote = urlparse(kwargs['remote_uri'])
if remote.username is None:
@@ -64,20 +64,16 @@ class BaseClient(object):
'hostname is required for remote_uri,'
' expected format "ssh://user@hostname/path_to_socket".')
- if kwargs.get('identity_file') is None:
- raise ValueError('identity_file is required.')
-
- if not os.path.isfile(kwargs['identity_file']):
- raise FileNotFoundError(
- errno.ENOENT,
- os.strerror(errno.ENOENT),
- kwargs['identity_file'],
- )
-
return RemoteClient(
- Context(uri, interface, local_path, remote.path,
- remote.username, remote.hostname,
- kwargs['identity_file']))
+ Context(
+ uri,
+ interface,
+ local_path,
+ remote.path,
+ remote.username,
+ remote.hostname,
+ kwargs.get('identity_file'),
+ ))
else:
return LocalClient(
Context(uri, interface, None, None, None, None, None))
diff --git a/contrib/python/podman/libs/__init__.py b/contrib/python/podman/podman/libs/__init__.py
index 3a8a35021..3a8a35021 100644
--- a/contrib/python/podman/libs/__init__.py
+++ b/contrib/python/podman/podman/libs/__init__.py
diff --git a/contrib/python/podman/libs/_containers_attach.py b/contrib/python/podman/podman/libs/_containers_attach.py
index df12fa998..df12fa998 100644
--- a/contrib/python/podman/libs/_containers_attach.py
+++ b/contrib/python/podman/podman/libs/_containers_attach.py
diff --git a/contrib/python/podman/libs/_containers_start.py b/contrib/python/podman/podman/libs/_containers_start.py
index ad9f32eab..ad9f32eab 100644
--- a/contrib/python/podman/libs/_containers_start.py
+++ b/contrib/python/podman/podman/libs/_containers_start.py
diff --git a/contrib/python/podman/libs/containers.py b/contrib/python/podman/podman/libs/containers.py
index 6dc2c141e..6dc2c141e 100644
--- a/contrib/python/podman/libs/containers.py
+++ b/contrib/python/podman/podman/libs/containers.py
diff --git a/contrib/python/podman/libs/errors.py b/contrib/python/podman/podman/libs/errors.py
index c28afd940..b98210481 100644
--- a/contrib/python/podman/libs/errors.py
+++ b/contrib/python/podman/podman/libs/errors.py
@@ -5,14 +5,21 @@ from varlink import VarlinkError
class VarlinkErrorProxy(VarlinkError):
"""Class to Proxy VarlinkError methods."""
- def __init__(self, obj):
+ def __init__(self, message, namespaced=False):
"""Construct proxy from Exception."""
- self._obj = obj
+ super().__init__(message.as_dict(), namespaced)
+ self._message = message
self.__module__ = 'libpod'
- def __getattr__(self, item):
- """Return item from proxied Exception."""
- return getattr(self._obj, item)
+ 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):
diff --git a/contrib/python/podman/libs/images.py b/contrib/python/podman/podman/libs/images.py
index 334ff873c..334ff873c 100644
--- a/contrib/python/podman/libs/images.py
+++ b/contrib/python/podman/podman/libs/images.py
diff --git a/contrib/python/podman/libs/system.py b/contrib/python/podman/podman/libs/system.py
index c59867760..c59867760 100644
--- a/contrib/python/podman/libs/system.py
+++ b/contrib/python/podman/podman/libs/system.py
diff --git a/contrib/python/podman/libs/tunnel.py b/contrib/python/podman/podman/libs/tunnel.py
index 42fd3356b..440eb3951 100644
--- a/contrib/python/podman/libs/tunnel.py
+++ b/contrib/python/podman/podman/libs/tunnel.py
@@ -1,5 +1,6 @@
"""Cache for SSH tunnels."""
import collections
+import logging
import os
import subprocess
import threading
@@ -96,25 +97,31 @@ class Tunnel(object):
def bore(self, id):
"""Create SSH tunnel from given context."""
- cmd = [
- 'ssh',
- '-nNTq',
- '-L',
- '{}:{}'.format(self.context.local_socket,
- self.context.remote_socket),
- '-i',
- self.context.identity_file,
- 'ssh://{}@{}'.format(self.context.username, self.context.hostname),
- ]
-
- if os.environ.get('PODMAN_DEBUG'):
- cmd.append('-vvv')
+ cmd = ['ssh']
+
+ ssh_opts = '-fNT'
+ if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
+ ssh_opts += 'v'
+ else:
+ ssh_opts += 'q'
+ cmd.append(ssh_opts)
+
+ 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('ssh://{}@{}'.format(self.context.username,
+ self.context.hostname))
+
+ logging.debug('Tunnel cmd "{}"'.format(' '.join(cmd)))
self._tunnel = subprocess.Popen(cmd, close_fds=True)
- for i in range(5):
+ for i in range(300):
+ # TODO: Make timeout configurable
if os.path.exists(self.context.local_socket):
break
- time.sleep(1)
+ time.sleep(0.5)
else:
raise TimeoutError('Failed to create tunnel using: {}'.format(
' '.join(cmd)))
diff --git a/contrib/python/requirements.txt b/contrib/python/podman/requirements.txt
index d294af3c7..d294af3c7 100644
--- a/contrib/python/requirements.txt
+++ b/contrib/python/podman/requirements.txt
diff --git a/contrib/python/setup.py b/contrib/python/podman/setup.py
index c9db30199..c9db30199 100644
--- a/contrib/python/setup.py
+++ b/contrib/python/podman/setup.py
diff --git a/contrib/python/test/__init__.py b/contrib/python/podman/test/__init__.py
index e69de29bb..e69de29bb 100644
--- a/contrib/python/test/__init__.py
+++ b/contrib/python/podman/test/__init__.py
diff --git a/contrib/python/test/podman_testcase.py b/contrib/python/podman/test/podman_testcase.py
index f96a3a013..f96a3a013 100644
--- a/contrib/python/test/podman_testcase.py
+++ b/contrib/python/podman/test/podman_testcase.py
diff --git a/contrib/python/test/test_client.py b/contrib/python/podman/test/test_client.py
index e642c8add..2abc60a24 100644
--- a/contrib/python/test/test_client.py
+++ b/contrib/python/podman/test/test_client.py
@@ -21,11 +21,10 @@ class TestClient(unittest.TestCase):
self.assertIsInstance(p._client, LocalClient)
self.assertIsInstance(p._client, BaseClient)
- mock_ping.assert_called_once()
+ mock_ping.assert_called_once_with()
- @patch('os.path.isfile', return_value=True)
@patch('podman.libs.system.System.ping', return_value=True)
- def test_remote(self, mock_ping, mock_isfile):
+ def test_remote(self, mock_ping):
p = Client(
uri='unix:/run/podman',
interface='io.projectatomic.podman',
@@ -33,5 +32,4 @@ class TestClient(unittest.TestCase):
identity_file='~/.ssh/id_rsa')
self.assertIsInstance(p._client, BaseClient)
- mock_ping.assert_called_once()
- mock_isfile.assert_called_once()
+ mock_ping.assert_called_once_with()
diff --git a/contrib/python/test/test_containers.py b/contrib/python/podman/test/test_containers.py
index ec2dcde03..ec2dcde03 100644
--- a/contrib/python/test/test_containers.py
+++ b/contrib/python/podman/test/test_containers.py
diff --git a/contrib/python/test/test_images.py b/contrib/python/podman/test/test_images.py
index 14bf90992..14bf90992 100644
--- a/contrib/python/test/test_images.py
+++ b/contrib/python/podman/test/test_images.py
diff --git a/contrib/python/test/test_libs.py b/contrib/python/podman/test/test_libs.py
index 202bed1d8..202bed1d8 100644
--- a/contrib/python/test/test_libs.py
+++ b/contrib/python/podman/test/test_libs.py
diff --git a/contrib/python/test/test_runner.sh b/contrib/python/podman/test/test_runner.sh
index 602e0d6fd..b3d2ba15b 100755
--- a/contrib/python/test/test_runner.sh
+++ b/contrib/python/podman/test/test_runner.sh
@@ -7,11 +7,11 @@ if [[ $(id -u) != 0 ]]; then
fi
# setup path to find new binaries _NOT_ system binaries
-if [[ ! -x ../../bin/podman ]]; then
+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
+export PATH=../../../bin:$PATH
function usage {
echo 1>&2 $0 [-v] [-h] [test.TestCase|test.TestCase.step]
diff --git a/contrib/python/test/test_system.py b/contrib/python/podman/test/test_system.py
index 3f6ca57a2..3f6ca57a2 100644
--- a/contrib/python/test/test_system.py
+++ b/contrib/python/podman/test/test_system.py
diff --git a/contrib/python/test/test_tunnel.py b/contrib/python/podman/test/test_tunnel.py
index 2522df0ab..719a2f9a4 100644
--- a/contrib/python/test/test_tunnel.py
+++ b/contrib/python/podman/test/test_tunnel.py
@@ -66,7 +66,7 @@ class TestTunnel(unittest.TestCase):
cmd = [
'ssh',
- '-nNTq',
+ '-fNTq',
'-L',
'{}:{}'.format(context.local_socket, context.remote_socket),
'-i',
diff --git a/contrib/python/pypodman/MANIFEST.in b/contrib/python/pypodman/MANIFEST.in
new file mode 100644
index 000000000..bb3ec5f0d
--- /dev/null
+++ b/contrib/python/pypodman/MANIFEST.in
@@ -0,0 +1 @@
+include README.md
diff --git a/contrib/python/pypodman/Makefile b/contrib/python/pypodman/Makefile
new file mode 100644
index 000000000..4d76b1a1e
--- /dev/null
+++ b/contrib/python/pypodman/Makefile
@@ -0,0 +1,21 @@
+PYTHON ?= /usr/bin/python3
+
+.PHONY: python-pypodman
+python-pypodman:
+ $(PYTHON) setup.py bdist
+
+.PHONY: integration
+integration:
+ true
+
+.PHONY: install
+install:
+ $(PYTHON) setup.py install --user
+
+.PHONY: clean
+clean:
+ $(PYTHON) setup.py clean --all
+ pip3 uninstall pypodman ||:
+ rm -rf pypodman.egg-info dist
+ find . -depth -name __pycache__ -exec rm -rf {} \;
+ find . -depth -name \*.pyc -exec rm -f {} \;
diff --git a/contrib/python/pypodman/README.md b/contrib/python/pypodman/README.md
new file mode 100644
index 000000000..8a1c293f1
--- /dev/null
+++ b/contrib/python/pypodman/README.md
@@ -0,0 +1,32 @@
+# pypodman - CLI interface for podman written in python
+
+## Status: Active Development
+
+See [libpod](https://github.com/projectatomic/libpod/contrib/python/cmd)
+
+## Releases
+
+To build the pypodman egg:
+
+```sh
+cd ~/libpod/contrib/python/cmd
+python3 setup.py clean -a && python3 setup.py bdist
+```
+
+## 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
+```
diff --git a/contrib/python/pypodman/docs/pypodman.1.md b/contrib/python/pypodman/docs/pypodman.1.md
new file mode 100644
index 000000000..1a6be994d
--- /dev/null
+++ b/contrib/python/pypodman/docs/pypodman.1.md
@@ -0,0 +1,82 @@
+% pypodman "1"
+
+## NAME
+
+pypodman - Simple management tool for containers and images
+
+## SYNOPSIS
+
+**pypodman** [*global options*] _command_ [*options*]
+
+## 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. 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.
+
+## GLOBAL OPTIONS
+
+**--help, -h**
+
+Print usage statement.
+
+**--version**
+
+Print program version number and exit.
+
+**--config-home**
+
+Directory that will be namespaced with `pypodman` to hold `pypodman.conf`. See FILES below for more details.
+
+**--log-level**
+
+Log events above specified level: DEBUG, INFO, WARNING (default), ERROR, or CRITICAL.
+
+**--run-dir**
+
+Directory that will be namespaced with `pypodman` to hold local socket bindings. The default is ``$XDG_RUNTIME_DIR\`.
+
+**--user**
+
+Authenicating user on remote host. `pypodman` defaults to the logged in user.
+
+**--host**
+
+Name of remote host. There is no default, if not given `pypodman` attempts to connect to `--remote-socket-path` on local host.
+
+**--remote-socket-path**
+
+Path on remote host for podman service's `AF_UNIX` socket. The default is `/run/podman/io.projectatomic.podman`.
+
+**--identity-file**
+
+The optional `ssh` identity file to authenicate when tunnelling to remote host. Default is None and will allow `ssh` to follow it's default methods for resolving the identity and private key using the logged in user.
+
+## COMMANDS
+
+See [podman(1)](podman.1.md)
+
+## FILES
+
+**pypodman/pypodman.conf** (`Any element of XDG_CONFIG_DIRS` and/or `XDG_CONFIG_HOME` and/or **--config-home**)
+
+pypodman.conf is one or more configuration files for running the pypodman command. pypodman.conf is a TOML file with the stanza `[default]`, with a map of option: value.
+
+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.
+
+- `pypodman/pypodman.conf` from any path element in `XDG_CONFIG_DIRS` or `\etc\xdg`
+- `XDG_CONFIG_HOME` or $HOME/.config + `pypodman/pypodman.conf`
+- From `--config-home` command line option + `pypodman/pypodman.conf`
+- From environment variable, for example: RUN_DIR
+- From command line option, for example: --run-dir
+
+This should provide Operators the ability to setup basic configurations and allow users to customize them.
+
+**XDG_RUNTIME_DIR** (`XDG_RUNTIME_DIR/io.projectatomic.podman`)
+
+Directory where pypodman stores non-essential runtime files and other file objects (such as sockets, named pipes, ...).
+
+## SEE ALSO
+`podman(1)`, `libpod(8)`
diff --git a/contrib/python/pypodman/lib/__init__.py b/contrib/python/pypodman/lib/__init__.py
new file mode 100644
index 000000000..5a8303668
--- /dev/null
+++ b/contrib/python/pypodman/lib/__init__.py
@@ -0,0 +1,11 @@
+"""Remote podman client support library."""
+from .action_base import AbstractActionBase
+from .config import PodmanArgumentParser
+from .report import Report, ReportColumn
+
+__all__ = [
+ 'AbstractActionBase',
+ 'PodmanArgumentParser',
+ 'Report',
+ 'ReportColumn',
+]
diff --git a/contrib/python/pypodman/lib/action_base.py b/contrib/python/pypodman/lib/action_base.py
new file mode 100644
index 000000000..ff2922262
--- /dev/null
+++ b/contrib/python/pypodman/lib/action_base.py
@@ -0,0 +1,84 @@
+"""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, parser):
+ """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()
+ """
+ parser.add_argument(
+ '--all',
+ action='store_true',
+ help=('list all items.'
+ ' (default: no-op, included for compatibility.)'))
+ parser.add_argument(
+ '--no-trunc',
+ '--notruncate',
+ action='store_false',
+ dest='truncate',
+ default=True,
+ help='Display extended information. (default: False)')
+ parser.add_argument(
+ '--noheading',
+ action='store_false',
+ dest='heading',
+ default=True,
+ help=('Omit the table headings from the output.'
+ ' (default: False)'))
+ parser.add_argument(
+ '--quiet',
+ action='store_true',
+ help='List only the IDs. (default: %(default)s)')
+
+ def __init__(self, args):
+ """Construct class."""
+ self._args = args
+
+ @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)
+ else:
+ 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,
+ )
diff --git a/contrib/python/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/lib/actions/__init__.py
new file mode 100644
index 000000000..cdc58b6ab
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/__init__.py
@@ -0,0 +1,7 @@
+"""Module to export all the podman subcommands."""
+from .images_action import Images
+from .ps_action import Ps
+from .rm_action import Rm
+from .rmi_action import Rmi
+
+__all__ = ['Images', 'Ps', 'Rm', 'Rmi']
diff --git a/contrib/python/pypodman/lib/actions/images_action.py b/contrib/python/pypodman/lib/actions/images_action.py
new file mode 100644
index 000000000..f6a7497e5
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/images_action.py
@@ -0,0 +1,88 @@
+"""Remote client commands dealing with images."""
+import operator
+from collections import OrderedDict
+
+import humanize
+import podman
+
+from .. 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)'))
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ '--digests',
+ action='store_true',
+ help='Include digests with images. (default: %(default)s)')
+ parser.set_defaults(class_=cls, method='list')
+
+ def __init__(self, args):
+ """Construct Images class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'name':
+ ReportColumn('name', 'REPOSITORY', 40),
+ '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 len(images) == 0:
+ return 0
+
+ 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.split(':', 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)
diff --git a/contrib/python/pypodman/lib/actions/ps_action.py b/contrib/python/pypodman/lib/actions/ps_action.py
new file mode 100644
index 000000000..4bbec5578
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/ps_action.py
@@ -0,0 +1,76 @@
+"""Remote client commands dealing with containers."""
+import operator
+from collections import OrderedDict
+
+import humanize
+import podman
+
+from .. 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', 14),
+ 'image':
+ ReportColumn('image', 'IMAGE', 30),
+ 'command':
+ ReportColumn('column', 'COMMAND', 20),
+ 'createdat':
+ ReportColumn('createdat', 'CREATED', 12),
+ 'status':
+ ReportColumn('status', 'STATUS', 10),
+ 'ports':
+ ReportColumn('ports', 'PORTS', 28),
+ 'names':
+ ReportColumn('names', 'NAMES', 18)
+ })
+
+ def list(self):
+ """List containers."""
+ # TODO: Verify sorting on dates and size
+ ctnrs = sorted(
+ self.client.containers.list(),
+ key=operator.attrgetter(self._args.sort))
+ if len(ctnrs) == 0:
+ return 0
+
+ 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)
diff --git a/contrib/python/pypodman/lib/actions/rm_action.py b/contrib/python/pypodman/lib/actions/rm_action.py
new file mode 100644
index 000000000..bd8950bd6
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/rm_action.py
@@ -0,0 +1,51 @@
+"""Remote client command for deleting containers."""
+import sys
+
+import podman
+
+from .. 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_argument(
+ '-f',
+ '--force',
+ action='store_true',
+ help=('force delete of running container(s).'
+ ' (default: %(default)s)'))
+ parser.add_argument(
+ 'targets', nargs='*', help='container id(s) to delete')
+ parser.set_defaults(class_=cls, method='remove')
+
+ def __init__(self, args):
+ """Construct Rm class."""
+ super().__init__(args)
+ if len(args.targets) < 1:
+ raise ValueError('You must supply at least one container id'
+ ' or name to be deleted.')
+
+ def remove(self):
+ """Remove container(s)."""
+ for id in self._args.targets:
+ try:
+ ctnr = self.client.containers.get(id)
+ ctnr.remove(self._args.force)
+ print(id)
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/lib/actions/rmi_action.py b/contrib/python/pypodman/lib/actions/rmi_action.py
new file mode 100644
index 000000000..91f0deeaf
--- /dev/null
+++ b/contrib/python/pypodman/lib/actions/rmi_action.py
@@ -0,0 +1,50 @@
+"""Remote client command for deleting images."""
+import sys
+
+import podman
+
+from .. import AbstractActionBase
+
+
+class Rmi(AbstractActionBase):
+ """Clas 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_argument(
+ '-f',
+ '--force',
+ action='store_true',
+ help=('force delete of image(s) and associated containers.'
+ ' (default: %(default)s)'))
+ parser.add_argument('targets', nargs='*', help='image id(s) to delete')
+ parser.set_defaults(class_=cls, method='remove')
+
+ def __init__(self, args):
+ """Construct Rmi class."""
+ super().__init__(args)
+ if len(args.targets) < 1:
+ raise ValueError('You must supply at least one image id'
+ ' or name to be deleted.')
+
+ def remove(self):
+ """Remove image(s)."""
+ for id in self._args.targets:
+ try:
+ img = self.client.images.get(id)
+ img.remove(self._args.force)
+ print(id)
+ except podman.ImageNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Image {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
diff --git a/contrib/python/pypodman/lib/config.py b/contrib/python/pypodman/lib/config.py
new file mode 100644
index 000000000..e687697ef
--- /dev/null
+++ b/contrib/python/pypodman/lib/config.py
@@ -0,0 +1,212 @@
+import argparse
+import curses
+import getpass
+import inspect
+import logging
+import os
+import sys
+
+import pkg_resources
+
+import pytoml
+
+# TODO: setup.py and obtain __version__ from rpm.spec
+try:
+ __version__ = pkg_resources.get_distribution('pypodman').version
+except Exception:
+ __version__ = '0.0.0'
+
+
+class HelpFormatter(argparse.RawDescriptionHelpFormatter):
+ """Set help width to screen size."""
+
+ def __init__(self, *args, **kwargs):
+ """Construct HelpFormatter using screen width."""
+ if 'width' not in kwargs:
+ kwargs['width'] = 80
+ try:
+ height, width = curses.initscr().getmaxyx()
+ kwargs['width'] = width
+ finally:
+ curses.endwin()
+ super().__init__(*args, **kwargs)
+
+
+class PodmanArgumentParser(argparse.ArgumentParser):
+ """Default remote podman configuration."""
+
+ def __init__(self, **kwargs):
+ """Construct the parser."""
+ kwargs['add_help'] = True
+ kwargs['allow_abbrev'] = True
+ kwargs['description'] = __doc__
+ kwargs['formatter_class'] = HelpFormatter
+
+ super().__init__(**kwargs)
+
+ def initialize_parser(self):
+ """Initialize parser without causing recursion meltdown."""
+ self.add_argument(
+ '--version',
+ action='version',
+ version='%(prog)s v. ' + __version__)
+ self.add_argument(
+ '--log-level',
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
+ default='WARNING',
+ type=str.upper,
+ help='set logging level for events. (default: %(default)s)',
+ )
+ self.add_argument(
+ '--run-dir',
+ metavar='DIRECTORY',
+ help=('directory to place local socket bindings.'
+ ' (default: XDG_RUNTIME_DIR/pypodman'))
+ self.add_argument(
+ '--user',
+ default=getpass.getuser(),
+ help='Authenicating user on remote host. (default: %(default)s)')
+ self.add_argument(
+ '--host', help='name of remote host. (default: None)')
+ self.add_argument(
+ '--remote-socket-path',
+ metavar='PATH',
+ help=('path of podman socket on remote host'
+ ' (default: /run/podman/io.projectatomic.podman)'))
+ self.add_argument(
+ '--identity-file',
+ metavar='PATH',
+ help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
+ self.add_argument(
+ '--config-home',
+ metavar='DIRECTORY',
+ help=('home of configuration "pypodman.conf".'
+ ' (default: XDG_CONFIG_HOME/pypodman'))
+
+ actions_parser = self.add_subparsers(
+ dest='subparser_name', help='actions')
+
+ # pull in plugin(s) code for each subcommand
+ for name, obj in inspect.getmembers(
+ sys.modules['lib.actions'],
+ lambda member: inspect.isclass(member)):
+ if hasattr(obj, 'subparser'):
+ try:
+ obj.subparser(actions_parser)
+ except NameError as e:
+ logging.critical(e)
+ logging.warning(
+ 'See subparser configuration for Class "{}"'.format(
+ 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
+ try:
+ with open(os.path.join(dir_, 'pypodman/pypodman.conf'),
+ 'r') as stream:
+ config.update(pytoml.load(stream))
+ except OSError:
+ pass
+
+ def reqattr(name, value):
+ if value:
+ setattr(args, name, value)
+ return value
+ self.error('Required argument "%s" is not configured.' % name)
+
+ reqattr(
+ 'run_dir',
+ getattr(args, 'run_dir')
+ or os.environ.get('RUN_DIR')
+ or config['default'].get('run_dir')
+ or os.path.join(args.xdg_runtime_dir, 'pypodman')
+ ) # yapf: disable
+
+ setattr(
+ args,
+ 'host',
+ getattr(args, 'host')
+ or os.environ.get('HOST')
+ or config['default'].get('host')
+ ) # yapf:disable
+
+ reqattr(
+ 'user',
+ getattr(args, 'user')
+ or os.environ.get('USER')
+ or config['default'].get('user')
+ or getpass.getuser()
+ ) # yapf:disable
+
+ reqattr(
+ 'remote_socket_path',
+ getattr(args, 'remote_socket_path')
+ or os.environ.get('REMOTE_SOCKET_PATH')
+ or config['default'].get('remote_socket_path')
+ or '/run/podman/io.projectatomic.podman'
+ ) # yapf:disable
+
+ reqattr(
+ 'log_level',
+ getattr(args, 'log_level')
+ or os.environ.get('LOG_LEVEL')
+ or config['default'].get('log_level')
+ or logging.WARNING
+ ) # yapf:disable
+
+ setattr(
+ args,
+ 'identity_file',
+ getattr(args, 'identity_file')
+ or os.environ.get('IDENTITY_FILE')
+ or config['default'].get('identity_file')
+ or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.user))
+ ) # yapf:disable
+
+ if not os.path.isfile(args.identity_file):
+ args.identity_file = None
+
+ if args.host:
+ args.local_socket_path = os.path.join(args.run_dir,
+ "podman.socket")
+ else:
+ args.local_socket_path = args.remote_socket_path
+
+ args.local_uri = "unix:{}".format(args.local_socket_path)
+ args.remote_uri = "ssh://{}@{}{}".format(args.user, args.host,
+ args.remote_socket_path)
+ 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('{}: {}'.format(self.prog, message))
+ logging.error("Try '{} --help' for more information.".format(
+ self.prog))
+ super().exit(2)
diff --git a/contrib/python/pypodman/lib/future_abstract.py b/contrib/python/pypodman/lib/future_abstract.py
new file mode 100644
index 000000000..75a1d42db
--- /dev/null
+++ b/contrib/python/pypodman/lib/future_abstract.py
@@ -0,0 +1,29 @@
+"""Utilities for with-statement contexts. See PEP 343."""
+
+import abc
+
+import _collections_abc
+
+try:
+ from contextlib import AbstractContextManager
+except ImportError:
+ # Copied from python3.7 library as "backport"
+ class AbstractContextManager(abc.ABC):
+ """An abstract base class for context managers."""
+
+ def __enter__(self):
+ """Return `self` upon entering the runtime context."""
+ return self
+
+ @abc.abstractmethod
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Raise any exception triggered within the runtime context."""
+ return None
+
+ @classmethod
+ def __subclasshook__(cls, C):
+ """Check whether subclass is considered a subclass of this ABC."""
+ if cls is AbstractContextManager:
+ return _collections_abc._check_methods(C, "__enter__",
+ "__exit__")
+ return NotImplemented
diff --git a/contrib/python/pypodman/lib/pypodman.py b/contrib/python/pypodman/lib/pypodman.py
new file mode 100755
index 000000000..4bc71a9cc
--- /dev/null
+++ b/contrib/python/pypodman/lib/pypodman.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Remote podman client."""
+
+import logging
+import os
+import sys
+
+import lib.actions
+from lib import PodmanArgumentParser
+
+assert lib.actions # silence pyflakes
+
+
+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 {}'.format(
+ 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 Exception as e:
+ logging.critical(repr(e), exc_info=want_tb())
+ logging.warning('See subparser "{}" configuration.'.format(
+ args.subparser_name))
+ sys.exit(5)
+
+ try:
+ returncode = getattr(obj, args.method)()
+ except AttributeError as e:
+ logging.critical(e, exc_info=want_tb())
+ logging.warning('See subparser "{}" configuration.'.format(
+ args.subparser_name))
+ returncode = 3
+ except KeyboardInterrupt:
+ pass
+ except (
+ 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())
diff --git a/contrib/python/pypodman/lib/report.py b/contrib/python/pypodman/lib/report.py
new file mode 100644
index 000000000..25fe2ae0d
--- /dev/null
+++ b/contrib/python/pypodman/lib/report.py
@@ -0,0 +1,67 @@
+"""Report Manager."""
+import sys
+from collections import namedtuple
+
+from .future_abstract import AbstractContextManager
+
+
+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(AbstractContextManager):
+ """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._heading = heading
+ self.epilog = epilog
+ self._format = None
+
+ def row(self, **fields):
+ """Print row for report."""
+ if self._heading:
+ hdrs = {k: v.display for (k, v) in self._columns.items()}
+ print(self._format.format(**hdrs), flush=True, file=self._file)
+ self._heading = False
+ fields = {k: str(v) for k, v in fields.items()}
+ print(self._format.format(**fields))
+
+ 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."""
+ format = []
+
+ for key in keys:
+ value = max(map(lambda x: len(str(x.get(key, ''))), iterable))
+ # print('key', key, 'value', value)
+
+ if truncate:
+ row = self._columns.get(
+ key, ReportColumn(key, key.upper(), len(key)))
+ if value < row.width:
+ step = row.width if value == 0 else value
+ value = max(len(key), step)
+ elif value > row.width:
+ value = row.width if row.width != 0 else value
+
+ format.append('{{{0}:{1}.{1}}}'.format(key, value))
+ self._format = ' '.join(format)
diff --git a/contrib/python/pypodman/requirements.txt b/contrib/python/pypodman/requirements.txt
new file mode 100644
index 000000000..f9cd4f904
--- /dev/null
+++ b/contrib/python/pypodman/requirements.txt
@@ -0,0 +1,4 @@
+humanize
+podman
+pytoml
+setuptools>=39.2.0
diff --git a/contrib/python/pypodman/setup.py b/contrib/python/pypodman/setup.py
new file mode 100644
index 000000000..0483eb71c
--- /dev/null
+++ b/contrib/python/pypodman/setup.py
@@ -0,0 +1,44 @@
+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()
+
+print(find_packages(where='pypodman', exclude=['test']))
+
+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 = lib.pypodman:main',
+ ]},
+ include_package_data=True,
+ install_requires=requirements,
+ keywords='varlink libpod podman pypodman',
+ packages=find_packages(exclude=['test']),
+ python_requires='>=3',
+ zip_safe=True,
+ url='http://github.com/projectatomic/libpod',
+ 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.6',
+ 'Topic :: System :: Systems Administration',
+ 'Topic :: Utilities',
+ ])
diff --git a/contrib/python/pypodman/test/test_report.py b/contrib/python/pypodman/test/test_report.py
new file mode 100644
index 000000000..280a9a954
--- /dev/null
+++ b/contrib/python/pypodman/test/test_report.py
@@ -0,0 +1,23 @@
+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')
diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in
index 7a5c1be85..9d8acce23 100644
--- a/contrib/spec/podman.spec.in
+++ b/contrib/spec/podman.spec.in
@@ -209,6 +209,22 @@ Summary: Python 3 bindings for %{name}
%description -n python3-%{name}
This package contains Python 3 bindings for %{name}.
+
+%package -n python3-py%{name}
+BuildArch: noarch
+BuildRequires: python3-devel
+BuildRequires: python3-setuptools
+BuildRequires: python3-varlink
+
+Requires: python3-setuptools
+Requires: python3-varlink
+Requires: python3-dateutil
+
+Provides: python3-py%{name} = %{version}-%{release}
+Summary: Python 3 tool for %{name}
+
+%description -n python3-py%{name}
+This package contains Python 3 tool for %{name}.
%endif # varlink
%if 0%{?with_devel}
@@ -389,7 +405,12 @@ BUILDTAGS=$BUILDTAGS make binaries docs
%if %{with varlink}
#untar contents for python-podman
-pushd contrib/python/dist
+pushd contrib/python/podman/dist
+tar zxf %{name}*.tar.gz
+popd
+
+#untar contents for python-pypodman
+pushd contrib/python/pypodman/dist
tar zxf %{name}*.tar.gz
popd
%endif #varlink
@@ -400,7 +421,12 @@ install -dp %{buildroot}%{_unitdir}
%if %{with varlink}
#install python-podman
-pushd contrib/python
+pushd contrib/python/podman
+%{__python3} setup.py install --root %{buildroot}
+popd
+
+#install python-pypodman
+pushd contrib/python/pypodman
%{__python3} setup.py install --root %{buildroot}
popd
%endif #varlink