From 8595b4c0012db953dfdda83f1d249db0cbd57649 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 18 Oct 2025 23:17:38 +0000 Subject: [PATCH] feat: automatic ProxyJump for turnkey temperature monitoring Make temperature monitoring truly turnkey by automatically configuring SSH ProxyJump when running in containers without pulse-sensor-proxy. How it works: 1. Setup script runs on Proxmox host (e.g., delly) 2. Detects Pulse is containerized but proxy unavailable 3. Automatically configures SSH ProxyJump through the current host 4. Writes SSH config to /home/pulse/.ssh/config in container 5. Temperature monitoring "just works" without manual configuration Changes: - Track TEMP_MONITORING_AVAILABLE flag during proxy installation - Auto-configure ProxyJump if proxy installation fails - Add /api/system/ssh-config endpoint to write SSH config - Only prompt for temperature monitoring if it can actually work - Automatic SSH config: ProxyJump through Proxmox host Before: User had to manually configure ProxyJump or install proxy After: Temperature monitoring works automatically after setup script This makes Docker deployments as turnkey as LXC deployments. --- internal/api/config_handlers.go | 50 +++++++++++++++++++++++++++++++-- internal/api/router.go | 1 + internal/api/system_settings.go | 45 +++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 73847c3..1fc3453 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -3618,6 +3618,9 @@ if command -v pct >/dev/null 2>&1; then fi fi +# Track whether temperature monitoring can work +TEMP_MONITORING_AVAILABLE=true + # If Pulse is containerized, try to install proxy automatically if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then # Try automatic installation - proxy keeps SSH credentials on the host for security @@ -3716,8 +3719,20 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then fi rm -f "$PROXY_INSTALLER" + else + # Proxy installer not available - configure automatic ProxyJump instead + echo "" + echo "â„šī¸ Proxy not available - configuring automatic SSH ProxyJump" + echo "" + + # Get the current Proxmox host's IP/hostname + PROXY_JUMP_HOST=$(hostname) + PROXY_JUMP_IP=$(hostname -I | awk '{print $1}') + + # We'll configure Pulse's SSH config to use this host as a jump point + # This will be done when temperature monitoring is enabled + CONFIGURE_PROXYJUMP=true fi - # Silently skip if installer unavailable - not an error for test/development setups fi fi @@ -3797,7 +3812,7 @@ if [ "$SSH_ALREADY_CONFIGURED" = true ]; then echo "Temperature monitoring configuration unchanged." fi fi -else +elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then echo "📊 Enable Temperature Monitoring?" echo "" echo "Collect CPU and drive temperatures via secure SSH connection." @@ -3909,6 +3924,37 @@ else fi echo " Temperature data will appear in the dashboard within 10 seconds" TEMPERATURE_ENABLED=true + + # Configure automatic ProxyJump if needed (for containerized Pulse) + if [ "$CONFIGURE_PROXYJUMP" = true ] && [ -n "$PROXY_JUMP_HOST" ]; then + echo "" + echo "Configuring automatic SSH ProxyJump for containerized Pulse..." + + # Create SSH config that uses this Proxmox host as jump point + SSH_CONFIG="Host ${PROXY_JUMP_HOST} + HostName ${PROXY_JUMP_IP} + User root + IdentityFile ~/.ssh/id_ed25519 + +Host * + ProxyJump ${PROXY_JUMP_HOST} + User root + IdentityFile ~/.ssh/id_ed25519 + StrictHostKeyChecking accept-new" + + # Write SSH config to Pulse container + # This will be written to /home/pulse/.ssh/config inside the container + echo "$SSH_CONFIG" | curl -s -X POST "%s/api/system/ssh-config" \ + -H "Content-Type: text/plain" \ + -H "Authorization: Bearer %s" \ + --data-binary @- > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ ProxyJump configured - temperature monitoring will work automatically" + else + echo " âš ī¸ Could not configure ProxyJump automatically" + fi + fi else echo "" echo "âš ī¸ SSH key not available from Pulse server" diff --git a/internal/api/router.go b/internal/api/router.go index 6e6d964..2c68bc2 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -849,6 +849,7 @@ func (r *Router) setupRoutes() { r.systemSettingsHandler = NewSystemSettingsHandler(r.config, r.persistence, r.wsHub, r.monitor, r.reloadSystemSettings) r.mux.HandleFunc("/api/system/settings", r.systemSettingsHandler.HandleGetSystemSettings) r.mux.HandleFunc("/api/system/settings/update", r.systemSettingsHandler.HandleUpdateSystemSettings) + r.mux.HandleFunc("/api/system/ssh-config", RequireAuth(r.config, r.systemSettingsHandler.HandleSSHConfig)) r.mux.HandleFunc("/api/system/verify-temperature-ssh", r.handleVerifyTemperatureSSH) r.mux.HandleFunc("/api/system/proxy-public-key", r.handleProxyPublicKey) // Old API token endpoints removed - now using /api/security/regenerate-token diff --git a/internal/api/system_settings.go b/internal/api/system_settings.go index 75076a5..592dbd9 100644 --- a/internal/api/system_settings.go +++ b/internal/api/system_settings.go @@ -4,7 +4,10 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "os" + "path/filepath" "time" "github.com/rcourtman/pulse-go-rewrite/internal/config" @@ -447,3 +450,45 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter log.Error().Err(err).Msg("Failed to write system settings update response") } } + +// HandleSSHConfig writes SSH configuration for Pulse user +func (h *SystemSettingsHandler) HandleSSHConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read SSH config content from request body + sshConfig, err := io.ReadAll(r.Body) + if err != nil { + log.Error().Err(err).Msg("Failed to read SSH config from request") + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + + // Get the Pulse user's home directory + homeDir := os.Getenv("HOME") + if homeDir == "" { + homeDir = "/home/pulse" // fallback + } + + // Create .ssh directory if it doesn't exist + sshDir := filepath.Join(homeDir, ".ssh") + if err := os.MkdirAll(sshDir, 0700); err != nil { + log.Error().Err(err).Str("dir", sshDir).Msg("Failed to create .ssh directory") + http.Error(w, "Failed to create SSH directory", http.StatusInternalServerError) + return + } + + // Write SSH config file + configPath := filepath.Join(sshDir, "config") + if err := os.WriteFile(configPath, sshConfig, 0600); err != nil { + log.Error().Err(err).Str("path", configPath).Msg("Failed to write SSH config") + http.Error(w, "Failed to write SSH config", http.StatusInternalServerError) + return + } + + log.Info().Str("path", configPath).Msg("SSH config written successfully") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]bool{"success": true}) +}