Fix backup type-aware orphan detection

This commit is contained in:
rcourtman 2026-04-13 11:54:46 +01:00
parent 3981df57a2
commit 9fb76579cc
2 changed files with 177 additions and 4 deletions

View file

@ -5507,6 +5507,43 @@ func BuildGuestKey(instance, node string, vmid int) string {
return fmt.Sprintf("%s:%s:%d", instance, node, vmid)
}
func canonicalBackupGuestType(kind string) string {
switch strings.ToLower(strings.TrimSpace(kind)) {
case "vm", "qemu":
return "vm"
case "ct", "lxc", "container":
return "ct"
default:
return ""
}
}
func guestMatchesBackupType(guest GuestLookup, backupType string) bool {
backupKind := canonicalBackupGuestType(backupType)
if backupKind == "" {
return true
}
guestKind := canonicalBackupGuestType(guest.Type)
if guestKind == "" {
return false
}
return guestKind == backupKind
}
func filterGuestsByBackupType(guests []GuestLookup, backupType string) []GuestLookup {
if canonicalBackupGuestType(backupType) == "" {
return guests
}
filtered := make([]GuestLookup, 0, len(guests))
for _, guest := range guests {
if guestMatchesBackupType(guest, backupType) {
filtered = append(filtered, guest)
}
}
return filtered
}
func isGuestMetricResourceType(resourceType string) bool {
switch strings.TrimSpace(resourceType) {
case "VM", "Container":
@ -5961,6 +5998,9 @@ func (m *Manager) CheckBackups(
vmid = strconv.Itoa(backup.VMID)
}
info := guestsByKey[key]
if !guestMatchesBackupType(info, backup.Type) {
info = GuestLookup{}
}
displayName := info.Name
if displayName == "" {
displayName = fmt.Sprintf("%s-%d", sanitizeAlertKey(backup.Node), backup.VMID)
@ -5998,13 +6038,14 @@ func (m *Manager) CheckBackups(
var node string
if exists && len(guests) > 0 {
typedGuests := filterGuestsByBackupType(guests, backup.BackupType)
// If we have exactly one match, use it directly
// If we have multiple matches, try to disambiguate using the PBS namespace
if len(guests) == 1 {
info = guests[0]
if len(typedGuests) == 1 {
info = typedGuests[0]
} else if backup.Namespace != "" {
// Try to match namespace to instance name
for _, g := range guests {
for _, g := range typedGuests {
if namespaceMatchesInstance(backup.Namespace, g.Instance) {
info = g
break
@ -6177,6 +6218,9 @@ func (m *Manager) CheckBackups(
if g.ResourceID == "" {
continue
}
if !guestMatchesBackupType(g, record.backupType) {
continue
}
if record.source == "PVE storage" && g.Instance != record.instance {
continue
}
@ -6185,7 +6229,7 @@ func (m *Manager) CheckBackups(
}
}
if !existsInInventory {
if g, ok := guestsByKey[record.key]; ok && g.ResourceID != "" {
if g, ok := guestsByKey[record.key]; ok && g.ResourceID != "" && guestMatchesBackupType(g, record.backupType) {
existsInInventory = true
}
}

View file

@ -1436,6 +1436,135 @@ func TestCheckBackupsVMIDCollisionNoNamespace(t *testing.T) {
}
}
func TestCheckBackupsPbsTypeMismatchCreatesOrphanedAlert(t *testing.T) {
m := newTestManager(t)
m.ClearActiveAlerts()
m.mu.Lock()
m.config.Enabled = true
m.config.BackupDefaults = BackupAlertConfig{
Enabled: true,
WarningDays: 3,
CriticalDays: 5,
}
m.mu.Unlock()
now := time.Now()
pbsBackups := []models.PBSBackup{
{
ID: "pbs-vm-101",
Instance: "pbs-main",
Datastore: "backup-store",
BackupType: "vm",
VMID: "101",
BackupTime: now.Add(-30 * 24 * time.Hour),
},
}
guestKey := BuildGuestKey("pve1", "node1", 101)
guestsByKey := map[string]GuestLookup{
guestKey: {
ResourceID: "lxc/101",
Name: "ct-101",
Instance: "pve1",
Node: "node1",
Type: "lxc",
VMID: 101,
},
}
guestsByVMID := map[string][]GuestLookup{
"101": {guestsByKey[guestKey]},
}
m.CheckBackups(nil, pbsBackups, nil, guestsByKey, guestsByVMID, nil)
orphanedID := "backup-orphaned-" + sanitizeAlertKey("pbs:pbs-main:vm:101")
ageID := "backup-age-" + sanitizeAlertKey("pbs:pbs-main:vm:101")
m.mu.RLock()
defer m.mu.RUnlock()
alert, exists := m.activeAlerts[orphanedID]
if !exists {
var keys []string
for k := range m.activeAlerts {
keys = append(keys, k)
}
t.Fatalf("expected orphaned alert %q, found keys: %v", orphanedID, keys)
}
if alert.Type != "backup-orphaned" {
t.Fatalf("expected backup-orphaned alert, got %s", alert.Type)
}
if _, exists := m.activeAlerts[ageID]; exists {
t.Fatalf("expected no backup-age alert for mismatched live guest type")
}
}
func TestCheckBackupsStorageTypeMismatchCreatesOrphanedAlert(t *testing.T) {
m := newTestManager(t)
m.ClearActiveAlerts()
m.mu.Lock()
m.config.Enabled = true
m.config.BackupDefaults = BackupAlertConfig{
Enabled: true,
WarningDays: 3,
CriticalDays: 5,
}
m.mu.Unlock()
now := time.Now()
storageBackups := []models.StorageBackup{
{
ID: "pve1-node1-101-backup",
Storage: "local",
Node: "node1",
Instance: "pve1",
Type: "qemu",
VMID: 101,
Time: now.Add(-30 * 24 * time.Hour),
},
}
guestKey := BuildGuestKey("pve1", "node1", 101)
guestsByKey := map[string]GuestLookup{
guestKey: {
ResourceID: "lxc/101",
Name: "ct-101",
Instance: "pve1",
Node: "node1",
Type: "lxc",
VMID: 101,
},
}
guestsByVMID := map[string][]GuestLookup{
"101": {guestsByKey[guestKey]},
}
m.CheckBackups(storageBackups, nil, nil, guestsByKey, guestsByVMID, nil)
orphanedID := "backup-orphaned-" + sanitizeAlertKey(guestKey)
ageID := "backup-age-" + sanitizeAlertKey(guestKey)
m.mu.RLock()
defer m.mu.RUnlock()
alert, exists := m.activeAlerts[orphanedID]
if !exists {
var keys []string
for k := range m.activeAlerts {
keys = append(keys, k)
}
t.Fatalf("expected orphaned alert %q, found keys: %v", orphanedID, keys)
}
if alert.Type != "backup-orphaned" {
t.Fatalf("expected backup-orphaned alert, got %s", alert.Type)
}
if _, exists := m.activeAlerts[ageID]; exists {
t.Fatalf("expected no backup-age alert for mismatched live guest type")
}
}
func TestCheckBackupsHandlesPmgBackups(t *testing.T) {
m := newTestManager(t)
m.ClearActiveAlerts()