From de3bb479308cd923b6849c7a9efc7655988a8ffd Mon Sep 17 00:00:00 2001 From: Richard Courtman Date: Sat, 18 Oct 2025 06:51:56 +0000 Subject: [PATCH] fix: improve turnkey temperature monitoring for standalone nodes - Fix script input handling to work with standard curl | bash pattern by prioritizing /dev/tty - Add Raspberry Pi temperature sensor support (cpu_thermal chip and generic temp sensors) - Add comprehensive documentation for turnkey standalone node setup - Fix printf formatting error in setup script --- docs/TEMPERATURE_MONITORING.md | 34 ++++++++++++++++++++++++++++++ internal/api/config_handlers.go | 10 +++++++-- internal/monitoring/temperature.go | 22 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 6ae6264..21d8b3f 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -158,6 +158,40 @@ The auto-setup script (Settings → Nodes → Setup Script) will prompt you to c If the node is part of a Proxmox cluster, the script will now detect the other members and offer to configure the same SSH/lm-sensors setup on each of them automatically—confirm when prompted to roll it out cluster-wide. +### Turnkey Setup for Standalone Nodes (v4.25.0+) + +**For standalone nodes** (not in a Proxmox cluster) running **containerized Pulse**, the setup script now automatically configures temperature monitoring with zero manual steps: + +1. The script detects the node is standalone (not in a cluster) +2. Automatically fetches the temperature proxy's SSH public key from your Pulse server via `/api/system/proxy-public-key` +3. Installs it with forced commands (`command="sensors -j"`) automatically +4. Temperature monitoring "just works" - no manual SSH key management needed! + +**Example output:** +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Standalone Node Temperature Setup +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Detected: This is a standalone node (not in a Proxmox cluster) + +Fetching temperature proxy public key... + ✓ Retrieved proxy public key + ✓ Temperature proxy key installed (restricted to sensors -j) + +✓ Standalone node temperature monitoring configured + The Pulse temperature proxy can now collect temperature data + from this node using secure SSH with forced commands. +``` + +**Security:** +- Public keys are safe to expose (it's in the name!) +- Forced commands restrict the key to only `command="sensors -j"` +- All other SSH features disabled (no-port-forwarding, no-pty, etc.) +- Works exactly like cluster setups, but fully automated + +**Note:** This only works for containerized Pulse deployments where the temperature proxy is running. For native (non-containerized) installs, you'll still need to provide your Pulse server's public key manually as described in step 3 above. + ## Setup (Manual) If you skipped SSH setup during auto-setup, you can configure it manually: diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 3a62e9a..68a356c 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -3847,8 +3847,11 @@ if [ "$SSH_ALREADY_CONFIGURED" = true ]; then if [ -t 0 ]; then read -p "> " -n 1 -r SSH_ACTION else + # When stdin is not a terminal (e.g., curl | bash), try /dev/tty first, then stdin for piped input if read -p "> " -n 1 -r SSH_ACTION /dev/null; then : + elif read -t 2 -n 1 -r SSH_ACTION 2>/dev/null && [ -n "$SSH_ACTION" ]; then + echo "$SSH_ACTION" else echo "(No terminal available - keeping existing configuration)" SSH_ACTION="k" @@ -3909,8 +3912,11 @@ else if [ -t 0 ]; then read -n 1 -r SSH_REPLY else + # When stdin is not a terminal (e.g., curl | bash), try /dev/tty first, then stdin for piped input if read -n 1 -r SSH_REPLY /dev/null; then : + elif read -t 2 -n 1 -r SSH_REPLY 2>/dev/null && [ -n "$SSH_REPLY" ]; then + echo "$SSH_REPLY" else echo "(No terminal available - skipping temperature monitoring)" SSH_REPLY="n" @@ -4247,12 +4253,12 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then else echo " Token Value: [See token output above]" fi - echo " Host URL: %s" + echo " Host URL: YOUR_PROXMOX_HOST:8006" echo "" fi `, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP, tokenName, tokenName, tokenName, tokenName, tokenName, tokenName, - authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshPublicKey, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, tokenName, serverHost) + authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshPublicKey, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, pulseURL, tokenName, serverHost) } else { // PBS script = fmt.Sprintf(`#!/bin/bash diff --git a/internal/monitoring/temperature.go b/internal/monitoring/temperature.go index af161d8..eed195b 100644 --- a/internal/monitoring/temperature.go +++ b/internal/monitoring/temperature.go @@ -226,6 +226,8 @@ func (tc *TemperatureCollector) parseSensorsJSON(jsonStr string) (*models.Temper // parseCPUTemps extracts CPU temperature data from a sensor chip func (tc *TemperatureCollector) parseCPUTemps(chipMap map[string]interface{}, temp *models.Temperature) { + foundPackageTemp := false + for sensorName, sensorData := range chipMap { sensorMap, ok := sensorData.(map[string]interface{}) if !ok { @@ -236,6 +238,7 @@ func (tc *TemperatureCollector) parseCPUTemps(chipMap map[string]interface{}, te if strings.Contains(sensorName, "Package id") || strings.Contains(sensorName, "Tdie") { if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) { temp.CPUPackage = tempVal + foundPackageTemp = true } } @@ -253,6 +256,25 @@ func (tc *TemperatureCollector) parseCPUTemps(chipMap map[string]interface{}, te } } } + + // If no package temperature was found (e.g., Raspberry Pi), look for generic temp sensors + if !foundPackageTemp { + for sensorName, sensorData := range chipMap { + sensorMap, ok := sensorData.(map[string]interface{}) + if !ok { + continue + } + + // Look for generic temperature sensors (e.g., "temp1" on Raspberry Pi) + if strings.HasPrefix(sensorName, "temp") || strings.HasPrefix(sensorName, "Temp") { + if tempVal := extractTempInput(sensorMap); !math.IsNaN(tempVal) && tempVal > 0 { + temp.CPUPackage = tempVal + temp.CPUMax = tempVal + break // Use the first valid generic temp sensor + } + } + } + } } // parseNVMeTemps extracts NVMe temperature data from a sensor chip