diff --git a/ansible-core-sitemap.xml b/ansible-core-sitemap.xml
index 84a048d3116..ea05e356f36 100644
--- a/ansible-core-sitemap.xml
+++ b/ansible-core-sitemap.xml
@@ -112,6 +112,11 @@
weekly
0.5
+
+ http://docs.ansible.com/ansible/playbooks_debugger.html
+ weekly
+ 0.5
+
http://docs.ansible.com/ansible/become.html
weekly
@@ -2713,4 +2718,4 @@
0.3
-
\ No newline at end of file
+
diff --git a/docsite/rst/playbooks_debugger.rst b/docsite/rst/playbooks_debugger.rst
new file mode 100644
index 00000000000..dc22d7b9a47
--- /dev/null
+++ b/docsite/rst/playbooks_debugger.rst
@@ -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 `_
+ Have a question? Stop by the google group!
+ `irc.freenode.net `_
+ #ansible IRC chat channel
diff --git a/docsite/rst/playbooks_special_topics.rst b/docsite/rst/playbooks_special_topics.rst
index 6593d20a54a..3e31d38e220 100644
--- a/docsite/rst/playbooks_special_topics.rst
+++ b/docsite/rst/playbooks_special_topics.rst
@@ -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
diff --git a/docsite/rst/playbooks_strategies.rst b/docsite/rst/playbooks_strategies.rst
index a9095bc0836..a34daa1c1f7 100644
--- a/docsite/rst/playbooks_strategies.rst
+++ b/docsite/rst/playbooks_strategies.rst
@@ -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`
diff --git a/lib/ansible/plugins/strategy/debug.py b/lib/ansible/plugins/strategy/debug.py
new file mode 100644
index 00000000000..fbfe01cbe1d
--- /dev/null
+++ b/lib/ansible/plugins/strategy/debug.py
@@ -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', '', '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