Files
FabledSteward/tests/plugins/snmp/test_scheduler.py
T
bvandeusenandClaude Opus 5.5 ad8bbd8d69
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m31s
CI / unit (push) Failing after 1m32s
CI / publish (push) Skipped
feat(snmp): a NAS's disk and volume I/O, CPU waiting on disk, available memory, interface errors (#5997)
- Synology: what each disk and volume reads and writes, bytes and requests
  a second and the load DSM gives it (SYNOLOGY-STORAGEIO- and -SPACEIO-MIB,
  64-bit byte counters), and each disk's reconnects and sectors not found.
- Linux server: CPU waiting on disk, the wait tick counter's share of the
  CPU's, and memory available (free + buffers + cache). A reading may now be
  worked out from others: the poll asks for its inputs, rates each counter,
  and stores only the result.
- Interface traffic: errors and discards in and out, per second.
- byte_count unit: a byte counter shown as bytes a second.

Asustor's MIB has nothing more for this; its Linux server readings gain the
same CPU wait and available memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:50:52 -04:00

111 lines
4.8 KiB
Python

"""An SNMP poll collects on schedule while the database hangs, and what it
held is stored oldest first, each at its own time, once it answers (#5995)."""
import asyncio
from contextlib import asynccontextmanager
from types import SimpleNamespace as NS
import pytest
import plugins.snmp.scheduler as scheduler
import plugins.snmp.service as service
from plugins.snmp.poller import Credentials, PollResult
def _device(presets=("system",)):
return NS(id="d1", name="nas", address="10.0.0.5", port=161, version="2c", presets=list(presets),
interfaces="", custom=None, tables={}, tables_at=None, last_error=None, texts={})
@asynccontextmanager
async def _hung_session():
await asyncio.Event().wait() # never answers: the device list comes from memory
yield None
def test_polls_go_on_while_the_database_hangs_and_are_stored_in_order_after(monkeypatch):
async def main():
stalled = asyncio.Event()
@asynccontextmanager
async def hung_session():
await stalled.wait() # the storage under Postgres has stalled
yield None
app = NS(db_sessionmaker=hung_session)
gate, stored = asyncio.Event(), []
async def store(app_, rounds):
await gate.wait()
stored.extend(rounds)
uptimes = iter([100.0, 200.0])
async def poll(address, port, creds, oids):
return PollResult(readings={"uptime": next(uptimes)})
monkeypatch.setattr(scheduler, "DEVICES", [_device()])
monkeypatch.setattr(scheduler, "DEVICES_READ_S", 0.05)
monkeypatch.setattr(scheduler, "STORE_WAIT_S", 0.05)
monkeypatch.setattr(scheduler, "_store", store)
monkeypatch.setattr(service, "credentials", lambda d: Credentials(community="public"))
scheduler.RATES = None
await scheduler._do_poll(app, poll=poll) # the device list is the last one read
await asyncio.sleep(0.01)
await scheduler._do_poll(app, poll=poll) # the first store hasn't returned: polled anyway
gate.set()
await app._holdings["SNMP poll"].flush(1.0)
return stored
rounds = asyncio.run(main())
assert [[(p.name, p.values) for p in r] for r in rounds] == [
[("nas", [("uptime", 100.0)])], [("nas", [("uptime", 200.0)])]]
assert rounds[0][0].at < rounds[1][0].at # each stored at the time it was collected
def test_a_reading_worked_out_from_others_is_asked_for_by_its_inputs_and_kept_alone(monkeypatch):
"""#5997: CPU waiting on disk from the tick counters' rates, and available
memory from three gauges; the inputs are neither stored nor named missing."""
mem = {"memory_free_kb": 1000.0, "memory_buffers_kb": 200.0, "memory_cached_kb": 3000.0}
answers = iter([
{"cpu_ticks_user": 1000.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 500.0,
"cpu_ticks_idle": 8000.0, "cpu_ticks_wait": 500.0} | mem,
{"cpu_ticks_user": 1100.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 550.0,
"cpu_ticks_idle": 8800.0, "cpu_ticks_wait": 550.0} | mem,
{"cpu_ticks_user": 1200.0, "cpu_ticks_nice": 0.0, "cpu_ticks_system": 600.0,
"cpu_ticks_idle": 9600.0} | mem]) # this time it doesn't answer the wait counter
asked, stored = [], []
async def poll(address, port, creds, oids):
asked.append(dict((k, o) for o, k in oids))
got = next(answers)
return PollResult(readings=got, missing=[k for _, k in oids if k not in got])
async def store(app_, rounds):
stored.extend(rounds)
async def main():
monkeypatch.setattr(scheduler, "DEVICES", [_device(["linux"])])
monkeypatch.setattr(scheduler, "DEVICES_READ_S", 0.05)
monkeypatch.setattr(scheduler, "_store", store)
monkeypatch.setattr(service, "credentials", lambda d: Credentials(community="public"))
scheduler.RATES = None
app = NS(db_sessionmaker=_hung_session)
for _ in range(3):
await scheduler._do_poll(app, poll=poll, walk=_no_tables)
await asyncio.sleep(0.01)
asyncio.run(main())
assert asked[0]["cpu_ticks_wait"] == "1.3.6.1.4.1.2021.11.54.0" and "cpu_iowait" not in asked[0]
assert "" not in asked[0].values()
first, second, third = (dict(r[0].values) for r in stored)
assert first == {"memory_available": 4200.0} # a counter needs two samples
assert second == {"cpu_iowait": pytest.approx(5.0), "memory_available": 4200.0} # 50 of 1,000 ticks waited
assert third == {"memory_available": 4200.0}
assert "cpu_iowait" in stored[2][0].missing and not any(k.startswith("cpu_ticks") for k in stored[2][0].missing)
async def _no_tables(address, port, creds, tables):
from plugins.snmp.poller import TableScan
return TableScan(rows={})