A custom stdout callback plugin that replaces Ansible's default playbook output with a clean, minimalistic, emoji-decorated display featuring animated per-host spinners, real-time grouped loop progress, verbosity support, failure summaries, and a color-coded recap table.
- Single-line results — each host outcome is one line:
✅ ok,🔄 changed,⏭️ skipped,❌ FAILED!,💀 UNREACHABLE!,⚠️ ignored - Multi-line failure detail — failed and unreachable hosts show the error message on indented follow-up lines
- Debug module output —
debugtasks always display theirmsgorvaroutput, regardless of verbosity - Real-time grouped loops — loop tasks are marked with
🔁; items appear in real-time under their host (each host shown exactly once) with[index/total] item_valueprogress - Animated per-host spinner — while a task runs, every active host gets its own spinning Braille indicator (
⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏); queued hosts for future batches display a static⏳ - Verbosity levels —
-vshows all module result fields + a failure summary table with error messages;-vvalso shows module name and arguments - Recap table — a Unicode box-drawing table with color-coded stats (green OK, yellow changed, red failed/unreachable, cyan skipped)
- Failure summary — at
-vor higher, a second table lists each failed host with the task that failed and the error message - No external dependencies — only Python stdlib +
ansible.plugins.callback.CallbackBase
▶ PLAY [Deploy App] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 TASK [Install packages] 🔁 ━━━━━━━━━━━━━━━━━━━━━━━━━
🔁 web-01 ← items appear in real-time
✅ [1/3] httpd
🔄 [2/3] php
✅ [3/3] mariadb
🔁 web-02
🔄 [1/3] httpd
🔄 [2/3] php
🔄 [3/3] mariadb
❌ db-01 · FAILED! ← final status after all items
✅ [1/3] httpd
❌ [2/3] php
msg: package not found
⏭️ [3/3] mariadb
⏭️ db-02 · skipped
📋 TASK [Show config] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ web-01 · ok
Current config is valid.
✅ web-02 · ok
Current config is valid.
📊 PLAY RECAP ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────────┬────┬─────────┬─────────────┬────────┬─────────┬─────────┬─────────┐
│ Host │ OK │ Changed │ Unreachable │ Failed │ Skipped │ Rescued │ Ignored │
├──────────┼────┼─────────┼─────────────┼────────┼─────────┼─────────┼─────────┤
│ db-01 │ 3 │ 0 │ 0 │ 1 │ 1 │ 0 │ 0 │
│ db-02 │ 5 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ web-01 │ 5 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ web-02 │ 4 │ 3 │ 0 │ 0 │ 0 │ 0 │ 0 │
└──────────┴────┴─────────┴─────────────┴────────┴─────────┴─────────┴─────────┘
❌ FAILURE SUMMARY ← only at -v or higher
┌──────────┬──────────────────┬─────────────────────┐
│ Host │ Task │ Error │
├──────────┼──────────────────┼─────────────────────┤
│ db-01 │ Install packages │ package not found │
└──────────┴──────────────────┴─────────────────────┘
- Place
modern_output.pyinside acallback_plugins/directory next to your playbook (or anywhere Ansible can find it):
project/
├── ansible.cfg
├── inventory
├── playbook.yml
└── callback_plugins/
└── modern_output.py
- Configure
ansible.cfg:
[defaults]
stdout_callback = modern_output
callback_plugins = ./callback_pluginsThat's it — run ansible-playbook as usual.
| Flag | What it adds |
|---|---|
| (none) | Status line per host. Debug msg/var always shown. Failures show msg/stderr. |
-v |
All result fields from every module. Failure summary table (Host, Task, Error) at play recap. |
-vv |
Module name and task arguments for every host result. |
Example at -vv (non-loop task):
🔄 web-01 · changed
module: ansible.builtin.dnf args: name=httpd, state=present
msg: Installed: httpd
rc: 0
Example at -vv (loop item):
🔁 web-01
✅ [1/3] httpd
module: ansible.builtin.dnf args: name=httpd, state=present
msg: Nothing to do
rc: 0
v2_playbook_on_play_start → ▶ PLAY [name] ━━━
v2_playbook_on_task_start → 📋 TASK [name] ━━━ (+ 🔁 if loop)
show all hosts as ⏳ (spinner)
v2_runner_on_start → promote host ⏳ → animated ⠋
v2_runner_item_on_* → buffer item, redraw live block grouped by host
v2_runner_on_ok / failed / … → print result ✅/❌/… (or finalize loop host)
v2_playbook_on_stats → 📊 PLAY RECAP table + failure summary
Spinner: runs in a background thread, redraws a multi-line block in-place using ANSI cursor-up sequences. Result output and the spinner share a lock so they never tear each other's lines.
Loop block: when the first loop item arrives, the spinner stops and a redrawable "live block" takes over. Each item is buffered per host and the entire block is erased + redrawn on every event, so each host appears exactly once with items accumulating underneath in real-time. When the aggregate result fires, the host header updates from 🔁 to its final status emoji. At the next task start, the block is finalized as permanent output.
- Ansible 2.8+ (uses
v2_runner_on_start) - A terminal with ANSI escape code and Unicode support
| Emoji | Meaning |
|---|---|
| ✅ | Task succeeded (ok) |
| 🔄 | Task succeeded with changes |
| ⏭️ | Task skipped |
| ❌ | Task failed |
| 💀 | Host unreachable |
Task failed but ignore_errors: true |
|
| ⠋ | Host actively executing (animated) |
| ⏳ | Host queued for a future batch |
| 🔔 | Handler running |
| 🔁 | Loop task / host with items in progress |

