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>
<priority>0.5</priority>
</url>
<url>
<loc>http://docs.ansible.com/ansible/playbooks_debugger.html</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>http://docs.ansible.com/ansible/become.html</loc>
<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_async
playbooks_checkmode
playbooks_debugger
playbooks_delegation
playbooks_environment
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
a code contribution.
One example is ``debug`` strategy. See :doc:`playbooks_debugger` for details.
.. seealso::
: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