The SNMP poll asks each device's sysName in the same GET and keeps it (snmp_003_sys_name). A syslog source told apart by name finds its own address from the SNMP device (reported or given name) or host of that name, so its lines reach the SNMP device's and host's pages again; the edit page gains Device address, which always wins (syslog_004). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
171 lines
9.7 KiB
Python
171 lines
9.7 KiB
Python
"""The Syslog page (#5999): whether the listener is open or why not, the
|
|
sources kept, and the senders offered to an admin to add or ignore."""
|
|
from datetime import datetime, timedelta, timezone
|
|
from types import SimpleNamespace as NS
|
|
|
|
from tests._templates import render_plugin_template
|
|
from tests.test_style_tokens import hex_colours
|
|
|
|
|
|
def _listener(**over):
|
|
base = {"port": 5514, "protocols": ["UDP", "TCP"], "listening": True, "error": None,
|
|
"bound_at": datetime.now(timezone.utc) - timedelta(hours=2), "received": 1204,
|
|
"dropped": 0, "unoffered": 0, "connections": 1, "senders_full": False, "max_senders": 200}
|
|
return {**base, **over}
|
|
|
|
|
|
def _source(name, state, level=6, line="nfsd: server not responding", host=None):
|
|
now = datetime.now(timezone.utc)
|
|
return {"id": f"id-{name}", "address": "192.168.0.7", "state": state, "name": name,
|
|
"hostname_seen": "synnas", "host": host, "first_seen_at": now - timedelta(days=1),
|
|
"last_seen_at": now - timedelta(minutes=3), "lines_total": 1204,
|
|
"severity": "err" if level == 3 else "info", "level": level, "last_line": line,
|
|
"suggested_host": "h1", "rate": 12 if state == "accepted" else None, "sender": "", "device_address": ""}
|
|
|
|
|
|
async def _render(app, role, **ctx):
|
|
from quart import session
|
|
ctx = {"listener": _listener(), "sources": [], "offered": [], "ignored": [],
|
|
"hosts": [NS(id="h1", name="SynNAS", address="192.168.0.7")],
|
|
"server": "steward.lan", "now": datetime.now(timezone.utc), **ctx}
|
|
async with app.test_request_context("/plugins/syslog/"):
|
|
session["user_role"] = role
|
|
page = await render_plugin_template(app, "syslog", "syslog/index.html", **ctx)
|
|
return " ".join(page.split())
|
|
|
|
|
|
async def test_an_offered_sender_can_be_added_or_ignored_by_an_admin(app):
|
|
page = await _render(app, "admin", offered=[_source("synnas", "offered", level=3)])
|
|
assert "Sending, not added" in page and "calls itself synnas" in page
|
|
assert 'action="/plugins/syslog/sources/id-synnas/add"' in page
|
|
assert 'action="/plugins/syslog/sources/id-synnas/ignore"' in page
|
|
assert '<option value="h1" selected>SynNAS</option>' in page # the host at its address
|
|
assert 'class="tone-crit">err</span> nfsd: server not responding' in page
|
|
assert "UDP and TCP port 5514" in page and "1,204 lines received" in page
|
|
main = page[page.index("<main"):]
|
|
assert "font-weight:600" not in main and not hex_colours(main), hex_colours(main)
|
|
|
|
|
|
async def test_a_viewer_sees_sources_but_no_controls(app):
|
|
page = await _render(app, "viewer", sources=[_source("SynNAS", "accepted", host=NS(id="h1", name="SynNAS"))],
|
|
offered=[_source("router", "offered")])
|
|
assert 'href="/hosts/h1"' in page and 'href="/plugins/syslog/sources/id-SynNAS"' in page
|
|
assert "/add" not in page and "/ignore" not in page and "/edit" not in page
|
|
assert "Sending, not added" in page
|
|
|
|
|
|
async def test_a_port_it_cannot_open_is_said(app):
|
|
page = await _render(app, "admin", listener=_listener(
|
|
listening=False, error="Port 514 couldn't be opened (UDP: Permission denied)."))
|
|
assert "Not listening" in page and "Port 514 couldn't be opened" in page
|
|
assert "below 1024" in page
|
|
assert "No sources yet." in page
|
|
|
|
|
|
async def test_setup_help_names_the_address_set_and_opens_until_a_device_sends(app):
|
|
page = await _render(app, "viewer", server="192.168.0.10")
|
|
assert '<details class="card" id="syslog-setup" open>' in page
|
|
assert "send to <span class=\"mono\">192.168.0.10</span>, port <span class=\"mono\">5514</span>, UDP or TCP" in page
|
|
assert "*.* @@192.168.0.10:5514" in page and "ASUSTOR (ADM)" in page
|
|
assert "<strong>Ingress</strong>" in page and "mode: host" in page
|
|
added = await _render(app, "viewer", sources=[_source("SynNAS", "accepted")])
|
|
assert '<details class="card" id="syslog-setup">' in added # closed once one sends
|
|
|
|
|
|
async def test_setup_help_never_guesses_the_address(app):
|
|
page = await _render(app, "viewer", server="")
|
|
assert "*.* @@ADDRESS:5514" in page and '"Send to"' in page
|
|
assert "steward.lan" not in page
|
|
|
|
|
|
async def test_a_sender_on_dockers_own_network_is_called_out(app):
|
|
hidden = {**_source("ingress", "offered"), "address": "10.0.0.2", "docker_address": True}
|
|
page = await _render(app, "admin", offered=[hidden, {**hidden, "id": "id-2"}])
|
|
assert "Lines from 10.0.0.2 come through Docker's own address and carry no device name" in page
|
|
assert page.count("Docker's address, not the device's") == 2
|
|
assert "carry no device name" not in await _render(app, "admin", offered=[_source("synnas", "offered")])
|
|
|
|
|
|
async def test_devices_through_dockers_address_read_by_their_own_names(app):
|
|
via = {**_source("SynNAS", "offered"), "address": "10.0.0.2", "docker_address": True, "sender": "SynNAS"}
|
|
page = await _render(app, "admin", offered=[via, {**via, "id": "id-2", "sender": "AsusNAS"}])
|
|
assert "through Docker's ingress" in page and '<span class="mono">AsusNAS</span>' in page
|
|
assert '<span class="mono">192.168.0.7</span>, through Docker' not in page
|
|
placed = await _render(app, "admin", offered=[{**via, "device_address": "192.168.0.7"}])
|
|
assert '<span class="mono">192.168.0.7</span>, through Docker's ingress' not in placed
|
|
assert '<span class="mono">192.168.0.7</span>, through Docker\'s ingress' in placed
|
|
assert "can't be told apart" not in page and "not the device's" not in page
|
|
alone = await _render(app, "admin", offered=[{**via, "sender": ""}])
|
|
assert "carry no device name" in alone
|
|
|
|
|
|
async def test_the_edit_page_has_its_trail_and_a_remove(app):
|
|
from quart import session
|
|
source = NS(id="s1", name="SynNAS", hostname_seen="synnas", address="192.168.0.7", host_id=None)
|
|
async with app.test_request_context("/plugins/syslog/sources/s1/edit"):
|
|
session["user_role"] = "admin"
|
|
page = await render_plugin_template(app, "syslog", "syslog/edit.html", source=source,
|
|
hosts=[], label="SynNAS")
|
|
page = " ".join(page.split())
|
|
assert 'href="/plugins/syslog/">Syslog</a>' in page
|
|
assert 'action="/plugins/syslog/sources/s1/delete"' in page and "data-confirm=" in page
|
|
|
|
|
|
def _line(i, level=6, app="kernel", message="nfsd: server not responding", minutes=1):
|
|
now = datetime.now(timezone.utc)
|
|
return {"id": f"l{i}", "received_at": now - timedelta(minutes=minutes), "sent_at": None,
|
|
"level": level, "severity": {3: "err", 4: "warning"}.get(level, "info"),
|
|
"facility": "kern", "hostname": "synnas", "app": app, "message": message,
|
|
"group": "Today", "opens_group": i == 0}
|
|
|
|
|
|
async def test_a_source_page_filters_its_lines_and_scrolls_on(app):
|
|
from quart import session
|
|
from plugins.syslog.service import SEVERITY_CHOICES, LineFilter
|
|
src = {**_source("SynNAS", "accepted"), "rate": 42}
|
|
async with app.test_request_context("/plugins/syslog/sources/id-SynNAS"):
|
|
session["user_role"] = "viewer"
|
|
page = await render_plugin_template(
|
|
app, "syslog", "syslog/source.html", source=src, f=LineFilter(severity=3), filtered=True,
|
|
lines=[_line(0, level=3), _line(1, app="", message="a <b> tag")],
|
|
more_url="/plugins/syslog/sources/id-SynNAS?severity=3&before=x", severities=SEVERITY_CHOICES,
|
|
apps=["kernel", "nfsd"], ranges=["1h", "24h"])
|
|
page = " ".join(page.split())
|
|
assert 'href="/plugins/syslog/">Syslog</a>' in page # its trail
|
|
assert "42 lines in the last hour" in page
|
|
assert '<option value="3" selected>Errors and worse</option>' in page
|
|
assert '<tr class="table-divider"><th colspan="4" scope="rowgroup">Today</th></tr>' in page
|
|
assert '<span class="tone-crit">err</span>' in page and "a <b> tag" in page
|
|
assert 'hx-get="/plugins/syslog/sources/id-SynNAS?severity=3&before=x"' in page
|
|
assert "/edit" not in page # a viewer
|
|
main = page[page.index("<main"):]
|
|
assert "font-weight:600" not in main and not hex_colours(main), hex_colours(main)
|
|
|
|
|
|
async def test_a_filtered_source_with_no_lines_offers_them_all_back(app):
|
|
from quart import session
|
|
from plugins.syslog.service import LineFilter
|
|
async with app.test_request_context("/plugins/syslog/sources/id-SynNAS"):
|
|
session["user_role"] = "viewer"
|
|
table = await render_plugin_template(
|
|
app, "syslog", "syslog/_lines_table.html", source=_source("SynNAS", "accepted"),
|
|
f=LineFilter(text="nope"), filtered=True, lines=[], more_url=None)
|
|
assert "No lines match these filters." in table and 'href="/plugins/syslog/sources/id-SynNAS"' in table
|
|
|
|
|
|
async def test_the_panel_names_each_source_when_there_are_several(app):
|
|
from quart import session
|
|
lines = [{**_line(0, level=4), "source": "SynNAS", "source_id": "s1"},
|
|
{**_line(1), "source": "router", "source_id": "s2"}]
|
|
async with app.test_request_context("/plugins/syslog/panel"):
|
|
session["user_role"] = "viewer"
|
|
one = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=lines[:1],
|
|
sources=[{"id": "s1", "name": "SynNAS"}])
|
|
two = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=lines,
|
|
sources=[{"id": "s1", "name": "SynNAS"}, {"id": "s2", "name": "router"}])
|
|
none = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=[],
|
|
sources=[{"id": "s1", "name": "SynNAS"}])
|
|
assert "<th>Source</th>" not in one and '<span class="tone-warn">warning</span>' in one
|
|
assert "<th>Source</th>" in two and 'href="/plugins/syslog/sources/s2">router</a>' in two
|
|
assert "No lines in the last day." in none
|