"""The host page: what sits where (#5214).""" import re from types import SimpleNamespace from quart import render_template, session from steward.hosts.service import HOST_STATES, NO_GROUP def _list_ctx(**over): """What the Hosts list route hands its template (#5294), with no filter set.""" ctx = dict(hosts=[], host_status={}, uptime={}, agent={}, groups={}, group_names=[], f={"q": "", "group": "", "state": "", "sort": "status", "dir": "asc"}, filter_url=lambda **kw: "/hosts/?" + "&".join(f"{k}={v}" for k, v in kw.items()), sort_url=lambda key: f"/hosts/?sort={key}", states=HOST_STATES, no_group=NO_GROUP, add=None) ctx.update(over) ctx.setdefault("total", len(ctx["hosts"])) return ctx def _headers(page): """Each table header's text, tags and arrows stripped.""" return [re.sub(r"<[^>]+>|[↑↓]", "", h).strip() for h in re.findall(r"]*>(.*?)", page, re.S)] async def test_ansible_comes_before_the_docker_list(app, monkeypatch): # The Docker list can run to dozens of containers; Ansible under it was # easy to miss, so it sits above. monkeypatch.setitem(app.config, "PLUGINS", {"docker": {"enabled": True}}) host = SimpleNamespace(id="h1", name="box", address="10.0.0.5") async with app.test_request_context("/hosts/h1"): session["user_role"] = "operator" page = await render_template( "hosts/detail.html", host=host, monitors=[], uptime=None, alerts=[], linked_target=None, linkable_targets=[], ansible_sources=[]) docker = page.index('hx-get="/plugins/docker/host/h1"') # #5290: Monitors and Agent share a row whose cards drop their own margin. assert '
' in page assert page.index(">Ansible") < docker assert page.index('hx-get="/plugins/host_agent/panel/h1"') < docker # Monitors are added on their own form, filed under this host (#5277); # each row's own page has the labelled Edit/Pause/Delete. assert 'href="/monitors/new?host_id=h1"' in page assert "row-actions" not in page and 'action="/monitors/add"' not in page async def test_adding_a_monitor_from_a_host_page_says_whose_it_is(app): host = SimpleNamespace(id="h1", name="box", address="10.0.0.5") async with app.test_request_context("/monitors/new?host_id=h1"): session["user_role"] = "operator" page = await render_template( "monitors/form.html", monitor=None, host=host, mtype="icmp", config={}, types=[], values={"name": "", "target": "10.0.0.5", "host_id": "h1", "check_interval_seconds": 0}) assert 'box' in page # crumb and hint name the host assert 'name="host_id" value="h1"' in page and 'value="10.0.0.5"' in page assert 'href="/hosts/h1" class="btn btn-sm btn-ghost">Cancel' in page async def test_the_agent_card_keeps_its_actions_under_manage(app): """#5278: "Remove" said nothing about what it removed.""" from datetime import datetime, timezone from tests._templates import render_plugin_template host = SimpleNamespace(id="h1", name="box", address="10.0.0.5") reg = SimpleNamespace(agent_version="1.8.0", last_seen_at=datetime.now(timezone.utc)) target = SimpleNamespace(id="t1", name="box") async with app.test_request_context("/plugins/host_agent/panel/h1"): session["user_role"] = "admin" page = await render_plugin_template( app, "host_agent", "panel.html", host=host, reg=reg, reporting=True, stale=False, target=target, ansible_available=True, managed_key_set=True) manage = page.index(">Manage agent") assert page.index("Full metrics →") < manage for action in (">Update agent", ">Rotate token", ">Unregister agent"): assert page.index(action) > manage assert ">Remove" not in page async def test_the_hosts_list_says_what_is_wrong_and_labels_its_figures(app): """#5279–#5281: labelled CPU and Memory, one uptime figure, status with how long, no SLA tab and no per-row edit/delete.""" from datetime import datetime, timedelta, timezone now = datetime.now(timezone.utc) down = SimpleNamespace(id="h1", name="nas", address="10.0.0.5") quiet = SimpleNamespace(id="h2", name="pi", address="") async with app.test_request_context("/hosts/"): session["user_role"] = "admin" page = await render_template("hosts/list.html", **_list_ctx( hosts=[down, quiet], host_status={"h1": {"state": "down", "latency_ms": None, "count": 2, "down_since": now - timedelta(minutes=12)}}, uptime={"h1": {"24h": 91.5, "7d": 98.0, "30d": 99.42}}, agent={"pi": {"fresh": False, "last_at": now - timedelta(minutes=7), "cpu_pct": 3.0, "mem_used_pct": 41.0}})) # No inventory groups: no Groups column. assert _headers(page) == ["Name", "Address", "Status", "Latency", "Monitors", "30d uptime", "CPU", "Memory"] assert "for 12m" in page and "agent silent" in page and "for 7m" in page assert "99.42%" in page and 'title="24h 91.50% · 7d 98.00%"' in page assert ">41%<" in page and "tone-dim" in page # a silent agent's last reading, greyed assert "/hosts/uptime" not in page and ">Delete<" not in page and ">Edit<" not in page async def test_a_host_without_an_agent_can_get_a_manual_install(app): """#5282: the manual install moved here from the fleet's add-host form.""" from tests._templates import render_plugin_template host = SimpleNamespace(id="h1", name="box", address="10.0.0.5") async with app.test_request_context("/plugins/host_agent/panel/h1"): session["user_role"] = "admin" page = await render_plugin_template( app, "host_agent", "panel.html", host=host, reg=None, reporting=False, stale=False, target=None, ansible_available=False, managed_key_set=False) assert 'action="/plugins/host_agent/fleet/h1/register"' in page and ">Install manually" in page async def test_add_host_is_a_dialog_on_the_hosts_list(app): """#5291: the form was two fields on a page of its own.""" async with app.test_request_context("/hosts/"): session["user_role"] = "operator" closed = await render_template("hosts/list.html", **_list_ctx()) dialog = re.search(r'', closed, re.S).group(0) assert 'action="/hosts/"' in dialog and ">Cancel" in dialog assert re.search(r']*name="address"[^>]*>', dialog).group(0).count("required") == 0 assert "showModal()\">Add host" in closed assert "getElementById('add-host').showModal();" not in closed assert "/hosts/new" not in closed async with app.test_request_context("/hosts/"): session["user_role"] = "viewer" viewer = await render_template("hosts/list.html", **_list_ctx(add={})) assert 'id="add-host"' not in viewer and ">Add host<" not in viewer async def test_add_host_sends_a_clash_back_with_what_was_typed(app): """#5283, #5291: the list again, the dialog open, an inline error and the typed values.""" async with app.test_request_context("/hosts/"): session["user_role"] = "operator" page = await render_template("hosts/list.html", **_list_ctx( add={"values": {"name": "nas", "address": "10.0.0.9"}, "error": "A host named nas already exists."})) assert 'role="alert">A host named nas already exists.<' in page assert 'value="nas"' in page and 'value="10.0.0.9"' in page assert "getElementById('add-host').showModal();" in page async def test_edit_host_keeps_its_page(app): host = SimpleNamespace(id="h1", name="nas", address="10.0.0.5") async with app.test_request_context("/hosts/h1/edit"): session["user_role"] = "operator" page = await render_template("hosts/form.html", host=host) assert 'action="/hosts/h1"' in page and 'value="nas"' in page and 'value="10.0.0.5"' in page assert 'nas' in page and ">Save" in page async def test_the_hosts_list_has_a_filter_bar_sortable_headers_and_group_chips(app): """#5294: search, group and status filters; headers that sort; a host's groups as chips that filter the list.""" swarm = SimpleNamespace(id="h1", name="docker02", address="10.0.0.12") lone = SimpleNamespace(id="h2", name="util01", address="10.0.0.9") async with app.test_request_context("/hosts/"): session["user_role"] = "viewer" page = await render_template("hosts/list.html", **_list_ctx( hosts=[swarm, lone], groups={"h1": ["Docker Swarm"]}, group_names=["Docker Swarm"], f={"q": "", "group": "", "state": "", "sort": "cpu", "dir": "desc"})) bar = re.search(r'
', page, re.S).group(0) assert 'type="search" name="q"' in bar and 'hx-target="#hosts-table"' in bar and 'hx-push-url="true"' in bar assert '' in bar assert [o for o in re.findall(r'