- 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>
130 lines
7.9 KiB
Python
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&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
|