Files
FabledSteward/tests/core/test_ansible_playbooks_page.py
bvandeusenandClaude Opus 5.5 0c72892f58
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 59s
CI / unit (push) Failing after 1m22s
CI / publish (push) Skipped
feat(ansible): schedules read as sentences, each with its own page (#5332)
- The Schedules tab: a verdict line (failing first, paused, next due),
  each schedule as one line (what runs where, how often, when next, how it
  last went); the row opens its page; Run now and Pause stay in the row.
- /ansible/schedules/new and /ansible/schedules/<id>: the form on classes,
  dry run beside the button, the playbook's variables rendered with the
  page; at the foot Delete behind the confirm dialog. ?source=&playbook=
  start a new one on a playbook (Schedule it on the playbook's page).
- A run started by a schedule links to that schedule's page.
- The run page's id guard allows the Run again link's ?from=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:51:15 -04:00

130 lines
7.9 KiB
Python

"""The Playbooks tab and a playbook's own page (#5331): one list across
sources whose row opens the playbook; the page carries the form that runs
it (filled from an earlier run for "Run again"), the schedules that use it
and its recent runs."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from quart import render_template, session
from steward.ansible.routes import run_prefill, run_view
from steward.models.ansible import AnsibleRunStatus
from tests.test_style_tokens import hex_colours
def _run(rid, status, mins_ago, params=None):
now = datetime.now(timezone.utc)
return SimpleNamespace(id=rid, playbook_path="maintenance/prune.yml", source_name="steward-builtin",
inventory_scope="steward:all", status=status,
started_at=now - timedelta(minutes=mins_ago),
finished_at=now - timedelta(minutes=mins_ago - 1), params=params,
results={"hosts": {"a": {"ok": 2, "changed": 1}}}, output="")
def test_playbook_urls_reach_their_own_routes(app):
"""The playbook page's path converter must not swallow the editor's URLs."""
urls = app.url_map.bind("localhost")
assert urls.match("/ansible/playbooks/steward-builtin/maintenance/prune.yml")[0] == "ansible.playbook_page"
assert urls.match("/ansible/playbooks/edit/steward-local/a/b.yml")[0] == "ansible.edit_playbook"
assert urls.match("/ansible/playbooks/new")[0] == "ansible.new_playbook"
assert urls.match("/ansible/playbooks")[0] == "ansible.playbooks"
import pytest
from werkzeug.exceptions import NotFound
with pytest.raises(NotFound):
urls.match("/ansible/browse") # moved, not redirected (rule 122)
async def test_the_playbooks_list_is_one_table_across_sources(app):
last = _run("r1", AnsibleRunStatus.success, 60)
rows = [{"source": "steward-builtin", "path": "maintenance/prune.yml", "description": "Reclaim disk",
"category": "maintenance", "confirm": True, "last": last},
{"source": "steward-builtin", "path": "host_agent/update.yml", "description": "Update the agent",
"category": "", "confirm": False, "last": None}]
empty = [{"name": "steward-local", "path": "/data/ansible/playbooks", "editable": True}]
async with app.test_request_context("/ansible/playbooks"):
session["user_role"] = "admin"
page = await render_template("ansible/playbooks.html", rows=rows, empty_sources=empty,
sources=[{"name": "steward-builtin"}, {"name": "steward-local"}],
can_add=True)
main = page[page.index("<main"):]
assert main.count('<tr class="row-link">') == 2
assert 'class="row-target mono" href="/ansible/playbooks/steward-builtin/maintenance/prune.yml"' in main
assert "2 playbooks from 1 source" in main and "1 flagged destructive by its author" in main
assert ">destructive</span>" in main and '<span class="chip">maintenance</span>' in main
assert '<span class="badge badge-ok">Succeeded</span>' in main and "never run" in main
assert "steward-local" in main and "has no playbooks yet." in main and 'href="/ansible/playbooks/new"' in main
assert '<a href="/ansible/playbooks" aria-current="page">Playbooks</a>' in main
assert "/run-form/" not in main # the form lives on the playbook's page
assert not hex_colours(main), hex_colours(main)
def _page_ctx(role_runs=True, prefill=None, schedules=None):
now = datetime.now(timezone.utc)
run = _run("r9", AnsibleRunStatus.failed, 30, params={"check": True})
return {
"source": {"name": "steward-builtin"}, "source_name": "steward-builtin",
"playbook_path": "maintenance/prune.yml", "contents": "- hosts: all\n",
"description": "Reclaim disk", "category": "maintenance", "confirm": True,
"variables": [{"name": "prune_volumes", "prompt": "", "default": "false", "required": False, "secret": False}],
"editable": False,
"runs": [{**run_view(run, {"steward:all": "all inventory targets"}, now),
"origin": {"text": "alice", "url": None, "kind": "user"}}] if role_runs else [],
"schedules": schedules or [], "targets": [SimpleNamespace(id="t1", name="web-1")],
"groups": [SimpleNamespace(id="g1", name="Docker Swarm")], "inventories": [],
"prefill": prefill or run_prefill(None, set()),
}
async def test_a_playbook_page_runs_it_and_shows_its_history(app):
now = datetime.now(timezone.utc)
sched = SimpleNamespace(id="s1", name="Nightly prune", enabled=True, interval_seconds=86400,
last_run_at=now - timedelta(hours=22), inventory_scope="steward:group:g1")
async with app.test_request_context("/ansible/playbooks/steward-builtin/maintenance/prune.yml"):
session["user_role"] = "operator"
page = await render_template("ansible/playbook.html", **_page_ctx(schedules=[
{"s": sched, "scope_label": "group Docker Swarm",
"next": {"id": "s1", "name": "Nightly prune", "at": now + timedelta(hours=2)}}]))
main = page[page.index("<main"):]
assert 'href="/ansible/playbooks"' in page # crumbs back to the list
verdict = main[main.index('<p class="page-verdict">'):]
verdict = verdict[:verdict.index("</p>")]
assert "Reclaim disk" in verdict and "flagged destructive" in verdict and "last ran" in verdict
assert 'id="run-form"' in main and 'hx-post="/ansible/runs"' in main
assert 'name="confirmed"' in main # the destructive flag's gate
assert 'name="var__prune_volumes"' in main and 'name="check"' in main
assert "Nightly prune" in main and "on group Docker Swarm · daily" in main
assert 'href="/ansible/schedules/s1"' in main
assert 'href="/ansible/schedules/new?source=steward-builtin&amp;playbook=maintenance/prune.yml"' in main
assert 'href="/ansible/runs/r9"' in main and "End of the list." not in main
assert 'href="/ansible/?q=maintenance/prune.yml"' in main
assert "/ansible/playbooks/edit/" not in main # a built-in playbook is read-only
assert not hex_colours(main), hex_colours(main)
async def test_a_viewer_reads_the_playbook_but_gets_no_form(app):
async with app.test_request_context("/ansible/playbooks/steward-builtin/maintenance/prune.yml"):
session["user_role"] = "viewer"
page = await render_template("ansible/playbook.html", **_page_ctx(role_runs=False))
assert 'id="run-form"' not in page and "never run" in page and "No schedule runs it." in page
def test_run_again_fills_the_form_from_the_earlier_run():
run = SimpleNamespace(inventory_scope="steward:group:g1", params={
"extra_vars_map": {"prune_volumes": "true", "other": "x"}, "limit": "web*", "tags": "a", "check": True})
assert run_prefill(run, {"prune_volumes"}) == {
"scope": "steward:group:g1", "values": {"prune_volumes": "true"}, "extra": "other=x",
"limit": "web*", "tags": "a", "check": True}
assert run_prefill(None, set())["scope"] == ""
async def test_the_form_comes_back_as_the_earlier_run_left_it(app):
prefill = run_prefill(SimpleNamespace(inventory_scope="steward:group:g1", params={
"extra_vars_map": {"prune_volumes": "true", "other": "x"}, "limit": "web*", "check": True}), {"prune_volumes"})
async with app.test_request_context("/ansible/playbooks/x"):
session["user_role"] = "operator"
page = await render_template("ansible/playbook.html", **_page_ctx(prefill=prefill))
assert '<option value="steward:group:g1" selected>Group: Docker Swarm</option>' in page
assert 'name="var__prune_volumes"' in page and 'value="true"' in page
assert ">other=x</textarea>" in page and 'value="web*"' in page
assert '<details class="run-more" open>' in page and 'name="check" checked' in page