diff --git a/agent/disk.go b/agent/disk.go index 9881c81dc..1f3bf8e11 100644 --- a/agent/disk.go +++ b/agent/disk.go @@ -97,21 +97,41 @@ func isDockerSpecialMountpoint(mountpoint string) bool { return false } +// evalSymlinks resolves device symlinks; it is a seam so tests can fake the +// /dev topology (e.g. /dev/vg/lv -> /dev/dm-N created by udev for LVM). +var evalSymlinks = filepath.EvalSymlinks + // registerFilesystemStats resolves the tracked key and stats payload for a // filesystem before it is inserted into fsStats. func registerFilesystemStats(existing map[string]*system.FsStats, device, mountpoint string, root bool, customName string, ctx fsRegistrationContext) (string, *system.FsStats, bool) { key := device + resolvedKey := "" if !ctx.isWindows { key = filepath.Base(device) + // Device-mapper mounts appear as symlinked paths like /dev/vg/lv whose + // base name matches neither the diskstats name (dm-N) nor the dm label + // (vg-lv); the resolved target's base is one of those existing names. + // Bare names (folder devices, ZFS datasets) are skipped because they + // would resolve relative to the agent's working directory. + if filepath.IsAbs(device) { + if resolved, err := evalSymlinks(device); err == nil { + if base := filepath.Base(resolved); base != key { + resolvedKey = base + } + } + } } if root { // Try to map root device to a diskIoCounters entry. First checks for an // exact key match, then uses findIoDevice for normalized / prefix-based - // matching (e.g. nda0p2 -> nda0), and finally falls back to FILESYSTEM. + // matching (e.g. nda0p2 -> nda0) and the symlink-resolved device name, + // and finally falls back to FILESYSTEM. if _, ioMatch := ctx.diskIoCounters[key]; !ioMatch { if matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match { key = matchedKey + } else if matchedKey, match := findIoDevice(resolvedKey, ctx.diskIoCounters); match { + key = matchedKey } else if ctx.filesystem != "" { if matchedKey, match := findIoDevice(ctx.filesystem, ctx.diskIoCounters); match { key = matchedKey @@ -137,6 +157,8 @@ func registerFilesystemStats(existing map[string]*system.FsStats, device, mountp if _, ioMatch = ctx.diskIoCounters[key]; !ioMatch { if matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match { key = matchedKey + } else if matchedKey, match := findIoDevice(resolvedKey, ctx.diskIoCounters); match { + key = matchedKey } } } diff --git a/agent/disk_test.go b/agent/disk_test.go index 8ee0c12d1..bba4b9f46 100644 --- a/agent/disk_test.go +++ b/agent/disk_test.go @@ -273,6 +273,175 @@ func TestBuildFsStatRegistration(t *testing.T) { assert.Empty(t, key) assert.Nil(t, stats) }) + + t.Run("maps lvm symlinked device to io device through resolved name", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + if path == "/dev/vg1/volume_1" { + return "/dev/dm-1", nil + } + return path, nil + }) + + key, stats, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "/dev/vg1/volume_1", + "/volume1", + false, + "", + fsRegistrationContext{ + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "dm-0": {Name: "dm-0", Label: "vg1-syno_vg_reserved_area"}, + "dm-1": {Name: "dm-1", Label: "vg1-volume_1"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "dm-1", key) + assert.Equal(t, "/volume1", stats.Mountpoint) + }) + + t.Run("maps lvm device through resolved mapper label", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + if path == "/dev/vg1/volume_1" { + return "/dev/mapper/vg1-volume_1", nil + } + return path, nil + }) + + key, _, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "/dev/vg1/volume_1", + "/volume1", + false, + "", + fsRegistrationContext{ + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "dm-1": {Name: "dm-1", Label: "vg1-volume_1"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "dm-1", key) + }) + + t.Run("maps lvm root device through resolved name", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + if path == "/dev/vg1/volume_1" { + return "/dev/dm-1", nil + } + return path, nil + }) + + key, stats, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "/dev/vg1/volume_1", + "/", + true, + "", + fsRegistrationContext{ + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "dm-1": {Name: "dm-1", Label: "vg1-volume_1"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "dm-1", key) + assert.True(t, stats.Root) + }) + + t.Run("resolved device wins over filesystem fallback", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + if path == "/dev/vg1/volume_1" { + return "/dev/dm-1", nil + } + return path, nil + }) + + key, _, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "/dev/vg1/volume_1", + "/", + true, + "", + fsRegistrationContext{ + filesystem: "sda", + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "dm-1": {Name: "dm-1", Label: "vg1-volume_1"}, + "sda": {Name: "sda"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "dm-1", key) + }) + + t.Run("keeps base name when symlink resolution fails", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + return "", os.ErrNotExist + }) + + key, _, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "/dev/vg1/volume_1", + "/volume1", + false, + "", + fsRegistrationContext{ + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "dm-1": {Name: "dm-1", Label: "vg1-volume_1"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "volume_1", key) + }) + + t.Run("does not resolve symlinks for relative device names", func(t *testing.T) { + setEvalSymlinks(t, func(path string) (string, error) { + return "/dev/sdb1", nil + }) + + key, _, ok := registerFilesystemStats( + map[string]*system.FsStats{}, + "data", + "/mnt/data", + false, + "", + fsRegistrationContext{ + isWindows: false, + efPath: "/extra-filesystems", + diskIoCounters: map[string]disk.IOCountersStat{ + "sdb1": {Name: "sdb1"}, + }, + }, + ) + + assert.True(t, ok) + assert.Equal(t, "data", key) + }) +} + +// setEvalSymlinks swaps the device-symlink resolver for the duration of a test. +func setEvalSymlinks(t *testing.T, fn func(string) (string, error)) { + t.Helper() + old := evalSymlinks + evalSymlinks = fn + t.Cleanup(func() { evalSymlinks = old }) } func TestAddConfiguredRootFs(t *testing.T) {