From e4a6106ea5a2ee43e889821970ad3dc89bc82cc2 Mon Sep 17 00:00:00 2001 From: Kishin Yagami Date: Sat, 9 Apr 2016 03:39:08 +0900 Subject: [PATCH] Add debug strategy plugin (#15125) * Add debug strategy plugin * Fix Python 2-3 compatiblity issue * Add document for debug strategy --- ansible-core-sitemap.xml | 7 +- docsite/rst/playbooks_debugger.rst | 159 ++++++++++++++++++++++ docsite/rst/playbooks_special_topics.rst | 1 + docsite/rst/playbooks_strategies.rst | 2 + lib/ansible/plugins/strategy/debug.py | 162 +++++++++++++++++++++++ 5 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 docsite/rst/playbooks_debugger.rst create mode 100644 lib/ansible/plugins/strategy/debug.py 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