mirror of
https://github.com/containers/podman.git
synced 2025-06-20 00:51:16 +08:00
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:
@ -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"])))
|
|
5
contrib/python/cmd/lib/__init__.py
Normal file
5
contrib/python/cmd/lib/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Remote podman client support library."""
|
||||||
|
from .action_base import AbstractActionBase
|
||||||
|
from .report import Report, ReportColumn
|
||||||
|
|
||||||
|
__all__ = ['AbstractActionBase', 'Report', 'ReportColumn']
|
80
contrib/python/cmd/lib/action_base.py
Normal file
80
contrib/python/cmd/lib/action_base.py
Normal 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,
|
||||||
|
)
|
7
contrib/python/cmd/lib/actions/__init__.py
Normal file
7
contrib/python/cmd/lib/actions/__init__.py
Normal 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']
|
88
contrib/python/cmd/lib/actions/images_action.py
Normal file
88
contrib/python/cmd/lib/actions/images_action.py
Normal 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)
|
76
contrib/python/cmd/lib/actions/ps_action.py
Normal file
76
contrib/python/cmd/lib/actions/ps_action.py
Normal 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)
|
51
contrib/python/cmd/lib/actions/rm_action.py
Normal file
51
contrib/python/cmd/lib/actions/rm_action.py
Normal 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)
|
50
contrib/python/cmd/lib/actions/rmi_action.py
Normal file
50
contrib/python/cmd/lib/actions/rmi_action.py
Normal 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)
|
29
contrib/python/cmd/lib/future_abstract.py
Normal file
29
contrib/python/cmd/lib/future_abstract.py
Normal 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
|
67
contrib/python/cmd/lib/report.py
Normal file
67
contrib/python/cmd/lib/report.py
Normal 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)
|
@ -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
|
|
@ -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
248
contrib/python/cmd/pydman.py
Executable 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)
|
@ -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)
|
|
@ -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"])
|
|
@ -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"])
|
|
@ -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"
|
|
@ -5,14 +5,21 @@ from varlink import VarlinkError
|
|||||||
class VarlinkErrorProxy(VarlinkError):
|
class VarlinkErrorProxy(VarlinkError):
|
||||||
"""Class to Proxy VarlinkError methods."""
|
"""Class to Proxy VarlinkError methods."""
|
||||||
|
|
||||||
def __init__(self, obj):
|
def __init__(self, message, namespaced=False):
|
||||||
"""Construct proxy from Exception."""
|
"""Construct proxy from Exception."""
|
||||||
self._obj = obj
|
super().__init__(message.as_dict(), namespaced)
|
||||||
|
self._message = message
|
||||||
self.__module__ = 'libpod'
|
self.__module__ = 'libpod'
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, method):
|
||||||
"""Return item from proxied Exception."""
|
"""Return attribute from proxied Exception."""
|
||||||
return getattr(self._obj, item)
|
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):
|
class ContainerNotFound(VarlinkErrorProxy):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Cache for SSH tunnels."""
|
"""Cache for SSH tunnels."""
|
||||||
import collections
|
import collections
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
@ -96,9 +97,15 @@ class Tunnel(object):
|
|||||||
|
|
||||||
def bore(self, id):
|
def bore(self, id):
|
||||||
"""Create SSH tunnel from given context."""
|
"""Create SSH tunnel from given context."""
|
||||||
|
ssh_opts = '-nNT'
|
||||||
|
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||||
|
ssh_opts += 'v'
|
||||||
|
else:
|
||||||
|
ssh_opts += 'q'
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'ssh',
|
'ssh',
|
||||||
'-nNTq',
|
ssh_opts,
|
||||||
'-L',
|
'-L',
|
||||||
'{}:{}'.format(self.context.local_socket,
|
'{}:{}'.format(self.context.local_socket,
|
||||||
self.context.remote_socket),
|
self.context.remote_socket),
|
||||||
@ -106,15 +113,14 @@ class Tunnel(object):
|
|||||||
self.context.identity_file,
|
self.context.identity_file,
|
||||||
'ssh://{}@{}'.format(self.context.username, self.context.hostname),
|
'ssh://{}@{}'.format(self.context.username, self.context.hostname),
|
||||||
]
|
]
|
||||||
|
logging.debug('Tunnel cmd "{}"'.format(' '.join(cmd)))
|
||||||
if os.environ.get('PODMAN_DEBUG'):
|
|
||||||
cmd.append('-vvv')
|
|
||||||
|
|
||||||
self._tunnel = subprocess.Popen(cmd, close_fds=True)
|
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):
|
if os.path.exists(self.context.local_socket):
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(0.5)
|
||||||
else:
|
else:
|
||||||
raise TimeoutError('Failed to create tunnel using: {}'.format(
|
raise TimeoutError('Failed to create tunnel using: {}'.format(
|
||||||
' '.join(cmd)))
|
' '.join(cmd)))
|
||||||
|
Reference in New Issue
Block a user