diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 17067d3..1292da4 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -4002,6 +4002,7 @@ TEMPERATURE_ENABLED=false TEMP_MONITORING_AVAILABLE=true MIN_PROXY_VERSION="%s" PULSE_VERSION_ENDPOINT="%s/api/version" +STANDALONE_PROXY_DEPLOYED=false version_ge() { if command -v dpkg >/dev/null 2>&1; then @@ -4060,6 +4061,12 @@ if command -v pct >/dev/null 2>&1; then fi fi +# Determine if this node is standalone (not joined to a cluster) +IS_STANDALONE_NODE=false +if ! command -v pvecm >/dev/null 2>&1 || ! pvecm status >/dev/null 2>&1; then + IS_STANDALONE_NODE=true +fi + # Track whether temperature monitoring can work (may be disabled by checks above) # For containerized Pulse, verify version supports proxy (v4.24.0+) @@ -4327,6 +4334,42 @@ elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then if [[ $SSH_REPLY =~ ^[Yy]$ ]]; then echo "Configuring temperature monitoring..." + if [ "$IS_STANDALONE_NODE" = true ]; then + echo " • Deploying hardened pulse-sensor-proxy..." + PROXY_INSTALLER_URL="$PULSE_URL/api/install/install-sensor-proxy.sh" + PROXY_INSTALLER=$(mktemp) + if curl -fsSL "$PROXY_INSTALLER_URL" -o "$PROXY_INSTALLER" 2>/dev/null; then + chmod +x "$PROXY_INSTALLER" + if "$PROXY_INSTALLER" --standalone --http-mode --pulse-server "$PULSE_URL"; then + echo " ✓ pulse-sensor-proxy installed and registered with Pulse" + STANDALONE_PROXY_DEPLOYED=true + TEMPERATURE_ENABLED=true + else + echo " ⚠️ Proxy installer reported an error; falling back to SSH-based collector" + fi + rm -f "$PROXY_INSTALLER" + else + echo " ⚠️ Unable to download proxy installer from $PULSE_URL" + echo " Falling back to SSH-based temperature monitoring." + fi + fi + + if [ "$STANDALONE_PROXY_DEPLOYED" = true ]; then + echo "" + echo "✓ Temperature monitoring will use pulse-sensor-proxy on this host." + echo " Temperature data will appear in the dashboard within 10 seconds." + if [ -f /root/.ssh/authorized_keys ]; then + TMP_AUTH_KEYS=$(mktemp) + if grep -v '# pulse-' /root/.ssh/authorized_keys > "$TMP_AUTH_KEYS" 2>/dev/null; then + chmod --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || chmod 600 "$TMP_AUTH_KEYS" + chown --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || true + mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys + else + rm -f "$TMP_AUTH_KEYS" + fi + fi + else + if [ -n "$SSH_SENSORS_PUBLIC_KEY" ]; then # Add keys to root's authorized_keys mkdir -p /root/.ssh @@ -4519,6 +4562,7 @@ Host ${NODE} echo " journalctl -u pulse -n 100 | grep -i ssh" fi fi + fi # End hardened proxy branch else echo "Temperature monitoring skipped." fi @@ -4693,14 +4737,8 @@ fi # Standalone Node Configuration (for non-cluster nodes) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Check if this node is standalone (not in a cluster) -IS_STANDALONE=false -if ! command -v pvecm >/dev/null 2>&1 || ! pvecm status >/dev/null 2>&1; then - IS_STANDALONE=true -fi - -# If standalone and temperature monitoring was enabled, try to fetch proxy key -if [ "$IS_STANDALONE" = true ] && [ "$TEMPERATURE_ENABLED" = true ]; then +# If standalone and temperature monitoring was enabled via SSH fallback, ensure proxy key is configured +if [ "$IS_STANDALONE_NODE" = true ] && [ "$TEMPERATURE_ENABLED" = true ] && [ "$STANDALONE_PROXY_DEPLOYED" != true ]; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Standalone Node Temperature Setup" diff --git a/internal/api/router.go b/internal/api/router.go index 379a0c7..c3d12bc 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -198,6 +198,7 @@ func (r *Router) setupRoutes() { r.mux.HandleFunc("/api/install/pulse-sensor-proxy", r.handleDownloadPulseSensorProxy) r.mux.HandleFunc("/api/install/install-sensor-proxy.sh", r.handleDownloadInstallerScript) r.mux.HandleFunc("/api/install/migrate-sensor-proxy-control-plane.sh", r.handleDownloadMigrationScript) + r.mux.HandleFunc("/api/install/migrate-temperature-proxy.sh", r.handleDownloadTemperatureProxyMigrationScript) r.mux.HandleFunc("/api/install/install-docker.sh", r.handleDownloadDockerInstallerScript) r.mux.HandleFunc("/api/config", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleConfig))) r.mux.HandleFunc("/api/backups", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleBackups))) @@ -1330,6 +1331,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { "/api/install/install-sensor-proxy.sh", // Temperature proxy installer fallback "/api/install/pulse-sensor-proxy", // Temperature proxy binary fallback "/api/install/migrate-sensor-proxy-control-plane.sh", // Proxy migration helper + "/api/install/migrate-temperature-proxy.sh", // SSH-to-proxy migration helper "/api/install/install-docker.sh", // Docker turnkey installer "/api/system/proxy-public-key", // Temperature proxy public key for setup script "/api/temperature-proxy/register", // Temperature proxy registration (called by installer) @@ -3796,6 +3798,31 @@ func (r *Router) handleDownloadMigrationScript(w http.ResponseWriter, req *http. } } +func (r *Router) handleDownloadTemperatureProxyMigrationScript(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil) + return + } + + scriptPath := "/opt/pulse/scripts/migrate-temperature-proxy.sh" + content, err := os.ReadFile(scriptPath) + if err != nil { + scriptPath = filepath.Join(r.projectRoot, "scripts", "migrate-temperature-proxy.sh") + content, err = os.ReadFile(scriptPath) + if err != nil { + log.Error().Err(err).Str("path", scriptPath).Msg("Failed to read temperature migration script") + writeErrorResponse(w, http.StatusInternalServerError, "read_error", "Failed to read temperature migration script", nil) + return + } + } + + w.Header().Set("Content-Type", "text/x-shellscript") + w.Header().Set("Content-Disposition", "attachment; filename=migrate-temperature-proxy.sh") + if _, err := w.Write(content); err != nil { + log.Error().Err(err).Msg("Failed to write temperature migration script to client") + } +} + func (r *Router) resolvePublicURL(req *http.Request) string { if publicURL := strings.TrimSpace(r.config.PublicURL); publicURL != "" { return strings.TrimRight(publicURL, "/") diff --git a/scripts/migrate-temperature-proxy.sh b/scripts/migrate-temperature-proxy.sh new file mode 100644 index 0000000..603cba0 --- /dev/null +++ b/scripts/migrate-temperature-proxy.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# migrate-temperature-proxy.sh +# Converts SSH-based temperature collection to pulse-sensor-proxy on standalone hosts. + +set -euo pipefail + +PULSE_SERVER="" +KEEP_SSH_KEYS=false +SKIP_RESTART=false + +print_usage() { + cat <<'EOF' +Usage: migrate-temperature-proxy.sh --pulse-server https://pulse.example.com:7655 [--keep-ssh-keys] [--skip-restart] + +Installs pulse-sensor-proxy in standalone mode and removes legacy SSH fallback keys. +EOF +} + +log() { echo "[INFO] $*"; } +warn() { echo "[WARN] $*" >&2; } +err() { echo "[ERROR] $*" >&2; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --pulse-server) + PULSE_SERVER="$2" + shift 2 + ;; + --keep-ssh-keys) + KEEP_SSH_KEYS=true + shift + ;; + --skip-restart) + SKIP_RESTART=true + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + err "Unknown option: $1" + print_usage + exit 1 + ;; + esac +done + +if [[ $EUID -ne 0 ]]; then + err "This script must be run as root" + exit 1 +fi + +if [[ -z "$PULSE_SERVER" ]]; then + err "--pulse-server is required" + exit 1 +fi + +INSTALLER_URL="${PULSE_SERVER%/}/api/install/install-sensor-proxy.sh" +INSTALLER=$(mktemp) +cleanup() { + rm -f "$INSTALLER" +} +trap cleanup EXIT + +log "Downloading pulse-sensor-proxy installer from $INSTALLER_URL" +if ! curl -fsSL "$INSTALLER_URL" -o "$INSTALLER"; then + err "Failed to download installer script" + exit 1 +fi +chmod +x "$INSTALLER" + +INSTALL_ARGS=(--standalone --http-mode --pulse-server "$PULSE_SERVER") +if [[ "$SKIP_RESTART" == true ]]; then + INSTALL_ARGS+=(--skip-restart) +fi + +log "Running pulse-sensor-proxy installer..." +if ! "$INSTALLER" "${INSTALL_ARGS[@]}"; then + err "pulse-sensor-proxy installation failed" + exit 1 +fi + +remove_legacy_keys() { + local auth="/root/.ssh/authorized_keys" + if [[ ! -f "$auth" ]]; then + return + fi + local tmp + tmp=$(mktemp) + if grep -v -E '# pulse-sensors|# pulse-proxy-key' "$auth" > "$tmp"; then + chmod --reference="$auth" "$tmp" 2>/dev/null || chmod 600 "$tmp" + chown --reference="$auth" "$tmp" 2>/dev/null || true + mv "$tmp" "$auth" + log "Removed legacy SSH fallback keys from $auth" + else + rm -f "$tmp" + fi +} + +if [[ "$KEEP_SSH_KEYS" == false ]]; then + remove_legacy_keys +else + warn "Legacy SSH keys retained at user request (--keep-ssh-keys)" +fi + +log "Migration complete. This host now uses pulse-sensor-proxy for temperature collection." +log "Verify status with: systemctl status pulse-sensor-proxy --no-pager"