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>
This commit is contained in:
Jhon Honce
2018-06-27 21:37:42 -07:00
parent 14a6d51a84
commit 44b523c946
19 changed files with 725 additions and 308 deletions

View File

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

View File

@ -0,0 +1,5 @@
"""Remote podman client support library."""
from .action_base import AbstractActionBase
from .report import Report, ReportColumn
__all__ = ['AbstractActionBase', 'Report', 'ReportColumn']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

248
contrib/python/cmd/pydman.py Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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