feat(ansible): Inventory on one page, add behind dialogs, delete behind the confirm (#5333)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 57s
CI / unit (push) Successful in 1m23s
CI / publish (push) Successful in 1m25s

/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>
This commit is contained in:
2026-10-07 23:57:54 -04:00
co-authored by Claude Opus 5.5
parent 4acaa42a97
commit 7c346354f2
12 changed files with 519 additions and 249 deletions
+61 -1
View File
@@ -1,4 +1,5 @@
"""Integration: the Ansible Runs tab (#5329) against real Postgres. Its
"""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."""
@@ -94,3 +95,62 @@ def test_the_runs_tab_counts_and_filters(app):
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/")