diff options
Diffstat (limited to 'contrib/python/cmd/lib')
-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 |
9 files changed, 453 insertions, 0 deletions
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) |