Refuse a seeded disk slot mapping when the disk was replaced (#2667)

`ingestRecord` reuses an existing source-key mapping before any identity
matching. For physical disks it checked only the type and parent scope.
Proxmox keys a disk by its slot (`ProxmoxPhysicalDiskSourceID`, e.g.
`pve1-node1--dev-sdb`), so in a registry seeded from persisted unified
resources, a disk swapped into the slot mapped straight back to the
previous disk's canonical resource. Where an agent report had been
merged in, `mergeInto` also kept the old disk's serial, WWN, temperature
and FAILED health. This was found while reviewing #2658, and its
contract note said the seeded mapping was not covered.

**Change.** A Proxmox disk observation now refuses the mapping when the
two disks carry hardware identities naming different disks
(`diskinventory.HardwareIdentityConflict` from #2658):
- Only fields reported on both sides count. A missing serial or WWN,
including Proxmox's literal `unknown`, keeps the mapping.
- Differing serials do not count for SAS, because Proxmox may report the
SAS address there (#1595).
- They also do not count when the mapped disk was merged with an agent
report and Proxmox did not read the drive's own serial. This is the same
rule monitoring's `hostAgentSMARTSerialComparable` applies: NVMe on both
sides, or agent SATA under Proxmox vendor `ATA`, and never a Proxmox
`usb` disk.
- The refused key falls through to the linked-disk join, `findMatch` and
`chooseNewID`, but none of them may merge it back into the refused disk.
An ID landing on it is replaced with the key's source-specific ID.

**Reachability.** No current caller replays Proxmox disk records over a
seeded mapping. The monitor rebuilds a fresh registry each poll. The
resource API, host-continuity and Patrol registries seeded from unified
resources do not replay Proxmox disk records. This closes the registry
contract rather than a reported symptom.

**Proof.** `TestRegistrySeededSlotMappingRefusesReplacedDisk` (in
`internal/monitoring/issue1595_collection_trust_test.go`) builds the
previous disk through `IngestSnapshot` with fixtures shaped like
`monitor_pve`'s output, rehydrates with `IngestResources(List())`, polls
Proxmox, then rehydrates a second time.
- 5 replacement cases: SATA with and without a new WWN, SAS, a
USB-enclosure swap ingested Proxmox-first, and a disk Proxmox alone
reports.
- 8 same-disk controls: re-ingested, no serial, a SAS address serial,
SAS without a WWN, a USB bridge serial, a bridge serial under vendor
`ATA`, a SCSI designator, and a SCSI designator from an agent without
collection status.

Negative controls via `go test -overlay` each fail only their intended
cases: the whole fix removed, the refused-disk exclusion removed, serial
comparability always true or always false, the Proxmox-only branch
removed, a collection-status provenance rule instead, and the USB veto
removed. The combined tree of main, #2660 and this change also passes
`internal/unifiedresources` and the monitoring disk tests.

**Residuals (in the unified-resources contract).**
- Drives that share a serial they do not own, such as a USB bridge's,
still read as one disk, as they do in identity matching.
- The refused disk keeps its Proxmox facet until a rebuild, so the
slot's PVE alert reference resolves as ambiguous rather than to either
disk.
This commit is contained in:
rcourtman
2026-10-07 15:38:50 +01:00
committed by GitHub
3 changed files with 275 additions and 10 deletions
@@ -6517,9 +6517,52 @@ conflict): the agent's row may be the slot's previous occupant, retained by a
silent agent, and the join would hand the replacement that disk's serial,
readings and canonical resource. A hardware identity match still joins on its
own, and without a WWN on either side the path fallback stands. The seeded
source mapping reused after a restart is a separate path this does not cover.
`TestRegistrySASPathJoinRefusesContradictingWWN` covers the stale row and both
same-disk shapes; monitoring's SMART merge applies the matching guard first.
source mapping a rehydrated registry reuses applies the same refusal (next
section). `TestRegistrySASPathJoinRefusesContradictingWWN` covers the stale
row and both same-disk shapes; monitoring's SMART merge applies the matching
guard first.
### Seeded slot mapping refuses a replaced disk
`ingestRecord` honours a source key's existing mapping, such as one
`IngestResources` seeds from persisted unified resources, before any
identity matching. Proxmox keys a disk by its slot
(`ProxmoxPhysicalDiskSourceID`), so a disk swapped into the slot arrived
under the previous disk's key, and `mergeInto` kept that disk's canonical ID
and, where an agent report had been merged in, its serial, WWN, temperature
and failed health. A Proxmox disk observation now refuses the mapping when
the two carry hardware identities naming different disks
(`proxmoxDiskSlotHoldsAnotherDisk`: `HardwareIdentityConflict` after
`HardwareIdentityMatch`). Only fields reported on both sides count, so a
missing serial or WWN, including Proxmox's literal `unknown`, is no evidence.
Differing serials are no evidence when either side is SAS, because Proxmox
may report the SAS address there (#1595), or when the mapped disk was merged
with an agent report, whose serial `mergeInto` may have kept (collection
status does not reliably say which serial it holds), unless Proxmox read the
drive's own serial too, by the rule monitoring's
`hostAgentSMARTSerialComparable` applies: NVMe on both sides, or an agent
SATA disk under Proxmox vendor `ATA` that Proxmox does not type `usb`, not a
SCSI designator or a USB bridge's serial. A refused key falls through to the linked-disk join,
`findMatch` and `chooseNewID` like an unmapped one, except that none of them
may merge it into the refused disk: an ID that lands on it is replaced with
the key's source-specific ID. Agreement still wins, as it does in identity
matching, so drives that share a serial they do not own, such as a USB
bridge's, read as one disk: a second swap in the same enclosure joins the
first replacement, which reported the same bridge serial. That belongs to
identity matching, not this guard. The refused disk keeps its Proxmox
facet and is not updated, so until the registry is rebuilt both disks answer
to the slot's PVE alert reference, and canonical reference resolution treats
that reference as ambiguous rather than handing it to either. A later
rehydration may seed the slot key to the old disk again, and the refusal
repeats. No current caller ingests Proxmox disk records over a seeded
mapping: the monitor rebuilds a fresh registry each poll, and the resource
API, host-continuity and Patrol registries seeded from unified resources do
not replay Proxmox disk records. This closes the registry contract, not a
reported symptom. `TestRegistrySeededSlotMappingRefusesReplacedDisk` covers
SATA (with and without a new WWN), SAS, USB-enclosure and Proxmox-only
replacements, plus eight same-disk controls (including a SCSI designator
from an agent without collection status and a USB bridge serial under
Proxmox vendor `ATA`), each through two rehydrations.
### Drawer tab selection survives a transient snapshot change (#1723)