diff options
author | Matthew Heon <matthew.heon@gmail.com> | 2018-07-13 16:34:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-13 16:34:51 -0400 |
commit | a689639a6502bab3f49b853bc2983c1b44363b2f (patch) | |
tree | 75ba256d70545d79aa61d7c57c20df886be1555f | |
parent | 14a6d51a8432fc0c3324fec02e8729d3032f2af2 (diff) | |
parent | 74ccd9ce5f29a1df4ffe70b4d8bd00c29d5d9d15 (diff) | |
download | podman-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-- | Makefile | 18 | ||||
-rw-r--r-- | contrib/python/cmd/images.py | 21 | ||||
-rw-r--r-- | contrib/python/cmd/pman.py | 42 | ||||
-rw-r--r-- | contrib/python/cmd/ps.py | 19 | ||||
-rw-r--r-- | contrib/python/cmd/remote_client.py | 136 | ||||
-rw-r--r-- | contrib/python/cmd/rm.py | 22 | ||||
-rw-r--r-- | contrib/python/cmd/rmi.py | 25 | ||||
-rw-r--r-- | contrib/python/cmd/utils.py | 32 | ||||
-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-x | contrib/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-x | contrib/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.in | 1 | ||||
-rw-r--r-- | contrib/python/pypodman/Makefile | 21 | ||||
-rw-r--r-- | contrib/python/pypodman/README.md | 32 | ||||
-rw-r--r-- | contrib/python/pypodman/docs/pypodman.1.md | 82 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/__init__.py | 11 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/action_base.py | 84 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/actions/__init__.py | 7 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/actions/images_action.py | 88 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/actions/ps_action.py | 76 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/actions/rm_action.py | 51 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/actions/rmi_action.py | 50 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/config.py | 212 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/future_abstract.py | 29 | ||||
-rwxr-xr-x | contrib/python/pypodman/lib/pypodman.py | 76 | ||||
-rw-r--r-- | contrib/python/pypodman/lib/report.py | 67 | ||||
-rw-r--r-- | contrib/python/pypodman/requirements.txt | 4 | ||||
-rw-r--r-- | contrib/python/pypodman/setup.py | 44 | ||||
-rw-r--r-- | contrib/python/pypodman/test/test_report.py | 23 | ||||
-rw-r--r-- | contrib/spec/podman.spec.in | 30 |
60 files changed, 1056 insertions, 348 deletions
@@ -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 |