Add debug strategy plugin (#15125)

* Add debug strategy plugin

* Fix Python 2-3 compatiblity issue

* Add document for debug strategy
This commit is contained in:
Kishin Yagami 2016-04-09 03:39:08 +09:00 committed by Brian Coca
parent 0eb2844cc6
commit e4a6106ea5
5 changed files with 330 additions and 1 deletions

View file

@ -112,6 +112,11 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.5</priority> <priority>0.5</priority>
</url> </url>
<url>
<loc>http://docs.ansible.com/ansible/playbooks_debugger.html</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url> <url>
<loc>http://docs.ansible.com/ansible/become.html</loc> <loc>http://docs.ansible.com/ansible/become.html</loc>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>

View file

@ -0,0 +1,159 @@
Playbook Debugger
=================
.. contents:: Topics
In 2.1 we added a ``debug`` strategy. This strategy enables you to invoke a debugger when a task is
failed, and check several info, such as the value of a variable. Also, it is possible to update module
arguments in the debugger, and run the failed task again with new arguments to consider how you
can fix an issue.
To use ``debug`` strategy, change ``strategy`` attribute like this::
- hosts: test
strategy: debug
tasks:
...
For example, run the playbook below::
- hosts: test
strategy: debug
gather_facts: no
vars:
var1: value1
tasks:
- name: wrong variable
ping: data={{ wrong_var }}
The debugger is invoked since *wrong_var* variable is undefined. Let's change the module's args,
and run the task again::
PLAY ***************************************************************************
TASK [wrong variable] **********************************************************
fatal: [192.168.1.1]: FAILED! => {"failed": true, "msg": "ERROR! 'wrong_var' is undefined"}
Debugger invoked
(debug) p result
{'msg': u"ERROR! 'wrong_var' is undefined", 'failed': True}
(debug) p task.args
{u'data': u'{{ wrong_var }}'}
(debug) task.args['data'] = '{{ var1 }}'
(debug) p task.args
{u'data': '{{ var1 }}'}
(debug) redo
ok: [192.168.1.1]
PLAY RECAP *********************************************************************
192.168.1.1 : ok=1 changed=0 unreachable=0 failed=0
This time, the task runs successfully!
.. _available_commands:
Available Commands
++++++++++++++++++
.. _p_command:
p *task/vars/host/result*
`````````````````````````
Print values used to execute a module::
(debug) p task
TASK: install package
(debug) p task.args
{u'name': u'{{ pkg_name }}'}
(debug) p vars
{u'ansible_all_ipv4_addresses': [u'192.168.1.1'],
u'ansible_architecture': u'x86_64',
...
}
(debug) p vars['pkg_name']
u'bash'
(debug) p host
192.168.1.1
(debug) p result
{'_ansible_no_log': False,
'changed': False,
u'failed': True,
...
u'msg': u"No package matching 'not_exist' is available"}
.. _update_args_command:
task.args[*key*] = *value*
``````````````````````````
Update module's argument.
If you run a playbook like this::
- hosts: test
strategy: debug
gather_facts: yes
vars:
pkg_name: not_exist
tasks:
- name: install package
apt: name={{ pkg_name }}
Debugger is invoked due to wrong package name, so let's fix the module's args::
(debug) p task.args
{u'name': u'{{ pkg_name }}'}
(debug) task.args['name'] = 'bash'
(debug) p task.args
{u'name': 'bash'}
(debug) redo
Then the task runs again with new args.
.. _update_vars_command:
vars[*key*] = *value*
`````````````````````
Update vars.
Let's use the same playbook above, but fix vars instead of args::
(debug) p vars['pkg_name']
u'not_exist'
(debug) vars['pkg_name'] = 'bash'
(debug) p vars['pkg_name']
'bash'
(debug) redo
Then the task runs again with new vars.
.. _redo_command:
r(edo)
``````
Run the task again.
.. _continue_command:
c(ontinue)
``````````
Just continue.
.. _quit_command:
q(uit)
``````
Quit from the debugger. The playbook execution is aborted.
.. seealso::
:doc:`playbooks`
An introduction to playbooks
`User Mailing List <http://groups.google.com/group/ansible-devel>`_
Have a question? Stop by the google group!
`irc.freenode.net <http://irc.freenode.net>`_
#ansible IRC chat channel

View file

@ -11,6 +11,7 @@ and adopt these only if they seem relevant or useful to your environment.
playbooks_acceleration playbooks_acceleration
playbooks_async playbooks_async
playbooks_checkmode playbooks_checkmode
playbooks_debugger
playbooks_delegation playbooks_delegation
playbooks_environment playbooks_environment
playbooks_error_handling playbooks_error_handling

View file

@ -26,6 +26,8 @@ The strategies are implemented via a new type of plugin, this means that in the
execution types can be added, either locally by users or to Ansible itself by execution types can be added, either locally by users or to Ansible itself by
a code contribution. a code contribution.
One example is ``debug`` strategy. See :doc:`playbooks_debugger` for details.
.. seealso:: .. seealso::
:doc:`playbooks` :doc:`playbooks`

View file

@ -0,0 +1,162 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import cmd
import pprint
import sys
from ansible.plugins.strategy import linear
from ansible.plugins.strategy import StrategyBase
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class NextAction(object):
""" The next action after an interpreter's exit. """
REDO = 1
CONTINUE = 2
EXIT = 3
def __init__(self, result=EXIT):
self.result = result
class StrategyModule(linear.StrategyModule, StrategyBase):
# Usually inheriting linear.StrategyModule is enough. However, StrategyBase class must be
# direct ancestor to be considered as strategy plugin, and so we inherit the class here.
def __init__(self, tqm):
self.curr_tqm = tqm
StrategyBase.__init__(self, tqm)
def _queue_task(self, host, task, task_vars, play_context):
self.curr_host = host
self.curr_task = task
self.curr_task_vars = task_vars
self.curr_play_context = play_context
StrategyBase._queue_task(self, host, task, task_vars, play_context)
def _process_pending_results(self, iterator, one_pass=False):
if not hasattr(self, "curr_host"):
return StrategyBase._process_pending_results(self, iterator, one_pass)
prev_host_state = iterator.get_host_state(self.curr_host)
results = StrategyBase._process_pending_results(self, iterator, one_pass)
while self._need_debug(results):
next_action = NextAction()
dbg = Debugger(self, results, next_action)
dbg.cmdloop()
if next_action.result == NextAction.REDO:
# rollback host state
self.curr_tqm.clear_failed_hosts()
iterator._host_states[self.curr_host.name] = prev_host_state
if reduce(lambda total, res : res.is_failed() or total, results, False):
self._tqm._stats.failures[self.curr_host.name] -= 1
elif reduce(lambda total, res : res.is_unreachable() or total, results, False):
self._tqm._stats.dark[self.curr_host.name] -= 1
# redo
StrategyBase._queue_task(self, self.curr_host, self.curr_task, self.curr_task_vars, self.curr_play_context)
results = StrategyBase._process_pending_results(self, iterator, one_pass)
elif next_action.result == NextAction.CONTINUE:
break
elif next_action.result == NextAction.EXIT:
exit(1)
return results
def _need_debug(self, results):
return reduce(lambda total, res : res.is_failed() or res.is_unreachable() or total, results, False)
class Debugger(cmd.Cmd):
prompt = '(debug) ' # debugger
prompt_continuous = '> ' # multiple lines
def __init__(self, strategy_module, results, next_action):
# cmd.Cmd is old-style class
cmd.Cmd.__init__(self)
self.intro = "Debugger invoked"
self.scope = {}
self.scope['task'] = strategy_module.curr_task
self.scope['vars'] = strategy_module.curr_task_vars
self.scope['host'] = strategy_module.curr_host
self.scope['result'] = results[0]._result
self.scope['results'] = results # for debug of this debugger
self.next_action = next_action
def cmdloop(self):
try:
cmd.Cmd.cmdloop(self)
except KeyboardInterrupt:
pass
def do_EOF(self, args):
return self.do_quit(args)
def do_quit(self, args):
display.display('aborted')
self.next_action.result = NextAction.EXIT
return True
do_q = do_quit
def do_continue(self, args):
self.next_action.result = NextAction.CONTINUE
return True
do_c = do_continue
def do_redo(self, args):
self.next_action.result = NextAction.REDO
return True
do_r = do_redo
def evaluate(self, args):
try:
return eval(args, globals(), self.scope)
except:
t, v = sys.exc_info()[:2]
if isinstance(t, str):
exc_type_name = t
else:
exc_type_name = t.__name__
display.display('***%s:%s' % (exc_type_name, repr(v)))
raise
def do_p(self, args):
try:
result = self.evaluate(args)
display.display(pprint.pformat(result))
except:
pass
def execute(self, args):
try:
code = compile(args + '\n', '<stdin>', 'single')
exec(code, globals(), self.scope)
except:
t, v = sys.exc_info()[:2]
if type(t) == type(''):
exc_type_name = t
else:
exc_type_name = t.__name__
display.display('***%s:%s' % (exc_type_name, repr(v)))
raise
def default(self, line):
try:
self.execute(line)
display.display(pprint.pformat(result))
except:
pass