Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions snmp/Openwrt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# OpenWrt wireless + sensors SNMP agent

These scripts expose per-radio and aggregate wireless metrics (plus lm-sensors
temperatures) to LibreNMS through net-snmp `pass_persist` handlers. Radios and
VAPs are discovered live at request time, so there is no per-interface or
per-metric snmpd configuration to maintain. See
[Device-Notes/Openwrt.md](https://docs.librenms.org/Support/Device-Notes/#openwrt)
for the LibreNMS-side install and discovery steps.

## Scripts

| Script | Purpose |
| --- | --- |
| `openwrt-snmp-pass.sh` | Wireless `pass_persist` handler. Serves the OpenWrt wireless subtree (OPENWRT-WIRELESS-MIB, `.1.3.6.1.4.1.60652.102.1.10`) from a single snmpd line. |
| `lm-sensors-pass.sh` | Temperature `pass_persist` handler (LM-SENSORS-MIB emulation). |
| `wlInterfaces.sh` | Interface/label inventory. Helper used by `openwrt-snmp-pass.sh`. |
| `wlClients.sh [iface]` | Client counts (per interface, or aggregate). Helper. |
| `wlFrequency.sh <iface>` | Channel frequency (MHz). Helper. |
| `wlNoiseFloor.sh <iface>` | Noise floor (dBm). Helper. |
| `wlRate.sh <iface> <tx\|rx> <min\|avg\|max>` | TX/RX rate (Mbit/s). Helper. |
| `wlSNR.sh <iface> <min\|avg\|max>` | SNR (dB). Helper. |

The `wl*.sh` scripts are collection helpers invoked by `openwrt-snmp-pass.sh`;
they are not registered with snmpd directly. They can be run standalone for
debugging.

## snmpd configuration

Copy the scripts to a directory on the device (the helpers must sit next to
`openwrt-snmp-pass.sh`, which calls them by relative path):

```sh
mkdir -p /usr/libexec/openwrt-snmp
for s in openwrt-snmp-pass lm-sensors-pass wlInterfaces wlClients \
wlFrequency wlNoiseFloor wlRate wlSNR; do
wget -O "/usr/libexec/openwrt-snmp/$s.sh" \
"https://raw.githubusercontent.com/librenms/librenms-agent/master/snmp/Openwrt/$s.sh"
done
chmod +x /usr/libexec/openwrt-snmp/*.sh
```

Register the two handlers in `/etc/config/snmpd`:

```
config pass
option miboid '.1.3.6.1.4.1.60652.102.1.10'
option prog '/usr/libexec/openwrt-snmp/openwrt-snmp-pass.sh'
option persist '1'

config pass
option miboid '.1.3.6.1.4.1.2021.13.16.2.1'
option prog '/usr/libexec/openwrt-snmp/lm-sensors-pass.sh'
option persist '1'
```

OS detection additionally reads a `distro` and a `hardware` extend
(`nsExtendOutput1Line."distro"` / `."hardware"`); these use small inline
commands rather than shipped scripts:

```
config extend
option name 'distro'
option prog '/bin/sh'
option args '-c '\''. /etc/os-release; echo $PRETTY_NAME'\'''

config extend
option name 'hardware'
option prog '/bin/cat'
option args '/tmp/sysinfo/model'
```

(`/tmp/sysinfo/model` is OpenWrt's board-detection model string; it works on all
targets including x86, unlike `/sys/firmware/devicetree/base/model`.)

Restart snmpd:

```sh
/etc/init.d/snmpd restart
```

## Validation

From the LibreNMS host, walk the wireless subtree and the temperature table:

```sh
snmpwalk -v2c -c your_community_string <openwrt-host> .1.3.6.1.4.1.60652.102.1.10
snmpwalk -v2c -c your_community_string <openwrt-host> .1.3.6.1.4.1.2021.13.16
```

On the device, the handler can be exercised directly:

```sh
/usr/libexec/openwrt-snmp/openwrt-snmp-pass.sh --snapshot # full OID table
/usr/libexec/openwrt-snmp/openwrt-snmp-pass.sh -g <oid> # single GET
/usr/libexec/openwrt-snmp/openwrt-snmp-pass.sh -n <oid> # GETNEXT
```
130 changes: 130 additions & 0 deletions snmp/Openwrt/lm-sensors-pass.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/bin/sh

# lm-sensors-pass.sh
# SNMP pass script for LM-SENSORS-MIB thermal sensors
# Provides proper MIB structure at .1.3.6.1.4.1.2021.13.16.2.1

BASE_OID=".1.3.6.1.4.1.2021.13.16.2.1"

# Function to get all thermal zone data
# Output format: index:name:temp
# Re-indexes zones sequentially starting from 1
get_zones() {
idx=0
for zone in /sys/devices/virtual/thermal/thermal_zone*; do
[ -d "$zone" ] || continue
idx=$((idx + 1))
zone_type=$(cat "$zone/type" 2>/dev/null || echo "unknown")
zone_temp=$(cat "$zone/temp" 2>/dev/null || echo "0")
echo "$idx:$zone_type:$zone_temp"
done | sort -t':' -k1 -n
}

case "$1" in
-g)
# GET request - return exact OID match
REQ_OID="$2"
FOUND=0

