mirror of
https://github.com/containers/podman.git
synced 2025-06-26 21:07:02 +08:00
Update python directories to better support setup.py
Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
1
contrib/python/pypodman/MANIFEST.in
Normal file
1
contrib/python/pypodman/MANIFEST.in
Normal file
@ -0,0 +1 @@
|
||||
include README.md
|
21
contrib/python/pypodman/Makefile
Normal file
21
contrib/python/pypodman/Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
PYTHON ?= /usr/bin/python3
|
||||
|
||||
.PHONY: python-pypodman
|
||||
python-pypodman:
|
||||
$(PYTHON) setup.py bdist
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
true
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
$(PYTHON) setup.py install --user
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(PYTHON) setup.py clean --all
|
||||
pip3 uninstall pypodman ||:
|
||||
rm -rf pypodman.egg-info dist
|
||||
find . -depth -name __pycache__ -exec rm -rf {} \;
|
||||
find . -depth -name \*.pyc -exec rm -f {} \;
|
32
contrib/python/pypodman/README.md
Normal file
32
contrib/python/pypodman/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# pypodman - CLI interface for podman written in python
|
||||
|
||||
## Status: Active Development
|
||||
|
||||
See [libpod](https://github.com/projectatomic/libpod/contrib/python/cmd)
|
||||
|
||||
## Releases
|
||||
|
||||
To build the pypodman egg:
|
||||
|
||||
```sh
|
||||
cd ~/libpod/contrib/python/cmd
|
||||
python3 setup.py clean -a && python3 setup.py bdist
|
||||
```
|
||||
|
||||
## Running command:
|
||||
|
||||
### Against local podman service
|
||||
```sh
|
||||
$ pypodman images
|
||||
```
|
||||
### Against remote podman service
|
||||
```sh
|
||||
$ pypodman --host node001.example.org images
|
||||
```
|
||||
### Full help system available
|
||||
```sh
|
||||
$ pypodman -h
|
||||
```
|
||||
```sh
|
||||
$ pypodman images -h
|
||||
```
|
82
contrib/python/pypodman/docs/pypodman.1.md
Normal file
82
contrib/python/pypodman/docs/pypodman.1.md
Normal file
@ -0,0 +1,82 @@
|
||||
% pypodman "1"
|
||||
|
||||
## NAME
|
||||
|
||||
pypodman - Simple management tool for containers and images
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**pypodman** [*global options*] _command_ [*options*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
pypodman is a simple client only tool to help with debugging issues when daemons
|
||||
such as CRI runtime and the kubelet are not responding or failing. pypodman uses
|
||||
a VarLink API to commicate with a podman service running on either the local or
|
||||
remote machine. pypodman uses ssh to create secure tunnels when communicating
|
||||
with a remote service.
|
||||
|
||||
## GLOBAL OPTIONS
|
||||
|
||||
**--help, -h**
|
||||
|
||||
Print usage statement.
|
||||
|
||||
**--version**
|
||||
|
||||
Print program version number and exit.
|
||||
|
||||
**--config-home**
|
||||
|
||||
Directory that will be namespaced with `pypodman` to hold `pypodman.conf`. See FILES below for more details.
|
||||
|
||||
**--log-level**
|
||||
|
||||
Log events above specified level: DEBUG, INFO, WARNING (default), ERROR, or CRITICAL.
|
||||
|
||||
**--run-dir**
|
||||
|
||||
Directory that will be namespaced with `pypodman` to hold local socket bindings. The default is ``$XDG_RUNTIME_DIR\`.
|
||||
|
||||
**--user**
|
||||
|
||||
Authenicating user on remote host. `pypodman` defaults to the logged in user.
|
||||
|
||||
**--host**
|
||||
|
||||
Name of remote host. There is no default, if not given `pypodman` attempts to connect to `--remote-socket-path` on local host.
|
||||
|
||||
**--remote-socket-path**
|
||||
|
||||
Path on remote host for podman service's `AF_UNIX` socket. The default is `/run/podman/io.projectatomic.podman`.
|
||||
|
||||
**--identity-file**
|
||||
|
||||
The optional `ssh` identity file to authenicate when tunnelling to remote host. Default is None and will allow `ssh` to follow it's default methods for resolving the identity and private key using the logged in user.
|
||||
|
||||
## COMMANDS
|
||||
|
||||
See [podman(1)](podman.1.md)
|
||||
|
||||
## FILES
|
||||
|
||||
**pypodman/pypodman.conf** (`Any element of XDG_CONFIG_DIRS` and/or `XDG_CONFIG_HOME` and/or **--config-home**)
|
||||
|
||||
pypodman.conf is one or more configuration files for running the pypodman command. pypodman.conf is a TOML file with the stanza `[default]`, with a map of option: value.
|
||||
|
||||
pypodman follows the XDG (freedesktop.org) conventions for resolving it's configuration. The list below are read from top to bottom with later items overwriting earlier. Any missing items are ignored.
|
||||
|
||||
- `pypodman/pypodman.conf` from any path element in `XDG_CONFIG_DIRS` or `\etc\xdg`
|
||||
- `XDG_CONFIG_HOME` or $HOME/.config + `pypodman/pypodman.conf`
|
||||
- From `--config-home` command line option + `pypodman/pypodman.conf`
|
||||
- From environment variable, for example: RUN_DIR
|
||||
- From command line option, for example: --run-dir
|
||||
|
||||
This should provide Operators the ability to setup basic configurations and allow users to customize them.
|
||||
|
||||
**XDG_RUNTIME_DIR** (`XDG_RUNTIME_DIR/io.projectatomic.podman`)
|
||||
|
||||
Directory where pypodman stores non-essential runtime files and other file objects (such as sockets, named pipes, ...).
|
||||
|
||||
## SEE ALSO
|
||||
`podman(1)`, `libpod(8)`
|
11
contrib/python/pypodman/lib/__init__.py
Normal file
11
contrib/python/pypodman/lib/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Remote podman client support library."""
|
||||
from .action_base import AbstractActionBase
|
||||
from .config import PodmanArgumentParser
|
||||
from .report import Report, ReportColumn
|
||||
|
||||
__all__ = [
|
||||
'AbstractActionBase',
|
||||
'PodmanArgumentParser',
|
||||
'Report',
|
||||
'ReportColumn',
|
||||
]
|
84
contrib/python/pypodman/lib/action_base.py
Normal file
84
contrib/python/pypodman/lib/action_base.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""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 "class_" and "method". These will
|
||||
be invoked as class_(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."""
|
||||
if self._args.host is None:
|
||||
return podman.Client(
|
||||
uri=self.local_uri)
|
||||
else:
|
||||
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/pypodman/lib/actions/__init__.py
Normal file
7
contrib/python/pypodman/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/pypodman/lib/actions/images_action.py
Normal file
88
contrib/python/pypodman/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(class_=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/pypodman/lib/actions/ps_action.py
Normal file
76
contrib/python/pypodman/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(class_=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/pypodman/lib/actions/rm_action.py
Normal file
51
contrib/python/pypodman/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(class_=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/pypodman/lib/actions/rmi_action.py
Normal file
50
contrib/python/pypodman/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(class_=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)
|
212
contrib/python/pypodman/lib/config.py
Normal file
212
contrib/python/pypodman/lib/config.py
Normal file
@ -0,0 +1,212 @@
|
||||
import argparse
|
||||
import curses
|
||||
import getpass
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import pytoml
|
||||
|
||||
# TODO: setup.py and obtain __version__ from rpm.spec
|
||||
try:
|
||||
__version__ = pkg_resources.get_distribution('pypodman').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='WARNING',
|
||||
type=str.upper,
|
||||
help='set logging level for events. (default: %(default)s)',
|
||||
)
|
||||
self.add_argument(
|
||||
'--run-dir',
|
||||
metavar='DIRECTORY',
|
||||
help=('directory to place local socket bindings.'
|
||||
' (default: XDG_RUNTIME_DIR/pypodman'))
|
||||
self.add_argument(
|
||||
'--user',
|
||||
default=getpass.getuser(),
|
||||
help='Authenicating user on remote host. (default: %(default)s)')
|
||||
self.add_argument(
|
||||
'--host', help='name of remote host. (default: None)')
|
||||
self.add_argument(
|
||||
'--remote-socket-path',
|
||||
metavar='PATH',
|
||||
help=('path of podman socket on remote host'
|
||||
' (default: /run/podman/io.projectatomic.podman)'))
|
||||
self.add_argument(
|
||||
'--identity-file',
|
||||
metavar='PATH',
|
||||
help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
|
||||
self.add_argument(
|
||||
'--config-home',
|
||||
metavar='DIRECTORY',
|
||||
help=('home of configuration "pypodman.conf".'
|
||||
' (default: XDG_CONFIG_HOME/pypodman'))
|
||||
|
||||
actions_parser = self.add_subparsers(
|
||||
dest='subparser_name', help='actions')
|
||||
|
||||
# pull in plugin(s) code for each subcommand
|
||||
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."""
|
||||
args.xdg_runtime_dir = os.environ.get('XDG_RUNTIME_DIR', '/tmp')
|
||||
args.xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
|
||||
os.path.expanduser('~/.config'))
|
||||
args.xdg_config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||
|
||||
# Configuration file(s) are optional,
|
||||
# required arguments may be provided elsewhere
|
||||
config = {'default': {}}
|
||||
dirs = args.xdg_config_dirs.split(':')
|
||||
dirs.extend((args.xdg_config_home, args.config_home))
|
||||
for dir_ in dirs:
|
||||
if dir_ is None:
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(dir_, 'pypodman/pypodman.conf'),
|
||||
'r') as stream:
|
||||
config.update(pytoml.load(stream))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def reqattr(name, value):
|
||||
if value:
|
||||
setattr(args, name, value)
|
||||
return value
|
||||
self.error('Required argument "%s" is not configured.' % name)
|
||||
|
||||
reqattr(
|
||||
'run_dir',
|
||||
getattr(args, 'run_dir')
|
||||
or os.environ.get('RUN_DIR')
|
||||
or config['default'].get('run_dir')
|
||||
or os.path.join(args.xdg_runtime_dir, 'pypodman')
|
||||
) # yapf: disable
|
||||
|
||||
setattr(
|
||||
args,
|
||||
'host',
|
||||
getattr(args, 'host')
|
||||
or os.environ.get('HOST')
|
||||
or config['default'].get('host')
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'user',
|
||||
getattr(args, 'user')
|
||||
or os.environ.get('USER')
|
||||
or config['default'].get('user')
|
||||
or getpass.getuser()
|
||||
) # yapf:disable
|
||||
|
||||
reqattr(
|
||||
'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
|
||||
|
||||
reqattr(
|
||||
'log_level',
|
||||
getattr(args, 'log_level')
|
||||
or os.environ.get('LOG_LEVEL')
|
||||
or config['default'].get('log_level')
|
||||
or logging.WARNING
|
||||
) # yapf:disable
|
||||
|
||||
setattr(
|
||||
args,
|
||||
'identity_file',
|
||||
getattr(args, 'identity_file')
|
||||
or os.environ.get('IDENTITY_FILE')
|
||||
or config['default'].get('identity_file')
|
||||
or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.user))
|
||||
) # yapf:disable
|
||||
|
||||
if not os.path.isfile(args.identity_file):
|
||||
args.identity_file = None
|
||||
|
||||
if args.host:
|
||||
args.local_socket_path = os.path.join(args.run_dir,
|
||||
"podman.socket")
|
||||
else:
|
||||
args.local_socket_path = args.remote_socket_path
|
||||
|
||||
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)
|
29
contrib/python/pypodman/lib/future_abstract.py
Normal file
29
contrib/python/pypodman/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
|
76
contrib/python/pypodman/lib/pypodman.py
Executable file
76
contrib/python/pypodman/lib/pypodman.py
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remote podman client."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import lib.actions
|
||||
from lib import PodmanArgumentParser
|
||||
|
||||
assert lib.actions # silence pyflakes
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point."""
|
||||
# Setup logging so we use stderr and can change logging level later
|
||||
# Do it now before there is any chance of a default setup hardcoding crap.
|
||||
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.WARNING)
|
||||
|
||||
parser = PodmanArgumentParser()
|
||||
args = parser.parse_args()
|
||||
|
||||
log.setLevel(args.log_level)
|
||||
logging.debug('Logging initialized at level {}'.format(
|
||||
logging.getLevelName(logging.getLogger().getEffectiveLevel())))
|
||||
|
||||
def want_tb():
|
||||
"""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=want_tb())
|
||||
sys.exit(6)
|
||||
|
||||
# class_(args).method() are set by the sub-command's parser
|
||||
returncode = None
|
||||
try:
|
||||
obj = args.class_(args)
|
||||
except Exception as e:
|
||||
logging.critical(repr(e), exc_info=want_tb())
|
||||
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=want_tb())
|
||||
logging.warning('See subparser "{}" configuration.'.format(
|
||||
args.subparser_name))
|
||||
returncode = 3
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except (
|
||||
ConnectionRefusedError,
|
||||
ConnectionResetError,
|
||||
TimeoutError,
|
||||
) as e:
|
||||
logging.critical(e, exc_info=want_tb())
|
||||
logging.info('Review connection arguments for correctness.')
|
||||
returncode = 4
|
||||
|
||||
return 0 if returncode is None else returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
67
contrib/python/pypodman/lib/report.py
Normal file
67
contrib/python/pypodman/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)
|
4
contrib/python/pypodman/requirements.txt
Normal file
4
contrib/python/pypodman/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
humanize
|
||||
podman
|
||||
pytoml
|
||||
setuptools>=39.2.0
|
44
contrib/python/pypodman/setup.py
Normal file
44
contrib/python/pypodman/setup.py
Normal file
@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
root = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
with open(os.path.join(root, 'README.md')) as me:
|
||||
readme = me.read()
|
||||
|
||||
with open(os.path.join(root, 'requirements.txt')) as r:
|
||||
requirements = r.read().splitlines()
|
||||
|
||||
print(find_packages(where='pypodman', exclude=['test']))
|
||||
|
||||
setup(
|
||||
name='pypodman',
|
||||
version=os.environ.get('PODMAN_VERSION', '0.0.0'),
|
||||
description='A client for communicating with a Podman server',
|
||||
author_email='jhonce@redhat.com',
|
||||
author='Jhon Honce',
|
||||
license='Apache Software License',
|
||||
long_description=readme,
|
||||
entry_points={'console_scripts': [
|
||||
'pypodman = lib.pypodman:main',
|
||||
]},
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
keywords='varlink libpod podman pypodman',
|
||||
packages=find_packages(exclude=['test']),
|
||||
python_requires='>=3',
|
||||
zip_safe=True,
|
||||
url='http://github.com/projectatomic/libpod',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
])
|
23
contrib/python/pypodman/test/test_report.py
Normal file
23
contrib/python/pypodman/test/test_report.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
|
||||
from report import Report, ReportColumn
|
||||
|
||||
|
||||
class TestReport(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_report_column(self):
|
||||
rc = ReportColumn('k', 'v', 3)
|
||||
self.assertEqual(rc.key, 'k')
|
||||
self.assertEqual(rc.display, 'v')
|
||||
self.assertEqual(rc.width, 3)
|
||||
self.assertIsNone(rc.default)
|
||||
|
||||
rc = ReportColumn('k', 'v', 3, 'd')
|
||||
self.assertEqual(rc.key, 'k')
|
||||
self.assertEqual(rc.display, 'v')
|
||||
self.assertEqual(rc.width, 3)
|
||||
self.assertEqual(rc.default, 'd')
|
Reference in New Issue
Block a user