Pulse/internal/monitoring/monitor_backups_readstate_test.go
2026-03-18 16:06:30 +00:00

327 lines
9.4 KiB
Go

package monitoring
import (
"context"
"fmt"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
proxmoxmapper "github.com/rcourtman/pulse-go-rewrite/internal/recovery/mapper/proxmox"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
pveapi "github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
type canonicalBackupStorageClient struct {
mockPVEClientExtra
}
func (c *canonicalBackupStorageClient) GetStorage(ctx context.Context, node string) ([]pveapi.Storage, error) {
return []pveapi.Storage{
{Storage: "local", Content: "backup", Type: "dir", Enabled: 1, Active: 1},
}, nil
}
func (c *canonicalBackupStorageClient) GetStorageContent(ctx context.Context, node, storage string) ([]pveapi.StorageContent, error) {
return []pveapi.StorageContent{
{
Volid: "backup/vzdump-qemu-100-2026_03_11-10_00_00.vma.zst",
VMID: 100,
Size: 1024,
CTime: time.Date(2026, 3, 11, 10, 0, 0, 0, time.UTC).Unix(),
Content: "backup",
},
}, nil
}
func backupReadStateResourceStore(resources []unifiedresources.Resource) *resourceOnlyStore {
return &resourceOnlyStore{resources: resources}
}
func backupReadState(resources []unifiedresources.Resource) unifiedresources.ReadState {
registry := unifiedresources.NewRegistry(nil)
registry.IngestResources(resources)
return registry
}
func TestPopulateGuestNodeMapFromReadState_UsesCanonicalWorkloads(t *testing.T) {
readState := backupReadState([]unifiedresources.Resource{
{
ID: "vm-1",
Type: unifiedresources.ResourceTypeVM,
Name: "vm-100",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node-from-store",
VMID: 100,
},
},
{
ID: "ct-1",
Type: unifiedresources.ResourceTypeSystemContainer,
Name: "ct-200",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "ct-node-from-store",
VMID: 200,
},
},
})
guestNodeMap := map[int]string{}
populateGuestNodeMapFromReadState(readState, "pve1", guestNodeMap)
if guestNodeMap[100] != "node-from-store" {
t.Fatalf("expected VM node from canonical read-state, got %q", guestNodeMap[100])
}
if guestNodeMap[200] != "ct-node-from-store" {
t.Fatalf("expected container node from canonical read-state, got %q", guestNodeMap[200])
}
}
func TestStorageNamesForNode_UsesCanonicalStoragePools(t *testing.T) {
readState := backupReadState([]unifiedresources.Resource{
{
ID: "storage-local",
Type: unifiedresources.ResourceTypeStorage,
Name: "local",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node1",
},
Storage: &unifiedresources.StorageMeta{
Content: "images,backup",
},
},
{
ID: "storage-shared",
Type: unifiedresources.ResourceTypeStorage,
Name: "shared",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
},
Storage: &unifiedresources.StorageMeta{
Content: "backup",
Nodes: []string{"node2", "node3"},
},
},
{
ID: "storage-no-backup",
Type: unifiedresources.ResourceTypeStorage,
Name: "fast",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node2",
},
Storage: &unifiedresources.StorageMeta{
Content: "images",
},
},
})
got := storageNamesForNode(readState, "pve1", "node2")
if len(got) != 1 || got[0] != "shared" {
t.Fatalf("expected canonical backup storage names [shared], got %+v", got)
}
}
func TestMonitorCalculateBackupOperationTimeout_UsesCanonicalReadState(t *testing.T) {
resources := make([]unifiedresources.Resource, 0, 61)
for i := 0; i < 61; i++ {
resources = append(resources, unifiedresources.Resource{
ID: fmt.Sprintf("vm-%d", i),
Type: unifiedresources.ResourceTypeVM,
Name: fmt.Sprintf("vm-%d", i),
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node1",
VMID: 100 + i,
},
})
}
m := &Monitor{
state: models.NewState(),
resourceStore: backupReadStateResourceStore(resources),
}
timeout := m.calculateBackupOperationTimeout("pve1")
if want := 122 * time.Second; timeout != want {
t.Fatalf("expected timeout %v from canonical workload count, got %v", want, timeout)
}
}
func TestMonitorPollGuestSnapshots_UsesCanonicalReadState(t *testing.T) {
m := &Monitor{
state: models.NewState(),
resourceStore: backupReadStateResourceStore([]unifiedresources.Resource{
{
ID: "vm-store-100",
Type: unifiedresources.ResourceTypeVM,
Name: "vm100",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node1",
VMID: 100,
},
},
{
ID: "ct-store-200",
Type: unifiedresources.ResourceTypeSystemContainer,
Name: "ct200",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node1",
VMID: 200,
},
},
}),
}
client := &mockPVEClientSnapshots{
snapshots: []pveapi.Snapshot{{Name: "snap1", SnapTime: 1234567890, Description: "from store"}},
}
m.pollGuestSnapshots(context.Background(), "pve1", client)
snapshot := m.state.GetSnapshot()
if len(snapshot.PVEBackups.GuestSnapshots) != 2 {
t.Fatalf("expected guest snapshots from canonical workloads, got %+v", snapshot.PVEBackups.GuestSnapshots)
}
}
func TestMonitorPollStorageBackupsWithNodes_UsesCanonicalReadStateForGuestNodeLookup(t *testing.T) {
m := &Monitor{
state: models.NewState(),
resourceStore: backupReadStateResourceStore([]unifiedresources.Resource{
{
ID: "vm-store-100",
Type: unifiedresources.ResourceTypeVM,
Name: "vm100",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "node2",
VMID: 100,
},
},
}),
}
client := &canonicalBackupStorageClient{}
nodes := []pveapi.Node{{Node: "node1", Status: "online"}}
m.pollStorageBackupsWithNodes(context.Background(), "pve1", client, nodes, map[string]string{"node1": "online"})
backups := m.state.GetSnapshot().PVEBackups.StorageBackups
if len(backups) != 1 {
t.Fatalf("expected one storage backup, got %+v", backups)
}
if backups[0].Node != "node2" {
t.Fatalf("expected guest node from canonical read-state, got %q", backups[0].Node)
}
}
func TestBuildPBSGuestCandidates_UsesCanonicalReadState(t *testing.T) {
readState := backupReadState([]unifiedresources.Resource{
{
ID: "vm-store-100",
Type: unifiedresources.ResourceTypeVM,
Name: "vm100",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "nodeA",
VMID: 100,
},
},
{
ID: "ct-store-200",
Type: unifiedresources.ResourceTypeSystemContainer,
Name: "ct200",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "nodeB",
VMID: 200,
},
},
})
candidates := buildPBSGuestCandidates(readState)
assertCandidate := func(key string, resourceType unifiedresources.ResourceType, node string, vmid int) {
t.Helper()
entries := candidates[key]
if len(entries) != 1 {
t.Fatalf("expected one candidate for %s, got %+v", key, entries)
}
if entries[0] != (proxmoxmapper.GuestCandidate{
SourceID: fmt.Sprintf("%s-store-%d", map[unifiedresources.ResourceType]string{unifiedresources.ResourceTypeVM: "vm", unifiedresources.ResourceTypeSystemContainer: "ct"}[resourceType], vmid),
ResourceType: resourceType,
DisplayName: fmt.Sprintf("%s%d", map[unifiedresources.ResourceType]string{unifiedresources.ResourceTypeVM: "vm", unifiedresources.ResourceTypeSystemContainer: "ct"}[resourceType], vmid),
InstanceName: "pve1",
NodeName: node,
VMID: vmid,
}) {
t.Fatalf("unexpected candidate for %s: %+v", key, entries[0])
}
}
assertCandidate("vm:100", unifiedresources.ResourceTypeVM, "nodeA", 100)
assertCandidate("ct:200", unifiedresources.ResourceTypeSystemContainer, "nodeB", 200)
}
func TestBuildProxmoxGuestInfoIndex_UsesCanonicalReadState(t *testing.T) {
readState := backupReadState([]unifiedresources.Resource{
{
ID: "vm-store-100",
Type: unifiedresources.ResourceTypeVM,
Name: "vm100",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "nodeA",
VMID: 100,
},
},
{
ID: "ct-store-200",
Type: unifiedresources.ResourceTypeSystemContainer,
Name: "ct200",
Status: unifiedresources.StatusOnline,
Proxmox: &unifiedresources.ProxmoxData{
Instance: "pve1",
NodeName: "nodeB",
VMID: 200,
},
},
})
index := buildProxmoxGuestInfoIndex(readState)
assertInfo := func(key string, resourceType unifiedresources.ResourceType, name string) {
t.Helper()
info, ok := index[key]
if !ok {
t.Fatalf("expected info for %s, got none", key)
}
if info.ResourceType != resourceType {
t.Errorf("expected type %v, got %v", resourceType, info.ResourceType)
}
if info.Name != name {
t.Errorf("expected name %q, got %q", name, info.Name)
}
}
assertInfo("pve1|nodeA|100", unifiedresources.ResourceTypeVM, "vm100")
assertInfo("pve1|nodeB|200", unifiedresources.ResourceTypeSystemContainer, "ct200")
}