Add the plugin source
This commit is contained in:
parent
03abd69168
commit
7a9cb87ea6
|
@ -0,0 +1,573 @@
|
|||
# (c) 2016, Dag Wieers <dag@wieers.com>
|
||||
# (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 = '''
|
||||
callback: 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)
|
||||
version_added: "2.3"
|
||||
requirements:
|
||||
- set as stdout in configuation
|
||||
'''
|
||||
|
||||
from collections import MutableMapping, MutableSequence
|
||||
import os
|
||||
import yaml
|
||||
import glob
|
||||
|
||||
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.plugins.callback.default import CallbackModule as CallbackModule_default
|
||||
from ansible.utils.color import colorize, hostcolor
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
import sys
|
||||
|
||||
# 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_dense(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):
|
||||
|
||||
# TRANSLATIONS
|
||||
cur_dir = os.getcwd()
|
||||
|
||||
self.translations = {}
|
||||
for t_file in glob.glob("%s/i18n/*.yml" % cur_dir):
|
||||
lang = os.path.splitext(os.path.basename(t_file))[0]
|
||||
with open(t_file, 'r') as file_data:
|
||||
yaml_data = file_data.read()
|
||||
try:
|
||||
t = yaml.safe_load(yaml_data)
|
||||
if t != None:
|
||||
self.translations[lang] = t
|
||||
except yaml.YAMLError as e:
|
||||
# TODO: Nice warning message in ansible-friendly formatting
|
||||
print(e)
|
||||
continue
|
||||
|
||||
languages = self.translations.keys()
|
||||
|
||||
if len(languages) == 1:
|
||||
# Dumb me i can't extract it without iterating?
|
||||
for l in languages: self.language = l
|
||||
elif len(languages) == 0:
|
||||
self.language = None
|
||||
else:
|
||||
possible_lang = os.getenv('LANG')
|
||||
if len(possible_lang) < 2:
|
||||
# TODO: nice ansible error message
|
||||
self.language = None
|
||||
short_notation = possible_lang[0:2]
|
||||
if short_notation in languages:
|
||||
self.language = short_notation
|
||||
else:
|
||||
# TODO: nice ansible error message
|
||||
self.language = None
|
||||
|
||||
# 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'
|
||||
self.name = 'foo'
|
||||
self.role = None
|
||||
|
||||
# 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 prepare_output(self):
|
||||
if self.language:
|
||||
out_type = self.translate(self.type)
|
||||
out_name = self.translate(self.name)
|
||||
else:
|
||||
out_type = self.type
|
||||
out_name = self.name
|
||||
|
||||
if self.role:
|
||||
return "[%s] %s: %s" % (self.role, out_type, out_name)
|
||||
return "%s: %s" % (out_type, out_name)
|
||||
|
||||
def translate(self, key):
|
||||
if key in self.translations[self.language]:
|
||||
return self.translations[self.language][key]
|
||||
return key
|
||||
|
||||
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:
|
||||
self._display_task_banner()
|
||||
self._display_results(result, status)
|
||||
|
||||
def _clean_results(self, result):
|
||||
# Remove non-essential atributes
|
||||
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)
|
||||
|
||||
# TRANSLATIONS
|
||||
sys.stdout.write('%s %d:' % (self.translate(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)
|
||||
# TRANSLATIONS
|
||||
sys.stdout.write(self.prepare_output())
|
||||
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] + self.translate(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('%s' % self.translate(name))
|
||||
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.ask = task
|
||||
self.type = 'task'
|
||||
self.name = task.name
|
||||
|
||||
if task._role:
|
||||
self.role = task._role.get_name()
|
||||
|
||||
# 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)
|
||||
|
||||
# TRANSLATIONS
|
||||
sys.stdout.write(self.prepare_output())
|
||||
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'
|
||||
|
||||
self.name = task.name
|
||||
if task._role:
|
||||
self.role = task._role.get_name()
|
||||
|
||||
# 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)
|
||||
# TRANSLATIONS
|
||||
sys.stdout.write(self.prepare_output())
|
||||
sys.stdout.write(vt100.reset)
|
||||
sys.stdout.flush()
|
||||
|
||||
def v2_playbook_on_cleanup_task_start(self, task):
|
||||
# TBD
|
||||
# TRANSLATIONS
|
||||
sys.stdout.write(translate('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 + self.translate('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(self.translate('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" % (
|
||||
hostcolor(h, t),
|
||||
# TRANSLATIONS
|
||||
colorize(self.translate('ok'), t['ok'], C.COLOR_OK),
|
||||
colorize(self.translate('changed'), t['changed'], C.COLOR_CHANGED),
|
||||
colorize(self.translate('unreachable'), t['unreachable'], C.COLOR_UNREACHABLE),
|
||||
colorize(self.translate('failed'), t['failures'], C.COLOR_ERROR)),
|
||||
screen_only=True
|
||||
)
|
||||
|
||||
# When using -vv or higher, simply do the default action
|
||||
if display.verbosity >= 2 or not HAS_OD:
|
||||
CallbackModule = CallbackModule_default
|
||||
else:
|
||||
CallbackModule = CallbackModule_dense
|
Loading…
Reference in New Issue