# -*- coding: utf-8 -*- # Copyright (c) 2016, Dag Wieers # Copyright (c) 2017 Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' name: dense type: stdout short_description: minimal stdout output extends_documentation_fragment: - default_callback description: - When in verbose mode it will act the same as the default callback. author: - Dag Wieers (@dagwieers) requirements: - set as stdout in configuration ''' HAS_OD = False try: from collections import OrderedDict HAS_OD = True except ImportError: pass from ansible.module_utils.six import binary_type, text_type from ansible.module_utils.common._collections_compat import MutableMapping, MutableSequence from ansible.plugins.callback.default import CallbackModule as CallbackModule_default from ansible.utils.color import colorize, hostcolor from ansible.utils.display import Display import sys display = Display() # Design goals: # # + On screen there should only be relevant stuff # - How far are we ? (during run, last line) # - What issues occurred # - What changes occurred # - Diff output (in diff-mode) # # + If verbosity increases, act as default output # So that users can easily switch to default for troubleshooting # # + Rewrite the output during processing # - We use the cursor to indicate where in the task we are. # Output after the prompt is the output of the previous task. # - If we would clear the line at the start of a task, there would often # be no information at all, so we leave it until it gets updated # # + Use the same color-conventions of Ansible # # + Ensure the verbose output (-v) is also dense. # Remove information that is not essential (eg. timestamps, status) # TODO: # # + Properly test for terminal capabilities, and fall back to default # + Modify Ansible mechanism so we don't need to use sys.stdout directly # + Find an elegant solution for progress bar line wrapping # FIXME: Importing constants as C simply does not work, beats me :-/ # from ansible import constants as C class C: COLOR_HIGHLIGHT = 'white' COLOR_VERBOSE = 'blue' COLOR_WARN = 'bright purple' COLOR_ERROR = 'red' COLOR_DEBUG = 'dark gray' COLOR_DEPRECATE = 'purple' COLOR_SKIP = 'cyan' COLOR_UNREACHABLE = 'bright red' COLOR_OK = 'green' COLOR_CHANGED = 'yellow' # Taken from Dstat class vt100: black = '\033[0;30m' darkred = '\033[0;31m' darkgreen = '\033[0;32m' darkyellow = '\033[0;33m' darkblue = '\033[0;34m' darkmagenta = '\033[0;35m' darkcyan = '\033[0;36m' gray = '\033[0;37m' darkgray = '\033[1;30m' red = '\033[1;31m' green = '\033[1;32m' yellow = '\033[1;33m' blue = '\033[1;34m' magenta = '\033[1;35m' cyan = '\033[1;36m' white = '\033[1;37m' blackbg = '\033[40m' redbg = '\033[41m' greenbg = '\033[42m' yellowbg = '\033[43m' bluebg = '\033[44m' magentabg = '\033[45m' cyanbg = '\033[46m' whitebg = '\033[47m' reset = '\033[0;0m' bold = '\033[1m' reverse = '\033[2m' underline = '\033[4m' clear = '\033[2J' # clearline = '\033[K' clearline = '\033[2K' save = '\033[s' restore = '\033[u' save_all = '\0337' restore_all = '\0338' linewrap = '\033[7h' nolinewrap = '\033[7l' up = '\033[1A' down = '\033[1B' right = '\033[1C' left = '\033[1D' colors = dict( ok=vt100.darkgreen, changed=vt100.darkyellow, skipped=vt100.darkcyan, ignored=vt100.cyanbg + vt100.red, failed=vt100.darkred, unreachable=vt100.red, ) states = ('skipped', 'ok', 'changed', 'failed', 'unreachable') class CallbackModule(CallbackModule_default): ''' This is the dense callback interface, where screen estate is still valued. ''' CALLBACK_VERSION = 2.0 CALLBACK_TYPE = 'stdout' CALLBACK_NAME = 'dense' def __init__(self): # From CallbackModule self._display = display if HAS_OD: self.disabled = False self.super_ref = super(CallbackModule, self) self.super_ref.__init__() # Attributes to remove from results for more density self.removed_attributes = ( # 'changed', 'delta', # 'diff', 'end', 'failed', 'failed_when_result', 'invocation', 'start', 'stdout_lines', ) # Initiate data structures self.hosts = OrderedDict() self.keep = False self.shown_title = False self.count = dict(play=0, handler=0, task=0) self.type = 'foo' # Start immediately on the first line sys.stdout.write(vt100.reset + vt100.save + vt100.clearline) sys.stdout.flush() else: display.warning("The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling.") self.disabled = True def __del__(self): sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) def _add_host(self, result, status): name = result._host.get_name() # Add a new status in case a failed task is ignored if status == 'failed' and result._task.ignore_errors: status = 'ignored' # Check if we have to update an existing state (when looping over items) if name not in self.hosts: self.hosts[name] = dict(state=status) elif states.index(self.hosts[name]['state']) < states.index(status): self.hosts[name]['state'] = status # Store delegated hostname, if needed delegated_vars = result._result.get('_ansible_delegated_vars', None) if delegated_vars: self.hosts[name]['delegate'] = delegated_vars['ansible_host'] # Print progress bar self._display_progress(result) # # Ensure that tasks with changes/failures stay on-screen, and during diff-mode # if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)): # Ensure that tasks with changes/failures stay on-screen if status in ['changed', 'failed', 'unreachable']: self.keep = True if self._display.verbosity == 1: # Print task title, if needed self._display_task_banner() self._display_results(result, status) def _clean_results(self, result): # Remove non-essential attributes for attr in self.removed_attributes: if attr in result: del result[attr] # Remove empty attributes (list, dict, str) for attr in result.copy(): if isinstance(result[attr], (MutableSequence, MutableMapping, binary_type, text_type)): if not result[attr]: del result[attr] def _handle_exceptions(self, result): if 'exception' in result: # Remove the exception from the result so it's not shown every time del result['exception'] if self._display.verbosity == 1: return "An exception occurred during task execution. To see the full traceback, use -vvv." def _display_progress(self, result=None): # Always rewrite the complete line sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline) sys.stdout.write('%s %d:' % (self.type, self.count[self.type])) sys.stdout.write(vt100.reset) sys.stdout.flush() # Print out each host in its own status-color for name in self.hosts: sys.stdout.write(' ') if self.hosts[name].get('delegate', None): sys.stdout.write(self.hosts[name]['delegate'] + '>') sys.stdout.write(colors[self.hosts[name]['state']] + name + vt100.reset) sys.stdout.flush() # if result._result.get('diff', False): # sys.stdout.write('\n' + vt100.linewrap) sys.stdout.write(vt100.linewrap) # self.keep = True def _display_task_banner(self): if not self.shown_title: self.shown_title = True sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline) sys.stdout.write('%s %d: %s' % (self.type, self.count[self.type], self.task.get_name().strip())) sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) sys.stdout.flush() else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline) self.keep = False def _display_results(self, result, status): # Leave the previous task on screen (as it has changes/errors) if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline) self.keep = False self._clean_results(result._result) dump = '' if result._task.action == 'include': return elif status == 'ok': return elif status == 'ignored': dump = self._handle_exceptions(result._result) elif status == 'failed': dump = self._handle_exceptions(result._result) elif status == 'unreachable': dump = result._result['msg'] if not dump: dump = self._dump_results(result._result) if result._task.loop and 'results' in result._result: self._process_items(result) else: sys.stdout.write(colors[status] + status + ': ') delegated_vars = result._result.get('_ansible_delegated_vars', None) if delegated_vars: sys.stdout.write(vt100.reset + result._host.get_name() + '>' + colors[status] + delegated_vars['ansible_host']) else: sys.stdout.write(result._host.get_name()) sys.stdout.write(': ' + dump + '\n') sys.stdout.write(vt100.reset + vt100.save + vt100.clearline) sys.stdout.flush() if status == 'changed': self._handle_warnings(result._result) def v2_playbook_on_play_start(self, play): # Leave the previous task on screen (as it has changes/errors) if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.bold) else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.bold) # Reset at the start of each play self.keep = False self.count.update(dict(handler=0, task=0)) self.count['play'] += 1 self.play = play # Write the next play on screen IN UPPERCASE, and make it permanent name = play.get_name().strip() if not name: name = 'unnamed' sys.stdout.write('PLAY %d: %s' % (self.count['play'], name.upper())) sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) sys.stdout.flush() def v2_playbook_on_task_start(self, task, is_conditional): # Leave the previous task on screen (as it has changes/errors) if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline) else: # Do not clear line, since we want to retain the previous output sys.stdout.write(vt100.restore + vt100.reset + vt100.underline) # Reset at the start of each task self.keep = False self.shown_title = False self.hosts = OrderedDict() self.task = task self.type = 'task' # Enumerate task if not setup (task names are too long for dense output) if task.get_name() != 'setup': self.count['task'] += 1 # Write the next task on screen (behind the prompt is the previous output) sys.stdout.write('%s %d.' % (self.type, self.count[self.type])) sys.stdout.write(vt100.reset) sys.stdout.flush() def v2_playbook_on_handler_task_start(self, task): # Leave the previous task on screen (as it has changes/errors) if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline) else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline) # Reset at the start of each handler self.keep = False self.shown_title = False self.hosts = OrderedDict() self.task = task self.type = 'handler' # Enumerate handler if not setup (handler names may be too long for dense output) if task.get_name() != 'setup': self.count[self.type] += 1 # Write the next task on screen (behind the prompt is the previous output) sys.stdout.write('%s %d.' % (self.type, self.count[self.type])) sys.stdout.write(vt100.reset) sys.stdout.flush() def v2_playbook_on_cleanup_task_start(self, task): # TBD sys.stdout.write('cleanup.') sys.stdout.flush() def v2_runner_on_failed(self, result, ignore_errors=False): self._add_host(result, 'failed') def v2_runner_on_ok(self, result): if result._result.get('changed', False): self._add_host(result, 'changed') else: self._add_host(result, 'ok') def v2_runner_on_skipped(self, result): self._add_host(result, 'skipped') def v2_runner_on_unreachable(self, result): self._add_host(result, 'unreachable') def v2_runner_on_include(self, included_file): pass def v2_runner_on_file_diff(self, result, diff): sys.stdout.write(vt100.bold) self.super_ref.v2_runner_on_file_diff(result, diff) sys.stdout.write(vt100.reset) def v2_on_file_diff(self, result): sys.stdout.write(vt100.bold) self.super_ref.v2_on_file_diff(result) sys.stdout.write(vt100.reset) # Old definition in v2.0 def v2_playbook_item_on_ok(self, result): self.v2_runner_item_on_ok(result) def v2_runner_item_on_ok(self, result): if result._result.get('changed', False): self._add_host(result, 'changed') else: self._add_host(result, 'ok') # Old definition in v2.0 def v2_playbook_item_on_failed(self, result): self.v2_runner_item_on_failed(result) def v2_runner_item_on_failed(self, result): self._add_host(result, 'failed') # Old definition in v2.0 def v2_playbook_item_on_skipped(self, result): self.v2_runner_item_on_skipped(result) def v2_runner_item_on_skipped(self, result): self._add_host(result, 'skipped') def v2_playbook_on_no_hosts_remaining(self): if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline) self.keep = False sys.stdout.write(vt100.white + vt100.redbg + 'NO MORE HOSTS LEFT') sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) sys.stdout.flush() def v2_playbook_on_include(self, included_file): pass def v2_playbook_on_stats(self, stats): if self._display.verbosity == 0 and self.keep: sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) else: sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline) # In normal mode screen output should be sufficient, summary is redundant if self._display.verbosity == 0: return sys.stdout.write(vt100.bold + vt100.underline) sys.stdout.write('SUMMARY') sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline) sys.stdout.flush() hosts = sorted(stats.processed.keys()) for h in hosts: t = stats.summarize(h) self._display.display( u"%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'rescued', t['rescued'], C.COLOR_OK), colorize(u'ignored', t['ignored'], C.COLOR_WARN), ), screen_only=True ) # When using -vv or higher, simply do the default action if display.verbosity >= 2 or not HAS_OD: CallbackModule = CallbackModule_default