feat(syslog): a device through Docker's address is placed by its name, or a device address set on it (#6096)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m32s
CI / integration (push) Successful in 1m42s
CI / publish (push) Successful in 20s

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>
This commit is contained in:
2026-10-19 12:24:58 -04:00
co-authored by Claude Opus 5.5
parent 5ce3dfe69d
commit fcd58224de
14 changed files with 184 additions and 32 deletions
+41 -6
View File
@@ -102,14 +102,48 @@ async def rates(db, source_ids: list[str], now: datetime | None = None) -> dict[
return {sid: n for sid, n in rows}
def sources_for(sources: list[SyslogSource], host_id: str | None, address: str | None) -> list[SyslogSource]:
def device_address(s: SyslogSource, named: dict[str, str] | None = None) -> str:
"""The device's own address: the one set on the source; else, for a
device whose lines come through Docker's address (#6096), the address of
the SNMP device or host its name is; else the address its lines come from."""
if s.device_address:
return s.device_address
if s.sender and named:
found = named.get(s.sender.lower())
if found:
return found
return s.address
async def addresses_by_name(db) -> dict[str, str]:
"""{name: address} of the SNMP devices (by the name each reports, and the
one it was given) and hosts Steward knows, names lower-cased: how a device
behind Docker's address is placed by the name its lines carry."""
from steward.core.plugin_manager import loaded_plugin_submodule
from steward.models.hosts import Host
named: dict[str, str] = {}
for name, address in (await db.execute(select(Host.name, Host.address))).all():
if name and address:
named.setdefault(name.lower(), address)
snmp = loaded_plugin_submodule("snmp", "models")
if snmp is not None:
rows = await db.execute(select(snmp.SnmpDevice.sys_name, snmp.SnmpDevice.name, snmp.SnmpDevice.address))
for sys_name, name, address in rows.all():
for n in (sys_name, name):
if n:
named[n.lower()] = address # a device's own name over a host's
return named
def sources_for(sources: list[SyslogSource], host_id: str | None, address: str | None,
named: dict[str, str] | None = None) -> list[SyslogSource]:
"""The added sources a host's or a device's page shows: those bound to
the host, and those bound to no host whose address is the one given. A
binding is exclusive, as for SNMP devices: a source bound elsewhere never
matches by address."""
the host, and those bound to no host whose device address is the one
given. A binding is exclusive, as for SNMP devices: a source bound
elsewhere never matches by address."""
address = (address or "").strip().lower()
return [s for s in sources if s.state == "accepted" and (
s.host_id == host_id if s.host_id else bool(address) and s.address.lower() == address)]
s.host_id == host_id if s.host_id else bool(address) and device_address(s, named).lower() == address)]
# ── Lines beside the readings: an anomaly's circumstances, a chart's marks (#6002)
@@ -135,7 +169,8 @@ async def shown_on(db, host_id: str | None, address: str | None) -> list[SyslogS
host = await db.get(Host, host_id)
address = host.address if host is not None else None
accepted = list((await db.execute(select(SyslogSource).where(SyslogSource.state == "accepted"))).scalars())
return sources_for(accepted, host_id, address)
named = await addresses_by_name(db) if any(s.sender for s in accepted) else None
return sources_for(accepted, host_id, address, named)
async def place_for(db, source: str, resource: str) -> tuple[str | None, str | None]: