mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-21 21:26:16 +01:00
- Match FILESYSTEM directly against I/O devices if partition lookup fails - Fall back to the most active I/O device if no root device is detected - Add WARN logs in final fallback case to most active device
509 lines
15 KiB
Go
509 lines
15 KiB
Go
//go:build testing
|
|
|
|
package agent
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/henrygd/beszel/internal/entities/system"
|
|
"github.com/shirou/gopsutil/v4/disk"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestParseFilesystemEntry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expectedFs string
|
|
expectedName string
|
|
}{
|
|
{
|
|
name: "simple device name",
|
|
input: "sda1",
|
|
expectedFs: "sda1",
|
|
expectedName: "",
|
|
},
|
|
{
|
|
name: "device with custom name",
|
|
input: "sda1__my-storage",
|
|
expectedFs: "sda1",
|
|
expectedName: "my-storage",
|
|
},
|
|
{
|
|
name: "full device path with custom name",
|
|
input: "/dev/sdb1__backup-drive",
|
|
expectedFs: "/dev/sdb1",
|
|
expectedName: "backup-drive",
|
|
},
|
|
{
|
|
name: "NVMe device with custom name",
|
|
input: "nvme0n1p2__fast-ssd",
|
|
expectedFs: "nvme0n1p2",
|
|
expectedName: "fast-ssd",
|
|
},
|
|
{
|
|
name: "whitespace trimmed",
|
|
input: " sda2__trimmed-name ",
|
|
expectedFs: "sda2",
|
|
expectedName: "trimmed-name",
|
|
},
|
|
{
|
|
name: "empty custom name",
|
|
input: "sda3__",
|
|
expectedFs: "sda3",
|
|
expectedName: "",
|
|
},
|
|
{
|
|
name: "empty device name",
|
|
input: "__just-custom",
|
|
expectedFs: "",
|
|
expectedName: "just-custom",
|
|
},
|
|
{
|
|
name: "multiple underscores in custom name",
|
|
input: "sda1__my_custom_drive",
|
|
expectedFs: "sda1",
|
|
expectedName: "my_custom_drive",
|
|
},
|
|
{
|
|
name: "custom name with spaces",
|
|
input: "sda1__My Storage Drive",
|
|
expectedFs: "sda1",
|
|
expectedName: "My Storage Drive",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fsEntry := strings.TrimSpace(tt.input)
|
|
var fs, customName string
|
|
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
|
fs = strings.TrimSpace(parts[0])
|
|
customName = strings.TrimSpace(parts[1])
|
|
} else {
|
|
fs = fsEntry
|
|
}
|
|
|
|
assert.Equal(t, tt.expectedFs, fs)
|
|
assert.Equal(t, tt.expectedName, customName)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindIoDevice(t *testing.T) {
|
|
t.Run("matches by device name", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sda": {Name: "sda"},
|
|
"sdb": {Name: "sdb"},
|
|
}
|
|
|
|
device, ok := findIoDevice("sdb", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "sdb", device)
|
|
})
|
|
|
|
t.Run("matches by device label", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sda": {Name: "sda", Label: "rootfs"},
|
|
"sdb": {Name: "sdb"},
|
|
}
|
|
|
|
device, ok := findIoDevice("rootfs", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "sda", device)
|
|
})
|
|
|
|
t.Run("returns no match when not found", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sda": {Name: "sda"},
|
|
"sdb": {Name: "sdb"},
|
|
}
|
|
|
|
device, ok := findIoDevice("nvme0n1p1", ioCounters)
|
|
assert.False(t, ok)
|
|
assert.Equal(t, "", device)
|
|
})
|
|
|
|
t.Run("uses uncertain unique prefix fallback", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"nvme0n1": {Name: "nvme0n1"},
|
|
"sda": {Name: "sda"},
|
|
}
|
|
|
|
device, ok := findIoDevice("nvme0n1p2", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "nvme0n1", device)
|
|
})
|
|
|
|
t.Run("uses dominant activity when prefix matches are ambiguous", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sda": {Name: "sda", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
|
|
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
|
|
}
|
|
|
|
device, ok := findIoDevice("sd", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "sda", device)
|
|
})
|
|
|
|
t.Run("uses highest activity when ambiguous without dominance", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sda": {Name: "sda", ReadBytes: 3000, WriteBytes: 3000, ReadCount: 50, WriteCount: 50},
|
|
"sdb": {Name: "sdb", ReadBytes: 2500, WriteBytes: 2500, ReadCount: 40, WriteCount: 40},
|
|
}
|
|
|
|
device, ok := findIoDevice("sd", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "sda", device)
|
|
})
|
|
|
|
t.Run("matches /dev/-prefixed partition to parent disk", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"nda0": {Name: "nda0", ReadBytes: 1000, WriteBytes: 1000},
|
|
}
|
|
|
|
device, ok := findIoDevice("/dev/nda0p2", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "nda0", device)
|
|
})
|
|
|
|
t.Run("uses deterministic name tie-breaker", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sdb": {Name: "sdb", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
|
|
"sda": {Name: "sda", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
|
|
}
|
|
|
|
device, ok := findIoDevice("sd", ioCounters)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "sda", device)
|
|
})
|
|
}
|
|
|
|
func TestFilesystemMatchesPartitionSetting(t *testing.T) {
|
|
p := disk.PartitionStat{Device: "/dev/ada0p2", Mountpoint: "/"}
|
|
|
|
t.Run("matches mountpoint setting", func(t *testing.T) {
|
|
assert.True(t, filesystemMatchesPartitionSetting("/", p))
|
|
})
|
|
|
|
t.Run("matches exact partition setting", func(t *testing.T) {
|
|
assert.True(t, filesystemMatchesPartitionSetting("ada0p2", p))
|
|
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0p2", p))
|
|
})
|
|
|
|
t.Run("matches prefix-style parent setting", func(t *testing.T) {
|
|
assert.True(t, filesystemMatchesPartitionSetting("ada0", p))
|
|
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0", p))
|
|
})
|
|
|
|
t.Run("does not match unrelated device", func(t *testing.T) {
|
|
assert.False(t, filesystemMatchesPartitionSetting("sda", p))
|
|
assert.False(t, filesystemMatchesPartitionSetting("nvme0n1", p))
|
|
assert.False(t, filesystemMatchesPartitionSetting("", p))
|
|
})
|
|
}
|
|
|
|
func TestMostActiveIoDevice(t *testing.T) {
|
|
t.Run("returns most active device", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"nda0": {Name: "nda0", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
|
|
"nda1": {Name: "nda1", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
|
|
}
|
|
assert.Equal(t, "nda0", mostActiveIoDevice(ioCounters))
|
|
})
|
|
|
|
t.Run("uses deterministic tie-breaker", func(t *testing.T) {
|
|
ioCounters := map[string]disk.IOCountersStat{
|
|
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
|
|
"sda": {Name: "sda", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
|
|
}
|
|
assert.Equal(t, "sda", mostActiveIoDevice(ioCounters))
|
|
})
|
|
|
|
t.Run("returns empty for empty map", func(t *testing.T) {
|
|
assert.Equal(t, "", mostActiveIoDevice(map[string]disk.IOCountersStat{}))
|
|
})
|
|
}
|
|
|
|
func TestIsDockerSpecialMountpoint(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
mountpoint string
|
|
expected bool
|
|
}{
|
|
{name: "hosts", mountpoint: "/etc/hosts", expected: true},
|
|
{name: "resolv", mountpoint: "/etc/resolv.conf", expected: true},
|
|
{name: "hostname", mountpoint: "/etc/hostname", expected: true},
|
|
{name: "root", mountpoint: "/", expected: false},
|
|
{name: "passwd", mountpoint: "/etc/passwd", expected: false},
|
|
{name: "extra-filesystem", mountpoint: "/extra-filesystems/sda1", expected: false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.expected, isDockerSpecialMountpoint(tc.mountpoint))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
|
|
// Set up environment variables
|
|
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
|
|
defer func() {
|
|
if oldEnv != "" {
|
|
os.Setenv("EXTRA_FILESYSTEMS", oldEnv)
|
|
} else {
|
|
os.Unsetenv("EXTRA_FILESYSTEMS")
|
|
}
|
|
}()
|
|
|
|
// Test with custom names
|
|
os.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2")
|
|
|
|
// Mock disk partitions (we'll just test the parsing logic)
|
|
// Since the actual disk operations are system-dependent, we'll focus on the parsing
|
|
testCases := []struct {
|
|
envValue string
|
|
expectedFs []string
|
|
expectedNames map[string]string
|
|
}{
|
|
{
|
|
envValue: "sda1__my-storage,sdb1__backup-drive",
|
|
expectedFs: []string{"sda1", "sdb1"},
|
|
expectedNames: map[string]string{
|
|
"sda1": "my-storage",
|
|
"sdb1": "backup-drive",
|
|
},
|
|
},
|
|
{
|
|
envValue: "sda1,nvme0n1p2__fast-ssd",
|
|
expectedFs: []string{"sda1", "nvme0n1p2"},
|
|
expectedNames: map[string]string{
|
|
"nvme0n1p2": "fast-ssd",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run("env_"+tc.envValue, func(t *testing.T) {
|
|
os.Setenv("EXTRA_FILESYSTEMS", tc.envValue)
|
|
|
|
// Create mock partitions that would match our test cases
|
|
partitions := []disk.PartitionStat{}
|
|
for _, fs := range tc.expectedFs {
|
|
if strings.HasPrefix(fs, "/dev/") {
|
|
partitions = append(partitions, disk.PartitionStat{
|
|
Device: fs,
|
|
Mountpoint: fs,
|
|
})
|
|
} else {
|
|
partitions = append(partitions, disk.PartitionStat{
|
|
Device: "/dev/" + fs,
|
|
Mountpoint: "/" + fs,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test the parsing logic by calling the relevant part
|
|
// We'll create a simplified version to test just the parsing
|
|
extraFilesystems := tc.envValue
|
|
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
|
|
// Parse the entry
|
|
fsEntry = strings.TrimSpace(fsEntry)
|
|
var fs, customName string
|
|
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
|
fs = strings.TrimSpace(parts[0])
|
|
customName = strings.TrimSpace(parts[1])
|
|
} else {
|
|
fs = fsEntry
|
|
}
|
|
|
|
// Verify the device is in our expected list
|
|
assert.Contains(t, tc.expectedFs, fs, "parsed device should be in expected list")
|
|
|
|
// Check if custom name should exist
|
|
if expectedName, exists := tc.expectedNames[fs]; exists {
|
|
assert.Equal(t, expectedName, customName, "custom name should match expected")
|
|
} else {
|
|
assert.Empty(t, customName, "custom name should be empty when not expected")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFsStatsWithCustomNames(t *testing.T) {
|
|
// Test that FsStats properly stores custom names
|
|
fsStats := &system.FsStats{
|
|
Mountpoint: "/mnt/storage",
|
|
Name: "my-custom-storage",
|
|
DiskTotal: 100.0,
|
|
DiskUsed: 50.0,
|
|
}
|
|
|
|
assert.Equal(t, "my-custom-storage", fsStats.Name)
|
|
assert.Equal(t, "/mnt/storage", fsStats.Mountpoint)
|
|
assert.Equal(t, 100.0, fsStats.DiskTotal)
|
|
assert.Equal(t, 50.0, fsStats.DiskUsed)
|
|
}
|
|
|
|
func TestExtraFsKeyGeneration(t *testing.T) {
|
|
// Test the logic for generating ExtraFs keys with custom names
|
|
testCases := []struct {
|
|
name string
|
|
deviceName string
|
|
customName string
|
|
expectedKey string
|
|
}{
|
|
{
|
|
name: "with custom name",
|
|
deviceName: "sda1",
|
|
customName: "my-storage",
|
|
expectedKey: "my-storage",
|
|
},
|
|
{
|
|
name: "without custom name",
|
|
deviceName: "sda1",
|
|
customName: "",
|
|
expectedKey: "sda1",
|
|
},
|
|
{
|
|
name: "empty custom name falls back to device",
|
|
deviceName: "nvme0n1p2",
|
|
customName: "",
|
|
expectedKey: "nvme0n1p2",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Simulate the key generation logic from agent.go
|
|
key := tc.deviceName
|
|
if tc.customName != "" {
|
|
key = tc.customName
|
|
}
|
|
assert.Equal(t, tc.expectedKey, key)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiskUsageCaching(t *testing.T) {
|
|
t.Run("caching disabled updates all filesystems", func(t *testing.T) {
|
|
agent := &Agent{
|
|
fsStats: map[string]*system.FsStats{
|
|
"sda": {Root: true, Mountpoint: "/"},
|
|
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
|
|
},
|
|
diskUsageCacheDuration: 0, // caching disabled
|
|
}
|
|
|
|
var stats system.Stats
|
|
agent.updateDiskUsage(&stats)
|
|
|
|
// Both should be updated (non-zero values from disk.Usage)
|
|
// Root stats should be populated in systemStats
|
|
assert.True(t, agent.lastDiskUsageUpdate.IsZero() || !agent.lastDiskUsageUpdate.IsZero(),
|
|
"lastDiskUsageUpdate should be set when caching is disabled")
|
|
})
|
|
|
|
t.Run("caching enabled always updates root filesystem", func(t *testing.T) {
|
|
agent := &Agent{
|
|
fsStats: map[string]*system.FsStats{
|
|
"sda": {Root: true, Mountpoint: "/", DiskTotal: 100, DiskUsed: 50},
|
|
"sdb": {Root: false, Mountpoint: "/mnt/storage", DiskTotal: 200, DiskUsed: 100},
|
|
},
|
|
diskUsageCacheDuration: 1 * time.Hour,
|
|
lastDiskUsageUpdate: time.Now(), // cache is fresh
|
|
}
|
|
|
|
// Store original extra fs values
|
|
originalExtraTotal := agent.fsStats["sdb"].DiskTotal
|
|
originalExtraUsed := agent.fsStats["sdb"].DiskUsed
|
|
|
|
var stats system.Stats
|
|
agent.updateDiskUsage(&stats)
|
|
|
|
// Root should be updated (systemStats populated from disk.Usage call)
|
|
// We can't easily check if disk.Usage was called, but we verify the flow works
|
|
|
|
// Extra filesystem should retain cached values (not reset)
|
|
assert.Equal(t, originalExtraTotal, agent.fsStats["sdb"].DiskTotal,
|
|
"extra filesystem DiskTotal should be unchanged when cached")
|
|
assert.Equal(t, originalExtraUsed, agent.fsStats["sdb"].DiskUsed,
|
|
"extra filesystem DiskUsed should be unchanged when cached")
|
|
})
|
|
|
|
t.Run("first call always updates all filesystems", func(t *testing.T) {
|
|
agent := &Agent{
|
|
fsStats: map[string]*system.FsStats{
|
|
"sda": {Root: true, Mountpoint: "/"},
|
|
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
|
|
},
|
|
diskUsageCacheDuration: 1 * time.Hour,
|
|
// lastDiskUsageUpdate is zero (first call)
|
|
}
|
|
|
|
var stats system.Stats
|
|
agent.updateDiskUsage(&stats)
|
|
|
|
// After first call, lastDiskUsageUpdate should be set
|
|
assert.False(t, agent.lastDiskUsageUpdate.IsZero(),
|
|
"lastDiskUsageUpdate should be set after first call")
|
|
})
|
|
|
|
t.Run("expired cache updates extra filesystems", func(t *testing.T) {
|
|
agent := &Agent{
|
|
fsStats: map[string]*system.FsStats{
|
|
"sda": {Root: true, Mountpoint: "/"},
|
|
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
|
|
},
|
|
diskUsageCacheDuration: 1 * time.Millisecond,
|
|
lastDiskUsageUpdate: time.Now().Add(-1 * time.Second), // cache expired
|
|
}
|
|
|
|
var stats system.Stats
|
|
agent.updateDiskUsage(&stats)
|
|
|
|
// lastDiskUsageUpdate should be refreshed since cache expired
|
|
assert.True(t, time.Since(agent.lastDiskUsageUpdate) < time.Second,
|
|
"lastDiskUsageUpdate should be refreshed when cache expires")
|
|
})
|
|
}
|
|
|
|
func TestHasSameDiskUsage(t *testing.T) {
|
|
const toleranceBytes uint64 = 16 * 1024 * 1024
|
|
|
|
t.Run("returns true when totals and usage are equal", func(t *testing.T) {
|
|
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
|
|
b := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
|
|
assert.True(t, hasSameDiskUsage(a, b))
|
|
})
|
|
|
|
t.Run("returns true within tolerance", func(t *testing.T) {
|
|
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
|
|
b := &disk.UsageStat{
|
|
Total: a.Total + toleranceBytes - 1,
|
|
Used: a.Used - toleranceBytes + 1,
|
|
}
|
|
assert.True(t, hasSameDiskUsage(a, b))
|
|
})
|
|
|
|
t.Run("returns false when total exceeds tolerance", func(t *testing.T) {
|
|
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
|
|
b := &disk.UsageStat{
|
|
Total: a.Total + toleranceBytes + 1,
|
|
Used: a.Used,
|
|
}
|
|
assert.False(t, hasSameDiskUsage(a, b))
|
|
})
|
|
|
|
t.Run("returns false for nil or zero total", func(t *testing.T) {
|
|
assert.False(t, hasSameDiskUsage(nil, &disk.UsageStat{Total: 1, Used: 1}))
|
|
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 1, Used: 1}, nil))
|
|
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 0, Used: 0}, &disk.UsageStat{Total: 1, Used: 1}))
|
|
})
|
|
}
|