Let a per-resource severity override re-enable disabled offline alerts

applyThresholdOverride only ever copied DisableConnectivity=true from an
override, so once the global default disabled connectivity alerts no
override could turn them back on. A guest row set to Warning or Critical
saved its severity, showed as Custom, and never fired, while the
frontend already encodes re-enablement by pairing an explicit severity
with the row offline control (off stores DisableConnectivity=true).
Treat an override that carries an explicit powered-off severity as that
row offline control set to warning or critical and clear the inherited
disable, matching the Docker container state path which already works
this way.

Refs #1738

Contract-Neutral: Refs #1738: behavioral bugfix in alerts override merge, no API or contract shape change
This commit is contained in:
rcourtman
2026-08-22 06:24:04 +01:00
parent 9aa095a3b5
commit 022db18ef7
2 changed files with 65 additions and 0 deletions
+6
View File
@@ -676,6 +676,12 @@ func (m *Manager) applyThresholdOverride(base ThresholdConfig, override Threshol
}
if override.DisableConnectivity {
result.DisableConnectivity = true
} else if override.PoweredOffSeverity != "" {
// An override carrying an explicit powered-off severity is that row's
// offline control set to warning/critical, so it re-enables
// connectivity alerts over a disabled default. A per-row "off" is
// stored as DisableConnectivity=true and takes the branch above.
result.DisableConnectivity = false
}
if override.PoweredOffSeverity != "" {
result.PoweredOffSeverity = NormalizePoweredOffSeverity(override.PoweredOffSeverity)
@@ -42,3 +42,62 @@ func TestPoweredOffSeverityOverrideResolution(t *testing.T) {
run(t, AlertLevelCritical, "", AlertLevelCritical)
})
}
// A per-guest severity override must re-enable powered-off alerts when the
// global default has connectivity alerts disabled, while a per-guest "off"
// (DisableConnectivity=true) must stay off regardless of any severity the
// override also carries. Refs #1738.
func TestPoweredOffOverrideReenablesDisabledDefault(t *testing.T) {
setup := func(t *testing.T, override ThresholdConfig) *Manager {
m := newTestManager(t)
cfg := m.GetConfig()
cfg.GuestDefaults.DisableConnectivity = true
cfg.GuestDefaults.PoweredOffSeverity = AlertLevelCritical
if cfg.Overrides == nil {
cfg.Overrides = map[string]ThresholdConfig{}
}
cfg.Overrides["vm100"] = override
m.UpdateConfig(cfg)
m.checkGuestPoweredOff("vm100", "TestVM", "pve-node1", "pve-instance", "VM", false)
m.checkGuestPoweredOff("vm100", "TestVM", "pve-node1", "pve-instance", "VM", false)
return m
}
t.Run("severity override re-enables offline alerts", func(t *testing.T) {
m := setup(t, ThresholdConfig{PoweredOffSeverity: AlertLevelWarning})
m.mu.RLock()
alert := testRequireActiveAlert(t, m, "guest-powered-off-vm100")
m.mu.RUnlock()
if alert.Level != AlertLevelWarning {
t.Fatalf("expected %s, got %s", AlertLevelWarning, alert.Level)
}
})
t.Run("per-guest off wins over its own stored severity", func(t *testing.T) {
m := setup(t, ThresholdConfig{DisableConnectivity: true, PoweredOffSeverity: AlertLevelWarning})
m.mu.RLock()
_, exists := m.activeAlerts["guest-powered-off-vm100"]
m.mu.RUnlock()
if exists {
t.Fatal("expected no powered-off alert for per-guest disabled connectivity")
}
})
t.Run("override without severity keeps disabled default", func(t *testing.T) {
m := setup(t, ThresholdConfig{CPU: &HysteresisThreshold{Trigger: 90, Clear: 85}})
m.mu.RLock()
_, exists := m.activeAlerts["guest-powered-off-vm100"]
m.mu.RUnlock()
if exists {
t.Fatal("expected no powered-off alert when override never touched the offline control")
}
})
}