mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 12:47:49 +00:00
Keep Agent-only SMART disks out of PVE poll readback
Linked Agent SMART disks inherit a PVE instance for presentation. Restrict skipped-poll readback to actual PVE observations so an empty disks/list cannot alternate fabricated inventory, tags and history rows. Pin repeated cycles and the source boundary in the monitoring contracts. Change-source: pulse-maintainer
This commit is contained in:
parent
41bee3d475
commit
0204fe000f
4 changed files with 80 additions and 1 deletions
|
|
@ -136,6 +136,13 @@ monitoring metadata and does not change Agent registration, execution authority,
|
|||
or the Agent report wire contract. `internal/models/metrics_types_test.go` covers
|
||||
snapshot retention and isolation, and the monitoring collection-trust roundtrip
|
||||
covers merged Agent/Proxmox disks without losing the Proxmox source schedule.
|
||||
An Agent-only SMART disk linked to a PVE node may carry that node's instance
|
||||
for presentation, but the instance is not an Agent report of PVE disk
|
||||
inventory. Skipped PVE polls must not round-trip that disk into PVE-owned
|
||||
state; the monitoring readback requires an actual Proxmox source observation.
|
||||
This leaves Agent source identity, report admission and command authority
|
||||
unchanged. The repeated-cycle regression is
|
||||
`TestPhysicalDiskSkippedPollDoesNotPromoteAgentOnlySMARTToPVEInventory`.
|
||||
|
||||
Assistant historical metric wiring uses the current monitor's retained store
|
||||
and registry metrics coordinates. Historical reads do not alter enrollment,
|
||||
|
|
|
|||
|
|
@ -809,7 +809,17 @@ Proxmox physical-disk polling is also a continuity boundary. A failed or
|
|||
permission-denied `disks/list` call must remain an error so the monitor can use
|
||||
linked host-agent inventory or retain same-instance, same-node prior evidence;
|
||||
it must never become a successful empty inventory that removes valid boot or
|
||||
data disks. SMART enrichment matches serial, WWN, device path, and controller
|
||||
data disks. Between PVE disk polls, readback into `State.PhysicalDisks` accepts
|
||||
only a disk with an actual Proxmox source observation. A linked Agent-only
|
||||
SMART disk may inherit the PVE instance for presentation, but must not be
|
||||
written back as PVE inventory; doing so adds and removes a false source and
|
||||
change-journal rows when the PVE inventory is empty. Agent-only disks remain
|
||||
visible through their Agent source, and an explicit failed-query Agent fallback
|
||||
still supplies PVE inventory. `TestPhysicalDiskSkippedPollDoesNotPromoteAgentOnlySMARTToPVEInventory`
|
||||
checks repeated empty-inventory/skipped-poll cycles and journal row counts;
|
||||
`TestPhysicalDiskSkippedPollPreservesSourceIdentity` checks genuine PVE disk
|
||||
continuity. These fixtures do not establish the reporter's installed cause.
|
||||
SMART enrichment matches serial, WWN, device path, and controller
|
||||
member topology uniquely and fail-closed. Serial and WWN are interchangeable
|
||||
hardware-identity carriers across reporters: comparison may case-fold and
|
||||
remove only `naa.`, `eui.`, `wwn-`, and `0x` framing, but must reject
|
||||
|
|
|
|||
|
|
@ -854,6 +854,13 @@ func physicalDisksForInstanceFromReadState(readState unifiedresources.ReadState,
|
|||
if disk == nil || disk.Instance() != instance {
|
||||
continue
|
||||
}
|
||||
// A linked host Agent's SMART disk inherits the PVE instance for
|
||||
// presentation, but that does not make it a PVE inventory record.
|
||||
// Feeding it back into State.PhysicalDisks invents a PVE source and
|
||||
// alternates tags/history whenever the real disks/list is empty.
|
||||
if _, observedByPVE := disk.SourceStatus(unifiedresources.SourceProxmox); !observedByPVE {
|
||||
continue
|
||||
}
|
||||
out = append(out, physicalDiskFromReadStateView(disk))
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -179,6 +179,61 @@ func TestPhysicalDiskSkippedPollPreservesSourceIdentity(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// An agent disk may inherit its linked PVE node's instance for presentation,
|
||||
// even when the PVE disks/list endpoint did not report that disk. The skipped
|
||||
// poll must not turn that presentation scope into a PVE inventory observation:
|
||||
// an empty PVE inventory on the next full poll would then remove the invented
|
||||
// observation and record spurious configuration changes on every cycle (#2319).
|
||||
func TestPhysicalDiskSkippedPollDoesNotPromoteAgentOnlySMARTToPVEInventory(t *testing.T) {
|
||||
state := models.NewState()
|
||||
now := time.Now().UTC()
|
||||
state.UpdateNodesForInstance("pve", []models.Node{{
|
||||
ID: "pve-node", Name: "node", Instance: "pve", LinkedAgentID: "agent",
|
||||
Status: "online", LastSeen: now,
|
||||
}})
|
||||
state.UpsertHost(models.Host{
|
||||
ID: "agent", Hostname: "node", LinkedNodeID: "pve-node",
|
||||
Status: "online", LastSeen: now,
|
||||
Sensors: models.HostSensorSummary{SMART: []models.HostDiskSMART{{
|
||||
Device: "sda", Type: "sata", Health: "PASSED",
|
||||
}}},
|
||||
})
|
||||
store := unifiedresources.NewMemoryStore()
|
||||
adapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(store))
|
||||
monitor := &Monitor{
|
||||
state: state, resourceStore: adapter,
|
||||
lastPhysicalDiskPoll: map[string]time.Time{"pve": now},
|
||||
}
|
||||
for cycle := 0; cycle < 3; cycle++ {
|
||||
// A successful full PVE inventory read found no disks on this node.
|
||||
state.UpdatePhysicalDisks("pve", nil)
|
||||
adapter.PopulateFromSnapshot(state.GetSnapshot())
|
||||
disks := adapter.PhysicalDisks()
|
||||
if len(disks) != 1 || disks[0].Instance() != "pve" {
|
||||
t.Fatalf("cycle %d: linked Agent SMART disk missing from presentation: %+v", cycle, disks)
|
||||
}
|
||||
if _, hasPVE := disks[0].SourceStatus(unifiedresources.SourceProxmox); hasPVE {
|
||||
t.Fatalf("cycle %d: Agent-only disk unexpectedly has PVE source", cycle)
|
||||
}
|
||||
before, err := store.GetRecentChanges(disks[0].ID(), time.Time{}, 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
monitor.maybePollPhysicalDisksAsync(context.Background(), "pve", &config.PVEInstance{}, nil, nil, nil, nil)
|
||||
adapter.PopulateFromSnapshot(state.GetSnapshot())
|
||||
after, err := store.GetRecentChanges(disks[0].ID(), time.Time{}, 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(after) != len(before) {
|
||||
t.Fatalf("cycle %d: unchanged SMART disk emitted %d new history rows: %+v", cycle, len(after)-len(before), after)
|
||||
}
|
||||
if got := state.GetSnapshot().PhysicalDisks; len(got) != 0 {
|
||||
t.Fatalf("cycle %d: Agent-only disk was written into PVE inventory: %+v", cycle, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhysicalDiskReadbackSourceIDFallback(t *testing.T) {
|
||||
for _, resource := range []unifiedresources.Resource{
|
||||
{ID: "canonical"},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue