Skip to content
AI-driventedge-dot is developed largely by AI coding agents, and it is alpha software. Check what it does before you rely on it, and report what you find.

SNMP network devices

One snmp connector talks to equipment in both directions:

  • It polls objects with GET (v1) or GETBULK (v2c, v3) and writes them with SET.
  • It receives notifications: v1, v2c and v3 traps, and v2c and v3 informs, which it acknowledges. Each notification is routed to the device whose host it comes from.

What a notification means (an event or an alarm) is declared on the point and raised by the flows, so no trap-handling code is involved.

What you’ll build: one device, snmp-switch, that polls and writes to the demo’s pysnmp agent (snmp-agent-sim on udp/1161) and receives the traps that the net-snmp simulator (snmp-sim) sends to udp/162 every 10 seconds: link down and link up, cold starts, and an enterprise-specific pump notification.

With thin-edge.io and a tedge-dot package installed:

  1. Start the agent and the trap simulator. Both use host networking, so the traps reach udp/162 from 127.0.0.1, the address the demo device is matched by:

    Terminal window
    docker compose -f /usr/share/tedge-dot/demo/docker-compose.yaml up -d snmp-agent-sim snmp-sim
  2. Enable the demo config. Keep it mode 0640, because SNMP configs contain communities and passwords:

    Terminal window
    sudo install -m 0640 -o tedge -g tedge /usr/share/tedge-dot/demo/snmp.toml /etc/tedge/plugins/ot/snmp.toml

    If a host snmptrapd already holds udp/162, see Behind snmptrapd first.

  3. Watch measurements, events and alarms arrive, and send a trap of your own:

    Terminal window
    tedge mqtt sub 'te/+/+/+/+/#'
    docker compose -f /usr/share/tedge-dot/demo/docker-compose.yaml exec snmp-sim send-trap linkdown 2
demo/config/snmp.toml
[[device]]
name = "snmp-switch"
# SNMPv2c: GETBULK for the polled points, community "public" to read, "private" to SET.
protocol_address = { host = "127.0.0.1", port = 1161, version = "v2c", community = "public", write_community = "private" }
points_from = ["demo-sim"]
View demo/config/snmp.toml on GitHub

The host serves two purposes: it is where requests go, and it is the source address that notifications are matched against. The protocol_address keys:

KeyDefaultNotes
hostIP address or DNS name. Two devices can’t share a host.
port161Agent port for GET and SET.
version"v2c""v1", "v2c" or "v3". Notifications are accepted in whichever version they arrive in.
community / write_community"public" / communityv1 and v2c communities for GET and SET.
v3The SNMPv3 user (see below).
bulktrue for v2c and v3Set false to use GET instead of GETBULK.

request_timeout, retries and max_varbinds can be set per device, with defaults in [connection].

The demo device takes its points from a point library, and each point says what it means for the flows:

