chore: initial import from KNEL/PFVCluster@041d311 [#769]
Split per O&M lane work order. Full history: KNEL/PFVCluster. https://projects.knownelement.com/issues/769#note-4152
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
# lm-sensors extend for snmpd: native sensors + TEMPer USB block [#341/#439]
|
||||
#
|
||||
# Deployed to /usr/local/bin/lmsensors-extend on sensor hosts by deploy.sh.
|
||||
# Output = lm-sensors format, consumed by Home Assistant's SNMP integration
|
||||
# (nsExtendOutputFull."lmsensors") and, if enabled later, LibreNMS's
|
||||
# lm-sensors app.
|
||||
#
|
||||
# Layers (each optional, at least one required):
|
||||
# 1. native /usr/bin/sensors output (CPU/board temps, fans) — lm-sensors pkg
|
||||
# 2. TEMPer USB probe (temperusb in venv /opt/temper-venv) — probe-optional
|
||||
# (venv-on-host is founder-approved for sensor hosts, incl. Proxmox)
|
||||
set -euo pipefail
|
||||
|
||||
VENV_PY="${VENV_PY:-/opt/temper-venv/bin/python}"
|
||||
NATIVE_SENSORS_BIN="${NATIVE_SENSORS_BIN:-/usr/bin/sensors}"
|
||||
SYSFS_THERMAL_ROOT="${SYSFS_THERMAL_ROOT:-/sys/class/thermal}"
|
||||
CHIP_NAME="${CHIP_NAME:-temper-usb-1}"
|
||||
|
||||
# Render lm-sensors format block from internal/external Celsius values.
|
||||
# Empty/absent external renders internal only. Non-numeric input is rejected.
|
||||
format_block() {
|
||||
local internal="${1:-}" external="${2:-}"
|
||||
local num_re='^-?[0-9]+(\.[0-9]+)?$'
|
||||
[[ "$internal" =~ $num_re ]] || {
|
||||
echo "error: internal temp not numeric: '$internal'" >&2
|
||||
return 1
|
||||
}
|
||||
local signed
|
||||
printf '%s\nAdapter: USB adapter\n' "$CHIP_NAME"
|
||||
signed="${internal#+}"; [[ "$signed" == -* ]] || signed="+$signed"
|
||||
printf 'Internal: %s°C\n' "$signed"
|
||||
if [[ -n "$external" && "$external" =~ $num_re ]]; then
|
||||
signed="${external#+}"; [[ "$signed" == -* ]] || signed="+$signed"
|
||||
printf 'External: %s°C\n' "$signed"
|
||||
fi
|
||||
}
|
||||
|
||||
# True when a TEMPer USB dongle (0c45:7401) is attached and the venv exists.
|
||||
|
||||
# Numeric mode: print ONE Celsius float for snmpd extend lmsensors_n [#618].
|
||||
# Preference: TEMPer external > TEMPer internal > first native/sysfs temp
|
||||
# reading. Consumed by HA's SNMP config-entry integration (device_class
|
||||
# temperature; HA renders C in the instance display unit).
|
||||
numeric_pick() {
|
||||
local internal="${1:-}" external="${2:-}" native="${3:-}" pick
|
||||
local num_re='^-?[0-9]+(\.[0-9]+)?$'
|
||||
for pick in "$external" "$internal"; do
|
||||
pick="${pick#+}"
|
||||
[[ "$pick" =~ $num_re ]] && { printf '%s\n' "$pick"; return 0; }
|
||||
done
|
||||
if [[ -n "$native" ]]; then
|
||||
pick="$(grep -oE "\-?[0-9]+(\.[0-9]+)?°C" <<<"$native" | head -1)"; pick="${pick%°C}"
|
||||
[[ "$pick" =~ $num_re ]] && { printf '%s\n' "$pick"; return 0; }
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
numeric_main() {
|
||||
local internal external line
|
||||
if has_temper_probe; then
|
||||
if read -r internal external <<<"$(read_temper 2>/dev/null)"; then
|
||||
numeric_pick "${internal:-}" "${external:-}" "" && return 0
|
||||
fi
|
||||
fi
|
||||
line="$(run_native_sensors | grep -m1 '°C')"
|
||||
[[ -n "$line" ]] && numeric_pick "" "" "$line" && return 0
|
||||
line="$(read_sysfs_thermal 2>/dev/null | head -1 | awk '{printf "+%.1f°C", $2/1000}')"
|
||||
[[ -n "$line" ]] && numeric_pick "" "" "$line" && return 0
|
||||
echo "error: numeric mode found no sensor source" >&2
|
||||
return 1
|
||||
}
|
||||
has_temper_probe() {
|
||||
command -v lsusb >/dev/null 2>&1 || return 1
|
||||
[[ -x "$VENV_PY" ]] || return 1
|
||||
lsusb | grep -qi '0c45:7401'
|
||||
}
|
||||
|
||||
# Read the TEMPer probe via temperusb; prints "<internal> <external>" in C.
|
||||
# Sensor 0 = internal, sensor 1 = external probe (if attached).
|
||||
read_temper() {
|
||||
"$VENV_PY" - <<'PY'
|
||||
from temperusb import TemperHandler
|
||||
|
||||
internal = external = ""
|
||||
for device in TemperHandler().get_devices():
|
||||
try:
|
||||
temps = device.get_temperatures(sensors=[0, 1])
|
||||
except (ValueError, KeyError):
|
||||
temps = device.get_temperatures()
|
||||
for index, slot in ((0, "internal"), (1, "external")):
|
||||
reading = temps.get(index)
|
||||
if reading and reading.get("temperature_c") is not None:
|
||||
value = f"{reading['temperature_c']:.1f}"
|
||||
if slot == "internal":
|
||||
internal = value
|
||||
else:
|
||||
external = value
|
||||
print(f"{internal} {external}".strip())
|
||||
PY
|
||||
}
|
||||
|
||||
run_native_sensors() {
|
||||
[[ -x "$NATIVE_SENSORS_BIN" ]] || return 1
|
||||
"$NATIVE_SENSORS_BIN" 2>/dev/null
|
||||
}
|
||||
|
||||
# SBC fallback (Raspberry Pi et al.): lm-sensors often reports nothing; SoC
|
||||
# temperatures live in /sys/class/thermal/thermal_zone*/{type,temp} (mC).
|
||||
read_sysfs_thermal() {
|
||||
[[ -d "$SYSFS_THERMAL_ROOT" ]] || return 1
|
||||
local z type mc found=0
|
||||
for z in "$SYSFS_THERMAL_ROOT"/thermal_zone[0-9]*; do
|
||||
[[ -r "$z/type" && -r "$z/temp" ]] || continue
|
||||
type="$(<"$z/type")" mc="$(<"$z/temp")"
|
||||
[[ "$mc" =~ ^-?[0-9]+$ ]] || continue
|
||||
printf '%s %s\n' "$type" "$mc"
|
||||
found=1
|
||||
done
|
||||
((found)) || return 1
|
||||
}
|
||||
|
||||
# Render lm-sensors format block from read_sysfs_thermal lines "type millicelsius".
|
||||
format_sysfs_block() {
|
||||
local input="${1:-}" type mc signed n=0
|
||||
[[ -n "$input" ]] || return 1
|
||||
printf 'soc-thermal-virtual-0\nAdapter: Virtual device\n'
|
||||
while read -r type mc; do
|
||||
((++n))
|
||||
signed="$(awk -v mc="$mc" 'BEGIN { printf "%.1f", mc / 1000 }')"
|
||||
signed="${signed#+}"
|
||||
[[ "$signed" == -* ]] || signed="+$signed"
|
||||
printf 'temp%d: %s°C\n' "$n" "$signed"
|
||||
done <<<"$input"
|
||||
}
|
||||
|
||||
# Labeled per-zone temps (e.g. Jetson GPU-therm) [#736]. Hosts opt in by
|
||||
# listing zone types (one per line) in /etc/snmp/lmsensors-zone-types; no
|
||||
# snmpd.conf change or restart needed (extend re-execs per poll). Each type
|
||||
# renders its own chip block so HA templates can anchor on the chip name.
|
||||
ZONE_TYPES_FILE="${ZONE_TYPES_FILE:-/etc/snmp/lmsensors-zone-types}"
|
||||
|
||||
format_labeled_zones() {
|
||||
local z type mc want signed
|
||||
[[ -r "$ZONE_TYPES_FILE" ]] || return 1
|
||||
while IFS= read -r want; do
|
||||
want="${want%%#*}"; want="${want//[[:space:]]/}"
|
||||
[[ -n "$want" ]] || continue
|
||||
for z in "$SYSFS_THERMAL_ROOT"/thermal_zone[0-9]*; do
|
||||
[[ -r "$z/type" && -r "$z/temp" ]] || continue
|
||||
[[ "$(<"$z/type")" == "$want" ]] || continue
|
||||
mc="$(<"$z/temp")"
|
||||
[[ "$mc" =~ ^-?[0-9]+$ ]] || continue
|
||||
signed="$(awk -v mc="$mc" 'BEGIN { printf "%.1f", mc / 1000 }')"
|
||||
signed="${signed#+}"
|
||||
[[ "$signed" == -* ]] || signed="+$signed"
|
||||
printf 'zone-%s-virtual-0\nAdapter: Virtual device\ntemp1: %s°C\n\n' "$want" "$signed"
|
||||
done
|
||||
done <"$ZONE_TYPES_FILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
local out="" block internal external native="" sysinput
|
||||
if native="$(run_native_sensors)" && [[ -n "$native" ]]; then
|
||||
out+="$native"$'\n'
|
||||
else
|
||||
native=""
|
||||
fi
|
||||
if has_temper_probe; then
|
||||
if block="$(read_temper 2>/dev/null)" \
|
||||
&& read -r internal external <<<"$block" \
|
||||
&& block="$(format_block "${internal:-}" "${external:-}")"; then
|
||||
out+="$block"$'\n'
|
||||
else
|
||||
echo "warn: TEMPer probe present but read failed" >&2
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$native" ]] \
|
||||
&& sysinput="$(read_sysfs_thermal 2>/dev/null)" \
|
||||
&& block="$(format_sysfs_block "$sysinput")"; then
|
||||
out+="$block"$'\n'
|
||||
fi
|
||||
if block="$(format_labeled_zones)" && [[ -n "$block" ]]; then
|
||||
out+="$block"$'\n'
|
||||
fi
|
||||
[[ -n "$out" ]] || {
|
||||
echo "error: no sensor sources available (lm-sensors pkg? TEMPer venv? sysfs thermal?)" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
if [[ "${1:-}" == "-n" ]]; then numeric_main; else main; fi
|
||||
fi
|
||||
Reference in New Issue
Block a user