feat(syslog): devices through Swarm's ingress told apart by name; the send-to address is a setting (#6096)
Through ingress every device arrives from Docker's own address, so a source there is keyed by the hostname its lines carry (new column sender, unique with address). The set-up help no longer guesses the address from the web hostname: it shows the plugin's send_to setting, covers ingress and host mode, ASUSTOR ADM, and both syslog formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,41 @@ def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_devices_through_dockers_address_are_told_apart_by_name(app, monkeypatch):
|
||||
"""Through Swarm's ingress every device arrives from Docker's address
|
||||
(#6096): there each name its lines carry is its own source."""
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
SyslogSource = _mod("models").SyslogSource
|
||||
receiver = _mod("receiver")
|
||||
parse = _mod("parse").parse
|
||||
monkeypatch.setattr(receiver, "on_own_network", lambda address: address.startswith("10.0.0."))
|
||||
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
|
||||
|
||||
def line(address, host, text):
|
||||
return receiver.Received(now, address, parse(f"<14>Oct 19 01:12:03 {host} kernel: {text}"))
|
||||
|
||||
async def go():
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await s.execute(delete(SyslogSource))
|
||||
await receiver.store_lines(s, [line("10.0.0.2", "SynNAS", "a"), line("10.0.0.2", "AsusNAS", "b"),
|
||||
line("10.0.0.2", "SynNAS", "c")])
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await receiver.store_lines(s, [line("10.0.0.2", "AsusNAS", "d"), line("10.9.0.1", "renamed", "e")])
|
||||
async with app.db_sessionmaker() as s:
|
||||
rows = (await s.execute(select(SyslogSource))).scalars().all()
|
||||
got = sorted((r.address, r.sender, r.lines_total, r.last_line) for r in rows)
|
||||
assert got == [("10.0.0.2", "AsusNAS", 2, "d"), ("10.0.0.2", "SynNAS", 2, "c"),
|
||||
("10.9.0.1", "", 1, "e")]
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await s.execute(delete(SyslogSource))
|
||||
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_the_ring_keeps_each_source_within_its_window_and_cap(app):
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ def _source(name, state, level=6, line="nfsd: server not responding", host=None)
|
||||
"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}
|
||||
"suggested_host": "h1", "rate": 12 if state == "accepted" else None, "sender": ""}
|
||||
|
||||
|
||||
async def _render(app, role, **ctx):
|
||||
@@ -62,15 +62,22 @@ async def test_a_port_it_cannot_open_is_said(app):
|
||||
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")
|
||||
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\">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
|
||||
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"}])
|
||||
@@ -79,6 +86,15 @@ async def test_a_sender_on_dockers_own_network_is_called_out(app):
|
||||
assert "own container network" 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, 10.0.0.2" in page and '<span class="mono">AsusNAS</span>' in page
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user