mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Fix backup type-aware orphan detection
This commit is contained in:
@@ -5507,6 +5507,43 @@ func BuildGuestKey(instance, node string, vmid int) string {
|
|||||||
return fmt.Sprintf("%s:%s:%d", instance, node, vmid)
|
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 {
|
func isGuestMetricResourceType(resourceType string) bool {
|
||||||
switch strings.TrimSpace(resourceType) {
|
switch strings.TrimSpace(resourceType) {
|
||||||
case "VM", "Container":
|
case "VM", "Container":
|
||||||
@@ -5961,6 +5998,9 @@ func (m *Manager) CheckBackups(
|
|||||||
vmid = strconv.Itoa(backup.VMID)
|
vmid = strconv.Itoa(backup.VMID)
|
||||||
}
|
}
|
||||||
info := guestsByKey[key]
|
info := guestsByKey[key]
|
||||||
|
if !guestMatchesBackupType(info, backup.Type) {
|
||||||
|
info = GuestLookup{}
|
||||||
|
}
|
||||||
displayName := info.Name
|
displayName := info.Name
|
||||||
if displayName == "" {
|
if displayName == "" {
|
||||||
displayName = fmt.Sprintf("%s-%d", sanitizeAlertKey(backup.Node), backup.VMID)
|
displayName = fmt.Sprintf("%s-%d", sanitizeAlertKey(backup.Node), backup.VMID)
|
||||||
@@ -5998,13 +6038,14 @@ func (m *Manager) CheckBackups(
|
|||||||
var node string
|
var node string
|
||||||
|
|
||||||
if exists && len(guests) > 0 {
|
if exists && len(guests) > 0 {
|
||||||
|
typedGuests := filterGuestsByBackupType(guests, backup.BackupType)
|
||||||
// If we have exactly one match, use it directly
|
// If we have exactly one match, use it directly
|
||||||
// If we have multiple matches, try to disambiguate using the PBS namespace
|
// If we have multiple matches, try to disambiguate using the PBS namespace
|
||||||
if len(guests) == 1 {
|
if len(typedGuests) == 1 {
|
||||||
info = guests[0]
|
info = typedGuests[0]
|
||||||
} else if backup.Namespace != "" {
|
} else if backup.Namespace != "" {
|
||||||
// Try to match namespace to instance name
|
// Try to match namespace to instance name
|
||||||
for _, g := range guests {
|
for _, g := range typedGuests {
|
||||||
if namespaceMatchesInstance(backup.Namespace, g.Instance) {
|
if namespaceMatchesInstance(backup.Namespace, g.Instance) {
|
||||||
info = g
|
info = g
|
||||||
break
|
break
|
||||||
@@ -6177,6 +6218,9 @@ func (m *Manager) CheckBackups(
|
|||||||
if g.ResourceID == "" {
|
if g.ResourceID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !guestMatchesBackupType(g, record.backupType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if record.source == "PVE storage" && g.Instance != record.instance {
|
if record.source == "PVE storage" && g.Instance != record.instance {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -6185,7 +6229,7 @@ func (m *Manager) CheckBackups(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !existsInInventory {
|
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
|
existsInInventory = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestCheckBackupsHandlesPmgBackups(t *testing.T) {
|
||||||
m := newTestManager(t)
|
m := newTestManager(t)
|
||||||
m.ClearActiveAlerts()
|
m.ClearActiveAlerts()
|
||||||
|
|||||||
Reference in New Issue
Block a user