feat(snmp): NAS presets for Synology and Asustor, reading every disk and volume (#5985, #5986)
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 1m38s
CI / integration (push) Successful in 1m41s
CI / publish (push) Successful in 1m31s

A preset can now read a vendor's tables: one row per disk, volume, fan or
sensor. The scheduled poll discovers the rows by walking the device: on its
first poll, after a save, and then hourly, and only while it answers. Each row
becomes readings named by the device's own labels ("Disk 1 status"). Only the
columns a row answered are asked for.

A vendor status, whether a code (Synology) or a word (Asustor), is a state
reading. It is stored as a level (0 OK, 1 busy, 2 warning, 3 failed), so one
alert rule covers any vendor, and shown in the device's own word.

- Synology: system, power and fan status and temperature; per disk: status,
  health, temperature and bad sectors; per volume: RAID state (scrubbing,
  parity check, rebuild, degraded), free space and size.
- Asustor: per disk: status, SMART and temperature; per volume: status, free
  space and size; CPU and system temperature, free memory and fans.
  From ADM's ASUSTOR-DISK-MIB and ASUSTOR-SYSTEM-MIB.
- Linux server: plus each physical disk's 1-minute busy % (UCD-DISKIO-MIB).

Migration snmp_002_table_rows adds tables, tables_at and texts to
snmp_devices. Milestone 530.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 22:40:14 -04:00
co-authored by Claude Opus 5.5
parent 4ded8ecee4
commit d065cafd11
12 changed files with 593 additions and 48 deletions
+15
View File
@@ -87,6 +87,21 @@ def test_test_uses_what_is_typed_and_falls_back_to_the_stored_secret():
assert form_credentials(_form(), None)[2].community == "public"
def test_a_nas_preset_alone_is_a_choice_though_its_rows_come_with_the_first_poll():
assert parse_form(_form(presets=["asustor"]))["presets"] == ["asustor"]
with pytest.raises(FormError, match="Choose at least one preset"):
parse_form(_form(presets=[]))
def test_a_device_is_asked_for_its_stored_rows_or_the_ones_just_found():
row = NS(presets=["synology"], interfaces="", custom="", version="2c",
tables={"synology_volumes": [{"index": "0", "label": "Volume 1", "columns": ["status"]}]})
assert "raid0_status" in [r.key for r in readings_for(row)]
found = {"synology_disks": [{"index": "0", "label": "Disk 1", "columns": ["temp"]}]}
keys = [r.key for r in readings_for(row, found)]
assert "disk0_temp" in keys and "raid0_status" not in keys
def test_a_row_that_no_longer_parses_is_asked_for_nothing():
row = NS(presets=["system"], interfaces="eth0", custom="", version="2c")
assert readings_for(row) == []