From b6fd06c63d9f74fdca4a21cc5b6139ffc7eb4cc3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 7 Jul 2026 23:15:59 +0100 Subject: [PATCH] Let explicit disk temperature overrides beat per-type defaults DiskTempByType unconditionally replaced the resolved host threshold, so a host-level (or inherited linked-resource) diskTemperature override never applied to nvme/sas/sata disks. Explicit overrides now win; the per-type map still beats the global agent default. Refs #1540 --- internal/alerts/host.go | 29 +++++++++++++---- internal/alerts/host_disk_type_test.go | 44 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/internal/alerts/host.go b/internal/alerts/host.go index ab5fe1902..bd024fd49 100644 --- a/internal/alerts/host.go +++ b/internal/alerts/host.go @@ -69,31 +69,42 @@ func hostInstanceName(host models.Host) string { func (m *Manager) resolveHostThresholdsNoLock(hostID, linkedNodeID, linkedVMID, linkedContainerID string) ThresholdConfig { base := m.defaultThresholdsForResourceType("agent") + if override, exists := m.hostThresholdOverrideNoLock(hostID, linkedNodeID, linkedVMID, linkedContainerID); exists { + return m.applyThresholdOverride(base, override) + } + + return base +} + +// hostThresholdOverrideNoLock returns the first override in the host-agent +// resolution chain (host, linked node, linked guest), mirroring +// resolveHostThresholdsNoLock's precedence. Callers must hold m.mu. +func (m *Manager) hostThresholdOverrideNoLock(hostID, linkedNodeID, linkedVMID, linkedContainerID string) (ThresholdConfig, bool) { if hostID = strings.TrimSpace(hostID); hostID != "" { if override, exists := m.config.Overrides[hostID]; exists { - return m.applyThresholdOverride(base, override) + return override, true } } if linkedNodeID = strings.TrimSpace(linkedNodeID); linkedNodeID != "" { if override, exists := m.config.Overrides[linkedNodeID]; exists { - return m.applyThresholdOverride(base, override) + return override, true } } if linkedVMID = strings.TrimSpace(linkedVMID); linkedVMID != "" { if override, exists := lookupGuestOverride(m.config.Overrides, nil, linkedVMID); exists { - return m.applyThresholdOverride(base, override) + return override, true } } if linkedContainerID = strings.TrimSpace(linkedContainerID); linkedContainerID != "" { if override, exists := lookupGuestOverride(m.config.Overrides, nil, linkedContainerID); exists { - return m.applyThresholdOverride(base, override) + return override, true } } - return base + return ThresholdConfig{}, false } // resolveHostAlertThresholdsNoLock resolves thresholds for persisted host-agent alerts. @@ -217,6 +228,12 @@ func (m *Manager) CheckHost(host models.Host) { alertsEnabled := m.config.Enabled disableAllAgents := m.config.DisableAllAgents thresholds := m.resolveHostThresholdsNoLock(host.ID, host.LinkedNodeID, host.LinkedVMID, host.LinkedContainerID) + // An explicit disk temperature override (host or inherited linked-resource) + // beats the per-type defaults in DiskTempByType. + diskTempOverridden := false + if override, exists := m.hostThresholdOverrideNoLock(host.ID, host.LinkedNodeID, host.LinkedVMID, host.LinkedContainerID); exists && override.DiskTemperature != nil { + diskTempOverridden = true + } m.mu.RUnlock() if !alertsEnabled { @@ -318,7 +335,7 @@ func (m *Manager) CheckHost(host models.Host) { for _, disk := range host.Sensors.SMART { if disk.Temperature > 0 && !disk.Standby { effectiveTempThreshold := thresholds.DiskTemperature - if diskType := strings.ToLower(strings.TrimSpace(disk.Type)); diskType != "" { + if diskType := strings.ToLower(strings.TrimSpace(disk.Type)); diskType != "" && !diskTempOverridden { m.mu.RLock() if th, ok := m.config.DiskTempByType[diskType]; ok { t := th diff --git a/internal/alerts/host_disk_type_test.go b/internal/alerts/host_disk_type_test.go index 616bc53e5..9f26a52eb 100644 --- a/internal/alerts/host_disk_type_test.go +++ b/internal/alerts/host_disk_type_test.go @@ -314,3 +314,47 @@ func TestHostDiskTempPerTypeThresholdDoesNotOverrideDisabledGlobalDefault(t *tes } m.mu.RUnlock() } + +func TestHostDiskTempExplicitOverrideBeatsPerTypeThreshold(t *testing.T) { + m := configureDiskTempTypeHostManager(t) + + hostID := "host-temp-override-nvme" + m.mu.Lock() + m.config.Overrides[hostID] = ThresholdConfig{ + DiskTemperature: &HysteresisThreshold{Trigger: 60, Clear: 55}, + } + m.mu.Unlock() + + // 62C is below the nvme per-type trigger (70) but above the explicit + // override (60): the override must win and fire the alert. + hostAbove := hostWithSMARTDiskTemp(hostID, "nvme", 62) + m.CheckHost(hostAbove) + + trackingKey := hostDiskTempAlertID(hostAbove) + if _, exists := testLookupActiveAlert(t, m, trackingKey); !exists { + t.Fatalf("expected alert for nvme disk at 62C with explicit override trigger 60, active: %v", alertKeys(m)) + } + + m.ClearActiveAlerts() + + m.mu.Lock() + m.config.Overrides[hostID] = ThresholdConfig{ + DiskTemperature: &HysteresisThreshold{Trigger: 80, Clear: 75}, + } + m.mu.Unlock() + + // 75C is above the nvme per-type trigger (70) but below the explicit + // override (80): the override must win and stay quiet. + hostBelow := hostWithSMARTDiskTemp(hostID, "nvme", 75) + m.CheckHost(hostBelow) + + if _, exists := testLookupActiveAlert(t, m, trackingKey); exists { + t.Fatalf("expected no alert for nvme disk at 75C with explicit override trigger 80, active: %v", alertKeys(m)) + } + m.mu.RLock() + if _, pending := m.pendingAlerts[trackingKey]; pending { + m.mu.RUnlock() + t.Fatalf("expected no pending alert for nvme disk at 75C with explicit override trigger 80, but pendingAlerts has %q", trackingKey) + } + m.mu.RUnlock() +}