Add debug tool to paunch

This change adds various debugging abilities to paunch.  It lets you:
- Dump yaml or json of a single container configuration.
- Run a single container with a given configuration.
- Run a single container with overridden configuration elements.
- Print out the run command used to start a container.

Change-Id: If8995e1c94034e1b22cd92951ee3fd702048323b
This commit is contained in:
Ian Main 2017-06-23 16:52:47 -04:00
parent 039f2c9c02
commit efdcacc0de
8 changed files with 294 additions and 1 deletions

View File

@ -24,6 +24,8 @@ Features
becomes available.
* Accessable via the ``paunch`` command line utility, or by importing python
package ``paunch``.
* Builtin ``debug`` command lets you see how individual containers are run,
get configuration information for them, and run them any way you need to.
Running Paunch Commands
-----------------------
@ -147,6 +149,72 @@ updated image. As such it is recommended that stable image tags such as
release version tag in the configuration data is the recommended way of
propagating image changes to the running containers.
Debugging with Paunch
---------------------
The ``paunch debug`` command allows you to perform specific actions on a given
container. This can be used to:
* Run a container with a specific configuration.
* Dump the configuration of a given container in either json or yaml.
* Output the docker command line used to start the container.
* Run a container with any configuration additions you wish such that you can
run it with a shell as any user etc.
The configuration options you will likely be interested in here include:
::
--file <file> YAML or JSON file containing configuration data
--action <name> Action can be one of: "dump-json", "dump-yaml",
"print-cmd", or "run"
--container <name> Name of the container you wish to manipulate
--interactive Run container in interactive mode - modifies config
and execution of container
--shell Similar to interactive but drops you into a shell
--user <name> Start container as the specified user
--overrides <name> JSON configuration information used to override
default config values
``file`` is the name of the configuration file to use
containing the configuration for the container you wish to use.
Here is an example of using ``paunch debug`` to start a root shell inside the
test container:
::
# paunch debug --file examples/hello-world.yml --interactive --shell --user root --container hello --action run
This will drop you an interactive session inside the hello world container
starting /bin/bash running as root.
To see how this container is started normally:
::
# paunch debug --file examples/hello-world.yml --container hello --action print-cmd
You can also dump the configuration of this to a file so you can edit
it and rerun it with different a different configuration. This is more
useful when there are multiple configurations in a single file:
::
# paunch debug --file examples/hello-world.yml --container hello --action dump-json > hello.json
You can then use ``hello.json`` as your ``--file`` argument after
editing it to your liking.
You can also add any configuration elements you wish on the command line
to test paunch or debug containers etc. In this example I'm running
the hello container with ``net=host``.
::
# paunch debug --file examples/hello-world.yml --overrides '{"net": "host"}' --container hello --action run
Configuration Format
--------------------
@ -201,6 +269,15 @@ privileged:
restart:
String. Restart policy to apply when a container exits.
remove:
Boolean: Remove container after running.
interactive:
Boolean: Run container in interactive mode.
tty:
Boolean: Allocate a tty to interact with the container.
user:
String. Sets the username or UID used and optionally the groupname or GID for
the specified command.

View File

