Fix logging to stdout and file in classes/commands

Fix logging to console to depend on input CLI args. By default, keep
logging to stdout. Make --log-file argument working as well.

Use the input verbosity parameters as a controlling switch for
logs verbosity.

Evaluate log levels as:

  * 1 (WARNING+) - the default log level if neither -v nor --debug used
  * 2 (INFO+)    - applies if -v / --verbose
  * 4 (DEBUG+)   - applies if --debug, dumps command results to file,
    if --log-file is requested.
  * 5 (DEBUG+)   - applies if --debug and -v. Like the latter mode, but
    also dumps the executed commands results to console.

This is needed for better deployments troubleshootng.

Closes-Bug: #1799182

Change-Id: I653ac4cc520e40f3eb4d029e8c99ab482b17a859
Signed-off-by: Bogdan Dobrelya <bdobreli@redhat.com>
This commit is contained in:
Bogdan Dobrelya 2018-10-22 12:22:10 +02:00
parent 133bb38368
commit d3c83259bf
11 changed files with 291 additions and 157 deletions

View File

@ -13,21 +13,19 @@
'''Stable library interface to managing containers with paunch.'''
import json
import logging
import pbr.version
import yaml
from paunch.builder import compose1
from paunch.builder import podman
from paunch import runner
from paunch.utils import common
__version__ = pbr.version.VersionInfo('paunch').version_string()
LOG = logging.getLogger(__name__)
def apply(config_id, config, managed_by, labels=None, cont_cmd=None,
default_runtime='docker'):
default_runtime='docker', log_level=None, log_file=None):
"""Execute supplied container configuration.
:param str config_id: Unique config ID, should not be re-used until any
@ -42,34 +40,41 @@ def apply(config_id, config, managed_by, labels=None, cont_cmd=None,
:param str cont_cmd: Optional override to the container command to run.
:param str default_runtime: Optional override to the default runtime used
for containers.
:param int log_level: optional log level for loggers
:param int log_file: optional log file for messages
:returns (list, list, int) lists of stdout and stderr for each execution,
and a single return code representing the
overall success of the apply.
:rtype: tuple
"""
log = common.configure_logging(__name__, log_level, log_file)
if default_runtime == 'docker':
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd)
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log)
builder = compose1.ComposeV1Builder(
config_id=config_id,
config=config,
runner=r,
labels=labels
labels=labels,
log=log
)
elif default_runtime == 'podman':
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd)
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log)
builder = podman.PodmanBuilder(
config_id=config_id,
config=config,
runner=r,
labels=labels
labels=labels,
log=log
)
else:
LOG.error("container runtime not supported: %s" % default_runtime)
log.error("container runtime not supported: %s" % default_runtime)
return builder.apply()
def cleanup(config_ids, managed_by, cont_cmd=None, default_runtime='docker'):
def cleanup(config_ids, managed_by, cont_cmd=None, default_runtime='docker',
log_level=None, log_file=None):
"""Delete containers no longer applied, rename others to preferred name.
:param list config_ids: List of config IDs still applied. All containers
@ -80,19 +85,24 @@ def cleanup(config_ids, managed_by, cont_cmd=None, default_runtime='docker'):
:param str cont_cmd: Optional override to the container command to run.
:param str default_runtime: Optional override to the default runtime used
for containers.
:param int log_level: optional log level for loggers
:param int log_file: optional log file for messages
"""
log = common.configure_logging(__name__, log_level, log_file)
if default_runtime == 'docker':
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd)
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log)
elif default_runtime == 'podman':
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd)
LOG.warning("paunch cleanup is partially supported with podman")
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log)
log.warning("paunch cleanup is partially supported with podman")
else:
LOG.error("container runtime not supported: %s" % default_runtime)
log.error("container runtime not supported: %s" % default_runtime)
r.delete_missing_configs(config_ids)
r.rename_containers()
def list(managed_by, cont_cmd=None, default_runtime='docker'):
def list(managed_by, cont_cmd=None, default_runtime='docker', log_level=None,
log_file=None):
"""List all containers associated with all config IDs.
:param str managed_by: Name of the tool managing the containers. Only
@ -100,22 +110,27 @@ def list(managed_by, cont_cmd=None, default_runtime='docker'):
:param str cont_cmd: Optional override to the container command to run.
:param str default_runtime: Optional override to the default runtime used
for containers.
:param int log_level: optional log level for loggers
:param int log_file: optional log file for messages
:returns a dict where the key is the config ID and the value is a list of
'docker inspect' dicts for each container.
:rtype: defaultdict(list)
"""
log = common.configure_logging(__name__, log_level, log_file)
if default_runtime == 'docker':
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd)
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log)
elif default_runtime == 'podman':
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd)
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log)
else:
LOG.error("container runtime not supported: %s" % default_runtime)
log.error("container runtime not supported: %s" % default_runtime)
return r.list_configs()
def debug(config_id, container_name, action, config, managed_by, labels=None,
cont_cmd=None, default_runtime='docker'):
cont_cmd=None, default_runtime='docker', log_level=None,
log_file=None):
"""Execute supplied container configuration.
:param str config_id: Unique config ID, should not be re-used until any
@ -133,30 +148,35 @@ def debug(config_id, container_name, action, config, managed_by, labels=None,
:param str cont_cmd: Optional override to the container command to run.
:param str default_runtime: Optional override to the default runtime used
for containers.
:param int log_level: optional log level for loggers
:param int log_file: optional log file for messages
:returns integer return value from running command or failure for any
other reason.
:rtype: int
"""
log = common.configure_logging(__name__, log_level, log_file)
if default_runtime == 'docker':
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd)
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log)
builder = compose1.ComposeV1Builder(
config_id=config_id,
config=config,
runner=r,
labels=labels
labels=labels,
log=log
)
elif default_runtime == 'podman':
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd)
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log)
builder = podman.PodmanBuilder(
config_id=config_id,
config=config,
runner=r,
labels=labels
labels=labels,
log=log
)
else:
LOG.error("container runtime not supported: %s" % default_runtime)
log.error("container runtime not supported: %s" % default_runtime)
if action == 'print-cmd':
cmd = [
r.cont_cmd,
@ -174,7 +194,7 @@ def debug(config_id, container_name, action, config, managed_by, labels=None,
r.unique_container_name(container_name)
]
builder.container_run_args(cmd, container_name)
return r.execute_interactive(cmd)
return r.execute_interactive(cmd, log)
elif action == 'dump-yaml':
print(yaml.safe_dump(config, default_flow_style=False))
elif action == 'dump-json':
@ -184,7 +204,8 @@ def debug(config_id, container_name, action, config, managed_by, labels=None,
'"print-cmd", or "run"')
def delete(config_ids, managed_by, cont_cmd=None, default_runtime='docker'):
def delete(config_ids, managed_by, cont_cmd=None, default_runtime='docker',
log_level=None, log_file=None):
"""Delete containers with the specified config IDs.
:param list config_ids: List of config IDs to delete the containers for.
@ -194,15 +215,17 @@ def delete(config_ids, managed_by, cont_cmd=None, default_runtime='docker'):
:param str default_runtime: Optional override to the default runtime used
for containers.
"""
log = common.configure_logging(__name__, log_level, log_file)
if not config_ids:
LOG.warn('No config IDs specified')
log.warn('No config IDs specified')
if default_runtime == 'docker':
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd)
r = runner.DockerRunner(managed_by, cont_cmd=cont_cmd, log=log)
elif default_runtime == 'podman':
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd)
LOG.warning("paunch cleanup is partially supported with podman")
r = runner.PodmanRunner(managed_by, cont_cmd=cont_cmd, log=log)
log.warning("paunch cleanup is partially supported with podman")
else:
LOG.error("container runtime not supported: %s" % default_runtime)
log.error("container runtime not supported: %s" % default_runtime)
for conf_id in config_ids:
r.remove_containers(conf_id)

