mirror of
https://github.com/containers/podman.git
synced 2025-06-21 01:19:15 +08:00
Port to MacOS
* Refactor Tunnel to support selecting port for remote sshd * Refactor ssh tunnel to support MacOS version of ssh * Refactor Tunnel.close() to find and kill off zombie siblings * Add psutil dependency * Add logging setup, letting library produce debugging records * Clean up Tunnel API * Fix test_runner.sh to propagate returncode to caller Signed-off-by: Jhon Honce <jhonce@redhat.com> Closes: #1199 Approved by: rhatdan
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
"""A client for communicating with a Podman varlink service."""
|
"""A client for communicating with a Podman varlink service."""
|
||||||
import errno
|
import errno
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -34,13 +35,17 @@ class BaseClient():
|
|||||||
*args,
|
*args,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Construct a Client based on input."""
|
"""Construct a Client based on input."""
|
||||||
|
log_level = os.environ.get('LOG_LEVEL')
|
||||||
|
if log_level is not None:
|
||||||
|
logging.basicConfig(level=logging.getLevelName(log_level.upper()))
|
||||||
|
|
||||||
if uri is None:
|
if uri is None:
|
||||||
raise ValueError('uri is required and cannot be None')
|
raise ValueError('uri is required and cannot be None')
|
||||||
if interface is None:
|
if interface is None:
|
||||||
raise ValueError('interface is required and cannot be None')
|
raise ValueError('interface is required and cannot be None')
|
||||||
|
|
||||||
unsupported = set(kwargs.keys()).difference(
|
unsupported = set(kwargs.keys()).difference(
|
||||||
('uri', 'interface', 'remote_uri', 'identity_file'))
|
('uri', 'interface', 'remote_uri', 'port', 'identity_file'))
|
||||||
if unsupported:
|
if unsupported:
|
||||||
raise ValueError('Unknown keyword arguments: {}'.format(
|
raise ValueError('Unknown keyword arguments: {}'.format(
|
||||||
', '.join(unsupported)))
|
', '.join(unsupported)))
|
||||||
@ -50,38 +55,35 @@ class BaseClient():
|
|||||||
raise ValueError('path is required for uri,'
|
raise ValueError('path is required for uri,'
|
||||||
' expected format "unix://path_to_socket"')
|
' expected format "unix://path_to_socket"')
|
||||||
|
|
||||||
if kwargs.get('remote_uri'):
|
if kwargs.get('remote_uri') is None:
|
||||||
# Remote access requires the full tuple of information
|
return LocalClient(Context(uri, interface))
|
||||||
if kwargs.get('remote_uri') is None:
|
|
||||||
raise ValueError(
|
|
||||||
'remote_uri is required,'
|
|
||||||
' expected format "ssh://user@hostname/path_to_socket".')
|
|
||||||
remote = urlparse(kwargs['remote_uri'])
|
|
||||||
if remote.username is None:
|
|
||||||
raise ValueError(
|
|
||||||
'username is required for remote_uri,'
|
|
||||||
' expected format "ssh://user@hostname/path_to_socket".')
|
|
||||||
if remote.path == '':
|
|
||||||
raise ValueError(
|
|
||||||
'path is required for remote_uri,'
|
|
||||||
' expected format "ssh://user@hostname/path_to_socket".')
|
|
||||||
if remote.hostname is None:
|
|
||||||
raise ValueError(
|
|
||||||
'hostname is required for remote_uri,'
|
|
||||||
' expected format "ssh://user@hostname/path_to_socket".')
|
|
||||||
|
|
||||||
return RemoteClient(
|
required = ('{} is required, expected format'
|
||||||
Context(
|
' "ssh://user@hostname[:port]/path_to_socket".')
|
||||||
uri,
|
|
||||||
interface,
|
# Remote access requires the full tuple of information
|
||||||
local_path,
|
if kwargs.get('remote_uri') is None:
|
||||||
remote.path,
|
raise ValueError(required.format('remote_uri'))
|
||||||
remote.username,
|
|
||||||
remote.hostname,
|
remote = urlparse(kwargs['remote_uri'])
|
||||||
kwargs.get('identity_file'),
|
if remote.username is None:
|
||||||
))
|
raise ValueError(required.format('username'))
|
||||||
return LocalClient(
|
if remote.path == '':
|
||||||
Context(uri, interface, None, None, None, None, None))
|
raise ValueError(required.format('path'))
|
||||||
|
if remote.hostname is None:
|
||||||
|
raise ValueError(required.format('hostname'))
|
||||||
|
|
||||||
|
return RemoteClient(
|
||||||
|
Context(
|
||||||
|
uri,
|
||||||
|
interface,
|
||||||
|
local_path,
|
||||||
|
remote.path,
|
||||||
|
remote.username,
|
||||||
|
remote.hostname,
|
||||||
|
remote.port,
|
||||||
|
kwargs.get('identity_file'),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
class LocalClient(BaseClient):
|
class LocalClient(BaseClient):
|
||||||
@ -115,7 +117,7 @@ class RemoteClient(BaseClient):
|
|||||||
"""Context manager for API workers to access varlink."""
|
"""Context manager for API workers to access varlink."""
|
||||||
tunnel = self._portal.get(self._context.uri)
|
tunnel = self._portal.get(self._context.uri)
|
||||||
if tunnel is None:
|
if tunnel is None:
|
||||||
tunnel = Tunnel(self._context).bore(self._context.uri)
|
tunnel = Tunnel(self._context).bore()
|
||||||
self._portal[self._context.uri] = tunnel
|
self._portal[self._context.uri] = tunnel
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -123,7 +125,7 @@ class RemoteClient(BaseClient):
|
|||||||
self._iface = self._client.open(self._context.interface)
|
self._iface = self._client.open(self._context.interface)
|
||||||
return self._iface
|
return self._iface
|
||||||
except Exception:
|
except Exception:
|
||||||
tunnel.close(self._context.uri)
|
tunnel.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def __exit__(self, e_type, e, e_traceback):
|
def __exit__(self, e_type, e, e_traceback):
|
||||||
@ -133,6 +135,7 @@ class RemoteClient(BaseClient):
|
|||||||
self._iface.close()
|
self._iface.close()
|
||||||
|
|
||||||
# set timer to shutdown ssh tunnel
|
# set timer to shutdown ssh tunnel
|
||||||
|
# self._portal.get(self._context.uri).close()
|
||||||
if isinstance(e, VarlinkError):
|
if isinstance(e, VarlinkError):
|
||||||
raise error_factory(e)
|
raise error_factory(e)
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
"""Cache for SSH tunnels."""
|
"""Cache for SSH tunnels."""
|
||||||
import collections
|
import collections
|
||||||
|
import getpass
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import weakref
|
import weakref
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
Context = collections.namedtuple('Context', (
|
Context = collections.namedtuple('Context', (
|
||||||
'uri',
|
'uri',
|
||||||
@ -14,8 +18,10 @@ Context = collections.namedtuple('Context', (
|
|||||||
'remote_socket',
|
'remote_socket',
|
||||||
'username',
|
'username',
|
||||||
'hostname',
|
'hostname',
|
||||||
|
'port',
|
||||||
'identity_file',
|
'identity_file',
|
||||||
))
|
))
|
||||||
|
Context.__new__.__defaults__ = (None, ) * len(Context._fields)
|
||||||
|
|
||||||
|
|
||||||
class Portal(collections.MutableMapping):
|
class Portal(collections.MutableMapping):
|
||||||
@ -51,7 +57,7 @@ class Portal(collections.MutableMapping):
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
value, _ = self.data[key]
|
value, _ = self.data[key]
|
||||||
del self.data[key]
|
del self.data[key]
|
||||||
value.close(key)
|
value.close()
|
||||||
del value
|
del value
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@ -93,47 +99,92 @@ class Tunnel():
|
|||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
"""Construct Tunnel."""
|
"""Construct Tunnel."""
|
||||||
self.context = context
|
self.context = context
|
||||||
self._tunnel = None
|
self._closed = True
|
||||||
|
|
||||||
def bore(self, ident):
|
@property
|
||||||
|
def closed(self):
|
||||||
|
"""Is tunnel closed."""
|
||||||
|
return self._closed
|
||||||
|
|
||||||
|
def bore(self):
|
||||||
"""Create SSH tunnel from given context."""
|
"""Create SSH tunnel from given context."""
|
||||||
cmd = ['ssh']
|
cmd = ['ssh', '-fNT']
|
||||||
|
|
||||||
ssh_opts = '-fNT'
|
|
||||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||||
ssh_opts += 'v'
|
cmd.append('-v')
|
||||||
else:
|
else:
|
||||||
ssh_opts += 'q'
|
cmd.append('-q')
|
||||||
cmd.append(ssh_opts)
|
|
||||||
|
if self.context.port:
|
||||||
|
cmd.extend(('-p', str(self.context.port)))
|
||||||
|
|
||||||
cmd.extend(('-L', '{}:{}'.format(self.context.local_socket,
|
cmd.extend(('-L', '{}:{}'.format(self.context.local_socket,
|
||||||
self.context.remote_socket)))
|
self.context.remote_socket)))
|
||||||
if self.context.identity_file:
|
if self.context.identity_file:
|
||||||
cmd.extend(('-i', self.context.identity_file))
|
cmd.extend(('-i', self.context.identity_file))
|
||||||
|
|
||||||
cmd.append('ssh://{}@{}'.format(self.context.username,
|
cmd.append('{}@{}'.format(self.context.username,
|
||||||
self.context.hostname))
|
self.context.hostname))
|
||||||
|
|
||||||
logging.debug('Tunnel cmd "%s"', ' '.join(cmd))
|
logging.debug('Opening tunnel "%s", cmd "%s"', self.context.uri,
|
||||||
|
' '.join(cmd))
|
||||||
|
|
||||||
self._tunnel = subprocess.Popen(cmd, close_fds=True)
|
tunnel = subprocess.Popen(cmd, close_fds=True)
|
||||||
|
# The return value of Popen() has no long term value as that process
|
||||||
|
# has already exited by the time control is returned here. This is a
|
||||||
|
# side effect of the -f option. wait() will be called to clean up
|
||||||
|
# resources.
|
||||||
for _ in range(300):
|
for _ in range(300):
|
||||||
# TODO: Make timeout configurable
|
# TODO: Make timeout configurable
|
||||||
if os.path.exists(self.context.local_socket):
|
if os.path.exists(self.context.local_socket) \
|
||||||
|
or tunnel.returncode is not None:
|
||||||
break
|
break
|
||||||
time.sleep(0.5)
|
with suppress(subprocess.TimeoutExpired):
|
||||||
|
# waiting for either socket to be created
|
||||||
|
# or first child to exit
|
||||||
|
tunnel.wait(0.5)
|
||||||
else:
|
else:
|
||||||
raise TimeoutError('Failed to create tunnel using: {}'.format(
|
raise TimeoutError(
|
||||||
' '.join(cmd)))
|
'Failed to create tunnel "{}", using: "{}"'.format(
|
||||||
weakref.finalize(self, self.close, ident)
|
self.context.uri, ' '.join(cmd)))
|
||||||
|
if tunnel.returncode is not None and tunnel.returncode != 0:
|
||||||
|
raise subprocess.CalledProcessError(tunnel.returncode,
|
||||||
|
' '.join(cmd))
|
||||||
|
tunnel.wait()
|
||||||
|
|
||||||
|
self._closed = False
|
||||||
|
weakref.finalize(self, self.close)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def close(self, ident):
|
def close(self):
|
||||||
"""Close SSH tunnel."""
|
"""Close SSH tunnel."""
|
||||||
if self._tunnel is None:
|
logging.debug('Closing tunnel "%s"', self.context.uri)
|
||||||
|
|
||||||
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._tunnel.kill()
|
# Find all ssh instances for user with uri tunnel the hard way...
|
||||||
self._tunnel.wait(300)
|
targets = [
|
||||||
os.remove(self.context.local_socket)
|
p
|
||||||
self._tunnel = None
|
for p in psutil.process_iter(attrs=['name', 'username', 'cmdline'])
|
||||||
|
if p.info['username'] == getpass.getuser()
|
||||||
|
and p.info['name'] == 'ssh'
|
||||||
|
and self.context.local_socket in ' '.join(p.info['cmdline'])
|
||||||
|
] # yapf: disable
|
||||||
|
|
||||||
|
# ask nicely for ssh to quit, reap results
|
||||||
|
for proc in targets:
|
||||||
|
proc.terminate()
|
||||||
|
_, alive = psutil.wait_procs(targets, timeout=300)
|
||||||
|
|
||||||
|
# kill off the uncooperative, then report any stragglers
|
||||||
|
for proc in alive:
|
||||||
|
proc.kill()
|
||||||
|
_, alive = psutil.wait_procs(targets, timeout=300)
|
||||||
|
|
||||||
|
for proc in alive:
|
||||||
|
logging.info('process %d survived SIGKILL, giving up.', proc.pid)
|
||||||
|
|
||||||
|
with suppress(OSError):
|
||||||
|
os.remove(self.context.local_socket)
|
||||||
|
self._closed = True
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
psutil
|
||||||
python-dateutil
|
python-dateutil
|
||||||
setuptools>=39
|
setuptools>=39
|
||||||
varlink
|
varlink
|
||||||
|
@ -28,7 +28,7 @@ class TestClient(unittest.TestCase):
|
|||||||
p = Client(
|
p = Client(
|
||||||
uri='unix:/run/podman',
|
uri='unix:/run/podman',
|
||||||
interface='io.projectatomic.podman',
|
interface='io.projectatomic.podman',
|
||||||
remote_uri='ssh://user@hostname/run/podmain/podman',
|
remote_uri='ssh://user@hostname/run/podman/podman',
|
||||||
identity_file='~/.ssh/id_rsa')
|
identity_file='~/.ssh/id_rsa')
|
||||||
|
|
||||||
self.assertIsInstance(p._client, BaseClient)
|
self.assertIsInstance(p._client, BaseClient)
|
||||||
|
@ -19,9 +19,9 @@ function usage {
|
|||||||
|
|
||||||
while getopts "vh" arg; do
|
while getopts "vh" arg; do
|
||||||
case $arg in
|
case $arg in
|
||||||
v ) VERBOSE='-v' ;;
|
v ) VERBOSE='-v'; export LOG_LEVEL=debug ;;
|
||||||
h ) usage ; exit 0;;
|
h ) usage ; exit 0 ;;
|
||||||
\? ) usage ; exit 2;;
|
\? ) usage ; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
shift $((OPTIND -1))
|
shift $((OPTIND -1))
|
||||||
@ -113,29 +113,36 @@ PODMAN_ARGS="--storage-driver=vfs \
|
|||||||
--cni-config-dir=$CNI_CONFIG_PATH \
|
--cni-config-dir=$CNI_CONFIG_PATH \
|
||||||
"
|
"
|
||||||
if [[ -n $VERBOSE ]]; then
|
if [[ -n $VERBOSE ]]; then
|
||||||
PODMAN_ARGS="$PODMAN_ARGS --log-level=debug"
|
PODMAN_ARGS="$PODMAN_ARGS --log-level=$LOG_LEVEL"
|
||||||
fi
|
fi
|
||||||
PODMAN="podman $PODMAN_ARGS"
|
PODMAN="podman $PODMAN_ARGS"
|
||||||
|
|
||||||
# document what we're about to do...
|
# Run podman in background without systemd for test purposes
|
||||||
$PODMAN --version
|
cat >/tmp/test_podman.output <<-EOT
|
||||||
|
$($PODMAN --version)
|
||||||
|
$PODMAN varlink --timeout=0 ${PODMAN_HOST}
|
||||||
|
==========================================
|
||||||
|
EOT
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
# Run podman in background without systemd for test purposes
|
$PODMAN varlink --timeout=0 ${PODMAN_HOST} >>/tmp/test_podman.output 2>&1 &
|
||||||
$PODMAN varlink --timeout=0 ${PODMAN_HOST} >/tmp/test_runner.output 2>&1 &
|
|
||||||
|
|
||||||
if [[ -z $1 ]]; then
|
if [[ -z $1 ]]; then
|
||||||
export PYTHONPATH=.
|
export PYTHONPATH=.
|
||||||
python3 -m unittest discover -s . $VERBOSE
|
python3 -m unittest discover -s . $VERBOSE
|
||||||
|
RETURNCODE=$?
|
||||||
else
|
else
|
||||||
export PYTHONPATH=.:./test
|
export PYTHONPATH=.:./test
|
||||||
python3 -m unittest $1 $VERBOSE
|
python3 -m unittest $1 $VERBOSE
|
||||||
|
RETURNCODE=$?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
pkill -9 podman
|
pkill -9 podman
|
||||||
pkill -9 conmon
|
pkill -9 conmon
|
||||||
|
|
||||||
showlog /tmp/test_runner.output
|
showlog /tmp/test_podman.output
|
||||||
showlog /tmp/alpine.log
|
showlog /tmp/alpine.log
|
||||||
showlog /tmp/busybox.log
|
showlog /tmp/busybox.log
|
||||||
|
|
||||||
|
exit $RETURNCODE
|
||||||
|
@ -25,7 +25,7 @@ class TestSystem(unittest.TestCase):
|
|||||||
|
|
||||||
def test_remote_ping(self):
|
def test_remote_ping(self):
|
||||||
host = urlparse(self.host)
|
host = urlparse(self.host)
|
||||||
remote_uri = 'ssh://root@localhost/{}'.format(host.path)
|
remote_uri = 'ssh://root@localhost{}'.format(host.path)
|
||||||
|
|
||||||
local_uri = 'unix:{}/tunnel/podman.sock'.format(self.tmpdir)
|
local_uri = 'unix:{}/tunnel/podman.sock'.format(self.tmpdir)
|
||||||
with podman.Client(
|
with podman.Client(
|
||||||
@ -33,7 +33,7 @@ class TestSystem(unittest.TestCase):
|
|||||||
remote_uri=remote_uri,
|
remote_uri=remote_uri,
|
||||||
identity_file=os.path.expanduser('~/.ssh/id_rsa'),
|
identity_file=os.path.expanduser('~/.ssh/id_rsa'),
|
||||||
) as remote_client:
|
) as remote_client:
|
||||||
remote_client.system.ping()
|
self.assertTrue(remote_client.system.ping())
|
||||||
|
|
||||||
def test_versions(self):
|
def test_versions(self):
|
||||||
with podman.Client(self.host) as pclient:
|
with podman.Client(self.host) as pclient:
|
||||||
|
@ -4,7 +4,6 @@ import time
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import podman
|
|
||||||
from podman.libs.tunnel import Context, Portal, Tunnel
|
from podman.libs.tunnel import Context, Portal, Tunnel
|
||||||
|
|
||||||
|
|
||||||
@ -60,20 +59,22 @@ class TestTunnel(unittest.TestCase):
|
|||||||
'/run/podman/socket',
|
'/run/podman/socket',
|
||||||
'user',
|
'user',
|
||||||
'hostname',
|
'hostname',
|
||||||
|
None,
|
||||||
'~/.ssh/id_rsa',
|
'~/.ssh/id_rsa',
|
||||||
)
|
)
|
||||||
tunnel = Tunnel(context).bore('unix:/01')
|
tunnel = Tunnel(context).bore()
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'ssh',
|
'ssh',
|
||||||
'-fNTq',
|
'-fNT',
|
||||||
|
'-q',
|
||||||
'-L',
|
'-L',
|
||||||
'{}:{}'.format(context.local_socket, context.remote_socket),
|
'{}:{}'.format(context.local_socket, context.remote_socket),
|
||||||
'-i',
|
'-i',
|
||||||
context.identity_file,
|
context.identity_file,
|
||||||
'ssh://{}@{}'.format(context.username, context.hostname),
|
'{}@{}'.format(context.username, context.hostname),
|
||||||
]
|
]
|
||||||
|
|
||||||
mock_finalize.assert_called_once_with(tunnel, tunnel.close, 'unix:/01')
|
mock_finalize.assert_called_once_with(tunnel, tunnel.close)
|
||||||
mock_exists.assert_called_once_with(context.local_socket)
|
mock_exists.assert_called_once_with(context.local_socket)
|
||||||
mock_Popen.assert_called_once_with(cmd, close_fds=True)
|
mock_Popen.assert_called_once_with(cmd, close_fds=True)
|
||||||
|
@ -46,6 +46,11 @@ user.
|
|||||||
Name of remote host. There is no default, if not given \f[C]pypodman\f[]
|
Name of remote host. There is no default, if not given \f[C]pypodman\f[]
|
||||||
attempts to connect to \f[C]\-\-remote\-socket\-path\f[] on local host.
|
attempts to connect to \f[C]\-\-remote\-socket\-path\f[] on local host.
|
||||||
.PP
|
.PP
|
||||||
|
\f[B]\[en]port\f[]
|
||||||
|
.PP
|
||||||
|
The optional port for \f[C]ssh\f[] to connect tunnel to on remote host.
|
||||||
|
Default is None and will allow \f[C]ssh\f[] to follow it's default configuration.
|
||||||
|
.PP
|
||||||
\f[B]\[en]remote\-socket\-path\f[]
|
\f[B]\[en]remote\-socket\-path\f[]
|
||||||
.PP
|
.PP
|
||||||
Path on remote host for podman service's \f[C]AF_UNIX\f[] socket. The default is
|
Path on remote host for podman service's \f[C]AF_UNIX\f[] socket. The default is
|
||||||
|
@ -33,6 +33,28 @@ class HelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PortAction(argparse.Action):
|
||||||
|
"""Validate port 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 = 'port numbers must be a positive integer.'
|
||||||
|
raise argparse.ArgumentError(self, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class PathAction(argparse.Action):
|
||||||
|
"""Expand user- and relative-paths."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
"""Resolve full path value."""
|
||||||
|
setattr(namespace, self.dest,
|
||||||
|
os.path.abspath(os.path.expanduser(values)))
|
||||||
|
|
||||||
|
|
||||||
class PodmanArgumentParser(argparse.ArgumentParser):
|
class PodmanArgumentParser(argparse.ArgumentParser):
|
||||||
"""Default remote podman configuration."""
|
"""Default remote podman configuration."""
|
||||||
|
|
||||||
@ -64,10 +86,17 @@ class PodmanArgumentParser(argparse.ArgumentParser):
|
|||||||
' (default: XDG_RUNTIME_DIR/pypodman'))
|
' (default: XDG_RUNTIME_DIR/pypodman'))
|
||||||
self.add_argument(
|
self.add_argument(
|
||||||
'--user',
|
'--user',
|
||||||
|
'-l',
|
||||||
default=getpass.getuser(),
|
default=getpass.getuser(),
|
||||||
help='Authenicating user on remote host. (default: %(default)s)')
|
help='Authenicating user on remote host. (default: %(default)s)')
|
||||||
self.add_argument(
|
self.add_argument(
|
||||||
'--host', help='name of remote host. (default: None)')
|
'--host', help='name of remote host. (default: None)')
|
||||||
|
self.add_argument(
|
||||||
|
'--port',
|
||||||
|
'-p',
|
||||||
|
type=int,
|
||||||
|
action=PortAction,
|
||||||
|
help='port for ssh tunnel to remote host. (default: 22)')
|
||||||
self.add_argument(
|
self.add_argument(
|
||||||
'--remote-socket-path',
|
'--remote-socket-path',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
@ -75,11 +104,14 @@ class PodmanArgumentParser(argparse.ArgumentParser):
|
|||||||
' (default: /run/podman/io.projectatomic.podman)'))
|
' (default: /run/podman/io.projectatomic.podman)'))
|
||||||
self.add_argument(
|
self.add_argument(
|
||||||
'--identity-file',
|
'--identity-file',
|
||||||
|
'-i',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
|
action=PathAction,
|
||||||
help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
|
help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
|
||||||
self.add_argument(
|
self.add_argument(
|
||||||
'--config-home',
|
'--config-home',
|
||||||
metavar='DIRECTORY',
|
metavar='DIRECTORY',
|
||||||
|
action=PathAction,
|
||||||
help=('home of configuration "pypodman.conf".'
|
help=('home of configuration "pypodman.conf".'
|
||||||
' (default: XDG_CONFIG_HOME/pypodman'))
|
' (default: XDG_CONFIG_HOME/pypodman'))
|
||||||
|
|
||||||
@ -125,8 +157,8 @@ class PodmanArgumentParser(argparse.ArgumentParser):
|
|||||||
if dir_ is None:
|
if dir_ is None:
|
||||||
continue
|
continue
|
||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
with open(os.path.join(dir_, 'pypodman/pypodman.conf'),
|
with open(os.path.join(dir_,
|
||||||
'r') as stream:
|
'pypodman/pypodman.conf')) as stream:
|
||||||
config.update(pytoml.load(stream))
|
config.update(pytoml.load(stream))
|
||||||
|
|
||||||
def reqattr(name, value):
|
def reqattr(name, value):
|
||||||
@ -156,6 +188,7 @@ class PodmanArgumentParser(argparse.ArgumentParser):
|
|||||||
'user',
|
'user',
|
||||||
getattr(args, 'user')
|
getattr(args, 'user')
|
||||||
or os.environ.get('USER')
|
or os.environ.get('USER')
|
||||||
|
or os.environ.get('LOGNAME')
|
||||||
or config['default'].get('user')
|
or config['default'].get('user')
|
||||||
or getpass.getuser()
|
or getpass.getuser()
|
||||||
) # yapf:disable
|
) # yapf:disable
|
||||||
@ -195,8 +228,13 @@ class PodmanArgumentParser(argparse.ArgumentParser):
|
|||||||
args.local_socket_path = args.remote_socket_path
|
args.local_socket_path = args.remote_socket_path
|
||||||
|
|
||||||
args.local_uri = "unix:{}".format(args.local_socket_path)
|
args.local_uri = "unix:{}".format(args.local_socket_path)
|
||||||
args.remote_uri = "ssh://{}@{}{}".format(args.user, args.host,
|
|
||||||
args.remote_socket_path)
|
components = ['ssh://', args.user, '@', args.host]
|
||||||
|
if args.port:
|
||||||
|
components.extend((':', str(args.port)))
|
||||||
|
components.append(args.remote_socket_path)
|
||||||
|
|
||||||
|
args.remote_uri = ''.join(components)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def exit(self, status=0, message=None):
|
def exit(self, status=0, message=None):
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
|
||||||
from pypodman.lib import PodmanArgumentParser
|
from pypodman.lib import PodmanArgumentParser
|
||||||
|
|
||||||
@ -49,14 +50,15 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
returncode = getattr(obj, args.method)()
|
returncode = getattr(obj, args.method)()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logging.critical(e, exc_info=want_tb())
|
logging.critical(e, exc_info=want_tb())
|
||||||
logging.warning('See subparser "%s" configuration.',
|
logging.warning('See subparser "%s" configuration.',
|
||||||
args.subparser_name)
|
args.subparser_name)
|
||||||
returncode = 3
|
returncode = 3
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
except (
|
except (
|
||||||
|
CalledProcessError,
|
||||||
ConnectionRefusedError,
|
ConnectionRefusedError,
|
||||||
ConnectionResetError,
|
ConnectionResetError,
|
||||||
TimeoutError,
|
TimeoutError,
|
||||||
|
@ -203,6 +203,7 @@ BuildRequires: python3-varlink
|
|||||||
Requires: python3-setuptools
|
Requires: python3-setuptools
|
||||||
Requires: python3-varlink
|
Requires: python3-varlink
|
||||||
Requires: python3-dateutil
|
Requires: python3-dateutil
|
||||||
|
Requires: python3-psutil
|
||||||
|
|
||||||
Provides: python3-%{name} = %{version}-%{release}
|
Provides: python3-%{name} = %{version}-%{release}
|
||||||
Summary: Python 3 bindings and client for %{name}
|
Summary: Python 3 bindings and client for %{name}
|
||||||
|
@ -42,6 +42,7 @@ Requires: python3-humanize
|
|||||||
Requires: python3-pytoml
|
Requires: python3-pytoml
|
||||||
Requires: python3-setuptools
|
Requires: python3-setuptools
|
||||||
Requires: python3-varlink
|
Requires: python3-varlink
|
||||||
|
Requires: python3-psutil
|
||||||
Requires: podman
|
Requires: podman
|
||||||
|
|
||||||
%if 0%{?fedora}
|
%if 0%{?fedora}
|
||||||
|
Reference in New Issue
Block a user