- /ansible/playbooks lists every playbook across sources: what it says it does, its category and destructive flag, how it last ran; the whole row opens the playbook. A source with none is one quiet line. - /ansible/playbooks/<source>/<path>: the verdict (what it does, flagged destructive, last ran), the run form in place (dry run beside the button, options folded), the schedules that use it, its recent runs, the playbook itself folded. ?from=<run> fills the form from that run: Run again on a run's page. - /ansible/browse, its view page and the /run-form fragment are gone, not redirected (rule 122); every link points at the new pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""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": {"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?edit=s1#form"' 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
|
||||
@@ -92,7 +92,7 @@ async def test_ansible_pages_share_one_strip_of_tabs(app):
|
||||
page = await render_template("ansible/index.html", **_ctx([]))
|
||||
nav = page[page.index('<nav class="section-tabs" aria-label="Ansible">'):]
|
||||
nav = nav[:nav.index("</nav>")]
|
||||
for href in ("/ansible/", "/ansible/browse", "/ansible/schedules", "/ansible/inventory/targets"):
|
||||
for href in ("/ansible/", "/ansible/playbooks", "/ansible/schedules", "/ansible/inventory/targets"):
|
||||
assert f'href="{href}"' in nav, href
|
||||
assert nav.count('aria-current="page"') == 1 and '<a href="/ansible/" aria-current="page">Runs</a>' in nav
|
||||
assert "No runs yet." in page and "No runs in the last 30 days" in page
|
||||
@@ -180,7 +180,8 @@ async def test_run_detail_titles_the_run_and_keeps_the_trail(app):
|
||||
# Each host's result in words, the failed host first.
|
||||
assert page.index(">zulu<") < page.index(">alpha<")
|
||||
assert '<span class="tone-crit">1 failed</span> · 1 ok' in page and ">3 ok<" in page
|
||||
assert 'href="/ansible/browse/builtin/update.yml"' in page # the playbook it ran
|
||||
assert 'href="/ansible/playbooks/builtin/update.yml"' in page # the playbook it ran
|
||||
assert 'href="/ansible/playbooks/builtin/update.yml?from=dddd4444#run-form">Run again</a>' in page
|
||||
main = page[page.index("<main"):]
|
||||
assert 'style="' not in main[:main.index('id="run-output"')] # no hand-set colours
|
||||
assert not hex_colours(main), hex_colours(main)
|
||||
|
||||
@@ -60,7 +60,12 @@ def test_the_runs_tab_counts_and_filters(app):
|
||||
out = {"page": await get(f"/ansible/?q=it{tag}"),
|
||||
"failed": await get(f"/ansible/?q=it{tag}&status=failed", headers={"HX-Request": "true"}),
|
||||
"sched": await get(f"/ansible/?q=it{tag}&origin=schedule", headers={"HX-Request": "true"}),
|
||||
"person": await get(f"/ansible/?q=it{tag}&origin=manual", headers={"HX-Request": "true"})}
|
||||
"person": await get(f"/ansible/?q=it{tag}&origin=manual", headers={"HX-Request": "true"}),
|
||||
# The Playbooks tab and a built-in playbook's page (#5331): their
|
||||
# newest-run and schedule queries against Postgres.
|
||||
"playbooks": await get("/ansible/playbooks"),
|
||||
"playbook": await get("/ansible/playbooks/steward-builtin/maintenance/docker_prune.yml"),
|
||||
"missing": await get("/ansible/playbooks/steward-builtin/nope.yml")}
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
from sqlalchemy import delete
|
||||
@@ -83,3 +88,9 @@ def test_the_runs_tab_counts_and_filters(app):
|
||||
assert f"it{tag}/prune.yml" in sched and f"it{tag}/update.yml" not in sched
|
||||
person = out["person"][1]
|
||||
assert f"it{tag}/update.yml" in person and f"it{tag}/prune.yml" not in person
|
||||
|
||||
status, playbooks = out["playbooks"]
|
||||
assert status == 200 and 'href="/ansible/playbooks/steward-builtin/maintenance/docker_prune.yml"' in playbooks
|
||||
status, playbook = out["playbook"]
|
||||
assert status == 200 and "The playbook" in playbook and 'id="run-form"' not in playbook # a viewer
|
||||
assert out["missing"][0] == 404
|
||||
|
||||
Reference in New Issue
Block a user