while IFS=':' read -r idx name temp; do
case "$REQ_OID" in
"$BASE_OID.1.$idx")
echo "$REQ_OID"
echo "integer"
echo "$idx"
FOUND=1
break
;;
"$BASE_OID.2.$idx")
echo "$REQ_OID"
echo "string"
echo "$name"
FOUND=1
break
;;
"$BASE_OID.3.$idx")
echo "$REQ_OID"
echo "gauge"
echo "$temp"
FOUND=1
break
;;
esac
done << EOF
$(get_zones)
EOF

[ "$FOUND" -eq 0 ] && echo "NONE"
;;

-n)
# GETNEXT request - return next OID after requested
REQ_OID="$2"

# Create temporary file with all OIDs
TMP_FILE="$(mktemp /tmp/snmp_oids.XXXXXX 2>/dev/null || echo "/tmp/snmp_oids.$$")"
TMP_SORTED="${TMP_FILE}.sorted"
TMP_LIST="${TMP_FILE}.list"
: > "$TMP_FILE"
trap 'rm -f "$TMP_FILE" "$TMP_SORTED" "$TMP_LIST"' EXIT INT TERM

get_zones | while IFS=':' read -r idx name temp; do
# Pad index to ensure proper numeric sorting
# Format: column.index where index is zero-padded to 3 digits
{
printf '%d.%03d|%s.1.%s|integer|%s\n' 1 "$idx" "$BASE_OID" "$idx" "$idx"
printf '%d.%03d|%s.2.%s|string|%s\n' 2 "$idx" "$BASE_OID" "$idx" "$name"
printf '%d.%03d|%s.3.%s|gauge|%s\n' 3 "$idx" "$BASE_OID" "$idx" "$temp"
} >> "$TMP_FILE"
done

# Sort by our padded key, then extract and compare OIDs
sort -t'|' -k1 "$TMP_FILE" > "$TMP_SORTED"
cut -d'|' -f2- "$TMP_SORTED" > "$TMP_LIST"

FOUND=0
while IFS='|' read -r oid type value; do
# Use awk for proper numeric OID comparison
is_greater=$(awk -v req="$REQ_OID" -v curr="$oid" '
BEGIN {
# Split OIDs into arrays
req_len = split(req, req_parts, ".");
curr_len = split(curr, curr_parts, ".");
max_len = (curr_len > req_len) ? curr_len : req_len;

# Compare each part numerically
for (i = 1; i <= max_len; i++) {
req_val = (i <= req_len) ? req_parts[i] : 0;
curr_val = (i <= curr_len) ? curr_parts[i] : 0;

if (curr_val > req_val) {
print "1";
exit;
} else if (curr_val < req_val) {
print "0";
exit;
}
}
print "0";
}
')

if [ "$is_greater" = "1" ]; then
echo "$oid"
echo "$type"
echo "$value"
FOUND=1
break
fi
done < "$TMP_LIST"

if [ "$FOUND" -eq 0 ]; then
echo "NONE"
fi
;;

*)
echo "Usage: $0 -g|-n OID" >&2
exit 1
;;
esac
Loading