Files
2026-09-03 21:39:22 -05:00

147 lines
6.1 KiB
Bash

#!/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 192.168.3.252 192.168.3.253 100.70.181.72 100.71.171.20 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
if [[ "$src" == *:* ]]; then echo "rocommunity6 $COMMUNITY $src"; else echo "rocommunity $COMMUNITY $src"; fi
done
echo "extend lmsensors $WRAP_DST"
echo "extend lmsensors_n $WRAP_DST -n"
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"