demo/points.d/snmp/demo-sim.toml
# Point library for the SNMP demo simulators: what the pysnmp agent (connectors/snmp/agent, polled
# and written) serves and what the net-snmp notification simulator (connectors/snmp/sim) sends.
# Referenced by demo/config/snmp.toml with `points_from = ["demo-sim"]` (contract §3.4). The
# agent's seeds are documented in connectors/snmp/agent/agent.py (E = 1.3.6.1.4.1.99999.1).
#
# Each point declares what it means next to its address — measurements, events and alarms for the
# flows, a parameter for a writable object — so the demo shows up in Cumulocity with no flow params.
[library]
protocol = "snmp"
type = "snmp-demo-sim"
description = "tedge-dot SNMP demo simulators (pysnmp agent, net-snmp notifications)"
# --- polled from the agent ------------------------------------------------------------------
[[point]]
id = "sys_descr"
name = "System description"
datatype = "string"
address = { oid = "1.3.6.1.2.1.1.1.0" }
meta = { measurement = false }
# sysUpTime.0 is TimeTicks (hundredths of a second).
[[point]]
id = "uptime"
name = "Agent uptime"
datatype = "float64"
unit = "s"
transform = { divisor = 100 }
address = { oid = "1.3.6.1.2.1.1.3.0" }
# INTEGER E.2.0 -> 1234 (impl/c/ci/smoke.sh expects this value).
[[point]]
id = "pump_speed"
name = "Pump speed"
datatype = "int32"
unit = "rpm"
address = { oid = "1.3.6.1.4.1.99999.1.2.0" }
# Counter32 E.12.0: seconds since the agent started, so the chart moves.
[[point]]
id = "run_seconds"
name = "Run time"
datatype = "uint32"
unit = "s"
address = { oid = "1.3.6.1.4.1.99999.1.12.0" }
# Writable INTEGER E.20.0 (seed 10): a device parameter. The SET uses the device's
# write_community (or its v3 user).
[[point]]
id = "setpoint"
name = "Pump setpoint"
datatype = "int32"
access = "read_write"
address = { oid = "1.3.6.1.4.1.99999.1.20.0" }
meta = { parameter = { min = 0, max = 5000 } }
# Writable OCTET STRING E.21.0 (seed "initial").
[[point]]
id = "station_label"
name = "Station label"
datatype = "string"
access = "read_write"
address = { oid = "1.3.6.1.4.1.99999.1.21.0" }
# --- notifications ----------------------------------------------------------------------------
# linkDown / linkUp as one signal, typed as the notification OID: an alarm while the link is down.
[[point]]
id = "link_state"
name = "Link state"
description = "The last link notification: linkDown (…5.3) or linkUp (…5.4)"
datatype = "string"
address = { trap = ["1.3.6.1.6.3.1.1.5.3", "1.3.6.1.6.3.1.1.5.4"] }
meta = { measurement = false, alarm = { type = "link_down", severity = "major", text = "Link down on {device}", when = { equals = "1.3.6.1.6.3.1.1.5.3" } } }
# The interface a link notification is about.
[[point]]
id = "link_if"
name = "Link interface"
datatype = "int32"
address = { trap = ["1.3.6.1.6.3.1.1.5.3", "1.3.6.1.6.3.1.1.5.4"], oid = "1.3.6.1.2.1.2.2.1.1" }
meta = { measurement = false }
# A v1 coldStart: every one is an event, even when nothing else changed.
[[point]]
id = "cold_start"
name = "Cold start"
datatype = "bool"
address = { trap = "1.3.6.1.6.3.1.1.5.1" }
meta = { measurement = false, event = { type = "cold_start", text = "{device} restarted", every = true } }
# The enterprise-specific pump notification: its text as an event, its temperature as a measurement.
[[point]]
id = "pump_alarm_text"
name = "Pump notification"
datatype = "string"
address = { trap = "1.3.6.1.4.1.99999.0.1", oid = "1.3.6.1.4.1.99999.2.1" }
meta = { measurement = false, event = { type = "pump_notification", text = "{value}", every = true } }
[[point]]
id = "pump_temperature"
name = "Pump temperature"
datatype = "float64"
unit = "°C"
transform = { divisor = 10 }
address = { trap = "1.3.6.1.4.1.99999.0.1", oid = "1.3.6.1.4.1.99999.2.2" }
View demo/points.d/snmp/demo-sim.toml on GitHub

There are three kinds of point:

KindAddressDelivered by
object{ oid = "1.3.6.1.2.1.1.3.0" }, the full instance OIDPolling (GET or GETBULK). Writable with SET.
trap{ trap = "<OID>" } or a list of OIDsA notification whose trap OID is, or lies beneath, one of them
varbind{ trap = …, oid = "<OID>" }The first varbind at or beneath oid in a matching notification
  • Writable objects need an SNMP type for SET. It defaults from the datatype (signed integers map to integer, uint8–uint32 to unsigned32, string to octet_string), but floats need an explicit type. SET failures report the SNMP error (notWritable, wrongType, …).
  • Traps become events and alarms through the point’s meta: event = { …, every = true } raises an event for every notification, and an alarm with when = { equals = "<raise OID>" } is raised by one notification and cleared by another, as link_state does above. The ot-event and ot-alarm flows ship active, so nothing else needs configuring.
  • Trap and varbind points can’t be read on demand, so they have no heartbeat. Don’t give a change filter to a point whose event uses every = true: two identical traps are two occurrences.
demo/config/snmp.toml
[connection]
listen = "0.0.0.0:162"
community = "public"
View demo/config/snmp.toml on GitHub

listen is bound only while at least one device has a trap or varbind point, so a fresh install doesn’t take udp/162. community lists the communities that v1 and v2c notifications must carry. Remove the key to accept any community. Informs are acknowledged from the listening socket. Notifications from unknown sources, and notifications that fail authentication, are dropped and never acknowledged.

Port 162 is privileged. The packaged service has CAP_NET_BIND_SERVICE so it can bind it. If you choose a port above 1023, no privilege is needed, but your devices must send to that port.

Give the device a v3 table. The demo agent also accepts SNMPv3, with the users defined in connectors/snmp/agent/agent.py, and the demo config has a commented example:

demo/config/snmp.toml
# The same agent speaks SNMPv3 too (users in connectors/snmp/agent/agent.py), e.g.:
#
# protocol_address = { host = "127.0.0.1", port = 1161, version = "v3", v3 = { user = "rw-priv", auth_protocol = "SHA", auth_password = "rw-auth-pass-1", priv_protocol = "AES", priv_password = "rw-priv-pass-1" } }
View demo/config/snmp.toml on GitHub
  • auth_protocol: MD5 or SHA. SHA224 to SHA512 work in tedge-dot-rs only.
  • priv_protocol: DES or AES (AES-128). AES192 and AES256 work in tedge-dot-rs only.
  • auth_password_file and priv_password_file keep secrets out of a config file that management commands may rewrite. The connector reads the first line of the file.
  • v3 traps are authenticated with the same user. A v3 inform needs this receiver’s engine ID, [connection] engine_id, which is derived from /etc/machine-id by default and stays the same across restarts.