From 8245f0942811c2954d257ad8fac6cca8d36e896c Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Tue, 28 Aug 2018 10:00:19 -0700 Subject: Add support for remote commands * Add support for commit, export, inspect, kill, logs, mount, pause port commands * Refactored Report class to allow column lengths to be optionally driven by data * Refactored Ps class to truncate image names on the left vs right * Bug fixes Signed-off-by: Jhon Honce Closes: #1369 Approved by: rhatdan --- .papr.yml | 6 +- contrib/python/podman/podman/libs/containers.py | 2 +- contrib/python/pypodman/pypodman/lib/__init__.py | 2 +- .../python/pypodman/pypodman/lib/action_base.py | 3 +- .../pypodman/pypodman/lib/actions/__init__.py | 16 ++++ .../pypodman/pypodman/lib/actions/commit_action.py | 102 +++++++++++++++++++++ .../pypodman/pypodman/lib/actions/export_action.py | 60 ++++++++++++ .../pypodman/lib/actions/inspect_action.py | 90 ++++++++++++++++++ .../pypodman/pypodman/lib/actions/kill_action.py | 55 +++++++++++ .../pypodman/pypodman/lib/actions/logs_action.py | 75 +++++++++++++++ .../pypodman/pypodman/lib/actions/mount_action.py | 78 ++++++++++++++++ .../pypodman/pypodman/lib/actions/pause_action.py | 47 ++++++++++ .../pypodman/pypodman/lib/actions/port_action.py | 63 +++++++++++++ .../pypodman/pypodman/lib/actions/ps_action.py | 7 +- contrib/python/pypodman/pypodman/lib/report.py | 19 ++-- 15 files changed, 605 insertions(+), 20 deletions(-) create mode 100644 contrib/python/pypodman/pypodman/lib/actions/commit_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/export_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/inspect_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/kill_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/logs_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/mount_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/pause_action.py create mode 100644 contrib/python/pypodman/pypodman/lib/actions/port_action.py diff --git a/.papr.yml b/.papr.yml index 0348aa1a3..202659621 100644 --- a/.papr.yml +++ b/.papr.yml @@ -9,7 +9,7 @@ host: ram: 8192 cpus: 4 required: true -timeout: 90m +timeout: 120m tests: - CONTAINER_RUNTIME="podman" sh .papr_prepare.sh @@ -35,7 +35,7 @@ extra-repos: required: true -timeout: 90m +timeout: 120m tests: - sh .papr_prepare.sh @@ -63,7 +63,7 @@ tests: - CONTAINER_RUNTIME="podman" sh .papr_prepare.sh required: false -timeout: 90m +timeout: 120m context: "Fedora fedora/28/cloud Podman" --- diff --git a/contrib/python/podman/podman/libs/containers.py b/contrib/python/podman/podman/libs/containers.py index 88d2aa137..f56137e12 100644 --- a/contrib/python/podman/podman/libs/containers.py +++ b/contrib/python/podman/podman/libs/containers.py @@ -214,7 +214,7 @@ class Container(AttachMixin, StartMixin, collections.UserDict): """Retrieve container logs.""" with self._client() as podman: results = podman.GetContainerLogs(self._id) - yield from results + yield from results['container'] class Containers(): diff --git a/contrib/python/pypodman/pypodman/lib/__init__.py b/contrib/python/pypodman/pypodman/lib/__init__.py index e3654dc2b..80fa0e1e9 100644 --- a/contrib/python/pypodman/pypodman/lib/__init__.py +++ b/contrib/python/pypodman/pypodman/lib/__init__.py @@ -1,7 +1,7 @@ """Remote podman client support library.""" from pypodman.lib.action_base import AbstractActionBase from pypodman.lib.config import PodmanArgumentParser -from pypodman.lib.report import (Report, ReportColumn) +from pypodman.lib.report import Report, ReportColumn __all__ = [ 'AbstractActionBase', diff --git a/contrib/python/pypodman/pypodman/lib/action_base.py b/contrib/python/pypodman/pypodman/lib/action_base.py index 8b86c02df..5cc4c22a9 100644 --- a/contrib/python/pypodman/pypodman/lib/action_base.py +++ b/contrib/python/pypodman/pypodman/lib/action_base.py @@ -65,8 +65,7 @@ class AbstractActionBase(abc.ABC): def client(self): """Podman remote client for communicating.""" if self._args.host is None: - return podman.Client( - uri=self.local_uri) + return podman.Client(uri=self.local_uri) return podman.Client( uri=self.local_uri, remote_uri=self.remote_uri, diff --git a/contrib/python/pypodman/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/pypodman/lib/actions/__init__.py index 098f033e9..b0af3c589 100644 --- a/contrib/python/pypodman/pypodman/lib/actions/__init__.py +++ b/contrib/python/pypodman/pypodman/lib/actions/__init__.py @@ -1,7 +1,15 @@ """Module to export all the podman subcommands.""" from pypodman.lib.actions.attach_action import Attach +from pypodman.lib.actions.commit_action import Commit from pypodman.lib.actions.create_action import Create +from pypodman.lib.actions.export_action import Export from pypodman.lib.actions.images_action import Images +from pypodman.lib.actions.inspect_action import Inspect +from pypodman.lib.actions.kill_action import Kill +from pypodman.lib.actions.logs_action import Logs +from pypodman.lib.actions.mount_action import Mount +from pypodman.lib.actions.pause_action import Pause +from pypodman.lib.actions.port_action import Port from pypodman.lib.actions.ps_action import Ps from pypodman.lib.actions.pull_action import Pull from pypodman.lib.actions.rm_action import Rm @@ -9,8 +17,16 @@ from pypodman.lib.actions.rmi_action import Rmi __all__ = [ 'Attach', + 'Commit', 'Create', + 'Export', 'Images', + 'Inspect', + 'Kill', + 'Logs', + 'Mount', + 'Pause', + 'Port', 'Ps', 'Pull', 'Rm', diff --git a/contrib/python/pypodman/pypodman/lib/actions/commit_action.py b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py new file mode 100644 index 000000000..1e16550ad --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py @@ -0,0 +1,102 @@ +"""Remote client command for creating image from container.""" +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Commit(AbstractActionBase): + """Class for creating image from container.""" + + @classmethod + def subparser(cls, parent): + """Add Commit command to parent parser.""" + parser = parent.add_parser( + 'commit', help='create image from container') + parser.add_argument( + '--author', + help='Set the author for the committed image', + ) + parser.add_argument( + '--change', + '-c', + choices=('CMD', 'ENTRYPOINT', 'ENV', 'EXPOSE', 'LABEL', 'ONBUILD', + 'STOPSIGNAL', 'USER', 'VOLUME', 'WORKDIR'), + action='append', + type=str.upper, + help='Apply the following possible changes to the created image', + ) + parser.add_argument( + '--format', + '-f', + choices=('oci', 'docker'), + default='oci', + type=str.lower, + help='Set the format of the image manifest and metadata', + ) + parser.add_argument( + '--iidfile', + metavar='PATH', + help='Write the image ID to the file', + ) + parser.add_argument( + '--message', + '-m', + help='Set commit message for committed image', + ) + parser.add_argument( + '--pause', + '-p', + choices=('True', 'False'), + default=True, + type=bool, + help='Pause the container when creating an image', + ) + parser.add_argument( + '--quiet', + '-q', + help='Suppress output', + ) + parser.add_argument( + 'container', + nargs=1, + help='container to use as source', + ) + parser.add_argument( + 'image', + nargs=1, + help='image name to create', + ) + parser.set_defaults(class_=cls, method='commit') + + def __init__(self, args): + """Construct Commit class.""" + super().__init__(args) + if not args.container: + raise ValueError('You must supply one container id' + ' or name to be used as source.') + if not args.image: + raise ValueError('You must supply one image id' + ' or name to be created.') + + def commit(self): + """Create image from container.""" + try: + try: + ctnr = self.client.containers.get(self._args.container[0]) + ident = ctnr.commit(**self._args) + print(ident) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container {} not found.'.format(e.name), + file=sys.stderr, + flush=True) + return 1 + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/export_action.py b/contrib/python/pypodman/pypodman/lib/actions/export_action.py new file mode 100644 index 000000000..2a6c2a3cf --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/export_action.py @@ -0,0 +1,60 @@ +"""Remote client command for export container filesystem to tarball.""" +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Export(AbstractActionBase): + """Class for exporting container filesystem to tarball.""" + + @classmethod + def subparser(cls, parent): + """Add Export command to parent parser.""" + parser = parent.add_parser( + 'export', help='export container to tarball') + parser.add_argument( + '--output', + '-o', + metavar='PATH', + nargs=1, + help='Write to a file', + ) + parser.add_argument( + 'container', + nargs=1, + help='container to use as source', + ) + parser.set_defaults(class_=cls, method='export') + + def __init__(self, args): + """Construct Export class.""" + super().__init__(args) + if not args.container: + raise ValueError('You must supply one container id' + ' or name to be used as source.') + + if not args.output: + raise ValueError('You must supply one filename' + ' to be created as tarball using --output.') + + def export(self): + """Create tarball from container filesystem.""" + try: + try: + ctnr = self.client.containers.get(self._args.container[0]) + ctnr.export(self._args.output[0]) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container {} not found.'.format(e.name), + file=sys.stderr, + flush=True) + return 1 + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py new file mode 100644 index 000000000..0559cd40a --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py @@ -0,0 +1,90 @@ +"""Remote client command for inspecting podman objects.""" +import json +import logging +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Inspect(AbstractActionBase): + """Class for inspecting podman objects.""" + + @classmethod + def subparser(cls, parent): + """Add Inspect command to parent parser.""" + parser = parent.add_parser('inspect', help='inspect objects') + parser.add_argument( + '--type', + '-t', + choices=('all', 'container', 'image'), + default='all', + type=str.lower, + help='Type of object to inspect', + ) + parser.add_argument( + 'size', + action='store_true', + default=True, + help='Display the total file size if the type is a container.' + ' Always True.') + parser.add_argument( + 'objects', + nargs='+', + help='objects to inspect', + ) + parser.set_defaults(class_=cls, method='inspect') + + def __init__(self, args): + """Construct Inspect class.""" + super().__init__(args) + + def _get_container(self, ident): + try: + logging.debug("Get container %s", ident) + ctnr = self.client.containers.get(ident) + except podman.ContainerNotFound: + pass + else: + return ctnr.inspect() + + def _get_image(self, ident): + try: + logging.debug("Get image %s", ident) + img = self.client.images.get(ident) + except podman.ImageNotFound: + pass + else: + return img.inspect() + + def inspect(self): + """Inspect provided podman objects.""" + output = {} + try: + for ident in self._args.objects: + obj = None + + if self._args.type in ('all', 'container'): + obj = self._get_container(ident) + if obj is None and self._args.type in ('all', 'image'): + obj = self._get_image(ident) + + if obj is None: + if self._args.type == 'container': + msg = 'Container "{}" not found'.format(ident) + elif self._args.type == 'image': + msg = 'Image "{}" not found'.format(ident) + else: + msg = 'Object "{}" not found'.format(ident) + print(msg, file=sys.stderr, flush=True) + else: + output.update(obj._asdict()) + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 + else: + print(json.dumps(output, indent=2)) diff --git a/contrib/python/pypodman/pypodman/lib/actions/kill_action.py b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py new file mode 100644 index 000000000..3caa42cf0 --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py @@ -0,0 +1,55 @@ +"""Remote client command for signaling podman containers.""" +import signal +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Kill(AbstractActionBase): + """Class for sending signal to main process in container.""" + + @classmethod + def subparser(cls, parent): + """Add Kill command to parent parser.""" + parser = parent.add_parser('kill', help='signal container') + parser.add_argument( + '--signal', + '-s', + choices=range(1, signal.NSIG), + metavar='[1,{}]'.format(signal.NSIG), + default=9, + help='Signal to send to the container. (Default: 9)') + parser.add_argument( + 'containers', + nargs='+', + help='containers to signal', + ) + parser.set_defaults(class_=cls, method='kill') + + def __init__(self, args): + """Construct Kill class.""" + super().__init__(args) + + def kill(self): + """Signal provided containers.""" + try: + for ident in self._args.containers: + try: + ctnr = self.client.containers.get(ident) + ctnr.kill(self._args.signal) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container "{}" not found'.format(e.name), + file=sys.stderr, + flush=True) + else: + print(ident) + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/logs_action.py b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py new file mode 100644 index 000000000..764a4b9c7 --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py @@ -0,0 +1,75 @@ +"""Remote client command for retrieving container logs.""" +import argparse +import logging +import sys +from collections import deque + +import podman +from pypodman.lib import AbstractActionBase + + +class PositiveIntAction(argparse.Action): + """Validate number given is positive integer.""" + + def __call__(self, parser, namespace, values, option_string=None): + """Validate input.""" + if values > 0: + setattr(namespace, self.dest, values) + return + + msg = 'Must be a positive integer.' + raise argparse.ArgumentError(self, msg) + + +class Logs(AbstractActionBase): + """Class for retrieving logs from container.""" + + @classmethod + def subparser(cls, parent): + """Add Logs command to parent parser.""" + parser = parent.add_parser('logs', help='retrieve logs from container') + parser.add_argument( + '--tail', + metavar='LINES', + action=PositiveIntAction, + type=int, + help='Output the specified number of LINES at the end of the logs') + parser.add_argument( + 'container', + nargs=1, + help='retrieve container logs', + ) + parser.set_defaults(class_=cls, method='logs') + + def __init__(self, args): + """Construct Logs class.""" + super().__init__(args) + + def logs(self): + """Retrieve logs from containers.""" + try: + ident = self._args.container[0] + try: + logging.debug('Get container "%s" logs', ident) + ctnr = self.client.containers.get(ident) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container "{}" not found'.format(e.name), + file=sys.stderr, + flush=True) + else: + if self._args.tail: + logs = iter(deque(ctnr.logs(), maxlen=self._args.tail)) + else: + logs = ctnr.logs() + + for line in logs: + sys.stdout.write(line) + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/mount_action.py b/contrib/python/pypodman/pypodman/lib/actions/mount_action.py new file mode 100644 index 000000000..905eda6da --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/mount_action.py @@ -0,0 +1,78 @@ +"""Remote client command for retrieving mounts from containers.""" +import sys +from collections import OrderedDict + +import podman +from pypodman.lib import AbstractActionBase, Report, ReportColumn + + +class Mount(AbstractActionBase): + """Class for retrieving mounts from container.""" + + @classmethod + def subparser(cls, parent): + """Add mount command to parent parser.""" + parser = parent.add_parser( + 'mount', help='retrieve mounts from containers.') + super().subparser(parser) + parser.add_argument( + 'containers', + nargs='*', + help='containers to list ports', + ) + parser.set_defaults(class_=cls, method='mount') + + def __init__(self, args): + """Construct Mount class.""" + super().__init__(args) + + self.columns = OrderedDict({ + 'id': + ReportColumn('id', 'CONTAINER ID', 14), + 'destination': + ReportColumn('destination', 'DESTINATION', 0) + }) + + def mount(self): + """Retrieve mounts from containers.""" + try: + ctnrs = [] + if not self._args.containers: + ctnrs = self.client.containers.list() + else: + for ident in self._args.containers: + try: + ctnrs.append(self.client.containers.get(ident)) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container "{}" not found'.format(e.name), + file=sys.stderr, + flush=True) + + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 + + if not ctnrs: + print( + 'Unable to find any containers.', file=sys.stderr, flush=True) + return 1 + + rows = list() + for ctnr in ctnrs: + details = ctnr.inspect() + rows.append({ + 'id': ctnr.id, + 'destination': details.graphdriver['data']['mergeddir'] + }) + + with Report(self.columns, heading=self._args.heading) as report: + report.layout( + rows, self.columns.keys(), truncate=self._args.truncate) + for row in rows: + report.row(**row) diff --git a/contrib/python/pypodman/pypodman/lib/actions/pause_action.py b/contrib/python/pypodman/pypodman/lib/actions/pause_action.py new file mode 100644 index 000000000..ab64d8b81 --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/pause_action.py @@ -0,0 +1,47 @@ +"""Remote client command for pausing processes in containers.""" +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Pause(AbstractActionBase): + """Class for pausing processes in container.""" + + @classmethod + def subparser(cls, parent): + """Add Pause command to parent parser.""" + parser = parent.add_parser('pause', help='pause container processes') + parser.add_argument( + 'containers', + nargs='+', + help='containers to pause', + ) + parser.set_defaults(class_=cls, method='pause') + + def __init__(self, args): + """Construct Pause class.""" + super().__init__(args) + + def pause(self): + """Pause provided containers.""" + try: + for ident in self._args.containers: + try: + ctnr = self.client.containers.get(ident) + ctnr.pause() + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container "{}" not found'.format(e.name), + file=sys.stderr, + flush=True) + else: + print(ident) + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/port_action.py b/contrib/python/pypodman/pypodman/lib/actions/port_action.py new file mode 100644 index 000000000..60bbe12c4 --- /dev/null +++ b/contrib/python/pypodman/pypodman/lib/actions/port_action.py @@ -0,0 +1,63 @@ +"""Remote client command for retrieving ports from containers.""" +import sys + +import podman +from pypodman.lib import AbstractActionBase + + +class Port(AbstractActionBase): + """Class for retrieving ports from container.""" + + @classmethod + def subparser(cls, parent): + """Add Port command to parent parser.""" + parser = parent.add_parser( + 'port', help='retrieve ports from containers.') + parser.add_argument( + '--all', + '-a', + action='store_true', + default=False, + help='List all known port mappings for running containers') + parser.add_argument( + 'containers', + nargs='*', + default=None, + help='containers to list ports', + ) + parser.set_defaults(class_=cls, method='port') + + def __init__(self, args): + """Construct Port class.""" + super().__init__(args) + if not args.all and not args.containers: + ValueError('You must supply at least one' + ' container id or name, or --all.') + + def port(self): + """Retrieve ports from containers.""" + try: + ctnrs = [] + if self._args.all: + ctnrs = self.client.containers.list() + else: + for ident in self._args.containers: + try: + ctnrs.append(self.client.containers.get(ident)) + except podman.ContainerNotFound as e: + sys.stdout.flush() + print( + 'Container "{}" not found'.format(e.name), + file=sys.stderr, + flush=True) + + for ctnr in ctnrs: + print("{}\n{}".format(ctnr.id, ctnr.ports)) + + except podman.ErrorOccurred as e: + sys.stdout.flush() + print( + '{}'.format(e.reason).capitalize(), + file=sys.stderr, + flush=True) + return 1 diff --git a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py index 83954479c..8c0a739d7 100644 --- a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py +++ b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py @@ -3,8 +3,8 @@ import operator from collections import OrderedDict import humanize -import podman +import podman from pypodman.lib import AbstractActionBase, Report, ReportColumn @@ -44,7 +44,7 @@ class Ps(AbstractActionBase): 'status': ReportColumn('status', 'STATUS', 10), 'ports': - ReportColumn('ports', 'PORTS', 28), + ReportColumn('ports', 'PORTS', 0), 'names': ReportColumn('names', 'NAMES', 18) }) @@ -67,6 +67,9 @@ class Ps(AbstractActionBase): 'createdat': humanize.naturaldate(podman.datetime_parse(ctnr.createdat)), }) + + if self._args.truncate: + fields.update({'image': ctnr.image[-30:]}) rows.append(fields) with Report(self.columns, heading=self._args.heading) as report: diff --git a/contrib/python/pypodman/pypodman/lib/report.py b/contrib/python/pypodman/pypodman/lib/report.py index 0fa06f22c..a06e6055e 100644 --- a/contrib/python/pypodman/pypodman/lib/report.py +++ b/contrib/python/pypodman/pypodman/lib/report.py @@ -53,17 +53,14 @@ class Report(): fmt = [] for key in keys: - value = max(map(lambda x: len(str(x.get(key, ''))), iterable)) - # print('key', key, 'value', value) + slice_ = [i.get(key, '') for i in iterable] + data_len = len(max(slice_, key=len)) - 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 + info = self._columns.get(key, + ReportColumn(key, key.upper(), data_len)) + display_len = max(data_len, len(info.display)) + if truncate and info.width != 0: + display_len = info.width - fmt.append('{{{0}:{1}.{1}}}'.format(key, value)) + fmt.append('{{{0}:{1}.{1}}}'.format(key, display_len)) self._format = ' '.join(fmt) -- cgit v1.2.3-54-g00ecf