summaryrefslogtreecommitdiff
path: root/contrib/python/cmd/pydman.py
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python/cmd/pydman.py')
-rwxr-xr-xcontrib/python/cmd/pydman.py248
1 files changed, 248 insertions, 0 deletions
diff --git a/contrib/python/cmd/pydman.py b/contrib/python/cmd/pydman.py
new file mode 100755
index 000000000..5008c706d
--- /dev/null
+++ b/contrib/python/cmd/pydman.py
@@ -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)