View File

@ -12,22 +12,22 @@
#
import json
import logging
import re
import tenacity
from paunch.utils import common
from paunch.utils import systemd
LOG = logging.getLogger(__name__)
class BaseBuilder(object):
def __init__(self, config_id, config, runner, labels):
def __init__(self, config_id, config, runner, labels, log=None):
self.config_id = config_id
self.config = config
self.labels = labels
self.runner = runner
# Leverage pre-configured logger
self.log = log or common.configure_logging(__name__)
def apply(self):
@ -46,7 +46,7 @@ class BaseBuilder(object):
desired_names = set([cn[-1] for cn in container_names])
for container in sorted(self.config, key=key_fltr):
LOG.debug("Running container: %s" % container)
self.log.debug("Running container: %s" % container)
cconfig = self.config[container]
action = cconfig.get('action', 'run')
restart = cconfig.get('restart', 'none')
@ -59,7 +59,8 @@ class BaseBuilder(object):
if action == 'run':
if container in desired_names:
LOG.debug('Skipping existing container: %s' % container)
self.log.debug('Skipping existing container: %s' %
container)
continue
cmd = [
@ -74,23 +75,26 @@ class BaseBuilder(object):
cmd = [self.runner.cont_cmd, 'exec']
self.cont_exec_args(cmd, container)
(cmd_stdout, cmd_stderr, returncode) = self.runner.execute(cmd)
(cmd_stdout, cmd_stderr, returncode) = self.runner.execute(
cmd, self.log)
if cmd_stdout:
stdout.append(cmd_stdout)
if cmd_stderr:
stderr.append(cmd_stderr)
if returncode not in exit_codes:
LOG.error("Error running %s. [%s]\n" % (cmd, returncode))
LOG.error("stdout: %s" % cmd_stdout)
LOG.error("stderr: %s" % cmd_stderr)
self.log.error("Error running %s. [%s]\n" % (cmd, returncode))
self.log.error("stdout: %s" % cmd_stdout)
self.log.error("stderr: %s" % cmd_stderr)
deploy_status_code = returncode
else:
LOG.debug('Completed $ %s' % ' '.join(cmd))
LOG.info("stdout: %s" % cmd_stdout)
LOG.info("stderr: %s" % cmd_stderr)
self.log.debug('Completed $ %s' % ' '.join(cmd))
self.log.info("stdout: %s" % cmd_stdout)
self.log.info("stderr: %s" % cmd_stderr)
if systemd_managed:
systemd.service_create(container_name, cconfig)
systemd.service_create(container=container_name,
cconfig=cconfig,
log=self.log)
return stdout, stderr, deploy_status_code
def delete_missing_and_updated(self):
@ -100,15 +104,15 @@ class BaseBuilder(object):
# if the desired name is not in the config, delete it
if cn[-1] not in self.config:
LOG.debug("Deleting container (removed): %s" % container)
self.log.debug("Deleting container (removed): %s" % container)
self.runner.remove_container(container)
continue
ex_data_str = self.runner.inspect(
container, '{{index .Config.Labels "config_data"}}')
if not ex_data_str:
LOG.debug("Deleting container (no config_data): %s"
% container)
self.log.debug("Deleting container (no config_data): %s"
% container)
self.runner.remove_container(container)
continue
@ -119,8 +123,8 @@ class BaseBuilder(object):
new_data = self.config.get(cn[-1])
if new_data != ex_data:
LOG.debug("Deleting container (changed config_data): %s"
% container)
self.log.debug("Deleting container (changed config_data): %s"
% container)
self.runner.remove_container(container)
# deleting containers is an opportunity for renames to their
@ -208,13 +212,14 @@ class BaseBuilder(object):
returncode = e.rc
cmd_stdout = e.stdout
cmd_stderr = e.stderr
LOG.error("Error pulling %s. [%s]\n" % (image, returncode))
LOG.error("stdout: %s" % e.stdout)
LOG.error("stderr: %s" % e.stderr)
self.log.error("Error pulling %s. [%s]\n" %
(image, returncode))
self.log.error("stdout: %s" % e.stdout)
self.log.error("stderr: %s" % e.stderr)
else:
LOG.debug('Pulled %s' % image)
LOG.info("stdout: %s" % cmd_stdout)
LOG.info("stderr: %s" % cmd_stderr)
self.log.debug('Pulled %s' % image)
self.log.info("stdout: %s" % cmd_stdout)
self.log.info("stderr: %s" % cmd_stderr)
if cmd_stdout:
stdout.append(cmd_stdout)
@ -230,7 +235,7 @@ class BaseBuilder(object):
)
def _pull(self, image):
cmd = [self.runner.cont_cmd, 'pull', image]
(stdout, stderr, rc) = self.runner.execute(cmd)
(stdout, stderr, rc) = self.runner.execute(cmd, self.log)
if rc != 0:
raise PullException(stdout, stderr, rc)
return stdout, stderr

