The operator, reviewing SNMP on #5214: "this system looks incomplete like there's something missing from it". The one device shown was plugin.yaml's example (core-switch 192.168.1.1, community public), polled every minute; devices could only be defined by editing YAML in the image; a failed poll was logged at DEBUG and stored nowhere, so the page said "waiting for the next poll" forever; v3 ran as v2c; counters showed as lifetime totals. - Devices are rows (snmp_devices, migration snmp_001): address, port, v1/v2c community or a v3 user with auth/priv protocols and keys (secrets encrypted), presets plus custom readings, an optional host binding, and the newest poll's outcome. The YAML device list and its example are gone. - The SNMP page adds, edits and deletes them (admin, audited, with get_audit_entities so the audit log names and links a device). Readings come from presets (system uptime, interface traffic by number, UPS-MIB, Linux net-snmp) and custom "OID name [unit]" lines. - One GET per device with every OID, devices polled at once. The outcome is written to the row in words ("No reply from … within 5 s. Check the address and port, that SNMP is on, and the community"), with readings it lacks. v1's whole-request error drops that OID and asks again. SNMPv3 signs in with USM (MD5/SHA-1/SHA-2, DES/3DES/AES). - Test asks the unsaved form's device for its name, description, uptime and interfaces by number. It is an admin's explicit action; every page reads only stored rows (System 178's ruling). - Each reading has a unit; a counter is stored as a per-second rate (bytes as bit/s), a reset or wrap skipped. The page, widget and host panel say values in units and why a device is silent; the device page draws a chart per reading on a dated axis. Tests: readings and presets, the form and its secrets, v3 checks, rates, the poller against a stand-in pysnmp (one GET, v1 retry, timeout wording, USM), the pages, and an integration test of the poll writing outcomes and rates on Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
"""What an SNMP device is asked for and how each answer reads (milestone 491)."""
|
|
import pytest
|
|
|
|
from plugins.snmp.readings import (device_readings, format_value, interface_readings, parse_custom,
|
|
parse_interfaces, stored_unit)
|
|
|
|
|
|
def test_interface_numbers_read_as_a_list():
|
|
assert parse_interfaces("1, 2 5-7,2") == [1, 2, 5, 6, 7]
|
|
assert parse_interfaces("") == []
|
|
for bad in ("eth0", "7-3", "1-100"):
|
|
with pytest.raises(ValueError):
|
|
parse_interfaces(bad)
|
|
|
|
|
|
def test_interfaces_read_64_bit_counters_except_on_v1():
|
|
v2 = {r.key: r for r in interface_readings([3], "2c")}
|
|
v1 = {r.key: r for r in interface_readings([3], "1")}
|
|
assert set(v2) == {"if3_status", "if3_in", "if3_out"}
|
|
assert v2["if3_in"].oid == "1.3.6.1.2.1.31.1.1.1.6.3" and v2["if3_in"].counter
|
|
assert v1["if3_in"].oid == "1.3.6.1.2.1.2.2.1.10.3"
|
|
assert stored_unit(v2["if3_out"]) == "bits_per_second" and not v2["if3_status"].counter
|
|
|
|
|
|
def test_a_custom_line_is_an_oid_a_name_and_a_unit():
|
|
got = parse_custom("# a comment\n.1.3.6.1.4.1.9.1.0 fan_speed\n1.3.6.1.4.1.9.2.0 inlet_temp celsius\n")
|
|
assert [(r.oid, r.key, r.name, r.unit) for r in got] == [
|
|
("1.3.6.1.4.1.9.1.0", "fan_speed", "Fan speed", "number"),
|
|
("1.3.6.1.4.1.9.2.0", "inlet_temp", "Inlet temp", "celsius")]
|
|
|
|
|
|
@pytest.mark.parametrize("line, says", [
|
|
("1.3.6.1", "OID name"),
|
|
("sysUpTime uptime", "is not an OID"),
|
|
("1.3.6.1.2 Bad-Name", "must start with a letter"),
|
|
("1.3.6.1.2 temp furlongs", "is not a unit"),
|
|
])
|
|
def test_a_custom_line_that_does_not_read_says_which_and_why(line, says):
|
|
with pytest.raises(ValueError, match="Line 1") as exc:
|
|
parse_custom(line)
|
|
assert says in str(exc.value)
|
|
|
|
|
|
def test_presets_come_in_order_and_names_may_not_repeat():
|
|
keys = [r.key for r in device_readings(["ups", "system", "interfaces"], [1], "", "2c")]
|
|
assert keys[:4] == ["uptime", "if1_status", "if1_in", "if1_out"] and "battery_charge" in keys
|
|
with pytest.raises(ValueError, match="uptime"):
|
|
device_readings(["system"], [], "1.3.6.1.2.1.1.3.0 uptime", "2c")
|
|
|
|
|
|
@pytest.mark.parametrize("unit, value, text", [
|
|
("centiseconds", 105_120_000, "12d 4h"),
|
|
("percent", 99.6, "100%"),
|
|
("volts", 121.0, "121 V"),
|
|
("decivolts", 1203, "120.3 V"),
|
|
("minutes", 45, "45m"),
|
|
("kilobytes", 2048, "2.0 MiB"),
|
|
("bits_per_second", 12_345_678, "12.3 Mbit/s"),
|
|
("bits_per_second", 950, "950 bit/s"),
|
|
("if_status", 2, "down"),
|
|
("ups_battery", 3, "low"),
|
|
("if_status", 99, "code 99"),
|
|
("number", 41.25, "41.25"),
|
|
("number", None, "—"),
|
|
])
|
|
def test_a_value_reads_in_its_unit(unit, value, text):
|
|
assert format_value(unit, value) == text
|