Native lm-sensors output plus optional TEMPer USB probe, unified in
lm-sensors format behind one snmpd extend (lmsensors). Idempotent
deploy binds snmpd to explicit LAN+Tailscale addresses only and
source-scopes the community to the pollers (LibreNMS, Home Assistant
app-connector LAN IP, admin workstation, Cloudron) per the founder's
security ruling. TDD unit suite included; fleet verified live (6 hosts)
and negative-tested (refused source).
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# Deploy lm-sensors extend (native sensors + TEMPer) + snmpd on a host [#341/#439]
|
||||
#
|
||||
# Run ON the target host via the remote.sh chokepoint (bash -s). Copy the
|
||||
# wrapper over first (vm-copy / prox-copy). Idempotent: safe to re-run.
|
||||
#
|
||||
# Native lm-sensors (CPU/board temps, fans) is installed on every target.
|
||||
# The TEMPer USB layer (venv + temperusb + udev rule) is installed ONLY when
|
||||
# a 0c45:7401 probe is attached.
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# VM_IP=stlpc-artroom VM_USER=root bash tests/remote.sh vm-copy dcinfra/sensors/temper/lmsensors-extend.sh /tmp/lmsensors-extend.sh
|
||||
# VM_IP=stlpc-artroom VM_USER=root bash tests/remote.sh vm-file dcinfra/sensors/temper/deploy.sh
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-copy dcinfra/sensors/temper/lmsensors-extend.sh /tmp/lmsensors-extend.sh
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file dcinfra/sensors/temper/deploy.sh
|
||||
#
|
||||
# snmpd is a targeted exception: sensor hosts + tsys-* Proxmox hosts
|
||||
# (founder-directed 2026-08-27), not fleet-wide.
|
||||
set -euo pipefail
|
||||
|
||||
COMMUNITY="${SNMP_COMMUNITY:-kn3lmgmt}"
|
||||
VENV_DIR="${VENV_DIR:-/opt/temper-venv}"
|
||||
WRAP_SRC="${WRAP_SRC:-/tmp/lmsensors-extend.sh}"
|
||||
WRAP_DST="/usr/local/bin/lmsensors-extend"
|
||||
WRAP_DST_OLD="/usr/local/bin/temper-lmsensors"
|
||||
SNMPD_CONF="/etc/snmp/snmpd.conf"
|
||||
UDEV_RULE="/etc/udev/rules.d/99-temper.rules"
|
||||
MARK_BEGIN="# lmsensors-extend managed block begin"
|
||||
MARK_END="# lmsensors-extend managed block end"
|
||||
OLD_MARK_BEGIN="# temper-lmsensors managed block begin"
|
||||
OLD_MARK_END="# temper-lmsensors managed block end"
|
||||
# Source allowlist (founder ruling 2026-08-27: bind Tailscale-only where
|
||||
# possible; HA is a Tailscale APP connector so its polls source from its LAN
|
||||
# IP and must be ACL'd explicitly — no broad subnet grants).
|
||||
# 192.168.3.176 tsys-librenms LAN (poller)
|
||||
# 100.86.204.77 tsys-librenms Tailscale
|
||||
# 192.168.3.12 pfv-bms / Home Assistant LAN (app connector source)
|
||||
# 100.67.108.125 pfv-bms Tailscale
|
||||
# 100.101.187.119 admin workstation Tailscale
|
||||
# 152.53.37.179 + 2a0a:4cc0:2000:686::a496:3bff:fe35:610a Cloudron VPS
|
||||
# (future Uptime Kuma SNMP polling)
|
||||
SNMP_SOURCES="${SNMP_SOURCES:-192.168.3.176 100.86.204.77 192.168.3.12 100.67.108.125 100.101.187.119 152.53.37.179 2a0a:4cc0:2000:686:a496:3bff:fe35:610a}"
|
||||
|
||||
log() { echo "[deploy] $*"; }
|
||||
|
||||
has_probe() {
|
||||
command -v lsusb >/dev/null 2>&1 && lsusb | grep -qi '0c45:7401'
|
||||
}
|
||||
|
||||
host_lan_ip() {
|
||||
# first 192.168.* address (mgmt/user LAN; excludes 127/10.100 storage/dockers)
|
||||
ip -4 addr show 2>/dev/null | grep -oP 'inet \K192\.168\.[0-9.]+' | head -1
|
||||
}
|
||||
|
||||
host_ts_ip() {
|
||||
command -v tailscale >/dev/null 2>&1 && tailscale ip -4 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
||||
[[ -f "$WRAP_SRC" ]] || { echo "wrapper not found at $WRAP_SRC (copy it first)" >&2; exit 1; }
|
||||
|
||||
# --- native lm-sensors layer (all hosts) ---
|
||||
if ! dpkg -s lm-sensors >/dev/null 2>&1; then
|
||||
log "installing lm-sensors"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq lm-sensors
|
||||
fi
|
||||
if [[ -z "$(sensors 2>/dev/null)" ]]; then
|
||||
log "no sensor readings yet; trying coretemp/k10temp modules"
|
||||
modprobe coretemp 2>/dev/null || true
|
||||
modprobe k10temp 2>/dev/null || true
|
||||
fi
|
||||
log "native sensors: $(sensors 2>/dev/null | grep -c '°C') temp readings"
|
||||
|
||||
# --- TEMPer USB layer (probe-conditional) ---
|
||||
if has_probe; then
|
||||
log "TEMPer probe present: installing venv layer"
|
||||
if ! dpkg -s python3-venv >/dev/null 2>&1; then
|
||||
log "installing python3-venv"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-venv
|
||||
fi
|
||||
if [[ ! -x "$VENV_DIR/bin/pip" ]]; then
|
||||
[[ -d "$VENV_DIR" ]] && { log "removing broken venv $VENV_DIR"; rm -rf "$VENV_DIR"; }
|
||||
log "creating venv $VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
log "ensuring temperusb in venv"
|
||||
"$VENV_DIR/bin/pip" install --quiet --disable-pip-version-check temperusb
|
||||
|
||||
# TEMPer USB dongle must be readable by the unprivileged snmpd user
|
||||
# (extend runs as Debian-snmp; pyusb needs the raw usb device node).
|
||||
log "installing udev rule for 0c45:7401"
|
||||
cat >"$UDEV_RULE" <<'EOF'
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0c45", ATTRS{idProduct}=="7401", MODE="0666"
|
||||
EOF
|
||||
udevadm control --reload-rules
|
||||
udevadm trigger
|
||||
else
|
||||
log "no TEMPer probe: skipping venv/udev layer"
|
||||
fi
|
||||
|
||||
log "installing wrapper to $WRAP_DST"
|
||||
install -m 0755 "$WRAP_SRC" "$WRAP_DST"
|
||||
rm -f "$WRAP_DST_OLD"
|
||||
|
||||
if ! dpkg -s snmpd >/dev/null 2>&1 || ! command -v snmpget >/dev/null 2>&1; then
|
||||
log "installing snmpd + snmp client"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq snmpd snmp
|
||||
fi
|
||||
|
||||
log "configuring snmpd (community=$COMMUNITY, scoped sources)"
|
||||
LAN_IP="$(host_lan_ip)"
|
||||
TS_IP="$(host_ts_ip)"
|
||||
AGENT_ADDRS=""
|
||||
[[ -n "$LAN_IP" ]] && AGENT_ADDRS="udp:${LAN_IP}:161"
|
||||
[[ -n "$TS_IP" ]] && AGENT_ADDRS="${AGENT_ADDRS:+$AGENT_ADDRS,}udp:${TS_IP}:161"
|
||||
[[ -n "$AGENT_ADDRS" ]] || { echo "no LAN/Tailscale address found; refusing to bind 0.0.0.0" >&2; exit 1; }
|
||||
sed -i "/^$OLD_MARK_BEGIN\$/,/^$OLD_MARK_END\$/d" "$SNMPD_CONF"
|
||||
sed -i "/^$MARK_BEGIN\$/,/^$MARK_END\$/d" "$SNMPD_CONF"
|
||||
sed -i 's/^agentaddress/#agentaddress/' "$SNMPD_CONF"
|
||||
{
|
||||
echo "$MARK_BEGIN"
|
||||
echo "agentaddress $AGENT_ADDRS"
|
||||
for src in $SNMP_SOURCES; do
|
||||
echo "rocommunity $COMMUNITY $src"
|
||||
done
|
||||
echo "extend lmsensors $WRAP_DST"
|
||||
echo "$MARK_END"
|
||||
} >>"$SNMPD_CONF"
|
||||
log "agentaddress: $AGENT_ADDRS"
|
||||
|
||||
log "restarting snmpd"
|
||||
systemctl enable --now snmpd >/dev/null
|
||||
systemctl restart snmpd
|
||||
|
||||
log "wrapper output:"
|
||||
"$WRAP_DST"
|
||||
log "snmp extend self-test (expect lm-sensors block below):"
|
||||
sleep 1
|
||||
snmpget -v2c -c "$COMMUNITY" -Ovq localhost \
|
||||
.1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 2>/dev/null || \
|
||||
log "WARN: snmpget self-test failed (check snmpd logs)"
|
||||
log "done"
|
||||
Reference in New Issue
Block a user