@ -12,9 +12,10 @@
'''Stable library interface to managing containers with paunch.'''
import json
import logging
import pbr.version
import yaml
from paunch.builder import compose1
from paunch import runner
@ -79,6 +80,63 @@ def list(managed_by, docker_cmd=None):
return r.list_configs()
def debug(config_id, container_name, action, config, managed_by, labels=None,
docker_cmd=None):
"""Execute supplied container configuration.
:param str config_id: Unique config ID, should not be re-used until any
running containers with that config ID have been
deleted.
:param str container_name: Name of the container in the config you
wish to manipulate.
:param str action: Action to take.
:param dict config: Configuration data describing container actions to
apply.
:param str managed_by: Name of the tool managing the containers. Only
containers labeled with this will be modified.
:param dict labels: Optional keys/values of labels to apply to containers
created with this invocation.
:param str docker_cmd: Optional override to the docker command to run.
:returns integer return value from running command or failure for any
other reason.
:rtype: int
"""
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
builder = compose1.ComposeV1Builder(
config_id=config_id,
config=config,
runner=r,
labels=labels
)
if action == 'print-cmd':
cmd = [
r.docker_cmd,
'run',
'--name',
r.unique_container_name(container_name)
]
builder.docker_run_args(cmd, container_name)
print(' '.join(cmd))
elif action == 'run':
cmd = [
r.docker_cmd,
'run',
'--name',
r.unique_container_name(container_name)
]
builder.docker_run_args(cmd, container_name)
return r.execute_interactive(cmd)
elif action == 'dump-yaml':
print(yaml.safe_dump(config, default_flow_style=False))
elif action == 'dump-json':
print(json.dumps(config, indent=4))
else:
raise ValueError('action should be one of: "dump-json", "dump-yaml"',
'"print-cmd", or "run"')
def delete(config_ids, managed_by, docker_cmd=None):
"""Delete containers with the specified config IDs.

View File

@ -135,6 +135,12 @@ class ComposeV1Builder(object):
for v in cconfig.get('environment', []):
if v:
cmd.append('--env=%s' % v)
if cconfig.get('remove', False):
cmd.append('--rm')
if cconfig.get('interactive', False):
cmd.append('--interactive')
if cconfig.get('tty', False):
cmd.append('--tty')
if 'net' in cconfig:
cmd.append('--net=%s' % cconfig['net'])
if 'ipc' in cconfig:

View File

@ -16,6 +16,7 @@ import logging
from cliff import command
from cliff import lister
import json
import yaml
import paunch
@ -129,6 +130,134 @@ class Delete(command.Command):
paunch.delete(parsed_args.config_id, parsed_args.managed_by)
class Debug(command.Command):
log = logging.getLogger(__name__)
def get_parser(self, prog_name):
parser = super(Debug, self).get_parser(prog_name)
parser.add_argument(
'--file',
metavar='<file>',
required=True,
help=('YAML or JSON file containing configuration data')
)
parser.add_argument(
'--label',
metavar='<label=value>',
dest='labels',
default=[],
help=('Extra labels to apply to containers in this config, in the '
'form label=value.')
)
parser.add_argument(
'--managed-by',
metavar='<name>',
dest='managed_by',
default='paunch',
help=('Override the name of the tool managing the containers')
)
parser.add_argument(
'--action',
metavar='<name>',
dest='action',
default='print-cmd',
help=('Action can be one of: "dump-json", "dump-yaml", '
'"print-cmd", or "run"')
)
parser.add_argument(
'--container',
metavar='<name>',
dest='container_name',
required=True,
help=('Name of the container you wish to manipulate')
)
parser.add_argument(
'--interactive',
dest='interactive',
action='store_true',
default=False,
help=('Run container in interactive mode - modifies config and '
'execution of container')
)
parser.add_argument(
'--shell',
dest='shell',
action='store_true',
default=False,
help=('Similar to interactive but drops you into a shell')
)
parser.add_argument(
'--user',
metavar='<name>',
dest='user',
default='',
help=('Start container as the specified user')
)
parser.add_argument(
'--overrides',
metavar='<name>',
dest='overrides',
default='',
help=('JSON configuration information used to override default '
'config values')
)
parser.add_argument(
'--config-id',
metavar='<name>',
dest='config_id',
required=False,
default='debug',
help=('ID to assign to containers')
)
return parser
def take_action(self, parsed_args):
labels = collections.OrderedDict()
for l in parsed_args.labels:
k, v = l.split(('='), 1)
labels[k] = v
with open(parsed_args.file, 'r') as f:
config = yaml.safe_load(f)
container_name = parsed_args.container_name
cconfig = {}
cconfig[container_name] = config[container_name]
if parsed_args.interactive or parsed_args.shell:
iconfig = {
"interactive": True,
"tty": True,
"restart": "no",
"detach": False,
"remove": True
}
cconfig[container_name].update(iconfig)
if parsed_args.shell:
sconfig = {"command": "/bin/bash"}
cconfig[container_name].update(sconfig)
if parsed_args.user:
rconfig = {"user": parsed_args.user}
cconfig[container_name].update(rconfig)
conf_overrides = []
if parsed_args.overrides:
conf_overrides = json.loads(parsed_args.overrides)
cconfig[container_name].update(conf_overrides)
paunch.debug(
parsed_args.config_id,
container_name,
parsed_args.action,
cconfig,
parsed_args.managed_by,
labels=labels
)
class List(lister.Lister):
log = logging.getLogger(__name__)

View File

@ -40,6 +40,11 @@ class DockerRunner(object):
cmd_stderr.decode('utf-8'),
subproc.returncode)
@staticmethod
def execute_interactive(cmd):
LOG.debug('$ %s' % ' '.join(cmd))
return subprocess.call(cmd)
def current_config_ids(self):
# List all config_id labels for managed containers
cmd = [

View File

@ -347,6 +347,9 @@ three-12345678 three''', '', 0),
'image': 'centos:7',
'detach': False,
'command': 'ls -l /foo',
'remove': True,
'tty': True,
'interactive': True,
'environment': ['FOO=BAR', 'BAR=BAZ'],
'env_file': ['/tmp/foo.env', '/tmp/bar.env'],
'volumes': ['/foo:/foo:rw', '/bar:/bar:ro'],
@ -361,6 +364,7 @@ three-12345678 three''', '', 0),
['docker', 'run', '--name', 'one',
'--env-file=/tmp/foo.env', '--env-file=/tmp/bar.env',
'--env=FOO=BAR', '--env=BAR=BAZ',
'--rm', '--interactive', '--tty',
'--volume=/foo:/foo:rw', '--volume=/bar:/bar:ro',
'--volumes-from=two', '--volumes-from=three',
'centos:7', 'ls', '-l', '/foo'],

View File

@ -72,3 +72,16 @@ class TestPaunch(base.TestCase):
runner.return_value.remove_containers.assert_has_calls([
mock.call('foo'), mock.call('bar')
])
@mock.patch('paunch.builder.compose1.ComposeV1Builder', autospec=True)
@mock.patch('paunch.runner.DockerRunner')
def test_debug(self, runner, builder):
paunch.debug('foo', 'testcont', 'run', {'bar': 'baz'}, 'tester',
docker_cmd='docker')
builder.assert_called_once_with(
config_id='foo',
config={'bar': 'baz'},
runner=runner.return_value,
labels=None
)
runner.assert_called_once_with('tester', docker_cmd='docker')

View File

@ -31,6 +31,7 @@ paunch =
cleanup = paunch.cmd:Cleanup
delete = paunch.cmd:Delete
list = paunch.cmd:List
debug = paunch.cmd:Debug
[build_sphinx]
source-dir = doc/source