fix(snmp): a disk listed late in UCD-DISKIO is read, and Asustor disks are named by bay (#5992)
The table walk stopped after 64 raw entries, counting the RAM disks, loops,
partitions and md arrays that rows_like then drops, so a Synology's eighth
disk was never reached. The walk is now bounded at 1,024 entries per column;
64 still caps the rows kept.
ADM labels its disks "Disk0".."Disk4" while their indexes run 1..5; the
Asustor disk table now names each row by its index ("Disk 1"). Keys are
unchanged, so history carries on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -222,9 +222,13 @@ async def poll_device(address: str, port: int, creds: Credentials,
|
||||
_close_engine(engine)
|
||||
|
||||
|
||||
# Rows read of one table: enough for any NAS's disks and volumes, and a bound
|
||||
# on a device that answers a walk forever.
|
||||
# Rows kept of one table: enough for any NAS's disks and volumes.
|
||||
ROWS_PER_TABLE = 64
|
||||
# Entries walked of one column, before rows_like keeps the physical disks: a
|
||||
# Synology's UCD-DISKIO lists RAM disks, loops, partitions and md arrays ahead
|
||||
# of a disk added late, so the bound is on the walk, not on the rows kept, and
|
||||
# it only stops a device that answers a walk forever.
|
||||
ENTRIES_PER_COLUMN = 1024
|
||||
|
||||
|
||||
async def _walk_column(api, engine, auth, transport, column: str) -> dict[str, object] | str:
|
||||
@@ -243,7 +247,7 @@ async def _walk_column(api, engine, auth, transport, column: str) -> dict[str, o
|
||||
if type(val).__name__ in _ABSENT or not oid.startswith(column + "."):
|
||||
continue
|
||||
found[oid[len(column) + 1:]] = val
|
||||
if len(found) >= ROWS_PER_TABLE:
|
||||
if len(found) >= ENTRIES_PER_COLUMN:
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
@@ -132,13 +132,16 @@ class Table:
|
||||
by its label and column ("Disk 1 status"). A table with no label column
|
||||
(one temperature sensor, one memory) is keyed and named by its columns
|
||||
alone while it has one row. `rows_like` keeps only rows whose label
|
||||
matches it (physical disks, not partitions and loop devices)."""
|
||||
matches it (physical disks, not partitions and loop devices).
|
||||
`by_index` names each row by its noun and index instead ("Disk 1"), for
|
||||
a device whose own label doesn't say which bay it is (ADM's "Disk0")."""
|
||||
id: str
|
||||
prefix: str
|
||||
label: str
|
||||
noun: str
|
||||
columns: tuple[Column, ...]
|
||||
rows_like: str = ""
|
||||
by_index: bool = False
|
||||
|
||||
def column_oids(self) -> list[str]:
|
||||
return ([self.label] if self.label else []) + [c.oid for c in self.columns]
|
||||
@@ -154,7 +157,9 @@ def table_readings(table: Table, rows: list[dict]) -> list[Reading]:
|
||||
alone = not table.label and len(rows) == 1
|
||||
for row in rows:
|
||||
index, label = str(row["index"]), str(row.get("label") or "")
|
||||
if label.isdigit() or (table.label and not label):
|
||||
if table.by_index:
|
||||
label = f"{table.noun} {index}"
|
||||
elif label.isdigit() or (table.label and not label):
|
||||
label = f"{table.noun} {label or index}"
|
||||
for c in table.columns:
|
||||
if c.key not in row.get("columns", ()):
|
||||
@@ -233,7 +238,7 @@ TABLES: dict[str, tuple[Table, ...]] = {
|
||||
Table("asustor_disks", "disk", _asus("4.1.1.2"), "Disk",
|
||||
(Column(_asus("4.1.1.5"), "status", "status", "state", states="words"),
|
||||
Column(_asus("4.1.1.6"), "smart", "SMART", "state", states="words"),
|
||||
Column(_asus("4.1.1.7"), "temp", "temperature", "celsius"))),
|
||||
Column(_asus("4.1.1.7"), "temp", "temperature", "celsius")), by_index=True),
|
||||
Table("asustor_volumes", "volume", _asus("5.1.1.2"), "Volume",
|
||||
(Column(_asus("5.1.1.4"), "status", "status", "state", states="words"),
|
||||
Column(_asus("5.1.1.7"), "free", "free", "bytes", scale=_GIB),
|
||||
|
||||
@@ -185,6 +185,15 @@ def test_a_table_walk_finds_each_row_its_label_and_the_columns_it_answered(monke
|
||||
{"index": "1", "label": "Disk 1", "columns": ["status", "temp"]}]}
|
||||
|
||||
|
||||
def test_a_disk_listed_after_many_partitions_and_loops_is_still_found(monkeypatch):
|
||||
from plugins.snmp.readings import TABLES
|
||||
[diskio] = TABLES["linux"]
|
||||
labels = {str(i): f"loop{i}" for i in range(1, 80)} | {"80": "sdh", "81": "sdh1"}
|
||||
_walk(monkeypatch, {diskio.label: labels, diskio.columns[0].oid: {i: 5 for i in labels}})
|
||||
scan = asyncio.run(poller.walk_tables("10.0.0.2", 161, Credentials(community="public"), [diskio]))
|
||||
assert scan.rows == {"ucd_diskio": [{"index": "80", "label": "sdh", "columns": ["busy"]}]}
|
||||
|
||||
|
||||
def test_a_table_walk_with_no_reply_says_what_to_check(monkeypatch):
|
||||
from plugins.snmp.readings import Column, Table
|
||||
_walk(monkeypatch, {}, error="No SNMP response received before timeout")
|
||||
|
||||
@@ -86,6 +86,12 @@ def test_a_table_row_is_keyed_by_its_index_and_named_by_its_label():
|
||||
assert got[0].states == "words"
|
||||
|
||||
|
||||
def test_an_asustor_disk_is_named_by_its_bay_not_adms_zero_based_label():
|
||||
disks = next(t for t in TABLES["asustor"] if t.id == "asustor_disks")
|
||||
got = table_readings(disks, [{"index": "1", "label": "Disk0", "columns": ["status"]}])
|
||||
assert [(r.key, r.name) for r in got] == [("disk1_status", "Disk 1 status")]
|
||||
|
||||
|
||||
def test_a_table_without_labels_is_named_by_its_columns():
|
||||
fans = Table("f", "", "", "Fan", (Column("9.2", "fan", "Fan speed", "rpm"),))
|
||||
[one] = table_readings(fans, [{"index": "1", "label": "", "columns": ["fan"]}])
|
||||
|
||||
Reference in New Issue
Block a user