The Syslog page says where to send (this server, its port and protocols) and where the setting is on Synology, QNAP, TrueNAS, OpenWrt, UniFi and rsyslog, open until a device sends. A sender whose address is on Steward's own container network (Swarm ingress publishing hides the real sender) is flagged, with the host-mode ports to publish instead. compose files and the Dockerfile publish 5514/udp and /tcp; docs/plugins/syslog.md describes the plugin and the deploy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
8.5 KiB
Python
151 lines
8.5 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}
|
|
|
|
|
|
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_this_server_and_opens_until_a_device_sends(app):
|
|
page = await _render(app, "viewer")
|
|
assert '<details class="card" id="syslog-setup" open>' in page
|
|
assert "send to <span class=\"mono\">steward.lan</span>, port <span class=\"mono\">5514</span>, UDP or TCP" in page
|
|
assert "*.* @@steward.lan:5514" 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_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 "10.0.0.2 is on Steward's own container network" in page
|
|
assert page.count("Docker's address, not the device's") == 2
|
|
assert "own container network" not in await _render(app, "admin", offered=[_source("synnas", "offered")])
|
|
|
|
|
|
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
|