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
hostit 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.
Run the demo
Section titled “Run the demo”With thin-edge.io and a tedge-dot package installed:
-
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 -
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.tomlIf a host
snmptrapdalready holds udp/162, see Behind snmptrapd first. -
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
Poll and write the agent with no broker. just sim snmp publishes it on host port 1161. Use
tedge-dot from a package, or cargo run --manifest-path impl/rust/Cargo.toml -- in its place:
just sim snmpexport TEDGE_DOT_POINT_LIBRARY_PATH=$PWD/demo/points.dtedge-dot read -c demo/config/snmp.toml -p pump_speedtedge-dot write -c demo/config/snmp.toml -d snmp-switch -p setpoint --value 55just sim-down snmpThe device
Section titled “The device”[[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"]demo/config/snmp.toml on GitHubThe host serves two purposes: it is where requests go, and it is the source address that
notifications are matched against. The protocol_address keys:
| Key | Default | Notes |
|---|---|---|
host | IP address or DNS name. Two devices can’t share a host. | |
port | 161 | Agent port for GET and SET. |
version | "v2c" | "v1", "v2c" or "v3". Notifications are accepted in whichever version they arrive in. |
community / write_community | "public" / community | v1 and v2c communities for GET and SET. |
v3 | The SNMPv3 user (see below). | |
bulk | true for v2c and v3 | Set false to use GET instead of GETBULK. |
request_timeout, retries and max_varbinds can be set per device, with defaults in
[connection].
Points: objects, traps and varbinds
Section titled “Points: objects, traps and varbinds”The demo device takes its points from a point library, and each point says what it means for the flows:
# 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" }demo/points.d/snmp/demo-sim.toml on GitHubThere are three kinds of point:
| Kind | Address | Delivered by |
|---|---|---|
| object | { oid = "1.3.6.1.2.1.1.3.0" }, the full instance OID | Polling (GET or GETBULK). Writable with SET. |
| trap | { trap = "<OID>" } or a list of OIDs | A 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
typefor SET. It defaults from the datatype (signed integers map tointeger,uint8–uint32tounsigned32,stringtooctet_string), but floats need an explicittype. 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 withwhen = { equals = "<raise OID>" }is raised by one notification and cleared by another, aslink_statedoes above. Theot-eventandot-alarmflows 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.
Receiving notifications
Section titled “Receiving notifications”[connection]listen = "0.0.0.0:162"community = "public"demo/config/snmp.toml on GitHublisten 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.
If a host snmptrapd already owns udp/162, have it forward to the connector on another port,
and mark it as a trusted forwarder:
forward default udp:127.0.0.1:1162In /etc/tedge/plugins/ot/snmp.toml, set listen = "127.0.0.1:1162" and
forwarders = ["127.0.0.1"] in [connection].
A notification from a forwarder is routed by its last snmpTrapAddress.0 varbind instead of
its UDP source, and dropped if it has none. Stock snmptrapd 5.9.x does not add that
varbind, so the sending device must include it itself.
SNMPv3
Section titled “SNMPv3”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:
# 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" } }demo/config/snmp.toml on GitHubauth_protocol:MD5orSHA.SHA224toSHA512work intedge-dot-rsonly.priv_protocol:DESorAES(AES-128).AES192andAES256work intedge-dot-rsonly.auth_password_fileandpriv_password_filekeep 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-idby default and stays the same across restarts.