diff options
author | Jhon Honce <jhonce@redhat.com> | 2018-06-27 21:37:42 -0700 |
---|---|---|
committer | Jhon Honce <jhonce@redhat.com> | 2018-07-13 11:29:28 -0700 |
commit | 44b523c946c88e540b50d7ba59f441b5f8e0bad0 (patch) | |
tree | 744c09508b139c1aca3d7fc995fad7ff354e3667 /contrib/python | |
parent | 14a6d51a8432fc0c3324fec02e8729d3032f2af2 (diff) | |
download | podman-44b523c946c88e540b50d7ba59f441b5f8e0bad0.tar.gz podman-44b523c946c88e540b50d7ba59f441b5f8e0bad0.tar.bz2 podman-44b523c946c88e540b50d7ba59f441b5f8e0bad0.zip |
remote python client for podman
* Use podman library for access
* Verbose error checking
* Planned windows and macosx ports
Signed-off-by: Jhon Honce <jhonce@redhat.com>
Diffstat (limited to 'contrib/python')
-rw-r--r-- | contrib/python/cmd/images.py | 21 | ||||
-rw-r--r-- | contrib/python/cmd/lib/__init__.py | 5 | ||||
-rw-r--r-- | contrib/python/cmd/lib/action_base.py | 80 | ||||
-rw-r--r-- | contrib/python/cmd/lib/actions/__init__.py | 7 | ||||
-rw-r--r-- | contrib/python/cmd/lib/actions/images_action.py | 88 | ||||
-rw-r--r-- | contrib/python/cmd/lib/actions/ps_action.py | 76 | ||||
-rw-r--r-- | contrib/python/cmd/lib/actions/rm_action.py | 51 | ||||
-rw-r--r-- | contrib/python/cmd/lib/actions/rmi_action.py | 50 | ||||
-rw-r--r-- | contrib/python/cmd/lib/future_abstract.py | 29 | ||||
-rw-r--r-- | contrib/python/cmd/lib/report.py | 67 | ||||
-rw-r--r-- | contrib/python/cmd/pman.py | 42 | ||||
-rw-r--r-- | contrib/python/cmd/ps.py | 19 | ||||
-rwxr-xr-x | contrib/python/cmd/pydman.py | 248 | ||||
-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/libs/errors.py | 17 | ||||
-rw-r--r-- | contrib/python/podman/libs/tunnel.py | 18 |
19 files changed, 725 insertions, 308 deletions
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/lib/__init__.py b/contrib/python/cmd/lib/__init__.py new file mode 100644 index 000000000..db0f640b1 --- /dev/null +++ b/contrib/python/cmd/lib/__init__.py @@ -0,0 +1,5 @@ +"""Remote podman client support library.""" +from .action_base import AbstractActionBase +from .report import Report, ReportColumn + +__all__ = ['AbstractActionBase', 'Report', 'ReportColumn'] diff --git a/contrib/python/cmd/lib/action_base.py b/contrib/python/cmd/lib/action_base.py new file mode 100644 index 000000000..bafddea03 --- /dev/null +++ b/contrib/python/cmd/lib/action_base.py @@ -0,0 +1,80 @@ +"""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 "klass" and "method". These will + be invoked as klass(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.""" + 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/cmd/lib/actions/__init__.py b/contrib/python/cmd/lib/actions/__init__.py new file mode 100644 index 000000000..cdc58b6ab --- /dev/null +++ b/contrib/python/cmd/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/cmd/lib/actions/images_action.py b/contrib/python/cmd/lib/actions/images_action.py new file mode 100644 index 000000000..74c77edbb --- /dev/null +++ b/contrib/python/cmd/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(klass=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/cmd/lib/actions/ps_action.py b/contrib/python/cmd/lib/actions/ps_action.py new file mode 100644 index 000000000..9fc3a155b --- /dev/null +++ b/contrib/python/cmd/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(klass=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/cmd/lib/actions/rm_action.py b/contrib/python/cmd/lib/actions/rm_action.py new file mode 100644 index 000000000..7595fee6a --- /dev/null +++ b/contrib/python/cmd/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(klass=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/cmd/lib/actions/rmi_action.py b/contrib/python/cmd/lib/actions/rmi_action.py new file mode 100644 index 000000000..db59fe030 --- /dev/null +++ b/contrib/python/cmd/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(klass=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/cmd/lib/future_abstract.py b/contrib/python/cmd/lib/future_abstract.py new file mode 100644 index 000000000..75a1d42db --- /dev/null +++ b/contrib/python/cmd/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/cmd/lib/report.py b/contrib/python/cmd/lib/report.py new file mode 100644 index 000000000..25fe2ae0d --- /dev/null +++ b/contrib/python/cmd/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/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/pydman.py b/contrib/python/cmd/pydman.py new file mode 100755 index 000000000..5008c706d --- /dev/null +++ b/contrib/python/cmd/pydman.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Remote podman client.""" + +import argparse +import curses +import getpass +import inspect +import logging +import os +import sys + +import pkg_resources + +import lib.actions +import pytoml + +assert lib.actions # silence pyflakes + +# TODO: setup.py and obtain __version__ from rpm.spec +try: + __version__ = pkg_resources.get_distribution('pydman').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='INFO', + type=str.upper, + help='set logging level for events. (default: %(default)s)', + ) + self.add_argument( + '--run-dir', + help=('directory to place local socket bindings.' + ' (default: XDG_RUNTIME_DIR)')) + self.add_argument( + '--user', + help=('Authenicating user on remote host.' + ' (default: {})').format(getpass.getuser())) + self.add_argument( + '--host', help='name of remote host. (default: None)') + self.add_argument( + '--remote-socket-path', + help=('path of podman socket on remote host' + ' (default: /run/podman/io.projectatomic.podman)')) + self.add_argument( + '--identity-file', + help=('path to ssh identity file. (default: ~/.ssh/id_rsa)')) + self.add_argument( + '--config', + default='/etc/containers/podman_client.conf', + dest='config_file', + help='path of configuration file. (default: %(default)s)') + + actions_parser = self.add_subparsers( + dest='subparser_name', help='actions') + + 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.""" + try: + # Configuration file optionall, arguments may be provided elsewhere + with open(args.config_file, 'r') as stream: + config = pytoml.load(stream) + except OSError: + logging.info( + 'Failed to read: {}'.format(args.config_file), + exc_info=args.log_level == logging.DEBUG) + config = {'default': {}} + else: + if 'default' not in config: + config['default'] = {} + + def resolve(name, value): + if value: + setattr(args, name, value) + return value + self.error('Required argument "%s" is not configured.' % name) + + xdg = os.path.join(os.environ['XDG_RUNTIME_DIR'], 'podman') \ + if os.environ.get('XDG_RUNTIME_DIR') else None + + resolve( + 'run_dir', + getattr(args, 'run_dir') + or os.environ.get('RUN_DIR') + or config['default'].get('run_dir') + or xdg + or '/tmp/podman' if os.path.isdir('/tmp') else None + ) # yapf: disable + + args.local_socket_path = os.path.join(args.run_dir, "podman.socket") + + resolve( + 'host', + getattr(args, 'host') + or os.environ.get('HOST') + or config['default'].get('host') + ) # yapf:disable + + resolve( + 'user', + getattr(args, 'user') + or os.environ.get('USER') + or config['default'].get('user') + or getpass.getuser() + ) # yapf:disable + + resolve( + '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 + + resolve( + 'identity_file', + getattr(args, 'identity_file') + or os.environ.get('IDENTITY_FILE') + or config['default'].get('identity_file') + or os.path.expanduser('~{}/.ssh/id_rsa'.format(args.user)) + ) # yapf:disable + + 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) + + +if __name__ == '__main__': + # Setup logging so we use stderr and can change logging level later + # Do it now before there is any chance of a default setup. + 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.INFO) + + parser = PodmanArgumentParser() + args = parser.parse_args() + + log.setLevel(args.log_level) + logging.debug('Logging initialized at level {}'.format( + logging.getLevelName(logging.getLogger().getEffectiveLevel()))) + + def istraceback(): + """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=istraceback()) + sys.exit(6) + + # Klass(args).method() are setup by the sub-command's parser + returncode = None + try: + obj = args.klass(args) + except Exception as e: + logging.critical(repr(e), exc_info=istraceback()) + 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=istraceback()) + logging.warning('See subparser "{}" configuration.'.format( + args.subparser_name)) + returncode = 3 + except (ConnectionResetError, TimeoutError) as e: + logging.critical(e, exc_info=istraceback()) + logging.info('Review connection arguments for correctness.') + returncode = 4 + + sys.exit(0 if returncode is None else returncode) 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/podman/libs/errors.py b/contrib/python/podman/libs/errors.py index c28afd940..b98210481 100644 --- a/contrib/python/podman/libs/errors.py +++ b/contrib/python/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/tunnel.py b/contrib/python/podman/libs/tunnel.py index 42fd3356b..9effdff6c 100644 --- a/contrib/python/podman/libs/tunnel.py +++ b/contrib/python/podman/libs/tunnel.py @@ -1,5 +1,6 @@ """Cache for SSH tunnels.""" import collections +import logging import os import subprocess import threading @@ -96,9 +97,15 @@ class Tunnel(object): def bore(self, id): """Create SSH tunnel from given context.""" + ssh_opts = '-nNT' + if logging.getLogger().getEffectiveLevel() == logging.DEBUG: + ssh_opts += 'v' + else: + ssh_opts += 'q' + cmd = [ 'ssh', - '-nNTq', + ssh_opts, '-L', '{}:{}'.format(self.context.local_socket, self.context.remote_socket), @@ -106,15 +113,14 @@ class Tunnel(object): self.context.identity_file, 'ssh://{}@{}'.format(self.context.username, self.context.hostname), ] - - if os.environ.get('PODMAN_DEBUG'): - cmd.append('-vvv') + logging.debug('Tunnel cmd "{}"'.format(' '.join(cmd))) self._tunnel = subprocess.Popen(cmd, close_fds=True) - for i in range(5): + for i in range(10): + # 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))) |