- section_tabs macro; Runs · Playbooks · Schedules · Inventory on every Ansible page, replacing each page's own set of header buttons. - Runs: a verdict line (last 30 days, failures first with the latest named, running now, the schedule due next); under each status what the run did across its hosts (ansible/recap.py); a filter bar (playbook, status, who started it) that swaps the table and carries into the scroll paging. - Run page: status, duration, start and who started it in one line; each host's result in words, worst first; the playbook it ran links to it; the log and failures on classes rather than inline colours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.7 KiB
Python
64 lines
2.7 KiB
Python
"""A run's play recap in words (#5329, #5330): what the runs list says under a
|
|
run's status and what the run page says beside each host.
|
|
|
|
A run's `results` is {hosts: {name: {ok, changed, unreachable, failed,
|
|
skipped}}, failures: [...]}, parsed from the play recap. Model-free, so it is
|
|
unit-tested without a database.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
NAMES_SHOWN = 2
|
|
|
|
|
|
def _n(counts: dict | None, key: str) -> int:
|
|
return int((counts or {}).get(key) or 0)
|
|
|
|
|
|
def _names(names: list[str]) -> str:
|
|
""""a", "a and b", "a, b and 3 more"."""
|
|
if len(names) <= NAMES_SHOWN:
|
|
return " and ".join(names)
|
|
return f"{', '.join(names[:NAMES_SHOWN])} and {len(names) - NAMES_SHOWN} more"
|
|
|
|
|
|
def run_recap(results: dict | None, check: bool = False) -> list[tuple[str, str | None]]:
|
|
"""A run's outcome across its hosts as (text, tone) parts, the worst
|
|
first: "failed on web-1" (crit), "db-2 unreachable" (crit), "3 hosts",
|
|
"2 changed" ("would change" for a dry run, which changed nothing).
|
|
Empty when the run left no recap (it is still going, or died first)."""
|
|
hosts = (results or {}).get("hosts") or {}
|
|
if not hosts:
|
|
return []
|
|
failed = sorted(h for h, c in hosts.items() if _n(c, "failed"))
|
|
unreachable = sorted(h for h, c in hosts.items() if _n(c, "unreachable") and h not in failed)
|
|
changed = sum(1 for c in hosts.values() if _n(c, "changed"))
|
|
parts: list[tuple[str, str | None]] = []
|
|
if failed:
|
|
parts.append((f"failed on {_names(failed)}", "crit"))
|
|
if unreachable:
|
|
parts.append((f"{_names(unreachable)} unreachable", "crit"))
|
|
parts.append((f"{len(hosts)} host{'s' if len(hosts) != 1 else ''}", None))
|
|
if changed:
|
|
parts.append((f"{changed} would change" if check else f"{changed} changed", None))
|
|
elif not failed and not unreachable:
|
|
parts.append(("nothing to change" if check else "nothing changed", "muted"))
|
|
return parts
|
|
|
|
|
|
def host_recap(counts: dict | None, check: bool = False) -> list[tuple[str, str | None]]:
|
|
"""One host's task counts as (text, tone) parts, zeros left out and the
|
|
worst first: "unreachable", "1 failed", "3 ok", "1 changed", "2 skipped"."""
|
|
parts: list[tuple[str, str | None]] = []
|
|
if _n(counts, "unreachable"):
|
|
parts.append(("unreachable", "crit"))
|
|
if _n(counts, "failed"):
|
|
parts.append((f"{_n(counts, 'failed')} failed", "crit"))
|
|
if _n(counts, "ok"):
|
|
parts.append((f"{_n(counts, 'ok')} ok", None))
|
|
if _n(counts, "changed"):
|
|
word = "would change" if check else "changed"
|
|
parts.append((f"{_n(counts, 'changed')} {word}", None))
|
|
if _n(counts, "skipped"):
|
|
parts.append((f"{_n(counts, 'skipped')} skipped", "muted"))
|
|
return parts
|