Previously, SyncGuestBackupTimes matched backups to guests using only VMID. This caused newly created containers to incorrectly show old backup times from different containers on other Proxmox instances that happened to have the same VMID. Now uses composite key (instance+VMID) for PVE storage backups to ensure proper isolation. PBS backups still use VMID matching (since they aggregate from multiple sources) but only as a fallback. Fixes issue where ollama LXC showed 'last backup 3 months ago' despite being created yesterday.
924 lines
26 KiB
Go
924 lines
26 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestUpsertHost(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Test insert new host
|
|
host1 := Host{
|
|
ID: "host-1",
|
|
Hostname: "server-1",
|
|
Status: "online",
|
|
}
|
|
state.UpsertHost(host1)
|
|
|
|
hosts := state.GetHosts()
|
|
if len(hosts) != 1 {
|
|
t.Fatalf("Expected 1 host, got %d", len(hosts))
|
|
}
|
|
if hosts[0].ID != "host-1" {
|
|
t.Errorf("Expected host ID 'host-1', got %q", hosts[0].ID)
|
|
}
|
|
|
|
// Test update existing host
|
|
host1Updated := Host{
|
|
ID: "host-1",
|
|
Hostname: "server-1",
|
|
Status: "offline",
|
|
}
|
|
state.UpsertHost(host1Updated)
|
|
|
|
hosts = state.GetHosts()
|
|
if len(hosts) != 1 {
|
|
t.Fatalf("Expected 1 host after update, got %d", len(hosts))
|
|
}
|
|
if hosts[0].Status != "offline" {
|
|
t.Errorf("Expected status 'offline', got %q", hosts[0].Status)
|
|
}
|
|
|
|
// Test multiple hosts and sorting
|
|
state.UpsertHost(Host{ID: "host-2", Hostname: "alpha-server"})
|
|
state.UpsertHost(Host{ID: "host-3", Hostname: "zulu-server"})
|
|
|
|
hosts = state.GetHosts()
|
|
if len(hosts) != 3 {
|
|
t.Fatalf("Expected 3 hosts, got %d", len(hosts))
|
|
}
|
|
|
|
// Hosts should be sorted by hostname
|
|
if hosts[0].Hostname != "alpha-server" {
|
|
t.Errorf("First host should be 'alpha-server', got %q", hosts[0].Hostname)
|
|
}
|
|
}
|
|
|
|
func TestGetHosts_Copy(t *testing.T) {
|
|
state := NewState()
|
|
|
|
state.UpsertHost(Host{ID: "host-1", Hostname: "server-1"})
|
|
|
|
hosts1 := state.GetHosts()
|
|
hosts2 := state.GetHosts()
|
|
|
|
// Modify one slice
|
|
if len(hosts1) > 0 {
|
|
hosts1[0].Hostname = "modified"
|
|
}
|
|
|
|
// Other slice should be unchanged
|
|
if len(hosts2) > 0 && hosts2[0].Hostname == "modified" {
|
|
t.Error("GetHosts should return a copy, not the same slice")
|
|
}
|
|
|
|
// Original state should be unchanged
|
|
hosts3 := state.GetHosts()
|
|
if hosts3[0].Hostname == "modified" {
|
|
t.Error("State should be unchanged by modifications to returned slice")
|
|
}
|
|
}
|
|
|
|
func TestRemoveHost(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Insert hosts
|
|
state.UpsertHost(Host{ID: "host-1", Hostname: "server1"})
|
|
state.UpsertHost(Host{ID: "host-2", Hostname: "server2"})
|
|
|
|
// Remove existing host
|
|
removed, ok := state.RemoveHost("host-1")
|
|
if !ok {
|
|
t.Error("Expected RemoveHost to return true for existing host")
|
|
}
|
|
if removed.ID != "host-1" {
|
|
t.Errorf("Expected removed host ID 'host-1', got %q", removed.ID)
|
|
}
|
|
|
|
hosts := state.GetHosts()
|
|
if len(hosts) != 1 {
|
|
t.Fatalf("Expected 1 host after removal, got %d", len(hosts))
|
|
}
|
|
if hosts[0].ID != "host-2" {
|
|
t.Errorf("Remaining host should be 'host-2', got %q", hosts[0].ID)
|
|
}
|
|
|
|
// Remove non-existing host
|
|
removed, ok = state.RemoveHost("non-existent")
|
|
if ok {
|
|
t.Error("Expected RemoveHost to return false for non-existent host")
|
|
}
|
|
if removed.ID != "" {
|
|
t.Errorf("Expected empty Host for non-existent removal, got ID %q", removed.ID)
|
|
}
|
|
}
|
|
|
|
func TestSetHostStatus(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Test with no hosts
|
|
changed := state.SetHostStatus("host-1", "online")
|
|
if changed {
|
|
t.Error("SetHostStatus should return false when host doesn't exist")
|
|
}
|
|
|
|
// Add host and set status
|
|
state.UpsertHost(Host{ID: "host-1", Hostname: "server1", Status: "offline"})
|
|
|
|
changed = state.SetHostStatus("host-1", "online")
|
|
if !changed {
|
|
t.Error("SetHostStatus should return true when host exists")
|
|
}
|
|
|
|
hosts := state.GetHosts()
|
|
if hosts[0].Status != "online" {
|
|
t.Errorf("Expected status 'online', got %q", hosts[0].Status)
|
|
}
|
|
|
|
// Set same status (no change)
|
|
changed = state.SetHostStatus("host-1", "online")
|
|
if !changed {
|
|
t.Error("SetHostStatus should return true even when status unchanged")
|
|
}
|
|
}
|
|
|
|
func TestTouchHost(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Test with non-existent host
|
|
ok := state.TouchHost("host-1", now)
|
|
if ok {
|
|
t.Error("TouchHost should return false for non-existent host")
|
|
}
|
|
|
|
// Add host and touch it
|
|
state.UpsertHost(Host{ID: "host-1", Hostname: "server1"})
|
|
|
|
later := now.Add(time.Hour)
|
|
ok = state.TouchHost("host-1", later)
|
|
if !ok {
|
|
t.Error("TouchHost should return true for existing host")
|
|
}
|
|
|
|
hosts := state.GetHosts()
|
|
if !hosts[0].LastSeen.Equal(later) {
|
|
t.Errorf("LastSeen should be updated to %v, got %v", later, hosts[0].LastSeen)
|
|
}
|
|
}
|
|
|
|
func TestUpdateRecentlyResolved(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
alerts := []ResolvedAlert{
|
|
{
|
|
Alert: Alert{
|
|
ID: "alert-1",
|
|
Type: "cpu_high",
|
|
Level: "warning",
|
|
ResourceName: "server-1",
|
|
Message: "High CPU usage",
|
|
},
|
|
ResolvedTime: now,
|
|
},
|
|
{
|
|
Alert: Alert{
|
|
ID: "alert-2",
|
|
Type: "disk_low",
|
|
Level: "critical",
|
|
ResourceName: "server-2",
|
|
Message: "Low disk space",
|
|
},
|
|
ResolvedTime: now.Add(-time.Hour),
|
|
},
|
|
}
|
|
|
|
state.UpdateRecentlyResolved(alerts)
|
|
|
|
// The method updates internal state - verify via snapshot or other means
|
|
// Since RecentlyResolvedAlerts might be accessed differently, just verify no panic
|
|
// and method executes correctly
|
|
}
|
|
|
|
func TestSetConnectionHealth(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Set connection healthy
|
|
state.SetConnectionHealth("pve-cluster-1", true)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if healthy, ok := snapshot.ConnectionHealth["pve-cluster-1"]; !ok || !healthy {
|
|
t.Error("Expected connection to be healthy")
|
|
}
|
|
|
|
// Set connection unhealthy
|
|
state.SetConnectionHealth("pve-cluster-1", false)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if healthy, ok := snapshot.ConnectionHealth["pve-cluster-1"]; !ok || healthy {
|
|
t.Error("Expected connection to be unhealthy")
|
|
}
|
|
|
|
// Multiple connections
|
|
state.SetConnectionHealth("pve-cluster-2", true)
|
|
state.SetConnectionHealth("pbs-instance-1", true)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.ConnectionHealth) != 3 {
|
|
t.Errorf("Expected 3 connection health entries, got %d", len(snapshot.ConnectionHealth))
|
|
}
|
|
}
|
|
|
|
func TestRemoveConnectionHealth(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Set up connection health entries
|
|
state.SetConnectionHealth("pve-cluster-1", true)
|
|
state.SetConnectionHealth("pve-cluster-2", false)
|
|
|
|
// Remove one
|
|
state.RemoveConnectionHealth("pve-cluster-1")
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if _, ok := snapshot.ConnectionHealth["pve-cluster-1"]; ok {
|
|
t.Error("Expected pve-cluster-1 to be removed")
|
|
}
|
|
if _, ok := snapshot.ConnectionHealth["pve-cluster-2"]; !ok {
|
|
t.Error("Expected pve-cluster-2 to still exist")
|
|
}
|
|
|
|
// Remove non-existent (should not panic)
|
|
state.RemoveConnectionHealth("non-existent")
|
|
|
|
// Remove remaining
|
|
state.RemoveConnectionHealth("pve-cluster-2")
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.ConnectionHealth) != 0 {
|
|
t.Errorf("Expected empty connection health map, got %d entries", len(snapshot.ConnectionHealth))
|
|
}
|
|
}
|
|
|
|
func TestUpdatePBSBackups(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Add backups from first instance
|
|
backups1 := []PBSBackup{
|
|
{ID: "backup-1", Instance: "pbs-1", BackupTime: now},
|
|
{ID: "backup-2", Instance: "pbs-1", BackupTime: now.Add(-time.Hour)},
|
|
}
|
|
state.UpdatePBSBackups("pbs-1", backups1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PBSBackups) != 2 {
|
|
t.Fatalf("Expected 2 backups, got %d", len(snapshot.PBSBackups))
|
|
}
|
|
|
|
// Add backups from second instance
|
|
backups2 := []PBSBackup{
|
|
{ID: "backup-3", Instance: "pbs-2", BackupTime: now.Add(-30 * time.Minute)},
|
|
}
|
|
state.UpdatePBSBackups("pbs-2", backups2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PBSBackups) != 3 {
|
|
t.Fatalf("Expected 3 backups, got %d", len(snapshot.PBSBackups))
|
|
}
|
|
|
|
// Update first instance (should replace its backups)
|
|
backups1Updated := []PBSBackup{
|
|
{ID: "backup-4", Instance: "pbs-1", BackupTime: now.Add(time.Hour)},
|
|
}
|
|
state.UpdatePBSBackups("pbs-1", backups1Updated)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PBSBackups) != 2 {
|
|
t.Fatalf("Expected 2 backups after update, got %d", len(snapshot.PBSBackups))
|
|
}
|
|
|
|
// Verify pbs-1 backups were replaced
|
|
hasOldBackup := false
|
|
for _, b := range snapshot.PBSBackups {
|
|
if b.ID == "backup-1" || b.ID == "backup-2" {
|
|
hasOldBackup = true
|
|
break
|
|
}
|
|
}
|
|
if hasOldBackup {
|
|
t.Error("Old pbs-1 backups should have been replaced")
|
|
}
|
|
}
|
|
|
|
func TestUpdatePMGBackups(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Add backups from first instance
|
|
backups1 := []PMGBackup{
|
|
{Filename: "backup-1.conf", Instance: "pmg-1", BackupTime: now},
|
|
{Filename: "backup-2.conf", Instance: "pmg-1", BackupTime: now.Add(-time.Hour)},
|
|
}
|
|
state.UpdatePMGBackups("pmg-1", backups1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PMGBackups) != 2 {
|
|
t.Fatalf("Expected 2 backups, got %d", len(snapshot.PMGBackups))
|
|
}
|
|
|
|
// Backups should be sorted by time (newest first)
|
|
if snapshot.PMGBackups[0].BackupTime.Before(snapshot.PMGBackups[1].BackupTime) {
|
|
t.Error("Backups should be sorted by time descending")
|
|
}
|
|
|
|
// Add backups from second instance
|
|
backups2 := []PMGBackup{
|
|
{Filename: "backup-3.conf", Instance: "pmg-2", BackupTime: now.Add(-30 * time.Minute)},
|
|
}
|
|
state.UpdatePMGBackups("pmg-2", backups2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PMGBackups) != 3 {
|
|
t.Fatalf("Expected 3 backups, got %d", len(snapshot.PMGBackups))
|
|
}
|
|
|
|
// Update first instance with empty (should remove its backups)
|
|
state.UpdatePMGBackups("pmg-1", []PMGBackup{})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PMGBackups) != 1 {
|
|
t.Fatalf("Expected 1 backup after clearing pmg-1, got %d", len(snapshot.PMGBackups))
|
|
}
|
|
if snapshot.PMGBackups[0].Instance != "pmg-2" {
|
|
t.Error("Remaining backup should be from pmg-2")
|
|
}
|
|
}
|
|
|
|
func TestUpdatePhysicalDisks(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add disks from first instance
|
|
disks1 := []PhysicalDisk{
|
|
{ID: "disk-1", Instance: "pve-1", Node: "node1", DevPath: "/dev/sda"},
|
|
{ID: "disk-2", Instance: "pve-1", Node: "node1", DevPath: "/dev/sdb"},
|
|
}
|
|
state.UpdatePhysicalDisks("pve-1", disks1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PhysicalDisks) != 2 {
|
|
t.Fatalf("Expected 2 disks, got %d", len(snapshot.PhysicalDisks))
|
|
}
|
|
|
|
// Add disks from second instance
|
|
disks2 := []PhysicalDisk{
|
|
{ID: "disk-3", Instance: "pve-2", Node: "node2", DevPath: "/dev/sda"},
|
|
}
|
|
state.UpdatePhysicalDisks("pve-2", disks2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PhysicalDisks) != 3 {
|
|
t.Fatalf("Expected 3 disks, got %d", len(snapshot.PhysicalDisks))
|
|
}
|
|
|
|
// Update first instance (should replace its disks)
|
|
disks1Updated := []PhysicalDisk{
|
|
{ID: "disk-4", Instance: "pve-1", Node: "node1", DevPath: "/dev/nvme0n1"},
|
|
}
|
|
state.UpdatePhysicalDisks("pve-1", disks1Updated)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PhysicalDisks) != 2 {
|
|
t.Fatalf("Expected 2 disks after update, got %d", len(snapshot.PhysicalDisks))
|
|
}
|
|
|
|
// Verify sorting by node then devpath
|
|
if snapshot.PhysicalDisks[0].Node > snapshot.PhysicalDisks[1].Node {
|
|
t.Error("Disks should be sorted by node")
|
|
}
|
|
}
|
|
|
|
func TestUpdateStorageForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add storage from first instance
|
|
storage1 := []Storage{
|
|
{ID: "storage-1", Instance: "pve-1", Name: "local"},
|
|
{ID: "storage-2", Instance: "pve-1", Name: "ceph-pool"},
|
|
}
|
|
state.UpdateStorageForInstance("pve-1", storage1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.Storage) != 2 {
|
|
t.Fatalf("Expected 2 storage entries, got %d", len(snapshot.Storage))
|
|
}
|
|
|
|
// Add storage from second instance
|
|
storage2 := []Storage{
|
|
{ID: "storage-3", Instance: "pve-2", Name: "local"},
|
|
}
|
|
state.UpdateStorageForInstance("pve-2", storage2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.Storage) != 3 {
|
|
t.Fatalf("Expected 3 storage entries, got %d", len(snapshot.Storage))
|
|
}
|
|
|
|
// Update first instance with empty (should remove its storage)
|
|
state.UpdateStorageForInstance("pve-1", []Storage{})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.Storage) != 1 {
|
|
t.Fatalf("Expected 1 storage after clearing pve-1, got %d", len(snapshot.Storage))
|
|
}
|
|
}
|
|
|
|
func TestUpdatePBSInstances(t *testing.T) {
|
|
state := NewState()
|
|
|
|
instances := []PBSInstance{
|
|
{ID: "pbs-1", Name: "Backup Server 1"},
|
|
{ID: "pbs-2", Name: "Backup Server 2"},
|
|
}
|
|
state.UpdatePBSInstances(instances)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PBSInstances) != 2 {
|
|
t.Fatalf("Expected 2 PBS instances, got %d", len(snapshot.PBSInstances))
|
|
}
|
|
|
|
// Replace with different instances
|
|
newInstances := []PBSInstance{
|
|
{ID: "pbs-3", Name: "New Backup Server"},
|
|
}
|
|
state.UpdatePBSInstances(newInstances)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PBSInstances) != 1 {
|
|
t.Fatalf("Expected 1 PBS instance after replacement, got %d", len(snapshot.PBSInstances))
|
|
}
|
|
if snapshot.PBSInstances[0].ID != "pbs-3" {
|
|
t.Error("Expected pbs-3 instance")
|
|
}
|
|
}
|
|
|
|
func TestUpdatePBSInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add first instance
|
|
state.UpdatePBSInstance(PBSInstance{ID: "pbs-1", Name: "Server 1"})
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PBSInstances) != 1 {
|
|
t.Fatalf("Expected 1 PBS instance, got %d", len(snapshot.PBSInstances))
|
|
}
|
|
|
|
// Add second instance
|
|
state.UpdatePBSInstance(PBSInstance{ID: "pbs-2", Name: "Server 2"})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PBSInstances) != 2 {
|
|
t.Fatalf("Expected 2 PBS instances, got %d", len(snapshot.PBSInstances))
|
|
}
|
|
|
|
// Update existing instance
|
|
state.UpdatePBSInstance(PBSInstance{ID: "pbs-1", Name: "Server 1 Updated"})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PBSInstances) != 2 {
|
|
t.Fatalf("Expected 2 PBS instances after update, got %d", len(snapshot.PBSInstances))
|
|
}
|
|
|
|
// Find updated instance
|
|
var found bool
|
|
for _, inst := range snapshot.PBSInstances {
|
|
if inst.ID == "pbs-1" && inst.Name == "Server 1 Updated" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Expected pbs-1 to be updated with new name")
|
|
}
|
|
}
|
|
|
|
func TestUpdatePMGInstances(t *testing.T) {
|
|
state := NewState()
|
|
|
|
instances := []PMGInstance{
|
|
{ID: "pmg-1", Name: "Mail Gateway 1"},
|
|
{ID: "pmg-2", Name: "Mail Gateway 2"},
|
|
}
|
|
state.UpdatePMGInstances(instances)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PMGInstances) != 2 {
|
|
t.Fatalf("Expected 2 PMG instances, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
// Replace with empty
|
|
state.UpdatePMGInstances([]PMGInstance{})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PMGInstances) != 0 {
|
|
t.Fatalf("Expected 0 PMG instances after clearing, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
}
|
|
|
|
func TestUpdatePMGInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add first instance
|
|
state.UpdatePMGInstance(PMGInstance{ID: "pmg-1", Name: "Gateway 1"})
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PMGInstances) != 1 {
|
|
t.Fatalf("Expected 1 PMG instance, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
// Add second instance
|
|
state.UpdatePMGInstance(PMGInstance{ID: "pmg-2", Name: "Gateway 2"})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PMGInstances) != 2 {
|
|
t.Fatalf("Expected 2 PMG instances, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
// Update existing instance
|
|
state.UpdatePMGInstance(PMGInstance{ID: "pmg-1", Name: "Gateway 1 Updated"})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PMGInstances) != 2 {
|
|
t.Fatalf("Expected 2 PMG instances after update, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
// Verify update
|
|
var found bool
|
|
for _, inst := range snapshot.PMGInstances {
|
|
if inst.ID == "pmg-1" && inst.Name == "Gateway 1 Updated" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Expected pmg-1 to be updated with new name")
|
|
}
|
|
}
|
|
|
|
func TestUpdateCephClustersForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add clusters from first instance
|
|
clusters1 := []CephCluster{
|
|
{ID: "ceph-1", Instance: "pve-1", Name: "ceph-pool-1"},
|
|
{ID: "ceph-2", Instance: "pve-1", Name: "ceph-pool-2"},
|
|
}
|
|
state.UpdateCephClustersForInstance("pve-1", clusters1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.CephClusters) != 2 {
|
|
t.Fatalf("Expected 2 clusters, got %d", len(snapshot.CephClusters))
|
|
}
|
|
|
|
// Add clusters from second instance
|
|
clusters2 := []CephCluster{
|
|
{ID: "ceph-3", Instance: "pve-2", Name: "ceph-pool-1"},
|
|
}
|
|
state.UpdateCephClustersForInstance("pve-2", clusters2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.CephClusters) != 3 {
|
|
t.Fatalf("Expected 3 clusters, got %d", len(snapshot.CephClusters))
|
|
}
|
|
|
|
// Update first instance with empty (should remove its clusters)
|
|
state.UpdateCephClustersForInstance("pve-1", []CephCluster{})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.CephClusters) != 1 {
|
|
t.Fatalf("Expected 1 cluster after clearing pve-1, got %d", len(snapshot.CephClusters))
|
|
}
|
|
if snapshot.CephClusters[0].Instance != "pve-2" {
|
|
t.Error("Remaining cluster should be from pve-2")
|
|
}
|
|
}
|
|
|
|
func TestUpdateBackupTasksForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Add tasks from first instance
|
|
tasks1 := []BackupTask{
|
|
{ID: "pve-1-task-1", StartTime: now},
|
|
{ID: "pve-1-task-2", StartTime: now.Add(-time.Hour)},
|
|
}
|
|
state.UpdateBackupTasksForInstance("pve-1", tasks1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.BackupTasks) != 2 {
|
|
t.Fatalf("Expected 2 tasks, got %d", len(snapshot.PVEBackups.BackupTasks))
|
|
}
|
|
|
|
// Tasks should be sorted by start time descending
|
|
if snapshot.PVEBackups.BackupTasks[0].StartTime.Before(snapshot.PVEBackups.BackupTasks[1].StartTime) {
|
|
t.Error("Tasks should be sorted by start time descending")
|
|
}
|
|
|
|
// Add tasks from second instance
|
|
tasks2 := []BackupTask{
|
|
{ID: "pve-2-task-1", StartTime: now.Add(-30 * time.Minute)},
|
|
}
|
|
state.UpdateBackupTasksForInstance("pve-2", tasks2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.BackupTasks) != 3 {
|
|
t.Fatalf("Expected 3 tasks, got %d", len(snapshot.PVEBackups.BackupTasks))
|
|
}
|
|
|
|
// Update first instance (should replace its tasks)
|
|
tasks1Updated := []BackupTask{
|
|
{ID: "pve-1-task-3", StartTime: now.Add(time.Hour)},
|
|
}
|
|
state.UpdateBackupTasksForInstance("pve-1", tasks1Updated)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.BackupTasks) != 2 {
|
|
t.Fatalf("Expected 2 tasks after update, got %d", len(snapshot.PVEBackups.BackupTasks))
|
|
}
|
|
}
|
|
|
|
func TestUpdateReplicationJobsForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
// Add jobs from first instance
|
|
jobs1 := []ReplicationJob{
|
|
{ID: "job-1", Instance: "pve-1", GuestID: 100, JobNumber: 0},
|
|
{ID: "job-2", Instance: "pve-1", GuestID: 101, JobNumber: 0},
|
|
}
|
|
state.UpdateReplicationJobsForInstance("pve-1", jobs1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.ReplicationJobs) != 2 {
|
|
t.Fatalf("Expected 2 jobs, got %d", len(snapshot.ReplicationJobs))
|
|
}
|
|
|
|
// Add jobs from second instance
|
|
jobs2 := []ReplicationJob{
|
|
{ID: "job-3", Instance: "pve-2", GuestID: 200, JobNumber: 0},
|
|
}
|
|
state.UpdateReplicationJobsForInstance("pve-2", jobs2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.ReplicationJobs) != 3 {
|
|
t.Fatalf("Expected 3 jobs, got %d", len(snapshot.ReplicationJobs))
|
|
}
|
|
|
|
// Update first instance with empty (should remove its jobs)
|
|
state.UpdateReplicationJobsForInstance("pve-1", []ReplicationJob{})
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.ReplicationJobs) != 1 {
|
|
t.Fatalf("Expected 1 job after clearing pve-1, got %d", len(snapshot.ReplicationJobs))
|
|
}
|
|
if snapshot.ReplicationJobs[0].Instance != "pve-2" {
|
|
t.Error("Remaining job should be from pve-2")
|
|
}
|
|
}
|
|
|
|
func TestUpdateGuestSnapshotsForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Add snapshots from first instance
|
|
snapshots1 := []GuestSnapshot{
|
|
{ID: "pve-1-snap-1", VMID: 100, Name: "snapshot1", Time: now},
|
|
{ID: "pve-1-snap-2", VMID: 100, Name: "snapshot2", Time: now.Add(-time.Hour)},
|
|
}
|
|
state.UpdateGuestSnapshotsForInstance("pve-1", snapshots1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.GuestSnapshots) != 2 {
|
|
t.Fatalf("Expected 2 snapshots, got %d", len(snapshot.PVEBackups.GuestSnapshots))
|
|
}
|
|
|
|
// Add snapshots from second instance
|
|
snapshots2 := []GuestSnapshot{
|
|
{ID: "pve-2-snap-1", VMID: 200, Name: "snapshot1", Time: now.Add(-30 * time.Minute)},
|
|
}
|
|
state.UpdateGuestSnapshotsForInstance("pve-2", snapshots2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.GuestSnapshots) != 3 {
|
|
t.Fatalf("Expected 3 snapshots, got %d", len(snapshot.PVEBackups.GuestSnapshots))
|
|
}
|
|
|
|
// Update first instance (should replace its snapshots)
|
|
snapshots1Updated := []GuestSnapshot{
|
|
{ID: "pve-1-snap-3", VMID: 100, Name: "new-snapshot", Time: now.Add(time.Hour)},
|
|
}
|
|
state.UpdateGuestSnapshotsForInstance("pve-1", snapshots1Updated)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.GuestSnapshots) != 2 {
|
|
t.Fatalf("Expected 2 snapshots after update, got %d", len(snapshot.PVEBackups.GuestSnapshots))
|
|
}
|
|
}
|
|
|
|
func TestSyncGuestBackupTimes(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
oldBackup := now.Add(-24 * time.Hour)
|
|
newBackup := now.Add(-1 * time.Hour)
|
|
|
|
// Set up VMs and containers
|
|
state.UpdateVMs([]VM{
|
|
{VMID: 100, Name: "vm-100", Instance: "pve-1"},
|
|
{VMID: 101, Name: "vm-101", Instance: "pve-1"},
|
|
})
|
|
state.UpdateContainers([]Container{
|
|
{VMID: 200, Name: "ct-200", Instance: "pve-1"},
|
|
})
|
|
|
|
// Add storage backups for VM 100 (must include Instance for proper matching)
|
|
state.mu.Lock()
|
|
state.PVEBackups.StorageBackups = []StorageBackup{
|
|
{ID: "pve-1-backup-1", VMID: 100, Instance: "pve-1", Time: oldBackup},
|
|
{ID: "pve-1-backup-2", VMID: 100, Instance: "pve-1", Time: newBackup}, // newer
|
|
}
|
|
state.mu.Unlock()
|
|
|
|
// Add PBS backup for container 200
|
|
state.UpdatePBSBackups("pbs-1", []PBSBackup{
|
|
{ID: "pbs-backup-1", VMID: "200", BackupTime: newBackup},
|
|
})
|
|
|
|
// Sync backup times
|
|
state.SyncGuestBackupTimes()
|
|
|
|
snapshot := state.GetSnapshot()
|
|
|
|
// VM 100 should have the newer backup time
|
|
var vm100 *VM
|
|
for i := range snapshot.VMs {
|
|
if snapshot.VMs[i].VMID == 100 {
|
|
vm100 = &snapshot.VMs[i]
|
|
break
|
|
}
|
|
}
|
|
if vm100 == nil {
|
|
t.Fatal("VM 100 not found")
|
|
}
|
|
if !vm100.LastBackup.Equal(newBackup) {
|
|
t.Errorf("VM 100 LastBackup = %v, expected %v", vm100.LastBackup, newBackup)
|
|
}
|
|
|
|
// VM 101 should have no backup time (zero)
|
|
var vm101 *VM
|
|
for i := range snapshot.VMs {
|
|
if snapshot.VMs[i].VMID == 101 {
|
|
vm101 = &snapshot.VMs[i]
|
|
break
|
|
}
|
|
}
|
|
if vm101 == nil {
|
|
t.Fatal("VM 101 not found")
|
|
}
|
|
if !vm101.LastBackup.IsZero() {
|
|
t.Errorf("VM 101 LastBackup should be zero, got %v", vm101.LastBackup)
|
|
}
|
|
|
|
// Container 200 should have backup time from PBS
|
|
var ct200 *Container
|
|
for i := range snapshot.Containers {
|
|
if snapshot.Containers[i].VMID == 200 {
|
|
ct200 = &snapshot.Containers[i]
|
|
break
|
|
}
|
|
}
|
|
if ct200 == nil {
|
|
t.Fatal("Container 200 not found")
|
|
}
|
|
if !ct200.LastBackup.Equal(newBackup) {
|
|
t.Errorf("Container 200 LastBackup = %v, expected %v", ct200.LastBackup, newBackup)
|
|
}
|
|
}
|
|
|
|
// TestSyncGuestBackupTimesCrossInstance verifies that backup matching uses instance+VMID.
|
|
// This prevents a newly created container on one instance from incorrectly showing
|
|
// backup time from a different container with the same VMID on another instance.
|
|
func TestSyncGuestBackupTimesCrossInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
threeMonthsAgo := now.Add(-90 * 24 * time.Hour)
|
|
|
|
// Set up containers with the SAME VMID on DIFFERENT instances
|
|
// This simulates: pve-1 has VMID 100 with old backup, pve-2 has newly created VMID 100
|
|
state.UpdateContainers([]Container{
|
|
{VMID: 100, Name: "old-container", Instance: "pve-1"},
|
|
{VMID: 100, Name: "new-container", Instance: "pve-2"}, // newly created, no backup
|
|
})
|
|
|
|
// Add a 3-month-old backup for pve-1's container (VMID 100)
|
|
state.mu.Lock()
|
|
state.PVEBackups.StorageBackups = []StorageBackup{
|
|
{ID: "pve-1-backup-old", VMID: 100, Instance: "pve-1", Time: threeMonthsAgo},
|
|
}
|
|
state.mu.Unlock()
|
|
|
|
// Sync backup times
|
|
state.SyncGuestBackupTimes()
|
|
|
|
snapshot := state.GetSnapshot()
|
|
|
|
// Find both containers
|
|
var oldContainer, newContainer *Container
|
|
for i := range snapshot.Containers {
|
|
if snapshot.Containers[i].Instance == "pve-1" && snapshot.Containers[i].VMID == 100 {
|
|
oldContainer = &snapshot.Containers[i]
|
|
}
|
|
if snapshot.Containers[i].Instance == "pve-2" && snapshot.Containers[i].VMID == 100 {
|
|
newContainer = &snapshot.Containers[i]
|
|
}
|
|
}
|
|
|
|
if oldContainer == nil {
|
|
t.Fatal("pve-1 container not found")
|
|
}
|
|
if newContainer == nil {
|
|
t.Fatal("pve-2 container not found")
|
|
}
|
|
|
|
// The old container on pve-1 SHOULD have the backup time
|
|
if !oldContainer.LastBackup.Equal(threeMonthsAgo) {
|
|
t.Errorf("pve-1 container LastBackup = %v, expected %v", oldContainer.LastBackup, threeMonthsAgo)
|
|
}
|
|
|
|
// The NEW container on pve-2 should NOT have any backup time (it's a different container!)
|
|
if !newContainer.LastBackup.IsZero() {
|
|
t.Errorf("pve-2 container (newly created) should have no backup, got %v", newContainer.LastBackup)
|
|
}
|
|
}
|
|
|
|
func TestUpdateStorageBackupsForInstance(t *testing.T) {
|
|
state := NewState()
|
|
|
|
now := time.Now()
|
|
|
|
// Set up a VM so node normalization has something to work with
|
|
state.UpdateVMsForInstance("pve-1", []VM{
|
|
{VMID: 100, Instance: "pve-1", Node: "node1"},
|
|
})
|
|
|
|
// Add backups from first instance
|
|
backups1 := []StorageBackup{
|
|
{ID: "pve-1-backup-1", VMID: 100, Time: now, Node: "node1"},
|
|
{ID: "pve-1-backup-2", VMID: 100, Time: now.Add(-time.Hour), Node: "node1"},
|
|
}
|
|
state.UpdateStorageBackupsForInstance("pve-1", backups1)
|
|
|
|
snapshot := state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.StorageBackups) != 2 {
|
|
t.Fatalf("Expected 2 backups, got %d", len(snapshot.PVEBackups.StorageBackups))
|
|
}
|
|
|
|
// Backups should be sorted by time descending
|
|
if snapshot.PVEBackups.StorageBackups[0].Time.Before(snapshot.PVEBackups.StorageBackups[1].Time) {
|
|
t.Error("Backups should be sorted by time descending")
|
|
}
|
|
|
|
// Add backups from second instance
|
|
backups2 := []StorageBackup{
|
|
{ID: "pve-2-backup-1", VMID: 200, Time: now.Add(-30 * time.Minute), Node: "node2"},
|
|
}
|
|
state.UpdateStorageBackupsForInstance("pve-2", backups2)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.StorageBackups) != 3 {
|
|
t.Fatalf("Expected 3 backups, got %d", len(snapshot.PVEBackups.StorageBackups))
|
|
}
|
|
|
|
// Update first instance (should replace its backups)
|
|
backups1Updated := []StorageBackup{
|
|
{ID: "pve-1-backup-3", VMID: 100, Time: now.Add(time.Hour), Node: "node1"},
|
|
}
|
|
state.UpdateStorageBackupsForInstance("pve-1", backups1Updated)
|
|
|
|
snapshot = state.GetSnapshot()
|
|
if len(snapshot.PVEBackups.StorageBackups) != 2 {
|
|
t.Fatalf("Expected 2 backups after update, got %d", len(snapshot.PVEBackups.StorageBackups))
|
|
}
|
|
|
|
// Verify old pve-1 backups were replaced
|
|
for _, b := range snapshot.PVEBackups.StorageBackups {
|
|
if b.ID == "pve-1-backup-1" || b.ID == "pve-1-backup-2" {
|
|
t.Error("Old pve-1 backups should have been replaced")
|
|
}
|
|
}
|
|
}
|