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:
parent
039f2c9c02
commit
efdcacc0de
77
README.rst
77
README.rst
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
129
paunch/cmd.py
129
paunch/cmd.py
@ -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__)
|
||||
|
@ -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 = [
|
||||
|
@ -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'],
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user