Files
Pulse/internal/monitoring/pbs_convert_test.go
rcourtman 9b49d3171d feat(pbs): add datastore exclusion to reduce PBS log noise
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
2026-01-14 12:26:18 +00:00

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)
}
})
}