mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Reject shared-node PBS ties despite learned source
A globally observed second cluster can leave a PBS source apparently mapped to the first while both clusters match a shared-node snapshot. Do not promote a positive but tied guest match into the VMID-only inference path. Regression covers both VM and CT completed and in-flight snapshots with the second cluster visible on another PBS instance. Contract-Neutral: Preserve existing PBS backup attribution safety without changing API fields, persisted shapes, or source contracts. Change-source: pulse-maintainer
This commit is contained in:
@@ -98,11 +98,15 @@ func TestSyncGuestBackupTimesNewestPBSBackupPreservesCollisionGuard(t *testing.T
|
||||
got := make(map[string]time.Time)
|
||||
if backupType == "vm" {
|
||||
for _, guest := range snapshot.VMs {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
if guest.VMID == 112 {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, guest := range snapshot.Containers {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
if guest.VMID == 112 {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
if !got["cluster-a"].Equal(recentA) {
|
||||
@@ -132,11 +136,13 @@ func TestSyncGuestBackupTimesNewestPBSBackupSameNodeCollision(t *testing.T) {
|
||||
state.UpdateVMs([]VM{
|
||||
{VMID: 112, Name: "guest-a", Instance: "cluster-a", Node: "pve"},
|
||||
{VMID: 112, Name: "guest-b", Instance: "cluster-b", Node: "pve"},
|
||||
{VMID: 113, Name: "other-b", Instance: "cluster-b", Node: "pve"},
|
||||
})
|
||||
} else {
|
||||
state.UpdateContainers([]Container{
|
||||
{VMID: 112, Name: "guest-a", Instance: "cluster-a", Node: "pve"},
|
||||
{VMID: 112, Name: "guest-b", Instance: "cluster-b", Node: "pve"},
|
||||
{VMID: 113, Name: "other-b", Instance: "cluster-b", Node: "pve"},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,6 +156,10 @@ func TestSyncGuestBackupTimesNewestPBSBackupSameNodeCollision(t *testing.T) {
|
||||
// Both guests match node pve, but guest-b is the stronger match.
|
||||
{ID: "recent-b", VMID: "112", BackupType: backupType, BackupTime: recentB,
|
||||
Instance: "pbs-main", Namespace: "pve", Comment: "guest-b"},
|
||||
// B is visible on a different PBS instance. That is not proof
|
||||
// that pbs-main's tied snapshot belongs to A rather than B.
|
||||
{ID: "other-b", VMID: "113", BackupType: backupType, BackupTime: recentB,
|
||||
Instance: "pbs-other"},
|
||||
// Neither guest owns an otherwise indistinguishable newer copy.
|
||||
{ID: "shared-node", VMID: "112", BackupType: backupType, BackupTime: now.Add(-time.Hour),
|
||||
Instance: "pbs-main", Namespace: "pve"},
|
||||
@@ -167,11 +177,15 @@ func TestSyncGuestBackupTimesNewestPBSBackupSameNodeCollision(t *testing.T) {
|
||||
got := make(map[string]time.Time)
|
||||
if backupType == "vm" {
|
||||
for _, guest := range snapshot.VMs {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
if guest.VMID == 112 {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, guest := range snapshot.Containers {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
if guest.VMID == 112 {
|
||||
got[guest.Instance] = guest.LastBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
if !got["cluster-a"].Equal(recentA) {
|
||||
@@ -182,12 +196,18 @@ func TestSyncGuestBackupTimesNewestPBSBackupSameNodeCollision(t *testing.T) {
|
||||
}
|
||||
if backupType == "vm" {
|
||||
for _, guest := range snapshot.VMs {
|
||||
if guest.VMID != 112 {
|
||||
continue
|
||||
}
|
||||
if guest.BackupInProgress != (guest.Instance == "cluster-b") {
|
||||
t.Errorf("%s BackupInProgress = %v, want only cluster-b running", guest.Instance, guest.BackupInProgress)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, guest := range snapshot.Containers {
|
||||
if guest.VMID != 112 {
|
||||
continue
|
||||
}
|
||||
if guest.BackupInProgress != (guest.Instance == "cluster-b") {
|
||||
t.Errorf("%s BackupInProgress = %v, want only cluster-b running", guest.Instance, guest.BackupInProgress)
|
||||
}
|
||||
|
||||
@@ -4745,8 +4745,9 @@ func (s *State) SyncGuestBackupTimes() {
|
||||
// A namespace such as "pve" can match two clusters with the
|
||||
// same node label. A positive score is not attribution when a
|
||||
// different connection matches the same snapshot at least as
|
||||
// strongly. A stronger competitor rules this guest out; a tie
|
||||
// needs independent storage/source evidence below (#2292).
|
||||
// strongly. Do not use batch-learned source inference to break a
|
||||
// positive tie: it can see the other connection on another PBS
|
||||
// instance without proving which one wrote this snapshot (#2292).
|
||||
strongestOther := 0
|
||||
for _, other := range subjectGuests[subjectKey] {
|
||||
if other.instance == instance {
|
||||
@@ -4760,12 +4761,9 @@ func (s *State) SyncGuestBackupTimes() {
|
||||
strongestOther = otherScore
|
||||
}
|
||||
}
|
||||
if strongestOther > score {
|
||||
if strongestOther >= score {
|
||||
continue
|
||||
}
|
||||
if strongestOther == score {
|
||||
score = 0
|
||||
}
|
||||
}
|
||||
if score == 0 {
|
||||
if !subjectIsAmbiguous[subjectKey] {
|
||||
|
||||
Reference in New Issue
Block a user