From 335795d354972436b466b61020cc531e09638f7a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 21 Nov 2025 10:07:42 +0000 Subject: [PATCH] Ensure sensor proxy wrapper delivers SMART temps locally --- cmd/pulse-sensor-proxy/ssh.go | 34 +++++++++++++++++++++++++----- cmd/pulse-sensor-proxy/ssh_test.go | 21 ++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cmd/pulse-sensor-proxy/ssh.go b/cmd/pulse-sensor-proxy/ssh.go index ca2d936..7fe6195 100644 --- a/cmd/pulse-sensor-proxy/ssh.go +++ b/cmd/pulse-sensor-proxy/ssh.go @@ -25,6 +25,19 @@ const ( tempWrapperScript = `#!/bin/sh set -eu +WRAPPER_PATH="${PULSE_SENSOR_WRAPPER:-/opt/pulse/sensor-proxy/bin/pulse-sensor-wrapper.sh}" + +# Prefer the full wrapper when available so SMART disk temperatures are included +if [ -x "$WRAPPER_PATH" ]; then + exec "$WRAPPER_PATH" +fi + +# Legacy locations (for older installations) +if [ -x /usr/local/bin/pulse-sensor-wrapper.sh ]; then + exec /usr/local/bin/pulse-sensor-wrapper.sh +fi + +# Fallback to basic lm-sensors output (no SMART data) if command -v sensors >/dev/null 2>&1; then OUTPUT="$(sensors -j 2>/dev/null || true)" if [ -n "$OUTPUT" ]; then @@ -575,12 +588,10 @@ func (p *Proxy) testSSHConnection(nodeHost string) error { func (p *Proxy) getTemperatureViaSSH(ctx context.Context, nodeHost string) (string, error) { startTime := time.Now() nodeLabel := sanitizeNodeLabel(nodeHost) + localNode := isLocalNode(nodeHost) - // Check if we're requesting the local node (self-monitoring) - // For standalone nodes, avoid SSH and run sensors directly - if isLocalNode(nodeHost) { - log.Debug().Str("node", nodeHost).Msg("Self-monitoring detected, collecting temperatures locally") - return p.getTemperatureLocal(ctx) + if localNode { + log.Debug().Str("node", nodeHost).Msg("Self-monitoring detected, using SSH wrapper for SMART temperatures") } privKeyPath := filepath.Join(p.sshKeyPath, "id_ed25519") @@ -617,6 +628,19 @@ func (p *Proxy) getTemperatureViaSSH(ctx context.Context, nodeHost string) (stri } if err != nil { + if localNode { + log.Warn(). + Str("node", nodeHost). + Err(err). + Msg("SSH temperature collection failed on local node, falling back to direct sensors") + + if fallback, localErr := p.getTemperatureLocal(ctx); localErr == nil && strings.TrimSpace(fallback) != "" { + p.metrics.sshRequests.WithLabelValues(nodeLabel, "success").Inc() + p.metrics.sshLatency.WithLabelValues(nodeLabel).Observe(time.Since(startTime).Seconds()) + return fallback, nil + } + } + p.metrics.sshRequests.WithLabelValues(nodeLabel, "error").Inc() p.metrics.sshLatency.WithLabelValues(nodeLabel).Observe(time.Since(startTime).Seconds()) stderrMsg := strings.TrimSpace(stderr) diff --git a/cmd/pulse-sensor-proxy/ssh_test.go b/cmd/pulse-sensor-proxy/ssh_test.go index 3ec45ea..fd71820 100644 --- a/cmd/pulse-sensor-proxy/ssh_test.go +++ b/cmd/pulse-sensor-proxy/ssh_test.go @@ -138,6 +138,27 @@ func TestTempWrapperPrefersSensorsOutput(t *testing.T) { } } +func TestTempWrapperPrefersOverrideWrapper(t *testing.T) { + scriptPath, _, binDir, baseDir := setupTempWrapper(t) + + overrideDir := filepath.Join(baseDir, "override") + if err := os.MkdirAll(overrideDir, 0o755); err != nil { + t.Fatalf("failed to create override directory: %v", err) + } + + overridePath := filepath.Join(overrideDir, "pulse-sensor-wrapper.sh") + expectedOutput := `{"smart":[{"device":"/dev/test","temperature":42}]}` + overrideScript := fmt.Sprintf("#!/bin/sh\nprintf '%s'\n", expectedOutput) + if err := os.WriteFile(overridePath, []byte(overrideScript), 0o755); err != nil { + t.Fatalf("failed to write override wrapper: %v", err) + } + + output := runTempWrapper(t, scriptPath, binDir, "PULSE_SENSOR_WRAPPER="+overridePath) + if strings.TrimSpace(string(output)) != expectedOutput { + t.Fatalf("expected override wrapper output %s, got %s", expectedOutput, strings.TrimSpace(string(output))) + } +} + func TestReadAllWithLimit(t *testing.T) { reader := bytes.NewBufferString("abcdefg") data, exceeded, err := readAllWithLimit(reader, 4)