feat(syslog): a device through Docker's address is placed by its name, or a device address set on it (#6096)
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:
@@ -0,0 +1,27 @@
|
||||
"""SNMP devices keep the name each reports (sysName)
|
||||
|
||||
A device whose syslog lines arrive through Docker's address is placed by the
|
||||
name its lines carry; the name the device reports over SNMP is how that name
|
||||
finds the device (#6096).
|
||||
|
||||
Revision ID: snmp_003_sys_name
|
||||
Revises: snmp_002_table_rows
|
||||
Create Date: 2026-10-19
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "snmp_003_sys_name"
|
||||
down_revision: Union[str, None] = "snmp_002_table_rows"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("snmp_devices", sa.Column("sys_name", sa.String(255), nullable=False, server_default=""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("snmp_devices", "sys_name")
|
||||
@@ -46,3 +46,6 @@ class SnmpDevice(Base):
|
||||
tables: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|
||||
tables_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
texts: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|
||||
# The name the device reports (sysName), asked with every poll: how a
|
||||
# device whose syslog comes through Docker's address is found (#6096).
|
||||
sys_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
|
||||
@@ -77,6 +77,7 @@ class Polled:
|
||||
missing: list = field(default_factory=list)
|
||||
texts: dict = field(default_factory=dict)
|
||||
values: list = field(default_factory=list) # (metric, value)
|
||||
sys_name: str | None = None
|
||||
|
||||
|
||||
def _snapshot(device) -> SimpleNamespace:
|
||||
@@ -118,7 +119,7 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
global RATES
|
||||
from steward.core.holding import holding
|
||||
|
||||
from .poller import poll_device, walk_tables
|
||||
from .poller import SYS_NAME, poll_device, walk_tables
|
||||
from .readings import state_of, tables_for
|
||||
from .service import RateKeeper, credentials, readings_for
|
||||
|
||||
@@ -139,7 +140,9 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
scans[d.id] = await walk(d.address, d.port, creds, tables_for(d.presets or []))
|
||||
found = scans.get(d.id)
|
||||
asked[d.id] = readings_for(d, found.rows if found is not None and found.error is None else None)
|
||||
return await poll(d.address, d.port, creds, [(r.oid, r.key) for r in _asks(asked[d.id])])
|
||||
# The device's own name rides along in the same GET.
|
||||
return await poll(d.address, d.port, creds,
|
||||
[(r.oid, r.key) for r in _asks(asked[d.id])] + [(SYS_NAME, SYS_NAME_KEY)])
|
||||
|
||||
results = await asyncio.gather(*(one(d) for d in devices), return_exceptions=True)
|
||||
|
||||
@@ -151,7 +154,10 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
polls.append(Polled(now, device.id, device.name, failed=f"SNMP poll failed: {result}"))
|
||||
device.last_error = polls[-1].failed
|
||||
continue
|
||||
p = Polled(now, device.id, device.name, error=result.error, scan=scans.get(device.id),
|
||||
sys_name = result.texts.pop(SYS_NAME_KEY, None)
|
||||
if SYS_NAME_KEY in result.missing:
|
||||
result.missing.remove(SYS_NAME_KEY)
|
||||
p = Polled(now, device.id, device.name, error=result.error, scan=scans.get(device.id), sys_name=sys_name,
|
||||
answered=bool(result.readings or result.texts), missing=list(result.missing))
|
||||
if p.scan is not None and p.scan.error is None:
|
||||
# Kept on the copy too, so a stall doesn't walk it again every poll.
|
||||
@@ -195,6 +201,10 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
await held.flush(STORE_WAIT_S)
|
||||
|
||||
|
||||
# The key the device's sysName is asked under: never a reading's.
|
||||
SYS_NAME_KEY = "_sys_name"
|
||||
|
||||
|
||||
def _asks(readings: list) -> list:
|
||||
"""What the device is asked for: each reading, or the inputs of one
|
||||
worked out from others."""
|
||||
@@ -224,6 +234,8 @@ async def _store(app: "Quart", rounds: list[list[Polled]]) -> None:
|
||||
row.last_error = p.error
|
||||
if p.answered:
|
||||
row.last_ok_at = p.at
|
||||
if p.sys_name:
|
||||
row.sys_name = p.sys_name[:255]
|
||||
# A failed poll keeps the words it had; one that answered says them afresh.
|
||||
texts = dict(row.texts or {}) if p.error else {}
|
||||
texts.update(p.texts)
|
||||
|
||||
Reference in New Issue
Block a user