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 <jhonce@redhat.com>

Closes: #1369
Approved by: rhatdan
This commit is contained in:
Jhon Honce 2018-08-28 10:00:19 -07:00 committed by Atomic Bot
parent de414c4354
commit 8245f09428
15 changed files with 605 additions and 20 deletions

View file

@ -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"
---

View file

@ -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():

View file

@ -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',

View file

@ -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,

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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)