View File

@ -11,18 +11,14 @@
# under the License.
#
import logging
from paunch.builder import base
LOG = logging.getLogger(__name__)
class ComposeV1Builder(base.BaseBuilder):
def __init__(self, config_id, config, runner, labels=None):
def __init__(self, config_id, config, runner, labels=None, log=None):
super(ComposeV1Builder, self).__init__(config_id, config, runner,
labels)
labels, log)
def container_run_args(self, cmd, container):
cconfig = self.config[container]

View File

@ -11,18 +11,14 @@
# under the License.
#
import logging
from paunch.builder import base
LOG = logging.getLogger(__name__)
class PodmanBuilder(base.BaseBuilder):
def __init__(self, config_id, config, runner, labels=None):
def __init__(self, config_id, config, runner, labels=None, log=None):
super(PodmanBuilder, self).__init__(config_id, config, runner,
labels)
labels, log)
def container_run_args(self, cmd, container):
cconfig = self.config[container]

View File

@ -12,7 +12,6 @@
#
import collections
import logging
from cliff import command
from cliff import lister
@ -24,7 +23,7 @@ import paunch
class Apply(command.Command):
log = logging.getLogger(__name__)
log = None
def get_parser(self, prog_name):
parser = super(Apply, self).get_parser(prog_name)
@ -66,6 +65,11 @@ class Apply(command.Command):
def take_action(self, parsed_args):
# takes 1, or 2 if --verbose, or 4 - 5 if --debug
log_level = (self.app_args.verbose_level +
int(self.app_args.debug) * 3)
self.log = paunch.utils.common.configure_logging(
__name__, log_level, self.app_args.log_file)
labels = collections.OrderedDict()
for l in parsed_args.labels:
k, v = l.split(('='), 1)
@ -79,7 +83,9 @@ class Apply(command.Command):
config,
managed_by='paunch',
labels=labels,
default_runtime=parsed_args.default_runtime
default_runtime=parsed_args.default_runtime,
log_level=log_level,
log_file=self.app_args.log_file
)
return rc
@ -87,7 +93,7 @@ class Apply(command.Command):
class Cleanup(command.Command):
log = logging.getLogger(__name__)
log = None
def get_parser(self, prog_name):
parser = super(Cleanup, self).get_parser(prog_name)
@ -108,15 +114,22 @@ class Cleanup(command.Command):
return parser
def take_action(self, parsed_args):
# takes 1, or 2 if --verbose, or 4 - 5 if --debug
log_level = (self.app_args.verbose_level +
int(self.app_args.debug) * 3)
self.log = paunch.utils.common.configure_logging(
__name__, log_level, self.app_args.log_file)
paunch.cleanup(
parsed_args.config_id,
managed_by=parsed_args.managed_by
managed_by=parsed_args.managed_by,
log_level=log_level,
log_file=self.app_args.log_file
)
class Delete(command.Command):
log = logging.getLogger(__name__)
log = None
def get_parser(self, prog_name):
parser = super(Delete, self).get_parser(prog_name)
@ -136,12 +149,22 @@ class Delete(command.Command):
return parser
def take_action(self, parsed_args):
paunch.delete(parsed_args.config_id, parsed_args.managed_by)
# takes 1, or 2 if --verbose, or 4 - 5 if --debug
log_level = (self.app_args.verbose_level +
int(self.app_args.debug) * 3)
self.log = paunch.utils.common.configure_logging(
__name__, log_level, self.app_args.log_file)
paunch.delete(
parsed_args.config_id,
parsed_args.managed_by,
log_level=log_level,
log_file=self.app_args.log_file
)
class Debug(command.Command):
log = logging.getLogger(__name__)
log = None
def get_parser(self, prog_name):
parser = super(Debug, self).get_parser(prog_name)
@ -223,6 +246,11 @@ class Debug(command.Command):
def take_action(self, parsed_args):
# takes 1, or 2 if --verbose, or 4 - 5 if --debug
log_level = (self.app_args.verbose_level +
int(self.app_args.debug) * 3)
self.log = paunch.utils.common.configure_logging(
__name__, log_level, self.app_args.log_file)
labels = collections.OrderedDict()
for l in parsed_args.labels:
k, v = l.split(('='), 1)
@ -263,13 +291,15 @@ class Debug(command.Command):
parsed_args.action,
cconfig,
parsed_args.managed_by,
labels=labels
labels=labels,
log_level=log_level,
log_file=self.app_args.log_file
)
class List(lister.Lister):
log = logging.getLogger(__name__)
log = None
def get_parser(self, prog_name):
parser = super(List, self).get_parser(prog_name)
@ -283,7 +313,16 @@ class List(lister.Lister):
return parser
def take_action(self, parsed_args):
configs = paunch.list(parsed_args.managed_by)
# takes 1, or 2 if --verbose, or 4 - 5 if --debug
log_level = (self.app_args.verbose_level +
int(self.app_args.debug) * 3)
self.log = paunch.utils.common.configure_logging(
__name__, log_level, self.app_args.log_file)
configs = paunch.list(
parsed_args.managed_by,
log_level=log_level,
log_file=self.app_args.log_file
)
columns = [
'config',
'container',

View File

@ -13,36 +13,40 @@
import collections
import json
import logging
import random
import string
import subprocess
from paunch.utils import common
from paunch.utils import systemd
LOG = logging.getLogger(__name__)
class BaseRunner(object):
def __init__(self, managed_by, cont_cmd):
def __init__(self, managed_by, cont_cmd, log=None):
self.managed_by = managed_by
self.cont_cmd = cont_cmd
# Leverage pre-configured logger
self.log = log or common.configure_logging(__name__)
@staticmethod
def execute(cmd):
LOG.debug('$ %s' % ' '.join(cmd))
def execute(cmd, log=None):
if not log:
log = common.configure_logging(__name__)
log.debug('$ %s' % ' '.join(cmd))
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
cmd_stdout, cmd_stderr = subproc.communicate()
LOG.debug(cmd_stdout)
LOG.debug(cmd_stderr)
log.debug(cmd_stdout)
log.debug(cmd_stderr)
return (cmd_stdout.decode('utf-8'),
cmd_stderr.decode('utf-8'),
subproc.returncode)
@staticmethod
def execute_interactive(cmd):
LOG.debug('$ %s' % ' '.join(cmd))
def execute_interactive(cmd, log=None):
if not log:
log = common.configure_logging(__name__)
log.debug('$ %s' % ' '.join(cmd))
return subprocess.call(cmd)
def current_config_ids(self):
@ -52,7 +56,7 @@ class BaseRunner(object):
'--filter', 'label=managed_by=%s' % self.managed_by,
'--format', '{{.Label "config_id"}}'
]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
return set()
return set(cmd_stdout.split())
@ -63,7 +67,7 @@ class BaseRunner(object):
'--filter', 'label=managed_by=%s' % self.managed_by,
'--filter', 'label=config_id=%s' % conf_id
]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
return []
@ -75,7 +79,7 @@ class BaseRunner(object):
cmd.append('--format')
cmd.append(format)
cmd.append(name)
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd)
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd, self.log)
if returncode != 0:
return
try:
@ -84,7 +88,8 @@ class BaseRunner(object):
else:
return json.loads(cmd_stdout)[0]
except Exception as e:
LOG.error('Problem parsing %s inspect: %s' % (e, self.cont_cmd))
self.log.error('Problem parsing %s inspect: %s' %
(e, self.cont_cmd))
def unique_container_name(self, container):
container_name = container
@ -106,7 +111,7 @@ class BaseRunner(object):
'--format',
'{{.Names}}'
]
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd)
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd, self.log)
if returncode != 0:
return container
names = cmd_stdout.split()
@ -120,7 +125,8 @@ class BaseRunner(object):
for conf_id in self.current_config_ids():
if conf_id not in config_ids:
LOG.debug('%s no longer exists, deleting containers' % conf_id)
self.log.debug('%s no longer exists, deleting containers' %
conf_id)
self.remove_containers(conf_id)
def list_configs(self):
@ -143,7 +149,7 @@ class BaseRunner(object):
cmd.extend((
'--format', '{{.Names}} {{.Label "container_name"}}'
))
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
return
for line in cmd_stdout.split("\n"):
@ -156,12 +162,12 @@ class BaseRunner(object):
def remove_container(self, container):
if self.cont_cmd == 'podman':
systemd.service_delete(container)
systemd.service_delete(container=container, log=self.log)
cmd = [self.cont_cmd, 'rm', '-f', container]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
LOG.error('Error removing container: %s' % container)
LOG.error(cmd_stderr)
self.log.error('Error removing container: %s' % container)
self.log.error(cmd_stderr)
def rename_containers(self):
current_containers = []
@ -181,36 +187,36 @@ class BaseRunner(object):
for current, desired in sorted(need_renaming.items()):
if desired in current_containers:
LOG.info('Cannot rename "%s" since "%s" still exists' % (
self.log.info('Cannot rename "%s" since "%s" still exists' % (
current, desired))
else:
LOG.info('Renaming "%s" to "%s"' % (current, desired))
self.log.info('Renaming "%s" to "%s"' % (current, desired))
self.rename_container(current, desired)
current_containers.append(desired)
class DockerRunner(BaseRunner):
def __init__(self, managed_by, cont_cmd=None):
def __init__(self, managed_by, cont_cmd=None, log=None):
cont_cmd = cont_cmd or 'docker'
super(DockerRunner, self).__init__(managed_by, cont_cmd)
super(DockerRunner, self).__init__(managed_by, cont_cmd, log)
def rename_container(self, container, name):
cmd = [self.cont_cmd, 'rename', container, name]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
LOG.error('Error renaming container: %s' % container)
LOG.error(cmd_stderr)
self.log.error('Error renaming container: %s' % container)
self.log.error(cmd_stderr)
class PodmanRunner(BaseRunner):
def __init__(self, managed_by, cont_cmd=None):
def __init__(self, managed_by, cont_cmd=None, log=None):
cont_cmd = cont_cmd or 'podman'
super(PodmanRunner, self).__init__(managed_by, cont_cmd)
super(PodmanRunner, self).__init__(managed_by, cont_cmd, log)
def rename_container(self, container, name):
# TODO(emilien) podman doesn't support rename, we'll handle it
# in paunch itself, probably.
LOG.warning("container renaming isn't supported by podman")
self.log.warning("container renaming isn't supported by podman")
pass

View File

@ -94,40 +94,43 @@ class TestBaseBuilder(base.TestCase):
# inspect existing image centos:6
mock.call(
['docker', 'inspect', '--type', 'image',
'--format', 'exists', 'centos:6']
'--format', 'exists', 'centos:6'], mock.ANY
),
# inspect and pull missing image centos:7
mock.call(
['docker', 'inspect', '--type', 'image',
'--format', 'exists', 'centos:7']
'--format', 'exists', 'centos:7'], mock.ANY
),
# first pull attempt fails
mock.call(
['docker', 'pull', 'centos:7']
['docker', 'pull', 'centos:7'], mock.ANY
),
# second pull attempt succeeds
mock.call(
['docker', 'pull', 'centos:7']
['docker', 'pull', 'centos:7'], mock.ANY
),
# ps for delete_missing_and_updated container_names
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--filter', 'label=config_id=foo',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# ps for after delete_missing_and_updated renames
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# ps to only create containers which don't exist
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--filter', 'label=config_id=foo',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# run one
mock.call(
@ -136,7 +139,7 @@ class TestBaseBuilder(base.TestCase):
'--label', 'container_name=one',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['one']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# run two
mock.call(
@ -145,7 +148,7 @@ class TestBaseBuilder(base.TestCase):
'--label', 'container_name=two',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['two']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# run three
mock.call(
@ -154,7 +157,7 @@ class TestBaseBuilder(base.TestCase):
'--label', 'container_name=three',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['three']),
'--detach=true', 'centos:6']
'--detach=true', 'centos:6'], mock.ANY
),
# run four
mock.call(
@ -163,11 +166,11 @@ class TestBaseBuilder(base.TestCase):
'--label', 'container_name=four',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['four']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# execute within four
mock.call(
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
['docker', 'exec', 'four-12345678', 'ls', '-l', '/'], mock.ANY
),
])
@ -248,39 +251,42 @@ three-12345678 three''', '', 0),
# inspect image centos:7
mock.call(
['docker', 'inspect', '--type', 'image',
'--format', 'exists', 'centos:7']
'--format', 'exists', 'centos:7'], mock.ANY
),
# ps for delete_missing_and_updated container_names
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--filter', 'label=config_id=foo',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# rm containers not in config
mock.call(['docker', 'rm', '-f', 'five']),
mock.call(['docker', 'rm', '-f', 'six']),
mock.call(['docker', 'rm', '-f', 'five'], mock.ANY),
mock.call(['docker', 'rm', '-f', 'six'], mock.ANY),
# rm two, changed config
mock.call(['docker', 'inspect', '--type', 'container',
'--format', '{{index .Config.Labels "config_data"}}',
'two-12345678']),
mock.call(['docker', 'rm', '-f', 'two-12345678']),
'two-12345678'], mock.ANY),
mock.call(['docker', 'rm', '-f', 'two-12345678'], mock.ANY),
# check three, config hasn't changed
mock.call(['docker', 'inspect', '--type', 'container',
'--format', '{{index .Config.Labels "config_data"}}',
'three-12345678']),
'three-12345678'], mock.ANY),
# ps for after delete_missing_and_updated renames
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# ps to only create containers which don't exist
mock.call(
['docker', 'ps', '-a',
'--filter', 'label=managed_by=tester',
'--filter', 'label=config_id=foo',
'--format', '{{.Names}} {{.Label "container_name"}}']
'--format', '{{.Names}} {{.Label "container_name"}}'],
mock.ANY
),
# run one
mock.call(
@ -289,7 +295,7 @@ three-12345678 three''', '', 0),
'--label', 'container_name=one',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['one']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# run two
mock.call(
@ -298,7 +304,7 @@ three-12345678 three''', '', 0),
'--label', 'container_name=two',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['two']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# don't run three, its already running
# run four
@ -308,11 +314,11 @@ three-12345678 three''', '', 0),
'--label', 'container_name=four',
'--label', 'managed_by=tester',
'--label', 'config_data=%s' % json.dumps(config['four']),
'--detach=true', 'centos:7']
'--detach=true', 'centos:7'], mock.ANY
),
# execute within four
mock.call(
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
['docker', 'exec', 'four-12345678', 'ls', '-l', '/'], mock.ANY
),
])
@ -370,15 +376,15 @@ three-12345678 three''', '', 0),
# inspect existing image centos:6
mock.call(
['docker', 'inspect', '--type', 'image',
'--format', 'exists', 'centos:6']
'--format', 'exists', 'centos:6'], mock.ANY
),
# inspect and pull missing image centos:7
mock.call(
['docker', 'inspect', '--type', 'image',
'--format', 'exists', 'centos:7']
'--format', 'exists', 'centos:7'], mock.ANY
),
mock.call(
['docker', 'pull', 'centos:7']
['docker', 'pull', 'centos:7'], mock.ANY
),
])

View File

@ -24,12 +24,13 @@ class TestPaunch(base.TestCase):
@mock.patch('paunch.runner.DockerRunner', autospec=True)
def test_apply(self, runner, builder):
paunch.apply('foo', {'bar': 'baz'}, 'tester')
runner.assert_called_once_with('tester', cont_cmd=None)
runner.assert_called_once_with('tester', cont_cmd=None, log=mock.ANY)
builder.assert_called_once_with(
config_id='foo',
config={'bar': 'baz'},
runner=runner.return_value,
labels=None
labels=None,
log=mock.ANY
)
builder.return_value.apply.assert_called_once_with()
@ -42,19 +43,20 @@ class TestPaunch(base.TestCase):
managed_by='tester',
labels={'bink': 'boop'})
runner.assert_called_once_with('tester', cont_cmd=None)
runner.assert_called_once_with('tester', cont_cmd=None, log=mock.ANY)
builder.assert_called_once_with(
config_id='foo',
config={'bar': 'baz'},
runner=runner.return_value,
labels={'bink': 'boop'}
labels={'bink': 'boop'},
log=mock.ANY
)
builder.return_value.apply.assert_called_once_with()
@mock.patch('paunch.runner.DockerRunner', autospec=True)
def test_cleanup(self, runner):
paunch.cleanup(['foo', 'bar'], 'tester')
runner.assert_called_once_with('tester', cont_cmd=None)
runner.assert_called_once_with('tester', cont_cmd=None, log=mock.ANY)
runner.return_value.delete_missing_configs.assert_called_once_with(
['foo', 'bar'])
runner.return_value.rename_containers.assert_called_once_with()
@ -62,13 +64,13 @@ class TestPaunch(base.TestCase):
@mock.patch('paunch.runner.DockerRunner', autospec=True)
def test_list(self, runner):
paunch.list('tester')
runner.assert_called_once_with('tester', cont_cmd=None)
runner.assert_called_once_with('tester', cont_cmd=None, log=mock.ANY)
runner.return_value.list_configs.assert_called_once_with()
@mock.patch('paunch.runner.DockerRunner', autospec=True)
def test_delete(self, runner):
paunch.delete(['foo', 'bar'], 'tester')
runner.assert_called_once_with('tester', cont_cmd=None)
runner.assert_called_once_with('tester', cont_cmd=None, log=mock.ANY)
runner.return_value.remove_containers.assert_has_calls([
mock.call('foo'), mock.call('bar')
])
@ -77,11 +79,13 @@ class TestPaunch(base.TestCase):
@mock.patch('paunch.runner.DockerRunner')
def test_debug(self, runner, builder):
paunch.debug('foo', 'testcont', 'run', {'bar': 'baz'}, 'tester',
cont_cmd='docker')
cont_cmd='docker', log_level=42, log_file='/dev/null')
builder.assert_called_once_with(
config_id='foo',
config={'bar': 'baz'},
runner=runner.return_value,
labels=None
labels=None,
log=mock.ANY
)
runner.assert_called_once_with('tester', cont_cmd='docker')
runner.assert_called_once_with('tester', cont_cmd='docker',
log=mock.ANY)

46
paunch/utils/common.py Normal file
View File

@ -0,0 +1,46 @@
# Copyright 2018 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import sys
def configure_logging(name, level=3, log_file=None):
'''Mimic oslo_log default levels and formatting for the logger. '''
log = logging.getLogger(name)
if level and level > 2:
ll = logging.DEBUG
elif level and level == 2:
ll = logging.INFO
else:
ll = logging.WARNING
log.setLevel(ll)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(ll)
if log_file:
fhandler = logging.FileHandler(log_file)
formatter = logging.Formatter(
'%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
'%(name)s [ ] %(message)s',
'%Y-%m-%d %H:%M:%S')
fhandler.setLevel(ll)
fhandler.setFormatter(formatter)
log.addHandler(fhandler)
log.addHandler(handler)
log.propagate = False
return log

View File

@ -13,14 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import subprocess
LOG = logging.getLogger(__name__)
from paunch.utils import common
def service_create(container, cconfig, sysdir='/etc/systemd/system/'):
def service_create(container, cconfig, sysdir='/etc/systemd/system/',
log=None):
"""Create a service in systemd
:param container: container name
@ -31,7 +31,11 @@ def service_create(container, cconfig, sysdir='/etc/systemd/system/'):
:param sysdir: systemd unit files directory
:type sysdir: string
:param log: optional pre-defined logger for messages
:type log: logging.RootLogger
"""
log = log or common.configure_logging(__name__)
wants = " ".join(str(x) + '.service' for x in
cconfig.get('depends_on', []))
@ -46,7 +50,7 @@ def service_create(container, cconfig, sysdir='/etc/systemd/system/'):
restart = 'always'
sysd_unit_f = sysdir + container + '.service'
LOG.debug('Creating systemd unit file: %s' % sysd_unit_f)
log.debug('Creating systemd unit file: %s' % sysd_unit_f)
s_config = {
'name': container,
'wants': wants,
@ -70,20 +74,24 @@ WantedBy=multi-user.target""" % s_config)
subprocess.call(['systemctl', 'daemon-reload'])
def service_delete(container):
def service_delete(container, log=None):
"""Delete a service in systemd
:param container: container name
:type container: String
:param log: optional pre-defined logger for messages
:type log: logging.RootLogger
"""
log = log or common.configure_logging(__name__)
sysd_unit_f = '/etc/systemd/system/' + container + '.service'
if os.path.isfile(sysd_unit_f):
LOG.debug('Stopping and disabling systemd service for %s' % container)
log.debug('Stopping and disabling systemd service for %s' % container)
subprocess.call(['systemctl', 'stop', container])
subprocess.call(['systemctl', 'disable', container])
LOG.debug('Removing systemd unit file %s' % sysd_unit_f)
log.debug('Removing systemd unit file %s' % sysd_unit_f)
os.remove(sysd_unit_f)
subprocess.call(['systemctl', 'daemon-reload'])
else:
LOG.warning('No systemd unit file was found for %s' % container)
log.warning('No systemd unit file was found for %s' % container)

View File

@ -0,0 +1,5 @@
---
other:
- |
Logging verbosity and destination file can be controled with ``--debug``
``--verbose`` and ``--log-file``.