monitor.snmp

otto SNMP manager — general SNMP v2c polling for performance monitoring.

This is the reusable core that lets otto monitor any SNMP speaker — network gear, a Linux box running net-snmp, or a Zephyr device running otto’s test-bed agent — over a separate channel from command execution. It is not embedded-only: a Unix host may also be monitored via SNMP.

Two layers, mirroring the shell-monitoring split in otto.monitor.parsers (where MetricParser owns presentation and lab data owns “what command to run”):

  • AcquisitionSnmpClient: a thin async pysnmp v2c GET wrapper. Lab data (the host’s snmp block) supplies only connection params and the bare list of OIDs to poll. No presentation fields ever live in lab data.

  • PresentationSnmpMetric + the descriptor registry: maps each OID to how it is charted (label, chart group, unit, tab) and how its raw varbind is interpreted (scale). This is the SNMP analog of MetricParser; graphing decisions live here. Built-in descriptors cover a standard OID set; private/device OIDs register a descriptor from an init module via register_snmp_metric(). An OID with no registered descriptor falls back to default styling (resolve_snmp_metric()) so a host can add a bare OID with zero code and still get a chart.

The pysnmp dependency is imported lazily inside SnmpClient.get() so this module imports cleanly without it, and unit tests can mock at the get boundary rather than against pysnmp internals.

class otto.monitor.snmp.SnmpMetric(*, oid: str, label: str, chart: str, y_title: str = '', unit: str = '', tab: str = 'metrics', tab_label: str = 'Metrics', scale: float = 1.0, kind: 'gauge' | 'counter' = 'gauge', meta_of: str | None = None)

Bases: OttoModel

How a single OID’s value is interpreted and charted.

Mirrors the presentation attributes MetricParser already exposes (chart/y_title/unit/tab/tab_label) plus a scale factor that converts the raw integer varbind into a real value (e.g. sysUpTime is in hundredths of a second → scale=0.01 for seconds; a CPU OID reported in centi-percent → scale=0.01 for percent).

These are deliberately not sourced from lab data — graphing stays in the monitor module. frozen=True: a descriptor is an immutable, low-volume value object shared across ticks; the registry only ever replaces, never mutates. Built and registered through the public path (register_snmp_metric()) for first- and third-party descriptors alike.

oid : str
label : str
chart : str
y_title : str
unit : str
tab : str
tab_label : str
scale : float
kind : Literal['gauge', 'counter']

How the varbind is interpreted — a gauge charts its (scaled) value directly; a counter is monotonic and is converted to a per-second rate via the target’s RateTracker (negative delta -> re-baseline, skip the tick — see otto.monitor.rates).

meta_of : str | None

When set to another OID, this descriptor’s value is not charted as its own series — it is attached to the hover-meta dict of the series produced by the meta_of OID, under this descriptor’s label. A meta_of target absent this tick simply drops the meta; never an error.

to_point(raw: float) MetricDataPoint

Apply scale to a raw numeric varbind, returning a chartable point.

otto.monitor.snmp.register_snmp_metric(metric: SnmpMetric) None

Register (or override) the descriptor for metric.oid.

Call from an init module listed in .otto/settings.toml to teach otto how to chart a private/device-specific OID — the same extension pattern as otto.monitor.parsers.register_host_parsers() and otto.host.command_frame.register_command_frame().

otto.monitor.snmp.net_oids(index: int) list[str]

Return six network OIDs for interface index: rx/tx bytes, rx/tx packets, errors, drops.

otto.monitor.snmp.fs_oids(index: int) list[str]

Return two filesystem OIDs for filesystem index: bytes used, bytes total.

otto.monitor.snmp.expand_oid_bundles(oids: Sequence[str]) list[str]

Expand named OID bundles in a host’s snmp.oids list into raw OIDs.

Raw OIDs (anything starting with a digit) pass through untouched, so existing lab data keeps working. otto-net:N / otto-fs:N expand to interfaces / filesystems 0..N-1 (:N defaults to 1) and register the matching descriptors as a side effect — expansion is the moment the set of live indices is known. Unknown bundle names raise loudly.

otto.monitor.snmp.get_snmp_metric(oid: str) SnmpMetric | None

Return the registered descriptor for oid, or None.

otto.monitor.snmp.resolve_snmp_metric(oid: str) SnmpMetric

Return the descriptor for oid, or a default-styled fallback.

The fallback charts the OID under its own label with no unit on the generic metrics tab, so a host can poll a bare OID it declared in lab data without anyone having registered a descriptor for it.

otto.monitor.snmp.process_snmp_values(values: dict[str, float | None], *, rates: RateTracker, ts: datetime) list[tuple[str, MetricDataPoint, SnmpMetric]]

Turn one GET’s {oid: raw_value} into chartable (label, point, view) triples.

Applies each descriptor’s scale; converts kind="counter" values to per-second rates via rates (first sighting / reset ticks emit nothing); routes meta_of descriptors into their target series’ hover meta instead of their own series. OIDs with a None value are skipped.

class otto.monitor.snmp.SnmpClient(address: str, port: int = 161, community: str = 'public', version: '1' | '2c' = '2c', timeout: float = 2.0, retries: int = 1)

Bases: object

Async SNMP v1/v2c GET client for a single endpoint.

The endpoint (address, port) is whatever is reachable from the otto host — for a device behind a hop, that is the local end of a UDP forward / relay, not the device’s own address. Keeping the endpoint explicit means this client knows nothing about hop topology.

address : str
port : int
community : str
version : Literal['1', '2c']
timeout : float
retries : int
async get(oids: list[str]) dict[str, float | None]

GET oids in one PDU; return {oid: numeric_value_or_None}.

Non-numeric or errored varbinds map to None so the caller can skip them. On a transport/PDU error the whole batch returns None values and the error is logged — a failed tick is non-fatal, matching the shell collector’s per-host error handling.

class otto.monitor.snmp.SnmpSource(client: ~otto.monitor.snmp.SnmpClient, oids: list[str], rates: ~otto.monitor.rates.RateTracker = <factory>)

Bases: object

A MonitorTarget’s SNMP collection mode.

Pairs the SnmpClient (where/how to reach the agent) with the bare list of OIDs to poll each tick. Presentation for those OIDs comes from the descriptor registry, not from here — this is acquisition only.

client : SnmpClient
oids : list[str]
rates : RateTracker

Per-target counter->rate state for kind="counter" descriptors.