diff --git a/plugins/snmp/poller.py b/plugins/snmp/poller.py index 30a74d6..41f16d8 100644 --- a/plugins/snmp/poller.py +++ b/plugins/snmp/poller.py @@ -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 diff --git a/plugins/snmp/readings.py b/plugins/snmp/readings.py index 9f2cdc8..94ed183 100644 --- a/plugins/snmp/readings.py +++ b/plugins/snmp/readings.py @@ -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), diff --git a/tests/plugins/snmp/test_poller.py b/tests/plugins/snmp/test_poller.py index 75e6332..822861d 100644 --- a/tests/plugins/snmp/test_poller.py +++ b/tests/plugins/snmp/test_poller.py @@ -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") diff --git a/tests/plugins/snmp/test_readings.py b/tests/plugins/snmp/test_readings.py index f507365..4015931 100644 --- a/tests/plugins/snmp/test_readings.py +++ b/tests/plugins/snmp/test_readings.py @@ -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"]}])