From 123e0f04cae3f570e3788a17d54fac385c56fa00 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Oct 2025 18:53:45 +0000 Subject: [PATCH] feat: add comprehensive node cleanup system Implements automated cleanup workflow when nodes are deleted from Pulse, removing all monitoring footprint from the host. Changes include a new RPC handler in the sensor proxy for cleanup requests, enhanced node deletion modal with detailed cleanup explanations, and improved SSH key management with proper tagging for atomic updates. --- cmd/pulse-sensor-proxy/cleanup.go | 88 +++++++ cmd/pulse-sensor-proxy/main.go | 14 + cmd/pulse-sensor-proxy/ssh.go | 4 +- .../src/components/Settings/Settings.tsx | 133 +++++++++- internal/api/config_handlers.go | 239 ++++++++++++------ internal/monitoring/temperature.go | 29 ++- internal/tempproxy/client.go | 22 ++ scripts/install-sensor-proxy.sh | 88 +++++-- 8 files changed, 487 insertions(+), 130 deletions(-) create mode 100644 cmd/pulse-sensor-proxy/cleanup.go diff --git a/cmd/pulse-sensor-proxy/cleanup.go b/cmd/pulse-sensor-proxy/cleanup.go new file mode 100644 index 0000000..197dcc3 --- /dev/null +++ b/cmd/pulse-sensor-proxy/cleanup.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/rs/zerolog" +) + +const cleanupRequestFilename = "cleanup-request.json" + +func (p *Proxy) cleanupRequestPath() (string, error) { + workDir := p.workDir + if workDir == "" { + workDir = defaultWorkDir() + } + + if workDir == "" { + return "", errors.New("cleanup working directory not configured") + } + + return filepath.Join(workDir, cleanupRequestFilename), nil +} + +func (p *Proxy) handleRequestCleanup(ctx context.Context, req *RPCRequest, logger zerolog.Logger) (interface{}, error) { + cleanupPath, err := p.cleanupRequestPath() + if err != nil { + return nil, err + } + + dir := filepath.Dir(cleanupPath) + if err := os.MkdirAll(dir, 0o750); err != nil { + return nil, fmt.Errorf("ensure cleanup directory: %w", err) + } + + payload := map[string]any{ + "requestedAt": time.Now().UTC().Format(time.RFC3339), + } + if req != nil && req.Params != nil { + if host, ok := req.Params["host"].(string); ok && host != "" { + payload["host"] = host + } + if reason, ok := req.Params["reason"].(string); ok && reason != "" { + payload["reason"] = reason + } + } + + data, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("encode cleanup payload: %w", err) + } + + tmpFile, err := os.CreateTemp(dir, "cleanup-request-*.tmp") + if err != nil { + return nil, fmt.Errorf("prepare cleanup signal: %w", err) + } + tmpName := tmpFile.Name() + if _, err := tmpFile.Write(append(data, '\n')); err != nil { + tmpFile.Close() + os.Remove(tmpName) + return nil, fmt.Errorf("write cleanup payload: %w", err) + } + if err := tmpFile.Chmod(0o600); err != nil { + logger.Warn().Err(err).Str("path", tmpName).Msg("Failed to set cleanup payload permissions") + } + if err := tmpFile.Close(); err != nil { + os.Remove(tmpName) + return nil, fmt.Errorf("close cleanup payload: %w", err) + } + + // Replace any existing request atomically so systemd path units trigger on change. + if err := os.Rename(tmpName, cleanupPath); err != nil { + os.Remove(tmpName) + return nil, fmt.Errorf("activate cleanup payload: %w", err) + } + + logger.Info(). + Str("path", cleanupPath). + Interface("payload", payload). + Msg("Cleanup request signalled") + + return map[string]any{"queued": true}, nil +} diff --git a/cmd/pulse-sensor-proxy/main.go b/cmd/pulse-sensor-proxy/main.go index 7508591..d2891c6 100644 --- a/cmd/pulse-sensor-proxy/main.go +++ b/cmd/pulse-sensor-proxy/main.go @@ -35,6 +35,10 @@ const ( maxRequestBytes = 16 * 1024 // 16 KiB max request size ) +func defaultWorkDir() string { + return "/var/lib/pulse-sensor-proxy" +} + var rootCmd = &cobra.Command{ Use: "pulse-sensor-proxy", Short: "Pulse Sensor Proxy - Secure sensor data bridge for containerized Pulse", @@ -74,6 +78,7 @@ func main() { type Proxy struct { socketPath string sshKeyPath string + workDir string listener net.Listener rateLimiter *rateLimiter nodeGate *nodeGate @@ -93,6 +98,7 @@ const ( RPCRegisterNodes = "register_nodes" RPCGetTemperature = "get_temperature" RPCGetStatus = "get_status" + RPCRequestCleanup = "request_cleanup" ) // RPCRequest represents a request from Pulse @@ -158,12 +164,20 @@ func runProxy() { metrics: metrics, } + if wd, err := os.Getwd(); err == nil { + proxy.workDir = wd + } else { + log.Warn().Err(err).Msg("Failed to determine working directory; using default") + proxy.workDir = defaultWorkDir() + } + // Register RPC method handlers proxy.router = map[string]handlerFunc{ RPCGetStatus: proxy.handleGetStatusV2, RPCEnsureClusterKeys: proxy.handleEnsureClusterKeysV2, RPCRegisterNodes: proxy.handleRegisterNodesV2, RPCGetTemperature: proxy.handleGetTemperatureV2, + RPCRequestCleanup: proxy.handleRequestCleanup, } if err := proxy.initAuthRules(); err != nil { diff --git a/cmd/pulse-sensor-proxy/ssh.go b/cmd/pulse-sensor-proxy/ssh.go index 9d3179e..eeff108 100644 --- a/cmd/pulse-sensor-proxy/ssh.go +++ b/cmd/pulse-sensor-proxy/ssh.go @@ -136,7 +136,7 @@ func (p *Proxy) testSSHConnection(nodeHost string) error { privKeyPath := filepath.Join(p.sshKeyPath, "id_ed25519") cmd := fmt.Sprintf( - `ssh -i %s -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@%s "echo test"`, + `ssh -i %[1]s -T -n -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5 root@%[2]s "echo test"`, privKeyPath, nodeHost, ) @@ -166,7 +166,7 @@ func (p *Proxy) getTemperatureViaSSH(nodeHost string) (string, error) { // Since we use ForceCommand="sensors -j", any SSH command will run sensors // We don't need to specify the command cmd := fmt.Sprintf( - `ssh -i %s -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@%s ""`, + `ssh -i %[1]s -T -n -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5 root@%[2]s ""`, privKeyPath, nodeHost, ) diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 097c3fc..a74ea0a 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -335,6 +335,9 @@ const Settings: Component = (props) => { const [currentNodeType, setCurrentNodeType] = createSignal<'pve' | 'pbs' | 'pmg'>('pve'); const [modalResetKey, setModalResetKey] = createSignal(0); const [showPasswordModal, setShowPasswordModal] = createSignal(false); + const [showDeleteNodeModal, setShowDeleteNodeModal] = createSignal(false); + const [nodePendingDelete, setNodePendingDelete] = createSignal(null); + const [deleteNodeLoading, setDeleteNodeLoading] = createSignal(false); const [initialLoadComplete, setInitialLoadComplete] = createSignal(false); const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal({ scanning: false, @@ -1091,15 +1094,53 @@ const Settings: Component = (props) => { } }; - const deleteNode = async (nodeId: string) => { - if (!confirm('Are you sure you want to delete this node?')) return; + const nodePendingDeleteLabel = () => { + const node = nodePendingDelete(); + if (!node) return ''; + return node.displayName || node.name || node.host || node.id; + }; + const nodePendingDeleteHost = () => nodePendingDelete()?.host || ''; + const nodePendingDeleteType = () => nodePendingDelete()?.type || ''; + const nodePendingDeleteTypeLabel = () => { + switch (nodePendingDeleteType()) { + case 'pve': + return 'Proxmox VE node'; + case 'pbs': + return 'Proxmox Backup Server'; + case 'pmg': + return 'Proxmox Mail Gateway'; + default: + return 'Pulse node'; + } + }; + + const requestDeleteNode = (node: NodeConfigWithStatus) => { + setNodePendingDelete(node); + setShowDeleteNodeModal(true); + }; + + const cancelDeleteNode = () => { + if (deleteNodeLoading()) return; + setShowDeleteNodeModal(false); + setNodePendingDelete(null); + }; + + const deleteNode = async () => { + const pending = nodePendingDelete(); + if (!pending) return; + setDeleteNodeLoading(true); try { - await NodesAPI.deleteNode(nodeId); - setNodes(nodes().filter((n) => n.id !== nodeId)); - showSuccess('Node deleted successfully'); + await NodesAPI.deleteNode(pending.id); + setNodes(nodes().filter((n) => n.id !== pending.id)); + const label = pending.displayName || pending.name || pending.host || pending.id; + showSuccess(`${label} removed successfully`); } catch (error) { showError(error instanceof Error ? error.message : 'Failed to delete node'); + } finally { + setDeleteNodeLoading(false); + setShowDeleteNodeModal(false); + setNodePendingDelete(null); } }; @@ -1869,7 +1910,7 @@ const Settings: Component = (props) => { - + + + + + + {/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */} /dev/null; then - if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then - echo " • Stopping pulse-sensor-proxy service..." - systemctl stop pulse-sensor-proxy || true - fi - if systemctl is-enabled --quiet pulse-sensor-proxy 2>/dev/null; then - echo " • Disabling pulse-sensor-proxy service..." - systemctl disable pulse-sensor-proxy || true - fi - if [ -f /etc/systemd/system/pulse-sensor-proxy.service ]; then - echo " • Removing systemd unit file..." - rm -f /etc/systemd/system/pulse-sensor-proxy.service - systemctl daemon-reload || true + CLEANUP_HELPER_USED=false + if [ -x /usr/local/bin/pulse-sensor-cleanup.sh ]; then + echo " • Running cleanup helper..." + if /usr/local/bin/pulse-sensor-cleanup.sh; then + CLEANUP_HELPER_USED=true + else + echo " ✗ Cleanup helper failed, falling back to manual removal." fi + echo "" fi - # Remove pulse-sensor-proxy binary - if [ -f /usr/local/bin/pulse-sensor-proxy ]; then - echo " • Removing pulse-sensor-proxy binary..." - rm -f /usr/local/bin/pulse-sensor-proxy - fi - - # Remove pulse-sensor-proxy data directory - if [ -d /var/lib/pulse-sensor-proxy ]; then - echo " • Removing pulse-sensor-proxy data directory..." - rm -rf /var/lib/pulse-sensor-proxy - fi - - # Remove pulse-sensor-proxy user - if id -u pulse-sensor-proxy >/dev/null 2>&1; then - echo " • Removing pulse-sensor-proxy system user..." - userdel pulse-sensor-proxy 2>/dev/null || true - fi - - # Remove SSH keys from authorized_keys (only Pulse-managed entries) - if [ -f /root/.ssh/authorized_keys ]; then - echo " • Removing SSH keys from authorized_keys..." - # Create temporary file for safe atomic update - TMP_AUTH_KEYS=$(mktemp) - if [ -f "$TMP_AUTH_KEYS" ]; then - # Remove only lines with pulse-managed-key marker (preserves user keys) - grep -vF '# pulse-managed-key' /root/.ssh/authorized_keys > "$TMP_AUTH_KEYS" 2>/dev/null - GREP_EXIT=$? - - # Exit 0 = lines remain, Exit 1 = all lines removed (both are success) - if [ $GREP_EXIT -eq 0 ] || [ $GREP_EXIT -eq 1 ]; then - # Preserve ownership and permissions from original - 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 - # Atomically replace (abort if mv fails) - if mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys; then - : - else - # Cleanup temp file if move failed - rm -f "$TMP_AUTH_KEYS" - fi - else - # Cleanup temp file if grep had a real error (exit > 1) - rm -f "$TMP_AUTH_KEYS" + if [ "$CLEANUP_HELPER_USED" != true ]; then + # Stop and remove pulse-sensor services + if command -v systemctl &> /dev/null; then + if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then + echo " • Stopping pulse-sensor-proxy service..." + systemctl stop pulse-sensor-proxy || true + fi + if systemctl is-enabled --quiet pulse-sensor-proxy 2>/dev/null; then + echo " • Disabling pulse-sensor-proxy service..." + systemctl disable pulse-sensor-proxy || true + fi + if systemctl is-active --quiet pulse-sensor-cleanup.path 2>/dev/null; then + echo " • Stopping pulse-sensor-cleanup.path..." + systemctl stop pulse-sensor-cleanup.path || true + fi + if systemctl is-enabled --quiet pulse-sensor-cleanup.path 2>/dev/null; then + echo " • Disabling pulse-sensor-cleanup.path..." + systemctl disable pulse-sensor-cleanup.path || true + fi + if systemctl is-enabled --quiet pulse-sensor-cleanup.service 2>/dev/null; then + echo " • Disabling pulse-sensor-cleanup.service..." + systemctl disable pulse-sensor-cleanup.service || true + fi + if [ -f /etc/systemd/system/pulse-sensor-proxy.service ] || \ + [ -f /etc/systemd/system/pulse-sensor-cleanup.service ] || \ + [ -f /etc/systemd/system/pulse-sensor-cleanup.path ]; then + echo " • Removing systemd unit files..." + rm -f /etc/systemd/system/pulse-sensor-proxy.service + rm -f /etc/systemd/system/pulse-sensor-cleanup.service + rm -f /etc/systemd/system/pulse-sensor-cleanup.path + systemctl daemon-reload || true fi fi - fi - # Remove LXC bind mounts from all container configs - if [ -d /etc/pve/lxc ]; then - echo " • Removing LXC bind mounts from container configs..." - if compgen -G "/etc/pve/lxc/*.conf" > /dev/null; then - for conf in /etc/pve/lxc/*.conf; do - if [ -f "$conf" ] && grep -q "pulse-sensor-proxy" "$conf" 2>/dev/null; then - sed -i '/pulse-sensor-proxy/d' "$conf" || true - fi - done + # Remove pulse-sensor-proxy binary + if [ -f /usr/local/bin/pulse-sensor-proxy ]; then + echo " • Removing pulse-sensor-proxy binary..." + rm -f /usr/local/bin/pulse-sensor-proxy fi - fi - # Remove Pulse monitoring API tokens and user - echo " • Removing Pulse monitoring API tokens and user..." - if command -v pveum &> /dev/null; then - # List all tokens for pulse-monitor@pam and remove them (idempotent) - TOKEN_LIST=$(pveum user token list pulse-monitor@pam 2>/dev/null | awk 'NR>3 {print $2}' | grep -v '^$' || printf '') - if [ -n "$TOKEN_LIST" ]; then - while IFS= read -r TOKEN; do - if [ -n "$TOKEN" ]; then - pveum user token remove pulse-monitor@pam "$TOKEN" 2>/dev/null || true - fi - done <<< "$TOKEN_LIST" + # Remove cleanup helper script + if [ -f /usr/local/bin/pulse-sensor-cleanup.sh ]; then + echo " • Removing cleanup helper script..." + rm -f /usr/local/bin/pulse-sensor-cleanup.sh + fi + + # Remove pulse-sensor-proxy data directory + if [ -d /var/lib/pulse-sensor-proxy ]; then + echo " • Removing pulse-sensor-proxy data directory..." + rm -rf /var/lib/pulse-sensor-proxy + fi + + # Remove pulse-sensor-proxy user + if id -u pulse-sensor-proxy >/dev/null 2>&1; then + echo " • Removing pulse-sensor-proxy system user..." + userdel pulse-sensor-proxy 2>/dev/null || true + fi + + # Remove SSH keys from authorized_keys (only Pulse-managed entries) + if [ -f /root/.ssh/authorized_keys ]; then + echo " • Removing SSH keys from authorized_keys..." + TMP_AUTH_KEYS=$(mktemp) + if [ -f "$TMP_AUTH_KEYS" ]; then + grep -vF '# pulse-managed-key' /root/.ssh/authorized_keys > "$TMP_AUTH_KEYS" 2>/dev/null + GREP_EXIT=$? + if [ $GREP_EXIT -eq 0 ] || [ $GREP_EXIT -eq 1 ]; 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 + if mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys; then + : + else + rm -f "$TMP_AUTH_KEYS" + fi + else + rm -f "$TMP_AUTH_KEYS" + fi + fi + fi + + # Remove LXC bind mounts from all container configs + if [ -d /etc/pve/lxc ]; then + echo " • Removing LXC bind mounts from container configs..." + if compgen -G "/etc/pve/lxc/*.conf" > /dev/null; then + for conf in /etc/pve/lxc/*.conf; do + if [ -f "$conf" ] && grep -q "pulse-sensor-proxy" "$conf" 2>/dev/null; then + sed -i '/pulse-sensor-proxy/d' "$conf" || true + fi + done + fi + fi + + # Remove Pulse monitoring API tokens and user + echo " • Removing Pulse monitoring API tokens and user..." + if command -v pveum &> /dev/null; then + TOKEN_LIST=$(pveum user token list pulse-monitor@pam 2>/dev/null | awk 'NR>3 {print $2}' | grep -v '^$' || printf '') + if [ -n "$TOKEN_LIST" ]; then + while IFS= read -r TOKEN; do + if [ -n "$TOKEN" ]; then + pveum user token remove pulse-monitor@pam "$TOKEN" 2>/dev/null || true + fi + done <<< "$TOKEN_LIST" + fi + pveum user delete pulse-monitor@pam 2>/dev/null || true + pveum role delete PulseMonitor 2>/dev/null || true + fi + + if command -v proxmox-backup-manager &> /dev/null; then + proxmox-backup-manager user delete pulse-monitor@pbs 2>/dev/null || true fi - # Remove the user (idempotent) - pveum user delete pulse-monitor@pam 2>/dev/null || true - # Remove custom roles (idempotent) - pveum role delete PulseMonitor 2>/dev/null || true fi echo "" @@ -3670,7 +3725,18 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then export PULSE_SENSOR_PROXY_FALLBACK_URL="%s/api/install/pulse-sensor-proxy" # Run installer - if "$PROXY_INSTALLER" --ctid "$PULSE_CTID" --quiet 2>&1 | grep -E "✓|⚠️|ERROR"; then + INSTALL_OUTPUT=$("$PROXY_INSTALLER" --ctid "$PULSE_CTID" --quiet 2>&1) + INSTALL_STATUS=$? + + if [ -n "$INSTALL_OUTPUT" ]; then + echo "$INSTALL_OUTPUT" | grep -E "✓|⚠️|ERROR" || true + fi + + if [ -n "$INSTALL_OUTPUT" ]; then + echo "$INSTALL_OUTPUT" | grep -E "✓|⚠️|ERROR" || true + fi + + if [ $INSTALL_STATUS -eq 0 ]; then # Verify proxy health PROXY_HEALTHY=false if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then @@ -3735,6 +3801,11 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then else echo "" echo "⚠️ Proxy installation had issues - you may need to configure manually" + if [ -n "$INSTALL_OUTPUT" ]; then + echo "" + echo "$INSTALL_OUTPUT" | tail -n 40 + echo "" + fi fi rm -f "$PROXY_INSTALLER" diff --git a/internal/monitoring/temperature.go b/internal/monitoring/temperature.go index 5c3b817..0587e6b 100644 --- a/internal/monitoring/temperature.go +++ b/internal/monitoring/temperature.go @@ -17,10 +17,10 @@ import ( // TemperatureCollector handles SSH-based temperature collection from Proxmox nodes type TemperatureCollector struct { - sshUser string // SSH user (typically "root" or "pulse-monitor") - sshKeyPath string // Path to SSH private key + sshUser string // SSH user (typically "root" or "pulse-monitor") + sshKeyPath string // Path to SSH private key proxyClient *tempproxy.Client // Optional: unix socket client for proxy - useProxy bool // Whether to use proxy for temperature collection + useProxy bool // Whether to use proxy for temperature collection } // NewTemperatureCollector creates a new temperature collector @@ -103,6 +103,7 @@ func (tc *TemperatureCollector) runSSHCommand(ctx context.Context, host, command "-o", "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", // No password prompts + "-o", "LogLevel=ERROR", // Suppress host key warnings that break JSON parsing } // Add key if specified @@ -123,7 +124,17 @@ func (tc *TemperatureCollector) runSSHCommand(ctx context.Context, host, command return "", fmt.Errorf("ssh command failed: %w", err) } - return string(output), nil + outputStr := strings.TrimSpace(string(output)) + + // Strip any leading SSH noise (e.g., "Warning: Permanently added ...") so sensors JSON parses cleanly. + if idx := strings.Index(outputStr, "{"); idx > 0 { + outputStr = outputStr[idx:] + } + if idx := strings.LastIndex(outputStr, "}"); idx != -1 && idx < len(outputStr)-1 { + outputStr = outputStr[:idx+1] + } + + return outputStr, nil } // parseSensorsJSON parses the JSON output from `sensors -j` @@ -166,11 +177,11 @@ func (tc *TemperatureCollector) parseSensorsJSON(jsonStr string) (*models.Temper // Handle CPU temperature sensors chipLower := strings.ToLower(chipName) if strings.Contains(chipLower, "coretemp") || - strings.Contains(chipLower, "k10temp") || - strings.Contains(chipLower, "zenpower") || - strings.Contains(chipLower, "k8temp") || - strings.Contains(chipLower, "acpitz") || - strings.Contains(chipLower, "it87") { + strings.Contains(chipLower, "k10temp") || + strings.Contains(chipLower, "zenpower") || + strings.Contains(chipLower, "k8temp") || + strings.Contains(chipLower, "acpitz") || + strings.Contains(chipLower, "it87") { foundCPUChip = true tc.parseCPUTemps(chipMap, temp) } diff --git a/internal/tempproxy/client.go b/internal/tempproxy/client.go index da4d73d..08fa63a 100644 --- a/internal/tempproxy/client.go +++ b/internal/tempproxy/client.go @@ -168,3 +168,25 @@ func (c *Client) GetTemperature(nodeHost string) (string, error) { return tempStr, nil } + +// RequestCleanup signals the proxy to trigger host-side cleanup workflow. +func (c *Client) RequestCleanup(host string) error { + params := make(map[string]interface{}, 1) + if host != "" { + params["host"] = host + } + + resp, err := c.call("request_cleanup", params) + if err != nil { + return err + } + + if !resp.Success { + if resp.Error != "" { + return fmt.Errorf("proxy error: %s", resp.Error) + } + return fmt.Errorf("proxy rejected cleanup request") + } + + return nil +} diff --git a/scripts/install-sensor-proxy.sh b/scripts/install-sensor-proxy.sh index 2a51641..8716d65 100755 --- a/scripts/install-sensor-proxy.sh +++ b/scripts/install-sensor-proxy.sh @@ -29,6 +29,36 @@ print_success() { echo -e "${GREEN}✓${NC} $1" } +configure_local_authorized_key() { + local auth_line=$1 + local auth_keys_file="/root/.ssh/authorized_keys" + local tmp_auth + + tmp_auth=$(mktemp) + mkdir -p /root/.ssh + touch "$tmp_auth" + + if [[ -f "$auth_keys_file" ]]; then + grep -vF '# pulse-managed-key' "$auth_keys_file" >"$tmp_auth" 2>/dev/null || true + chmod --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || chmod 600 "$tmp_auth" + chown --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || true + else + chmod 600 "$tmp_auth" + fi + + echo "${auth_line}" >>"$tmp_auth" + if mv "$tmp_auth" "$auth_keys_file"; then + if [ "$QUIET" != true ]; then + print_success "SSH key configured on localhost" + fi + else + rm -f "$tmp_auth" + print_warn "Failed to configure SSH key on localhost" + print_info "Add this line manually to /root/.ssh/authorized_keys:" + print_info " ${auth_line}" + fi +} + # Check if running on Proxmox host if ! command -v pvecm >/dev/null 2>&1; then print_error "This script must be run on a Proxmox VE host" @@ -306,20 +336,41 @@ if command -v pvecm >/dev/null 2>&1; then # Configure SSH key with forced command restriction FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' - AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}" + AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key" # Track SSH key push results SSH_SUCCESS_COUNT=0 SSH_FAILURE_COUNT=0 declare -a SSH_FAILED_NODES=() + LOCAL_IPS=$(hostname -I 2>/dev/null || echo "") + LOCAL_HOSTNAMES="$(hostname 2>/dev/null || echo "") $(hostname -f 2>/dev/null || echo "")" + LOCAL_HANDLED=false # Push key to each cluster node for node_ip in $CLUSTER_NODES; do print_info "Authorizing proxy key on node $node_ip..." + IS_LOCAL=false + if [[ " $LOCAL_IPS " == *" $node_ip "* ]]; then + IS_LOCAL=true + fi + if [[ " $LOCAL_HOSTNAMES " == *" $node_ip "* ]]; then + IS_LOCAL=true + fi + if [[ "$node_ip" == "127.0.0.1" || "$node_ip" == "localhost" ]]; then + IS_LOCAL=true + fi + + if [[ "$IS_LOCAL" = true ]]; then + configure_local_authorized_key "$AUTH_LINE" + LOCAL_HANDLED=true + ((SSH_SUCCESS_COUNT++)) + continue + fi + # Remove any existing proxy keys first ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \ - "sed -i '/pulse-sensor-proxy\$/d' /root/.ssh/authorized_keys" 2>/dev/null || true + "sed -i '/# pulse-managed-key\$/d' /root/.ssh/authorized_keys" 2>/dev/null || true # Add new key with forced command SSH_ERROR=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \ @@ -348,30 +399,22 @@ if command -v pvecm >/dev/null 2>&1; then print_info "To retry failed nodes, re-run this script or manually run:" print_info " ssh root@ 'echo \"${AUTH_LINE}\" >> /root/.ssh/authorized_keys'" fi + if [[ "$LOCAL_HANDLED" = false ]]; then + configure_local_authorized_key "$AUTH_LINE" + ((SSH_SUCCESS_COUNT++)) + fi else # No cluster found - configure standalone node print_info "No cluster detected, configuring standalone node..." # Configure SSH key with forced command restriction FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' - AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}" + AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key" - # Configure localhost print_info "Authorizing proxy key on localhost..." - - # Remove any existing proxy keys first - sed -i '/pulse-sensor-proxy$/d' /root/.ssh/authorized_keys 2>/dev/null || touch /root/.ssh/authorized_keys - - # Add new key with forced command - if echo "${AUTH_LINE}" >> /root/.ssh/authorized_keys; then - print_success "SSH key configured on standalone node" - print_info "" - print_info "Standalone node configuration complete" - else - print_warn "Failed to configure SSH key on localhost" - print_info "Manually add this line to /root/.ssh/authorized_keys:" - print_info " ${AUTH_LINE}" - fi + configure_local_authorized_key "$AUTH_LINE" + print_info "" + print_info "Standalone node configuration complete" fi else # Proxmox host but pvecm not available (shouldn't happen, but handle it) @@ -380,14 +423,9 @@ else # Configure localhost as fallback FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' - AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}" + AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key" - sed -i '/pulse-sensor-proxy$/d' /root/.ssh/authorized_keys 2>/dev/null || touch /root/.ssh/authorized_keys - if echo "${AUTH_LINE}" >> /root/.ssh/authorized_keys; then - print_success "SSH key configured on localhost" - else - print_warn "Failed to configure SSH key" - fi + configure_local_authorized_key "$AUTH_LINE" fi # Ensure container mount via mp configuration