From 8a754e0858d922e955e71b253c139e071ecec432 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:04:21 +0200 Subject: Adding upstream version 2.14.3. Signed-off-by: Daniel Baumann --- lib/ansible/plugins/callback/__init__.py | 610 +++++++++++++++++++++++++++++++ lib/ansible/plugins/callback/default.py | 409 +++++++++++++++++++++ lib/ansible/plugins/callback/junit.py | 364 ++++++++++++++++++ lib/ansible/plugins/callback/minimal.py | 80 ++++ lib/ansible/plugins/callback/oneline.py | 77 ++++ lib/ansible/plugins/callback/tree.py | 86 +++++ 6 files changed, 1626 insertions(+) create mode 100644 lib/ansible/plugins/callback/__init__.py create mode 100644 lib/ansible/plugins/callback/default.py create mode 100644 lib/ansible/plugins/callback/junit.py create mode 100644 lib/ansible/plugins/callback/minimal.py create mode 100644 lib/ansible/plugins/callback/oneline.py create mode 100644 lib/ansible/plugins/callback/tree.py (limited to 'lib/ansible/plugins/callback') diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py new file mode 100644 index 0000000..d4fc347 --- /dev/null +++ b/lib/ansible/plugins/callback/__init__.py @@ -0,0 +1,610 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import difflib +import json +import re +import sys +import textwrap + +from collections import OrderedDict +from collections.abc import MutableMapping +from copy import deepcopy + +from ansible import constants as C +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.six import text_type +from ansible.parsing.ajson import AnsibleJSONEncoder +from ansible.parsing.yaml.dumper import AnsibleDumper +from ansible.parsing.yaml.objects import AnsibleUnicode +from ansible.plugins import AnsiblePlugin +from ansible.utils.color import stringc +from ansible.utils.display import Display +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText +from ansible.vars.clean import strip_internal_keys, module_response_deepcopy + +import yaml + +global_display = Display() + + +__all__ = ["CallbackBase"] + + +_DEBUG_ALLOWED_KEYS = frozenset(('msg', 'exception', 'warnings', 'deprecations')) +_YAML_TEXT_TYPES = (text_type, AnsibleUnicode, AnsibleUnsafeText, NativeJinjaUnsafeText) +# Characters that libyaml/pyyaml consider breaks +_YAML_BREAK_CHARS = '\n\x85\u2028\u2029' # NL, NEL, LS, PS +# regex representation of libyaml/pyyaml of a space followed by a break character +_SPACE_BREAK_RE = re.compile(fr' +([{_YAML_BREAK_CHARS}])') + + +class _AnsibleCallbackDumper(AnsibleDumper): + def __init__(self, lossy=False): + self._lossy = lossy + + def __call__(self, *args, **kwargs): + # pyyaml expects that we are passing an object that can be instantiated, but to + # smuggle the ``lossy`` configuration, we do that in ``__init__`` and then + # define this ``__call__`` that will mimic the ability for pyyaml to instantiate class + super().__init__(*args, **kwargs) + return self + + +def _should_use_block(scalar): + """Returns true if string should be in block format based on the existence of various newline separators""" + # This method of searching is faster than using a regex + for ch in _YAML_BREAK_CHARS: + if ch in scalar: + return True + return False + + +class _SpecialCharacterTranslator: + def __getitem__(self, ch): + # "special character" logic from pyyaml yaml.emitter.Emitter.analyze_scalar, translated to decimal + # for perf w/ str.translate + if (ch == 10 or + 32 <= ch <= 126 or + ch == 133 or + 160 <= ch <= 55295 or + 57344 <= ch <= 65533 or + 65536 <= ch < 1114111)\ + and ch != 65279: + return ch + return None + + +def _filter_yaml_special(scalar): + """Filter a string removing any character that libyaml/pyyaml declare as special""" + return scalar.translate(_SpecialCharacterTranslator()) + + +def _munge_data_for_lossy_yaml(scalar): + """Modify a string so that analyze_scalar in libyaml/pyyaml will allow block formatting""" + # we care more about readability than accuracy, so... + # ...libyaml/pyyaml does not permit trailing spaces for block scalars + scalar = scalar.rstrip() + # ...libyaml/pyyaml does not permit tabs for block scalars + scalar = scalar.expandtabs() + # ...libyaml/pyyaml only permits special characters for double quoted scalars + scalar = _filter_yaml_special(scalar) + # ...libyaml/pyyaml only permits spaces followed by breaks for double quoted scalars + return _SPACE_BREAK_RE.sub(r'\1', scalar) + + +def _pretty_represent_str(self, data): + """Uses block style for multi-line strings""" + data = text_type(data) + if _should_use_block(data): + style = '|' + if self._lossy: + data = _munge_data_for_lossy_yaml(data) + else: + style = self.default_style + + node = yaml.representer.ScalarNode('tag:yaml.org,2002:str', data, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node + + +for data_type in _YAML_TEXT_TYPES: + _AnsibleCallbackDumper.add_representer( + data_type, + _pretty_represent_str + ) + + +class CallbackBase(AnsiblePlugin): + + ''' + This is a base ansible callback class that does nothing. New callbacks should + use this class as a base and override any callback methods they wish to execute + custom actions. + ''' + + def __init__(self, display=None, options=None): + if display: + self._display = display + else: + self._display = global_display + + if self._display.verbosity >= 4: + name = getattr(self, 'CALLBACK_NAME', 'unnamed') + ctype = getattr(self, 'CALLBACK_TYPE', 'old') + version = getattr(self, 'CALLBACK_VERSION', '1.0') + self._display.vvvv('Loading callback plugin %s of type %s, v%s from %s' % (name, ctype, version, sys.modules[self.__module__].__file__)) + + self.disabled = False + self.wants_implicit_tasks = False + + self._plugin_options = {} + if options is not None: + self.set_options(options) + + self._hide_in_debug = ('changed', 'failed', 'skipped', 'invocation', 'skip_reason') + + ''' helper for callbacks, so they don't all have to include deepcopy ''' + _copy_result = deepcopy + + def set_option(self, k, v): + self._plugin_options[k] = v + + def get_option(self, k): + return self._plugin_options[k] + + def set_options(self, task_keys=None, var_options=None, direct=None): + ''' This is different than the normal plugin method as callbacks get called early and really don't accept keywords. + Also _options was already taken for CLI args and callbacks use _plugin_options instead. + ''' + + # load from config + self._plugin_options = C.config.get_plugin_options(self.plugin_type, self._load_name, keys=task_keys, variables=var_options, direct=direct) + + @staticmethod + def host_label(result): + """Return label for the hostname (& delegated hostname) of a task + result. + """ + label = "%s" % result._host.get_name() + if result._task.delegate_to and result._task.delegate_to != result._host.get_name(): + # show delegated host + label += " -> %s" % result._task.delegate_to + # in case we have 'extra resolution' + ahost = result._result.get('_ansible_delegated_vars', {}).get('ansible_host', result._task.delegate_to) + if result._task.delegate_to != ahost: + label += "(%s)" % ahost + return label + + def _run_is_verbose(self, result, verbosity=0): + return ((self._display.verbosity > verbosity or result._result.get('_ansible_verbose_always', False) is True) + and result._result.get('_ansible_verbose_override', False) is False) + + def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False, serialize=True): + try: + result_format = self.get_option('result_format') + except KeyError: + # Callback does not declare result_format nor extend result_format_callback + result_format = 'json' + + try: + pretty_results = self.get_option('pretty_results') + except KeyError: + # Callback does not declare pretty_results nor extend result_format_callback + pretty_results = None + + indent_conditions = ( + result.get('_ansible_verbose_always'), + pretty_results is None and result_format != 'json', + pretty_results is True, + self._display.verbosity > 2, + ) + + if not indent and any(indent_conditions): + indent = 4 + if pretty_results is False: + # pretty_results=False overrides any specified indentation + indent = None + + # All result keys stating with _ansible_ are internal, so remove them from the result before we output anything. + abridged_result = strip_internal_keys(module_response_deepcopy(result)) + + # remove invocation unless specifically wanting it + if not keep_invocation and self._display.verbosity < 3 and 'invocation' in result: + del abridged_result['invocation'] + + # remove diff information from screen output + if self._display.verbosity < 3 and 'diff' in result: + del abridged_result['diff'] + + # remove exception from screen output + if 'exception' in abridged_result: + del abridged_result['exception'] + + if not serialize: + # Just return ``abridged_result`` without going through serialization + # to permit callbacks to take advantage of ``_dump_results`` + # that want to further modify the result, or use custom serialization + return abridged_result + + if result_format == 'json': + try: + return json.dumps(abridged_result, cls=AnsibleJSONEncoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys) + except TypeError: + # Python3 bug: throws an exception when keys are non-homogenous types: + # https://bugs.python.org/issue25457 + # sort into an OrderedDict and then json.dumps() that instead + if not OrderedDict: + raise + return json.dumps(OrderedDict(sorted(abridged_result.items(), key=to_text)), + cls=AnsibleJSONEncoder, indent=indent, + ensure_ascii=False, sort_keys=False) + elif result_format == 'yaml': + # None is a sentinel in this case that indicates default behavior + # default behavior for yaml is to prettify results + lossy = pretty_results in (None, True) + if lossy: + # if we already have stdout, we don't need stdout_lines + if 'stdout' in abridged_result and 'stdout_lines' in abridged_result: + abridged_result['stdout_lines'] = '' + + # if we already have stderr, we don't need stderr_lines + if 'stderr' in abridged_result and 'stderr_lines' in abridged_result: + abridged_result['stderr_lines'] = '' + + return '\n%s' % textwrap.indent( + yaml.dump( + abridged_result, + allow_unicode=True, + Dumper=_AnsibleCallbackDumper(lossy=lossy), + default_flow_style=False, + indent=indent, + # sort_keys=sort_keys # This requires PyYAML>=5.1 + ), + ' ' * (indent or 4) + ) + + def _handle_warnings(self, res): + ''' display warnings, if enabled and any exist in the result ''' + if C.ACTION_WARNINGS: + if 'warnings' in res and res['warnings']: + for warning in res['warnings']: + self._display.warning(warning) + del res['warnings'] + if 'deprecations' in res and res['deprecations']: + for warning in res['deprecations']: + self._display.deprecated(**warning) + del res['deprecations'] + + def _handle_exception(self, result, use_stderr=False): + + if 'exception' in result: + msg = "An exception occurred during task execution. " + exception_str = to_text(result['exception']) + if self._display.verbosity < 3: + # extract just the actual error message from the exception text + error = exception_str.strip().split('\n')[-1] + msg += "To see the full traceback, use -vvv. The error was: %s" % error + else: + msg = "The full traceback is:\n" + exception_str + del result['exception'] + + self._display.display(msg, color=C.COLOR_ERROR, stderr=use_stderr) + + def _serialize_diff(self, diff): + try: + result_format = self.get_option('result_format') + except KeyError: + # Callback does not declare result_format nor extend result_format_callback + result_format = 'json' + + try: + pretty_results = self.get_option('pretty_results') + except KeyError: + # Callback does not declare pretty_results nor extend result_format_callback + pretty_results = None + + if result_format == 'json': + return json.dumps(diff, sort_keys=True, indent=4, separators=(u',', u': ')) + u'\n' + elif result_format == 'yaml': + # None is a sentinel in this case that indicates default behavior + # default behavior for yaml is to prettify results + lossy = pretty_results in (None, True) + return '%s\n' % textwrap.indent( + yaml.dump( + diff, + allow_unicode=True, + Dumper=_AnsibleCallbackDumper(lossy=lossy), + default_flow_style=False, + indent=4, + # sort_keys=sort_keys # This requires PyYAML>=5.1 + ), + ' ' + ) + + def _get_diff(self, difflist): + + if not isinstance(difflist, list): + difflist = [difflist] + + ret = [] + for diff in difflist: + if 'dst_binary' in diff: + ret.append(u"diff skipped: destination file appears to be binary\n") + if 'src_binary' in diff: + ret.append(u"diff skipped: source file appears to be binary\n") + if 'dst_larger' in diff: + ret.append(u"diff skipped: destination file size is greater than %d\n" % diff['dst_larger']) + if 'src_larger' in diff: + ret.append(u"diff skipped: source file size is greater than %d\n" % diff['src_larger']) + if 'before' in diff and 'after' in diff: + # format complex structures into 'files' + for x in ['before', 'after']: + if isinstance(diff[x], MutableMapping): + diff[x] = self._serialize_diff(diff[x]) + elif diff[x] is None: + diff[x] = '' + if 'before_header' in diff: + before_header = u"before: %s" % diff['before_header'] + else: + before_header = u'before' + if 'after_header' in diff: + after_header = u"after: %s" % diff['after_header'] + else: + after_header = u'after' + before_lines = diff['before'].splitlines(True) + after_lines = diff['after'].splitlines(True) + if before_lines and not before_lines[-1].endswith(u'\n'): + before_lines[-1] += u'\n\\ No newline at end of file\n' + if after_lines and not after_lines[-1].endswith('\n'): + after_lines[-1] += u'\n\\ No newline at end of file\n' + differ = difflib.unified_diff(before_lines, + after_lines, + fromfile=before_header, + tofile=after_header, + fromfiledate=u'', + tofiledate=u'', + n=C.DIFF_CONTEXT) + difflines = list(differ) + has_diff = False + for line in difflines: + has_diff = True + if line.startswith(u'+'): + line = stringc(line, C.COLOR_DIFF_ADD) + elif line.startswith(u'-'): + line = stringc(line, C.COLOR_DIFF_REMOVE) + elif line.startswith(u'@@'): + line = stringc(line, C.COLOR_DIFF_LINES) + ret.append(line) + if has_diff: + ret.append('\n') + if 'prepared' in diff: + ret.append(diff['prepared']) + return u''.join(ret) + + def _get_item_label(self, result): + ''' retrieves the value to be displayed as a label for an item entry from a result object''' + if result.get('_ansible_no_log', False): + item = "(censored due to no_log)" + else: + item = result.get('_ansible_item_label', result.get('item')) + return item + + def _process_items(self, result): + # just remove them as now they get handled by individual callbacks + del result._result['results'] + + def _clean_results(self, result, task_name): + ''' removes data from results for display ''' + + # mostly controls that debug only outputs what it was meant to + if task_name in C._ACTION_DEBUG: + if 'msg' in result: + # msg should be alone + for key in list(result.keys()): + if key not in _DEBUG_ALLOWED_KEYS and not key.startswith('_'): + result.pop(key) + else: + # 'var' value as field, so eliminate others and what is left should be varname + for hidme in self._hide_in_debug: + result.pop(hidme, None) + + def _print_task_path(self, task, color=C.COLOR_DEBUG): + path = task.get_path() + if path: + self._display.display(u"task path: %s" % path, color=color) + + def set_play_context(self, play_context): + pass + + def on_any(self, *args, **kwargs): + pass + + def runner_on_failed(self, host, res, ignore_errors=False): + pass + + def runner_on_ok(self, host, res): + pass + + def runner_on_skipped(self, host, item=None): + pass + + def runner_on_unreachable(self, host, res): + pass + + def runner_on_no_hosts(self): + pass + + def runner_on_async_poll(self, host, res, jid, clock): + pass + + def runner_on_async_ok(self, host, res, jid): + pass + + def runner_on_async_failed(self, host, res, jid): + pass + + def playbook_on_start(self): + pass + + def playbook_on_notify(self, host, handler): + pass + + def playbook_on_no_hosts_matched(self): + pass + + def playbook_on_no_hosts_remaining(self): + pass + + def playbook_on_task_start(self, name, is_conditional): + pass + + def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None, unsafe=None): + pass + + def playbook_on_setup(self): + pass + + def playbook_on_import_for_host(self, host, imported_file): + pass + + def playbook_on_not_import_for_host(self, host, missing_file): + pass + + def playbook_on_play_start(self, name): + pass + + def playbook_on_stats(self, stats): + pass + + def on_file_diff(self, host, diff): + pass + + # V2 METHODS, by default they call v1 counterparts if possible + def v2_on_any(self, *args, **kwargs): + self.on_any(args, kwargs) + + def v2_runner_on_failed(self, result, ignore_errors=False): + host = result._host.get_name() + self.runner_on_failed(host, result._result, ignore_errors) + + def v2_runner_on_ok(self, result): + host = result._host.get_name() + self.runner_on_ok(host, result._result) + + def v2_runner_on_skipped(self, result): + if C.DISPLAY_SKIPPED_HOSTS: + host = result._host.get_name() + self.runner_on_skipped(host, self._get_item_label(getattr(result._result, 'results', {}))) + + def v2_runner_on_unreachable(self, result): + host = result._host.get_name() + self.runner_on_unreachable(host, result._result) + + def v2_runner_on_async_poll(self, result): + host = result._host.get_name() + jid = result._result.get('ansible_job_id') + # FIXME, get real clock + clock = 0 + self.runner_on_async_poll(host, result._result, jid, clock) + + def v2_runner_on_async_ok(self, result): + host = result._host.get_name() + jid = result._result.get('ansible_job_id') + self.runner_on_async_ok(host, result._result, jid) + + def v2_runner_on_async_failed(self, result): + host = result._host.get_name() + # Attempt to get the async job ID. If the job does not finish before the + # async timeout value, the ID may be within the unparsed 'async_result' dict. + jid = result._result.get('ansible_job_id') + if not jid and 'async_result' in result._result: + jid = result._result['async_result'].get('ansible_job_id') + self.runner_on_async_failed(host, result._result, jid) + + def v2_playbook_on_start(self, playbook): + self.playbook_on_start() + + def v2_playbook_on_notify(self, handler, host): + self.playbook_on_notify(host, handler) + + def v2_playbook_on_no_hosts_matched(self): + self.playbook_on_no_hosts_matched() + + def v2_playbook_on_no_hosts_remaining(self): + self.playbook_on_no_hosts_remaining() + + def v2_playbook_on_task_start(self, task, is_conditional): + self.playbook_on_task_start(task.name, is_conditional) + + # FIXME: not called + def v2_playbook_on_cleanup_task_start(self, task): + pass # no v1 correspondence + + def v2_playbook_on_handler_task_start(self, task): + pass # no v1 correspondence + + def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None, unsafe=None): + self.playbook_on_vars_prompt(varname, private, prompt, encrypt, confirm, salt_size, salt, default, unsafe) + + # FIXME: not called + def v2_playbook_on_import_for_host(self, result, imported_file): + host = result._host.get_name() + self.playbook_on_import_for_host(host, imported_file) + + # FIXME: not called + def v2_playbook_on_not_import_for_host(self, result, missing_file): + host = result._host.get_name() + self.playbook_on_not_import_for_host(host, missing_file) + + def v2_playbook_on_play_start(self, play): + self.playbook_on_play_start(play.name) + + def v2_playbook_on_stats(self, stats): + self.playbook_on_stats(stats) + + def v2_on_file_diff(self, result): + if 'diff' in result._result: + host = result._host.get_name() + self.on_file_diff(host, result._result['diff']) + + def v2_playbook_on_include(self, included_file): + pass # no v1 correspondence + + def v2_runner_item_on_ok(self, result): + pass + + def v2_runner_item_on_failed(self, result): + pass + + def v2_runner_item_on_skipped(self, result): + pass + + def v2_runner_retry(self, result): + pass + + def v2_runner_on_start(self, host, task): + """Event used when host begins execution of a task + + .. versionadded:: 2.8 + """ + pass diff --git a/lib/ansible/plugins/callback/default.py b/lib/ansible/plugins/callback/default.py new file mode 100644 index 0000000..54ef452 --- /dev/null +++ b/lib/ansible/plugins/callback/default.py @@ -0,0 +1,409 @@ +# (c) 2012-2014, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: default + type: stdout + short_description: default Ansible screen output + version_added: historical + description: + - This is the default output callback for ansible-playbook. + extends_documentation_fragment: + - default_callback + - result_format_callback + requirements: + - set as stdout in configuration +''' + + +from ansible import constants as C +from ansible import context +from ansible.playbook.task_include import TaskInclude +from ansible.plugins.callback import CallbackBase +from ansible.utils.color import colorize, hostcolor +from ansible.utils.fqcn import add_internal_fqcns + + +class CallbackModule(CallbackBase): + + ''' + This is the default callback interface, which simply prints messages + to stdout when new callback events are received. + ''' + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'default' + + def __init__(self): + + self._play = None + self._last_task_banner = None + self._last_task_name = None + self._task_type_cache = {} + super(CallbackModule, self).__init__() + + def v2_runner_on_failed(self, result, ignore_errors=False): + + host_label = self.host_label(result) + self._clean_results(result._result, result._task.action) + + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + self._handle_exception(result._result, use_stderr=self.get_option('display_failed_stderr')) + self._handle_warnings(result._result) + + if result._task.loop and 'results' in result._result: + self._process_items(result) + + else: + if self._display.verbosity < 2 and self.get_option('show_task_path_on_failure'): + self._print_task_path(result._task) + msg = "fatal: [%s]: FAILED! => %s" % (host_label, self._dump_results(result._result)) + self._display.display(msg, color=C.COLOR_ERROR, stderr=self.get_option('display_failed_stderr')) + + if ignore_errors: + self._display.display("...ignoring", color=C.COLOR_SKIP) + + def v2_runner_on_ok(self, result): + + host_label = self.host_label(result) + + if isinstance(result._task, TaskInclude): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + return + elif result._result.get('changed', False): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + msg = "changed: [%s]" % (host_label,) + color = C.COLOR_CHANGED + else: + if not self.get_option('display_ok_hosts'): + return + + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + msg = "ok: [%s]" % (host_label,) + color = C.COLOR_OK + + self._handle_warnings(result._result) + + if result._task.loop and 'results' in result._result: + self._process_items(result) + else: + self._clean_results(result._result, result._task.action) + + if self._run_is_verbose(result): + msg += " => %s" % (self._dump_results(result._result),) + self._display.display(msg, color=color) + + def v2_runner_on_skipped(self, result): + + if self.get_option('display_skipped_hosts'): + + self._clean_results(result._result, result._task.action) + + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + if result._task.loop is not None and 'results' in result._result: + self._process_items(result) + + msg = "skipping: [%s]" % result._host.get_name() + if self._run_is_verbose(result): + msg += " => %s" % self._dump_results(result._result) + self._display.display(msg, color=C.COLOR_SKIP) + + def v2_runner_on_unreachable(self, result): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + host_label = self.host_label(result) + msg = "fatal: [%s]: UNREACHABLE! => %s" % (host_label, self._dump_results(result._result)) + self._display.display(msg, color=C.COLOR_UNREACHABLE, stderr=self.get_option('display_failed_stderr')) + + if result._task.ignore_unreachable: + self._display.display("...ignoring", color=C.COLOR_SKIP) + + def v2_playbook_on_no_hosts_matched(self): + self._display.display("skipping: no hosts matched", color=C.COLOR_SKIP) + + def v2_playbook_on_no_hosts_remaining(self): + self._display.banner("NO MORE HOSTS LEFT") + + def v2_playbook_on_task_start(self, task, is_conditional): + self._task_start(task, prefix='TASK') + + def _task_start(self, task, prefix=None): + # Cache output prefix for task if provided + # This is needed to properly display 'RUNNING HANDLER' and similar + # when hiding skipped/ok task results + if prefix is not None: + self._task_type_cache[task._uuid] = prefix + + # Preserve task name, as all vars may not be available for templating + # when we need it later + if self._play.strategy in add_internal_fqcns(('free', 'host_pinned')): + # Explicitly set to None for strategy free/host_pinned to account for any cached + # task title from a previous non-free play + self._last_task_name = None + else: + self._last_task_name = task.get_name().strip() + + # Display the task banner immediately if we're not doing any filtering based on task result + if self.get_option('display_skipped_hosts') and self.get_option('display_ok_hosts'): + self._print_task_banner(task) + + def _print_task_banner(self, task): + # args can be specified as no_log in several places: in the task or in + # the argument spec. We can check whether the task is no_log but the + # argument spec can't be because that is only run on the target + # machine and we haven't run it thereyet at this time. + # + # So we give people a config option to affect display of the args so + # that they can secure this if they feel that their stdout is insecure + # (shoulder surfing, logging stdout straight to a file, etc). + args = '' + if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT: + args = u', '.join(u'%s=%s' % a for a in task.args.items()) + args = u' %s' % args + + prefix = self._task_type_cache.get(task._uuid, 'TASK') + + # Use cached task name + task_name = self._last_task_name + if task_name is None: + task_name = task.get_name().strip() + + if task.check_mode and self.get_option('check_mode_markers'): + checkmsg = " [CHECK MODE]" + else: + checkmsg = "" + self._display.banner(u"%s [%s%s]%s" % (prefix, task_name, args, checkmsg)) + + if self._display.verbosity >= 2: + self._print_task_path(task) + + self._last_task_banner = task._uuid + + def v2_playbook_on_cleanup_task_start(self, task): + self._task_start(task, prefix='CLEANUP TASK') + + def v2_playbook_on_handler_task_start(self, task): + self._task_start(task, prefix='RUNNING HANDLER') + + def v2_runner_on_start(self, host, task): + if self.get_option('show_per_host_start'): + self._display.display(" [started %s on %s]" % (task, host), color=C.COLOR_OK) + + def v2_playbook_on_play_start(self, play): + name = play.get_name().strip() + if play.check_mode and self.get_option('check_mode_markers'): + checkmsg = " [CHECK MODE]" + else: + checkmsg = "" + if not name: + msg = u"PLAY%s" % checkmsg + else: + msg = u"PLAY [%s]%s" % (name, checkmsg) + + self._play = play + + self._display.banner(msg) + + def v2_on_file_diff(self, result): + if result._task.loop and 'results' in result._result: + for res in result._result['results']: + if 'diff' in res and res['diff'] and res.get('changed', False): + diff = self._get_diff(res['diff']) + if diff: + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + self._display.display(diff) + elif 'diff' in result._result and result._result['diff'] and result._result.get('changed', False): + diff = self._get_diff(result._result['diff']) + if diff: + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + self._display.display(diff) + + def v2_runner_item_on_ok(self, result): + + host_label = self.host_label(result) + if isinstance(result._task, TaskInclude): + return + elif result._result.get('changed', False): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + msg = 'changed' + color = C.COLOR_CHANGED + else: + if not self.get_option('display_ok_hosts'): + return + + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + msg = 'ok' + color = C.COLOR_OK + + msg = "%s: [%s] => (item=%s)" % (msg, host_label, self._get_item_label(result._result)) + self._clean_results(result._result, result._task.action) + if self._run_is_verbose(result): + msg += " => %s" % self._dump_results(result._result) + self._display.display(msg, color=color) + + def v2_runner_item_on_failed(self, result): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + host_label = self.host_label(result) + self._clean_results(result._result, result._task.action) + self._handle_exception(result._result, use_stderr=self.get_option('display_failed_stderr')) + + msg = "failed: [%s]" % (host_label,) + self._handle_warnings(result._result) + self._display.display( + msg + " (item=%s) => %s" % (self._get_item_label(result._result), self._dump_results(result._result)), + color=C.COLOR_ERROR, + stderr=self.get_option('display_failed_stderr') + ) + + def v2_runner_item_on_skipped(self, result): + if self.get_option('display_skipped_hosts'): + if self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) + + self._clean_results(result._result, result._task.action) + msg = "skipping: [%s] => (item=%s) " % (result._host.get_name(), self._get_item_label(result._result)) + if self._run_is_verbose(result): + msg += " => %s" % self._dump_results(result._result) + self._display.display(msg, color=C.COLOR_SKIP) + + def v2_playbook_on_include(self, included_file): + msg = 'included: %s for %s' % (included_file._filename, ", ".join([h.name for h in included_file._hosts])) + label = self._get_item_label(included_file._vars) + if label: + msg += " => (item=%s)" % label + self._display.display(msg, color=C.COLOR_SKIP) + + def v2_playbook_on_stats(self, stats): + self._display.banner("PLAY RECAP") + + hosts = sorted(stats.processed.keys()) + for h in hosts: + t = stats.summarize(h) + + self._display.display( + u"%s : %s %s %s %s %s %s %s" % ( + hostcolor(h, t), + colorize(u'ok', t['ok'], C.COLOR_OK), + colorize(u'changed', t['changed'], C.COLOR_CHANGED), + colorize(u'unreachable', t['unreachable'], C.COLOR_UNREACHABLE), + colorize(u'failed', t['failures'], C.COLOR_ERROR), + colorize(u'skipped', t['skipped'], C.COLOR_SKIP), + colorize(u'rescued', t['rescued'], C.COLOR_OK), + colorize(u'ignored', t['ignored'], C.COLOR_WARN), + ), + screen_only=True + ) + + self._display.display( + u"%s : %s %s %s %s %s %s %s" % ( + hostcolor(h, t, False), + colorize(u'ok', t['ok'], None), + colorize(u'changed', t['changed'], None), + colorize(u'unreachable', t['unreachable'], None), + colorize(u'failed', t['failures'], None), + colorize(u'skipped', t['skipped'], None), + colorize(u'rescued', t['rescued'], None), + colorize(u'ignored', t['ignored'], None), + ), + log_only=True + ) + + self._display.display("", screen_only=True) + + # print custom stats if required + if stats.custom and self.get_option('show_custom_stats'): + self._display.banner("CUSTOM STATS: ") + # per host + # TODO: come up with 'pretty format' + for k in sorted(stats.custom.keys()): + if k == '_run': + continue + self._display.display('\t%s: %s' % (k, self._dump_results(stats.custom[k], indent=1).replace('\n', ''))) + + # print per run custom stats + if '_run' in stats.custom: + self._display.display("", screen_only=True) + self._display.display('\tRUN: %s' % self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')) + self._display.display("", screen_only=True) + + if context.CLIARGS['check'] and self.get_option('check_mode_markers'): + self._display.banner("DRY RUN") + + def v2_playbook_on_start(self, playbook): + if self._display.verbosity > 1: + from os.path import basename + self._display.banner("PLAYBOOK: %s" % basename(playbook._file_name)) + + # show CLI arguments + if self._display.verbosity > 3: + if context.CLIARGS.get('args'): + self._display.display('Positional arguments: %s' % ' '.join(context.CLIARGS['args']), + color=C.COLOR_VERBOSE, screen_only=True) + + for argument in (a for a in context.CLIARGS if a != 'args'): + val = context.CLIARGS[argument] + if val: + self._display.display('%s: %s' % (argument, val), color=C.COLOR_VERBOSE, screen_only=True) + + if context.CLIARGS['check'] and self.get_option('check_mode_markers'): + self._display.banner("DRY RUN") + + def v2_runner_retry(self, result): + task_name = result.task_name or result._task + host_label = self.host_label(result) + msg = "FAILED - RETRYING: [%s]: %s (%d retries left)." % (host_label, task_name, result._result['retries'] - result._result['attempts']) + if self._run_is_verbose(result, verbosity=2): + msg += "Result was: %s" % self._dump_results(result._result) + self._display.display(msg, color=C.COLOR_DEBUG) + + def v2_runner_on_async_poll(self, result): + host = result._host.get_name() + jid = result._result.get('ansible_job_id') + started = result._result.get('started') + finished = result._result.get('finished') + self._display.display( + 'ASYNC POLL on %s: jid=%s started=%s finished=%s' % (host, jid, started, finished), + color=C.COLOR_DEBUG + ) + + def v2_runner_on_async_ok(self, result): + host = result._host.get_name() + jid = result._result.get('ansible_job_id') + self._display.display("ASYNC OK on %s: jid=%s" % (host, jid), color=C.COLOR_DEBUG) + + def v2_runner_on_async_failed(self, result): + host = result._host.get_name() + + # Attempt to get the async job ID. If the job does not finish before the + # async timeout value, the ID may be within the unparsed 'async_result' dict. + jid = result._result.get('ansible_job_id') + if not jid and 'async_result' in result._result: + jid = result._result['async_result'].get('ansible_job_id') + self._display.display("ASYNC FAILED on %s: jid=%s" % (host, jid), color=C.COLOR_DEBUG) + + def v2_playbook_on_notify(self, handler, host): + if self._display.verbosity > 1: + self._display.display("NOTIFIED HANDLER %s for %s" % (handler.get_name(), host), color=C.COLOR_VERBOSE, screen_only=True) diff --git a/lib/ansible/plugins/callback/junit.py b/lib/ansible/plugins/callback/junit.py new file mode 100644 index 0000000..75cdbc7 --- /dev/null +++ b/lib/ansible/plugins/callback/junit.py @@ -0,0 +1,364 @@ +# (c) 2016 Matt Clay +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: junit + type: aggregate + short_description: write playbook output to a JUnit file. + version_added: historical + description: + - This callback writes playbook output to a JUnit formatted XML file. + - "Tasks show up in the report as follows: + 'ok': pass + 'failed' with 'EXPECTED FAILURE' in the task name: pass + 'failed' with 'TOGGLE RESULT' in the task name: pass + 'ok' with 'TOGGLE RESULT' in the task name: failure + 'failed' due to an exception: error + 'failed' for other reasons: failure + 'skipped': skipped" + options: + output_dir: + name: JUnit output dir + default: ~/.ansible.log + description: Directory to write XML files to. + env: + - name: JUNIT_OUTPUT_DIR + task_class: + name: JUnit Task class + default: False + description: Configure the output to be one class per yaml file + env: + - name: JUNIT_TASK_CLASS + task_relative_path: + name: JUnit Task relative path + default: none + description: Configure the output to use relative paths to given directory + version_added: "2.8" + env: + - name: JUNIT_TASK_RELATIVE_PATH + replace_out_of_tree_path: + name: Replace out of tree path + default: none + description: Replace the directory portion of an out-of-tree relative task path with the given placeholder + version_added: "2.12.3" + env: + - name: JUNIT_REPLACE_OUT_OF_TREE_PATH + fail_on_change: + name: JUnit fail on change + default: False + description: Consider any tasks reporting "changed" as a junit test failure + env: + - name: JUNIT_FAIL_ON_CHANGE + fail_on_ignore: + name: JUnit fail on ignore + default: False + description: Consider failed tasks as a junit test failure even if ignore_on_error is set + env: + - name: JUNIT_FAIL_ON_IGNORE + include_setup_tasks_in_report: + name: JUnit include setup tasks in report + default: True + description: Should the setup tasks be included in the final report + env: + - name: JUNIT_INCLUDE_SETUP_TASKS_IN_REPORT + hide_task_arguments: + name: Hide the arguments for a task + default: False + description: Hide the arguments for a task + version_added: "2.8" + env: + - name: JUNIT_HIDE_TASK_ARGUMENTS + test_case_prefix: + name: Prefix to find actual test cases + default: + description: Consider a task only as test case if it has this value as prefix. Additionally failing tasks are recorded as failed test cases. + version_added: "2.8" + env: + - name: JUNIT_TEST_CASE_PREFIX + requirements: + - enable in configuration +''' + +import os +import time +import re + +from ansible import constants as C +from ansible.module_utils._text import to_bytes, to_text +from ansible.plugins.callback import CallbackBase +from ansible.utils._junit_xml import ( + TestCase, + TestError, + TestFailure, + TestSuite, + TestSuites, +) + + +class CallbackModule(CallbackBase): + """ + This callback writes playbook output to a JUnit formatted XML file. + + Tasks show up in the report as follows: + 'ok': pass + 'failed' with 'EXPECTED FAILURE' in the task name: pass + 'failed' with 'TOGGLE RESULT' in the task name: pass + 'ok' with 'TOGGLE RESULT' in the task name: failure + 'failed' due to an exception: error + 'failed' for other reasons: failure + 'skipped': skipped + + This plugin makes use of the following environment variables: + JUNIT_OUTPUT_DIR (optional): Directory to write XML files to. + Default: ~/.ansible.log + JUNIT_TASK_CLASS (optional): Configure the output to be one class per yaml file + Default: False + JUNIT_TASK_RELATIVE_PATH (optional): Configure the output to use relative paths to given directory + Default: none + JUNIT_FAIL_ON_CHANGE (optional): Consider any tasks reporting "changed" as a junit test failure + Default: False + JUNIT_FAIL_ON_IGNORE (optional): Consider failed tasks as a junit test failure even if ignore_on_error is set + Default: False + JUNIT_INCLUDE_SETUP_TASKS_IN_REPORT (optional): Should the setup tasks be included in the final report + Default: True + JUNIT_HIDE_TASK_ARGUMENTS (optional): Hide the arguments for a task + Default: False + JUNIT_TEST_CASE_PREFIX (optional): Consider a task only as test case if it has this value as prefix. Additionally failing tasks are recorded as failed + test cases. + Default: + """ + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'aggregate' + CALLBACK_NAME = 'junit' + CALLBACK_NEEDS_ENABLED = True + + def __init__(self): + super(CallbackModule, self).__init__() + + self._output_dir = os.getenv('JUNIT_OUTPUT_DIR', os.path.expanduser('~/.ansible.log')) + self._task_class = os.getenv('JUNIT_TASK_CLASS', 'False').lower() + self._task_relative_path = os.getenv('JUNIT_TASK_RELATIVE_PATH', '') + self._fail_on_change = os.getenv('JUNIT_FAIL_ON_CHANGE', 'False').lower() + self._fail_on_ignore = os.getenv('JUNIT_FAIL_ON_IGNORE', 'False').lower() + self._include_setup_tasks_in_report = os.getenv('JUNIT_INCLUDE_SETUP_TASKS_IN_REPORT', 'True').lower() + self._hide_task_arguments = os.getenv('JUNIT_HIDE_TASK_ARGUMENTS', 'False').lower() + self._test_case_prefix = os.getenv('JUNIT_TEST_CASE_PREFIX', '') + self._replace_out_of_tree_path = os.getenv('JUNIT_REPLACE_OUT_OF_TREE_PATH', None) + self._playbook_path = None + self._playbook_name = None + self._play_name = None + self._task_data = None + + self.disabled = False + + self._task_data = {} + + if self._replace_out_of_tree_path is not None: + self._replace_out_of_tree_path = to_text(self._replace_out_of_tree_path) + + if not os.path.exists(self._output_dir): + os.makedirs(self._output_dir) + + def _start_task(self, task): + """ record the start of a task for one or more hosts """ + + uuid = task._uuid + + if uuid in self._task_data: + return + + play = self._play_name + name = task.get_name().strip() + path = task.get_path() + action = task.action + + if not task.no_log and self._hide_task_arguments == 'false': + args = ', '.join(('%s=%s' % a for a in task.args.items())) + if args: + name += ' ' + args + + self._task_data[uuid] = TaskData(uuid, name, path, play, action) + + def _finish_task(self, status, result): + """ record the results of a task for a single host """ + + task_uuid = result._task._uuid + + if hasattr(result, '_host'): + host_uuid = result._host._uuid + host_name = result._host.name + else: + host_uuid = 'include' + host_name = 'include' + + task_data = self._task_data[task_uuid] + + if self._fail_on_change == 'true' and status == 'ok' and result._result.get('changed', False): + status = 'failed' + + # ignore failure if expected and toggle result if asked for + if status == 'failed' and 'EXPECTED FAILURE' in task_data.name: + status = 'ok' + elif 'TOGGLE RESULT' in task_data.name: + if status == 'failed': + status = 'ok' + elif status == 'ok': + status = 'failed' + + if task_data.name.startswith(self._test_case_prefix) or status == 'failed': + task_data.add_host(HostData(host_uuid, host_name, status, result)) + + def _build_test_case(self, task_data, host_data): + """ build a TestCase from the given TaskData and HostData """ + + name = '[%s] %s: %s' % (host_data.name, task_data.play, task_data.name) + duration = host_data.finish - task_data.start + + if self._task_relative_path and task_data.path: + junit_classname = to_text(os.path.relpath(to_bytes(task_data.path), to_bytes(self._task_relative_path))) + else: + junit_classname = task_data.path + + if self._replace_out_of_tree_path is not None and junit_classname.startswith('../'): + junit_classname = self._replace_out_of_tree_path + to_text(os.path.basename(to_bytes(junit_classname))) + + if self._task_class == 'true': + junit_classname = re.sub(r'\.yml:[0-9]+$', '', junit_classname) + + if host_data.status == 'included': + return TestCase(name=name, classname=junit_classname, time=duration, system_out=str(host_data.result)) + + res = host_data.result._result + rc = res.get('rc', 0) + dump = self._dump_results(res, indent=0) + dump = self._cleanse_string(dump) + + if host_data.status == 'ok': + return TestCase(name=name, classname=junit_classname, time=duration, system_out=dump) + + test_case = TestCase(name=name, classname=junit_classname, time=duration) + + if host_data.status == 'failed': + if 'exception' in res: + message = res['exception'].strip().split('\n')[-1] + output = res['exception'] + test_case.errors.append(TestError(message=message, output=output)) + elif 'msg' in res: + message = res['msg'] + test_case.failures.append(TestFailure(message=message, output=dump)) + else: + test_case.failures.append(TestFailure(message='rc=%s' % rc, output=dump)) + elif host_data.status == 'skipped': + if 'skip_reason' in res: + message = res['skip_reason'] + else: + message = 'skipped' + test_case.skipped = message + + return test_case + + def _cleanse_string(self, value): + """ convert surrogate escapes to the unicode replacement character to avoid XML encoding errors """ + return to_text(to_bytes(value, errors='surrogateescape'), errors='replace') + + def _generate_report(self): + """ generate a TestSuite report from the collected TaskData and HostData """ + + test_cases = [] + + for task_uuid, task_data in self._task_data.items(): + if task_data.action in C._ACTION_SETUP and self._include_setup_tasks_in_report == 'false': + continue + + for host_uuid, host_data in task_data.host_data.items(): + test_cases.append(self._build_test_case(task_data, host_data)) + + test_suite = TestSuite(name=self._playbook_name, cases=test_cases) + test_suites = TestSuites(suites=[test_suite]) + report = test_suites.to_pretty_xml() + + output_file = os.path.join(self._output_dir, '%s-%s.xml' % (self._playbook_name, time.time())) + + with open(output_file, 'wb') as xml: + xml.write(to_bytes(report, errors='surrogate_or_strict')) + + def v2_playbook_on_start(self, playbook): + self._playbook_path = playbook._file_name + self._playbook_name = os.path.splitext(os.path.basename(self._playbook_path))[0] + + def v2_playbook_on_play_start(self, play): + self._play_name = play.get_name() + + def v2_runner_on_no_hosts(self, task): + self._start_task(task) + + def v2_playbook_on_task_start(self, task, is_conditional): + self._start_task(task) + + def v2_playbook_on_cleanup_task_start(self, task): + self._start_task(task) + + def v2_playbook_on_handler_task_start(self, task): + self._start_task(task) + + def v2_runner_on_failed(self, result, ignore_errors=False): + if ignore_errors and self._fail_on_ignore != 'true': + self._finish_task('ok', result) + else: + self._finish_task('failed', result) + + def v2_runner_on_ok(self, result): + self._finish_task('ok', result) + + def v2_runner_on_skipped(self, result): + self._finish_task('skipped', result) + + def v2_playbook_on_include(self, included_file): + self._finish_task('included', included_file) + + def v2_playbook_on_stats(self, stats): + self._generate_report() + + +class TaskData: + """ + Data about an individual task. + """ + + def __init__(self, uuid, name, path, play, action): + self.uuid = uuid + self.name = name + self.path = path + self.play = play + self.start = None + self.host_data = {} + self.start = time.time() + self.action = action + + def add_host(self, host): + if host.uuid in self.host_data: + if host.status == 'included': + # concatenate task include output from multiple items + host.result = '%s\n%s' % (self.host_data[host.uuid].result, host.result) + else: + raise Exception('%s: %s: %s: duplicate host callback: %s' % (self.path, self.play, self.name, host.name)) + + self.host_data[host.uuid] = host + + +class HostData: + """ + Data about an individual host. + """ + + def __init__(self, uuid, name, status, result): + self.uuid = uuid + self.name = name + self.status = status + self.result = result + self.finish = time.time() diff --git a/lib/ansible/plugins/callback/minimal.py b/lib/ansible/plugins/callback/minimal.py new file mode 100644 index 0000000..c4d713f --- /dev/null +++ b/lib/ansible/plugins/callback/minimal.py @@ -0,0 +1,80 @@ +# (c) 2012-2014, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: minimal + type: stdout + short_description: minimal Ansible screen output + version_added: historical + description: + - This is the default output callback used by the ansible command (ad-hoc) + extends_documentation_fragment: + - result_format_callback +''' + +from ansible.plugins.callback import CallbackBase +from ansible import constants as C + + +class CallbackModule(CallbackBase): + + ''' + This is the default callback interface, which simply prints messages + to stdout when new callback events are received. + ''' + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'minimal' + + def _command_generic_msg(self, host, result, caption): + ''' output the result of a command run ''' + + buf = "%s | %s | rc=%s >>\n" % (host, caption, result.get('rc', -1)) + buf += result.get('stdout', '') + buf += result.get('stderr', '') + buf += result.get('msg', '') + + return buf + "\n" + + def v2_runner_on_failed(self, result, ignore_errors=False): + + self._handle_exception(result._result) + self._handle_warnings(result._result) + + if result._task.action in C.MODULE_NO_JSON and 'module_stderr' not in result._result: + self._display.display(self._command_generic_msg(result._host.get_name(), result._result, "FAILED"), color=C.COLOR_ERROR) + else: + self._display.display("%s | FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result, indent=4)), color=C.COLOR_ERROR) + + def v2_runner_on_ok(self, result): + self._clean_results(result._result, result._task.action) + + self._handle_warnings(result._result) + + if result._result.get('changed', False): + color = C.COLOR_CHANGED + state = 'CHANGED' + else: + color = C.COLOR_OK + state = 'SUCCESS' + + if result._task.action in C.MODULE_NO_JSON and 'ansible_job_id' not in result._result: + self._display.display(self._command_generic_msg(result._host.get_name(), result._result, state), color=color) + else: + self._display.display("%s | %s => %s" % (result._host.get_name(), state, self._dump_results(result._result, indent=4)), color=color) + + def v2_runner_on_skipped(self, result): + self._display.display("%s | SKIPPED" % (result._host.get_name()), color=C.COLOR_SKIP) + + def v2_runner_on_unreachable(self, result): + self._display.display("%s | UNREACHABLE! => %s" % (result._host.get_name(), self._dump_results(result._result, indent=4)), color=C.COLOR_UNREACHABLE) + + def v2_on_file_diff(self, result): + if 'diff' in result._result and result._result['diff']: + self._display.display(self._get_diff(result._result['diff'])) diff --git a/lib/ansible/plugins/callback/oneline.py b/lib/ansible/plugins/callback/oneline.py new file mode 100644 index 0000000..fd51b27 --- /dev/null +++ b/lib/ansible/plugins/callback/oneline.py @@ -0,0 +1,77 @@ +# (c) 2012-2014, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: oneline + type: stdout + short_description: oneline Ansible screen output + version_added: historical + description: + - This is the output callback used by the -o/--one-line command line option. +''' + +from ansible.plugins.callback import CallbackBase +from ansible import constants as C + + +class CallbackModule(CallbackBase): + + ''' + This is the default callback interface, which simply prints messages + to stdout when new callback events are received. + ''' + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'oneline' + + def _command_generic_msg(self, hostname, result, caption): + stdout = result.get('stdout', '').replace('\n', '\\n').replace('\r', '\\r') + if 'stderr' in result and result['stderr']: + stderr = result.get('stderr', '').replace('\n', '\\n').replace('\r', '\\r') + return "%s | %s | rc=%s | (stdout) %s (stderr) %s" % (hostname, caption, result.get('rc', -1), stdout, stderr) + else: + return "%s | %s | rc=%s | (stdout) %s" % (hostname, caption, result.get('rc', -1), stdout) + + def v2_runner_on_failed(self, result, ignore_errors=False): + if 'exception' in result._result: + if self._display.verbosity < 3: + # extract just the actual error message from the exception text + error = result._result['exception'].strip().split('\n')[-1] + msg = "An exception occurred during task execution. To see the full traceback, use -vvv. The error was: %s" % error + else: + msg = "An exception occurred during task execution. The full traceback is:\n" + result._result['exception'].replace('\n', '') + + if result._task.action in C.MODULE_NO_JSON and 'module_stderr' not in result._result: + self._display.display(self._command_generic_msg(result._host.get_name(), result._result, 'FAILED'), color=C.COLOR_ERROR) + else: + self._display.display(msg, color=C.COLOR_ERROR) + + self._display.display("%s | FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result, indent=0).replace('\n', '')), + color=C.COLOR_ERROR) + + def v2_runner_on_ok(self, result): + + if result._result.get('changed', False): + color = C.COLOR_CHANGED + state = 'CHANGED' + else: + color = C.COLOR_OK + state = 'SUCCESS' + + if result._task.action in C.MODULE_NO_JSON and 'ansible_job_id' not in result._result: + self._display.display(self._command_generic_msg(result._host.get_name(), result._result, state), color=color) + else: + self._display.display("%s | %s => %s" % (result._host.get_name(), state, self._dump_results(result._result, indent=0).replace('\n', '')), + color=color) + + def v2_runner_on_unreachable(self, result): + self._display.display("%s | UNREACHABLE!: %s" % (result._host.get_name(), result._result.get('msg', '')), color=C.COLOR_UNREACHABLE) + + def v2_runner_on_skipped(self, result): + self._display.display("%s | SKIPPED" % (result._host.get_name()), color=C.COLOR_SKIP) diff --git a/lib/ansible/plugins/callback/tree.py b/lib/ansible/plugins/callback/tree.py new file mode 100644 index 0000000..a9f65d2 --- /dev/null +++ b/lib/ansible/plugins/callback/tree.py @@ -0,0 +1,86 @@ +# (c) 2012-2014, Ansible, Inc +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: tree + type: notification + requirements: + - invoked in the command line + short_description: Save host events to files + version_added: "2.0" + options: + directory: + version_added: '2.11' + description: directory that will contain the per host JSON files. Also set by the C(--tree) option when using adhoc. + ini: + - section: callback_tree + key: directory + env: + - name: ANSIBLE_CALLBACK_TREE_DIR + default: "~/.ansible/tree" + type: path + description: + - "This callback is used by the Ansible (adhoc) command line option C(-t|--tree)." + - This produces a JSON dump of events in a directory, a file for each host, the directory used MUST be passed as a command line option. +''' + +import os + +from ansible.constants import TREE_DIR +from ansible.module_utils._text import to_bytes, to_text +from ansible.plugins.callback import CallbackBase +from ansible.utils.path import makedirs_safe, unfrackpath + + +class CallbackModule(CallbackBase): + ''' + This callback puts results into a host specific file in a directory in json format. + ''' + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'aggregate' + CALLBACK_NAME = 'tree' + CALLBACK_NEEDS_ENABLED = True + + def set_options(self, task_keys=None, var_options=None, direct=None): + ''' override to set self.tree ''' + + super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) + + if TREE_DIR: + # TREE_DIR comes from the CLI option --tree, only available for adhoc + self.tree = unfrackpath(TREE_DIR) + else: + self.tree = self.get_option('directory') + + def write_tree_file(self, hostname, buf): + ''' write something into treedir/hostname ''' + + buf = to_bytes(buf) + try: + makedirs_safe(self.tree) + except (OSError, IOError) as e: + self._display.warning(u"Unable to access or create the configured directory (%s): %s" % (to_text(self.tree), to_text(e))) + + try: + path = to_bytes(os.path.join(self.tree, hostname)) + with open(path, 'wb+') as fd: + fd.write(buf) + except (OSError, IOError) as e: + self._display.warning(u"Unable to write to %s's file: %s" % (hostname, to_text(e))) + + def result_to_tree(self, result): + self.write_tree_file(result._host.get_name(), self._dump_results(result._result)) + + def v2_runner_on_ok(self, result): + self.result_to_tree(result) + + def v2_runner_on_failed(self, result, ignore_errors=False): + self.result_to_tree(result) + + def v2_runner_on_unreachable(self, result): + self.result_to_tree(result) -- cgit v1.2.3