From 0fd7768e9d1379f8aa0126b33d4047b4c52c0d87 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:41:32 +0100 Subject: [PATCH] Preserve explicit filesystem disablement during deferred reads Separate operator policy from retained numeric evidence. Apply a filesystem disable override without evaluating its old usage or treating absent deferred inventory as authoritative removal. Change-source: pulse-maintainer --- .../v6/internal/subsystems/alerts.md | 3 ++- internal/alerts/guest.go | 26 ++++++++++++------- internal/alerts/guest_snapshot_test.go | 22 ++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 5f7c630e4f..7d9fc4f39e 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -30,7 +30,8 @@ Existing incidents retain their last trusted value, occurrence, acknowledgement, observation time and lifecycle/delivery evidence until a current observation resumes evaluation. Successful current inventory still resolves removed filesystems. Explicit disablement, suppression and stopped-guest behavior remain -effective independently of telemetry. Legacy unannotated memory and independent +effective independently of telemetry, including per-filesystem disable overrides +on retained identities. Legacy unannotated memory and independent current PVE/Pulse-agent memory/disks keep their existing policy, even while QGA is deferred; current CPU and I/O continue to evaluate. Producers own freshness; no new age threshold, QGA request, payload, alert family or UI surface is added. diff --git a/internal/alerts/guest.go b/internal/alerts/guest.go index 6bc19205af..25963829ef 100644 --- a/internal/alerts/guest.go +++ b/internal/alerts/guest.go @@ -300,11 +300,9 @@ func (m *Manager) CheckGuest(guest any, instanceName string) { // Retained/failed filesystem evidence is useful to display, but neither // its values nor an empty result can establish a breach, recovery or disk - // removal. CPU/I/O and independent current memory were evaluated above; - // explicit suppression/disablement and stopped-guest handling stay earlier. - if snapshot.DiskUnavailable { - return - } + // removal. CPU/I/O and independent current memory were evaluated above. + // Still apply explicit per-filesystem disablement to retained identities; + // this is operator policy, not recovery inferred from an old value. if len(disks) > 0 { seenDiskKeys := make(map[string]struct{}) @@ -342,6 +340,12 @@ func (m *Manager) CheckGuest(guest any, instanceName string) { effectiveDiskThreshold = ensureHysteresisThreshold(diskOverride.Disk) } } + if snapshot.DiskUnavailable { + if effectiveDiskThreshold == nil || effectiveDiskThreshold.Trigger <= 0 { + m.clearAlert(canonicalMetricStateID(perDiskResourceID, "disk")) + } + continue + } log.Debug(). Str("guest", name). @@ -387,11 +391,15 @@ func (m *Manager) CheckGuest(guest any, instanceName string) { MonitorOnly: monitorOnly, }) } - if cleared := m.cleanupGuestDiskAlerts(guestID, seenDiskResources); cleared > 0 { - m.saveActiveAlertsAsync("guest-disk-set-changed") + if !snapshot.DiskUnavailable { + if cleared := m.cleanupGuestDiskAlerts(guestID, seenDiskResources); cleared > 0 { + m.saveActiveAlertsAsync("guest-disk-set-changed") + } + } + } else if !snapshot.DiskUnavailable { + if cleared := m.cleanupGuestDiskAlerts(guestID, nil); cleared > 0 { + m.saveActiveAlertsAsync("guest-disk-alerts-cleared") } - } else if cleared := m.cleanupGuestDiskAlerts(guestID, nil); cleared > 0 { - m.saveActiveAlertsAsync("guest-disk-alerts-cleared") } } diff --git a/internal/alerts/guest_snapshot_test.go b/internal/alerts/guest_snapshot_test.go index 863dcec16a..2f89b24eda 100644 --- a/internal/alerts/guest_snapshot_test.go +++ b/internal/alerts/guest_snapshot_test.go @@ -63,6 +63,28 @@ func TestGuestSnapshotObservationAvailability(t *testing.T) { } } +func TestGuestAlertRetainedDiskStillHonoursExplicitOverride(t *testing.T) { + for _, override := range []ThresholdConfig{{Disabled: true}, {Disk: &HysteresisThreshold{Trigger: -1}}} { + m := guestObservationManager(t) + vm := guestObservationVM() + vm.Memory.Usage = 10 + m.CheckGuest(vm, "site") + if len(m.GetActiveAlerts()) != 2 { + t.Fatal("expected two current filesystem incidents") + } + _, keySource, key := guestDiskIdentity(vm.Disks[0], 0) + cfg := m.GetConfig() + cfg.Overrides[guestDiskOverrideKey(vm, vm.ID, keySource)] = override + m.UpdateConfig(cfg) + vm.DiskStatusReason = "prev-vm-locked" + m.CheckGuest(vm, "site") + active := m.GetActiveAlerts() + if len(active) != 1 || active[0].ResourceID == vm.ID+"-disk-"+key { + t.Fatalf("explicit filesystem disablement was lost or crossed disk identity: %+v", active) + } + } +} + func TestGuestSnapshotUsesCanonicalProxmoxCPUPercent(t *testing.T) { for _, tc := range []struct { name string