1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 16:01:55 +00:00
community.general/plugins/callback/dense.py
Felix Fontein 236b9c0e04
Sort imports with ruff check --fix (#11400)
Sort imports with ruff check --fix.
2026-01-09 07:40:58 +01:00

490 lines
17 KiB
Python

# Copyright (c) 2016, Dag Wieers <dag@wieers.com>
# 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 annotations
DOCUMENTATION = r"""
name: dense
type: stdout
short_description: Minimal stdout output
extends_documentation_fragment:
- default_callback
description:
- When in verbose mode it acts 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
import sys
from collections.abc 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
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()
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(f"{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, bytes, str)):
if not result[attr]:
del result[attr]
def _handle_exceptions(self, result):
if "exception" in result:
# Remove the exception from the result so it is 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(f"{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(f"{self.hosts[name]['delegate']}>")
sys.stdout.write(colors[self.hosts[name]["state"]] + name + vt100.reset)
sys.stdout.flush()
sys.stdout.write(vt100.linewrap)
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(f"{self.type} {self.count[self.type]}: {self.task.get_name().strip()}")
sys.stdout.write(f"{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(f"{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(f"{colors[status] + status}: ")
delegated_vars = result._result.get("_ansible_delegated_vars", None)
if delegated_vars:
sys.stdout.write(
f"{vt100.reset}{result._host.get_name()}>{colors[status]}{delegated_vars['ansible_host']}"
)
else:
sys.stdout.write(result._host.get_name())
sys.stdout.write(f": {dump}\n")
sys.stdout.write(f"{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(f"{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(f"PLAY {self.count['play']}: {name.upper()}")
sys.stdout.write(f"{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(f"{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(f"{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(f"{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(f"{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(f"{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(f"{vt100.white + vt100.redbg}NO MORE HOSTS LEFT")
sys.stdout.write(f"{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(f"{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(f"{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(
f"{hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('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 # type: ignore