mirror of
https://github.com/containers/podman.git
synced 2025-06-18 07:28:57 +08:00

* Use podman library for access * Verbose error checking * Planned windows and macosx ports Signed-off-by: Jhon Honce <jhonce@redhat.com>
249 lines
8.3 KiB
Python
Executable File
249 lines
8.3 KiB
Python
Executable File
#!/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)
|