mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Users with removable/unmounted datastores (e.g., external HDDs for offline backup) experienced excessive PBS log entries because Pulse was querying all datastores including unavailable ones. Added `excludeDatastores` field to PBS node configuration that accepts patterns to exclude specific datastores from monitoring: - Exact names: "exthdd1500gb" - Prefix patterns: "ext*" - Suffix patterns: "*hdd" - Contains patterns: "*removable*" Pattern matching is case-insensitive. Fixes #1105
380 lines
11 KiB
Go
380 lines
11 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
|
)
|
|
|
|
func TestMatchesDatastoreExclude(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
datastoreName string
|
|
patterns []string
|
|
expected bool
|
|
}{
|
|
// Empty patterns
|
|
{"empty patterns returns false", "exthdd1500gb", nil, false},
|
|
{"empty slice returns false", "exthdd1500gb", []string{}, false},
|
|
|
|
// Exact match (case-insensitive)
|
|
{"exact match", "exthdd1500gb", []string{"exthdd1500gb"}, true},
|
|
{"exact match case insensitive", "ExtHDD1500GB", []string{"exthdd1500gb"}, true},
|
|
{"exact match no match", "exthdd1500gb", []string{"backup"}, false},
|
|
|
|
// Prefix pattern (name*)
|
|
{"prefix pattern match", "exthdd1500gb", []string{"ext*"}, true},
|
|
{"prefix pattern match 2", "backup-external", []string{"backup*"}, true},
|
|
{"prefix pattern no match", "internal-storage", []string{"ext*"}, false},
|
|
{"prefix pattern case insensitive", "EXTHDD1500GB", []string{"ext*"}, true},
|
|
|
|
// Suffix pattern (*name)
|
|
{"suffix pattern match", "my-external-hdd", []string{"*hdd"}, true},
|
|
{"suffix pattern match 2", "backup-store", []string{"*store"}, true},
|
|
{"suffix pattern no match", "hdd-backup", []string{"*store"}, false},
|
|
{"suffix pattern case insensitive", "MY-EXTERNAL-HDD", []string{"*hdd"}, true},
|
|
|
|
// Contains pattern (*name*)
|
|
{"contains pattern match", "my-external-hdd", []string{"*external*"}, true},
|
|
{"contains pattern match middle", "backup-removable-drive", []string{"*removable*"}, true},
|
|
{"contains pattern no match", "internal-drive", []string{"*external*"}, false},
|
|
{"contains pattern case insensitive", "BACKUP-REMOVABLE-DRIVE", []string{"*removable*"}, true},
|
|
|
|
// Multiple patterns (any match)
|
|
{"multiple patterns first match", "exthdd1500gb", []string{"backup*", "ext*"}, true},
|
|
{"multiple patterns second match", "backup-drive", []string{"ext*", "backup*"}, true},
|
|
{"multiple patterns no match", "internal", []string{"ext*", "backup*"}, false},
|
|
|
|
// Edge cases
|
|
{"empty pattern in list", "exthdd", []string{"", "ext*"}, true},
|
|
{"whitespace pattern", "exthdd", []string{" ", "ext*"}, true},
|
|
{"pattern with whitespace", "exthdd", []string{" ext* "}, true},
|
|
{"single star", "anything", []string{"*"}, false}, // Single star doesn't match (needs prefix/suffix)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := matchesDatastoreExclude(tt.datastoreName, tt.patterns)
|
|
if result != tt.expected {
|
|
t.Errorf("matchesDatastoreExclude(%q, %v) = %t, want %t",
|
|
tt.datastoreName, tt.patterns, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertPBSSnapshots(t *testing.T) {
|
|
t.Run("empty input returns empty slice", func(t *testing.T) {
|
|
result := convertPBSSnapshots("pbs-1", "backup-store", "ns1", nil)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice, got %d items", len(result))
|
|
}
|
|
|
|
result = convertPBSSnapshots("pbs-1", "backup-store", "ns1", []pbs.BackupSnapshot{})
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice, got %d items", len(result))
|
|
}
|
|
})
|
|
|
|
t.Run("basic snapshot conversion", func(t *testing.T) {
|
|
backupTime := int64(1700000000)
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "100",
|
|
BackupTime: backupTime,
|
|
Size: 1024000,
|
|
Protected: true,
|
|
Comment: "Test backup",
|
|
Owner: "root@pam",
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-prod", "datastore1", "production", snapshots)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 backup, got %d", len(result))
|
|
}
|
|
|
|
backup := result[0]
|
|
expectedID := "pbs-pbs-prod-datastore1-production-vm-100-1700000000"
|
|
if backup.ID != expectedID {
|
|
t.Errorf("ID: expected %q, got %q", expectedID, backup.ID)
|
|
}
|
|
if backup.Instance != "pbs-prod" {
|
|
t.Errorf("Instance: expected %q, got %q", "pbs-prod", backup.Instance)
|
|
}
|
|
if backup.Datastore != "datastore1" {
|
|
t.Errorf("Datastore: expected %q, got %q", "datastore1", backup.Datastore)
|
|
}
|
|
if backup.Namespace != "production" {
|
|
t.Errorf("Namespace: expected %q, got %q", "production", backup.Namespace)
|
|
}
|
|
if backup.BackupType != "vm" {
|
|
t.Errorf("BackupType: expected %q, got %q", "vm", backup.BackupType)
|
|
}
|
|
if backup.VMID != "100" {
|
|
t.Errorf("VMID: expected %q, got %q", "100", backup.VMID)
|
|
}
|
|
expectedTime := time.Unix(backupTime, 0)
|
|
if !backup.BackupTime.Equal(expectedTime) {
|
|
t.Errorf("BackupTime: expected %v, got %v", expectedTime, backup.BackupTime)
|
|
}
|
|
if backup.Size != 1024000 {
|
|
t.Errorf("Size: expected %d, got %d", 1024000, backup.Size)
|
|
}
|
|
if !backup.Protected {
|
|
t.Error("Protected: expected true")
|
|
}
|
|
if backup.Verified {
|
|
t.Error("Verified: expected false (no verification data)")
|
|
}
|
|
if backup.Comment != "Test backup" {
|
|
t.Errorf("Comment: expected %q, got %q", "Test backup", backup.Comment)
|
|
}
|
|
if backup.Owner != "root@pam" {
|
|
t.Errorf("Owner: expected %q, got %q", "root@pam", backup.Owner)
|
|
}
|
|
})
|
|
|
|
t.Run("files as string array", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "ct",
|
|
BackupID: "200",
|
|
BackupTime: 1700000000,
|
|
Files: []interface{}{"file1.img", "file2.pxar", "file3.conf"},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if len(result[0].Files) != 3 {
|
|
t.Fatalf("expected 3 files, got %d", len(result[0].Files))
|
|
}
|
|
if result[0].Files[0] != "file1.img" {
|
|
t.Errorf("Files[0]: expected %q, got %q", "file1.img", result[0].Files[0])
|
|
}
|
|
if result[0].Files[1] != "file2.pxar" {
|
|
t.Errorf("Files[1]: expected %q, got %q", "file2.pxar", result[0].Files[1])
|
|
}
|
|
if result[0].Files[2] != "file3.conf" {
|
|
t.Errorf("Files[2]: expected %q, got %q", "file3.conf", result[0].Files[2])
|
|
}
|
|
})
|
|
|
|
t.Run("files as object array with filename field", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "300",
|
|
BackupTime: 1700000000,
|
|
Files: []interface{}{
|
|
map[string]interface{}{"filename": "disk-0.raw", "size": 10737418240},
|
|
map[string]interface{}{"filename": "vm.conf", "size": 1024},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if len(result[0].Files) != 2 {
|
|
t.Fatalf("expected 2 files, got %d", len(result[0].Files))
|
|
}
|
|
if result[0].Files[0] != "disk-0.raw" {
|
|
t.Errorf("Files[0]: expected %q, got %q", "disk-0.raw", result[0].Files[0])
|
|
}
|
|
if result[0].Files[1] != "vm.conf" {
|
|
t.Errorf("Files[1]: expected %q, got %q", "vm.conf", result[0].Files[1])
|
|
}
|
|
})
|
|
|
|
t.Run("files with missing filename field ignored", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "400",
|
|
BackupTime: 1700000000,
|
|
Files: []interface{}{
|
|
map[string]interface{}{"filename": "valid.raw"},
|
|
map[string]interface{}{"name": "invalid.raw"}, // Wrong field name
|
|
map[string]interface{}{}, // Empty object
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if len(result[0].Files) != 1 {
|
|
t.Fatalf("expected 1 file (only valid one), got %d", len(result[0].Files))
|
|
}
|
|
if result[0].Files[0] != "valid.raw" {
|
|
t.Errorf("Files[0]: expected %q, got %q", "valid.raw", result[0].Files[0])
|
|
}
|
|
})
|
|
|
|
t.Run("verification as string ok", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "500",
|
|
BackupTime: 1700000000,
|
|
Verification: "ok",
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if !result[0].Verified {
|
|
t.Error("Verified: expected true when verification is 'ok'")
|
|
}
|
|
})
|
|
|
|
t.Run("verification as string not ok", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "501",
|
|
BackupTime: 1700000000,
|
|
Verification: "failed",
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if result[0].Verified {
|
|
t.Error("Verified: expected false when verification is not 'ok'")
|
|
}
|
|
})
|
|
|
|
t.Run("verification as object with state ok", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "600",
|
|
BackupTime: 1700000000,
|
|
Verification: map[string]interface{}{
|
|
"state": "ok",
|
|
"upid": "UPID:pbs:00001234",
|
|
"snapshot": "2023-11-14T12:00:00Z",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if !result[0].Verified {
|
|
t.Error("Verified: expected true when verification.state is 'ok'")
|
|
}
|
|
})
|
|
|
|
t.Run("verification as object with state failed", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "601",
|
|
BackupTime: 1700000000,
|
|
Verification: map[string]interface{}{
|
|
"state": "failed",
|
|
"upid": "UPID:pbs:00005678",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if result[0].Verified {
|
|
t.Error("Verified: expected false when verification.state is 'failed'")
|
|
}
|
|
})
|
|
|
|
t.Run("verification as object without state field", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "602",
|
|
BackupTime: 1700000000,
|
|
Verification: map[string]interface{}{
|
|
"upid": "UPID:pbs:00009999",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
if result[0].Verified {
|
|
t.Error("Verified: expected false when verification object has no state field")
|
|
}
|
|
})
|
|
|
|
t.Run("multiple snapshots", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "100",
|
|
BackupTime: 1700000000,
|
|
},
|
|
{
|
|
BackupType: "ct",
|
|
BackupID: "200",
|
|
BackupTime: 1700001000,
|
|
},
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "100",
|
|
BackupTime: 1700002000, // Same VM, different time
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "ns", snapshots)
|
|
|
|
if len(result) != 3 {
|
|
t.Fatalf("expected 3 backups, got %d", len(result))
|
|
}
|
|
|
|
// Verify each has unique ID
|
|
ids := make(map[string]bool)
|
|
for _, backup := range result {
|
|
if ids[backup.ID] {
|
|
t.Errorf("duplicate ID found: %s", backup.ID)
|
|
}
|
|
ids[backup.ID] = true
|
|
}
|
|
})
|
|
|
|
t.Run("empty namespace in ID", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "vm",
|
|
BackupID: "100",
|
|
BackupTime: 1700000000,
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "", snapshots)
|
|
|
|
expectedID := "pbs-pbs-1-store--vm-100-1700000000"
|
|
if result[0].ID != expectedID {
|
|
t.Errorf("ID with empty namespace: expected %q, got %q", expectedID, result[0].ID)
|
|
}
|
|
})
|
|
|
|
t.Run("container backup type", func(t *testing.T) {
|
|
snapshots := []pbs.BackupSnapshot{
|
|
{
|
|
BackupType: "ct",
|
|
BackupID: "101",
|
|
BackupTime: 1700000000,
|
|
Size: 512000,
|
|
},
|
|
}
|
|
|
|
result := convertPBSSnapshots("pbs-1", "store", "ns", snapshots)
|
|
|
|
if result[0].BackupType != "ct" {
|
|
t.Errorf("BackupType: expected %q, got %q", "ct", result[0].BackupType)
|
|
}
|
|
})
|
|
}
|