/ansible/inventory/ shows every target (address, its linked Steward host, its groups) and every group (members, the schedules aimed at it) under a verdict line. The whole row opens the target or group. Add target and Add group open small dialogs; a failed add comes back with its dialog open and what was typed. The red x leaves the rows: Delete is on the target's or group's page, behind the confirm dialog, saying which schedules would be left with nothing to run on. The two list pages (/inventory/targets, /inventory/groups) are gone with no redirect (rule 122); their URLs keep only the Add POST. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
8.1 KiB
Python
157 lines
8.1 KiB
Python
"""Integration: the Ansible Runs tab (#5329) and the Inventory page (#5333)
|
|
against real Postgres. The Runs tab's
|
|
verdict counts the last 30 days; the filter bar narrows by playbook, status
|
|
and origin; an HTMX request from the bar gets the table alone, and the next
|
|
page as the reader scrolls keeps the filters. Requires STEWARD_DATABASE_URL."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import time
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
if not os.environ.get("STEWARD_DATABASE_URL"):
|
|
pytest.skip("needs Postgres")
|
|
from steward.app import create_app
|
|
return create_app(testing=False)
|
|
|
|
|
|
def test_the_runs_tab_counts_and_filters(app):
|
|
from steward.models.ansible import AnsibleRun, AnsibleRunStatus
|
|
from steward.models.users import User, UserRole
|
|
|
|
async def _go():
|
|
tag = uuid.uuid4().hex[:8]
|
|
uid = str(uuid.uuid4())
|
|
now = datetime.now(timezone.utc)
|
|
|
|
def run(name, status, mins, origin=None, by=None, results=None):
|
|
return AnsibleRun(playbook_path=f"it{tag}/{name}.yml", source_name="steward-builtin",
|
|
inventory_scope="steward:all", status=status, origin=origin,
|
|
triggered_by=by, started_at=now - timedelta(minutes=mins),
|
|
finished_at=now - timedelta(minutes=mins - 1), results=results)
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add(User(id=uid, username=f"runs-{tag}", email=f"runs-{tag}@test",
|
|
password_hash="x", role=UserRole.viewer, is_active=True))
|
|
async with s.begin():
|
|
s.add_all([
|
|
run("prune", AnsibleRunStatus.success, 10, origin="schedule",
|
|
results={"hosts": {"a": {"ok": 2, "changed": 1}, "b": {"ok": 2, "changed": 1}}}),
|
|
run("update", AnsibleRunStatus.failed, 20, by=uid,
|
|
results={"hosts": {f"db-{tag}": {"ok": 1, "failed": 1}}}),
|
|
])
|
|
|
|
client = app.test_client()
|
|
async with client.session_transaction() as sess:
|
|
sess.update(user_id=uid, user_role="viewer", username=f"runs-{tag}", login_at=time.time())
|
|
|
|
async def get(url, **kw):
|
|
resp = await client.get(url, **kw)
|
|
return resp.status_code, await resp.get_data(as_text=True)
|
|
|
|
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"}),
|
|
# 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
|
|
await s.execute(delete(AnsibleRun).where(AnsibleRun.playbook_path.like(f"it{tag}/%")))
|
|
await s.delete(await s.get(User, uid))
|
|
return tag, out
|
|
|
|
tag, out = asyncio.run(_go())
|
|
status, page = out["page"]
|
|
assert status == 200
|
|
assert '<p class="page-verdict">' in page and "in the last 30 days" in page # the verdict queries ran
|
|
assert f"it{tag}/prune.yml" in page and f"it{tag}/update.yml" in page
|
|
assert "2 hosts · 2 changed" in page and f'<span class="tone-crit">failed on db-{tag}</span>' in page
|
|
assert '<nav class="section-tabs" aria-label="Ansible">' in page
|
|
|
|
failed = out["failed"][1]
|
|
assert "<html" not in failed and '<p class="page-verdict">' not in failed # the table alone
|
|
assert f"it{tag}/update.yml" in failed and f"it{tag}/prune.yml" not in failed
|
|
sched = out["sched"][1]
|
|
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
|
|
|
|
|
|
def test_the_inventory_page_and_its_add_dialogs(app):
|
|
"""#5333: one Inventory page with each target's linked host and groups; a
|
|
failed add comes back to the page with its dialog open; a good add opens
|
|
the new target's page; Delete goes back to the Inventory page."""
|
|
from sqlalchemy import select
|
|
from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget
|
|
from steward.models.hosts import Host
|
|
from steward.models.users import User, UserRole
|
|
|
|
async def _go():
|
|
tag = uuid.uuid4().hex[:8]
|
|
uid, hid, gid, tid = (str(uuid.uuid4()) for _ in range(4))
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add(User(id=uid, username=f"inv-{tag}", email=f"inv-{tag}@test",
|
|
password_hash="x", role=UserRole.operator, is_active=True))
|
|
s.add(Host(id=hid, name=f"host-{tag}", address="10.9.8.1"))
|
|
async with s.begin():
|
|
grp = AnsibleGroup(id=gid, name=f"grp-{tag}", ansible_vars={})
|
|
s.add(grp)
|
|
s.add(AnsibleTarget(id=tid, name=f"tgt-{tag}", address="10.9.8.1", host_id=hid,
|
|
ansible_vars={}, groups=[grp]))
|
|
client = app.test_client()
|
|
async with client.session_transaction() as sess:
|
|
sess.update(user_id=uid, user_role="operator", username=f"inv-{tag}",
|
|
_csrf="test-token", login_at=time.time())
|
|
csrf = {"X-CSRF-Token": "test-token"}
|
|
page = await client.get("/ansible/inventory/")
|
|
dup = await client.post("/ansible/inventory/targets", headers=csrf,
|
|
form={"name": f"tgt-{tag}", "address": "10.9.8.2"})
|
|
new = await client.post("/ansible/inventory/targets", headers=csrf,
|
|
form={"name": f"new-{tag}", "address": "10.9.8.3"})
|
|
async with app.db_sessionmaker() as s:
|
|
new_id = (await s.execute(select(AnsibleTarget.id).where(
|
|
AnsibleTarget.name == f"new-{tag}"))).scalar_one()
|
|
gone = await client.post(f"/ansible/inventory/targets/{new_id}/delete", headers=csrf)
|
|
out = {"page": (page.status_code, await page.get_data(as_text=True)),
|
|
"dup": (dup.status_code, await dup.get_data(as_text=True)),
|
|
"new": (new.status_code, new.headers.get("Location")), "new_id": new_id,
|
|
"gone": (gone.status_code, gone.headers.get("Location"))}
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
for model, rid in ((AnsibleTarget, tid), (AnsibleGroup, gid), (Host, hid), (User, uid)):
|
|
await s.delete(await s.get(model, rid))
|
|
return tag, hid, tid, out
|
|
|
|
tag, hid, tid, out = asyncio.run(_go())
|
|
status, page = out["page"]
|
|
assert status == 200
|
|
assert f'<a class="row-target" href="/ansible/inventory/targets/{tid}">tgt-{tag}</a>' in page
|
|
assert f'<a class="row-inner" href="/hosts/{hid}">host-{tag}</a>' in page
|
|
assert f'<span class="chip">grp-{tag}</span>' in page
|
|
status, dup = out["dup"]
|
|
assert status == 400 and f"There is already a target named tgt-{tag}." in dup
|
|
assert "document.getElementById('add-target').showModal();" in dup and 'value="10.9.8.2"' in dup
|
|
assert out["new"] == (302, f"/ansible/inventory/targets/{out['new_id']}")
|
|
assert out["gone"] == (302, "/ansible/inventory/")
|