diff --git a/docs/checks/commands/check_mount.md b/docs/checks/commands/check_mount.md index be807fc3..2bbf9947 100644 --- a/docs/checks/commands/check_mount.md +++ b/docs/checks/commands/check_mount.md @@ -20,8 +20,14 @@ Checks the status for a mounted filesystem ### Default Check - check_mount mount=/ options=rw,relatime fstype=ext4 - OK - mounts are as expected + check_mount + OK - 3 mounts(s) found + + check_mount mount=/ options=rw,relatime fstype=ext4 + OK - 1 mount(s) found + + check_mount mount=X: + CRITICAL - mount X: not mounted ### Example using NRPE and Naemon @@ -48,7 +54,7 @@ Naemon Config | empty-state | 3 (UNKNOWN) | | empty-syntax | check_mount failed to find anything with this filter. | | top-syntax | \${status} - \${problem_list} | -| ok-syntax | \${status} - mounts are as expected | +| ok-syntax | \${status} - \${count} mount(s) found | | detail-syntax | mount \${mount} \${issues} | ## Check Specific Arguments diff --git a/pkg/snclient/check_mount.go b/pkg/snclient/check_mount.go index 48e68bd0..b48e7575 100644 --- a/pkg/snclient/check_mount.go +++ b/pkg/snclient/check_mount.go @@ -16,7 +16,7 @@ func init() { } type CheckMount struct { - mountPoint string + mountPoints []string expectOptions string expectFSType string } @@ -35,12 +35,12 @@ func (l *CheckMount) Build() *CheckData { State: CheckExitOK, }, args: map[string]CheckArgument{ - "mount": {value: &l.mountPoint, description: "The mount point to check"}, + "mount": {value: &l.mountPoints, description: "The mount point to check"}, "options": {value: &l.expectOptions, description: "The mount options to expect"}, "fstype": {value: &l.expectFSType, description: "The fstype to expect"}, }, detailSyntax: "mount ${mount} ${issues}", - okSyntax: "${status} - mounts are as expected", + okSyntax: "${status} - ${count} mount(s) found", topSyntax: "${status} - ${problem_list}", defaultWarning: "issues != ''", defaultCritical: "issues like 'not mounted'", @@ -54,17 +54,29 @@ func (l *CheckMount) Build() *CheckData { {name: "issues", description: "Issues found"}, }, exampleDefault: ` - check_mount mount=/ options=rw,relatime fstype=ext4 - OK - mounts are as expected + check_mount + OK - 3 mounts(s) found + + check_mount mount=/ options=rw,relatime fstype=ext4 + OK - 1 mount(s) found + + check_mount mount=X: + CRITICAL - mount X: not mounted `, exampleArgs: `'mount=/' 'options=rw,relatime'`, } } +//nolint:funlen // no need to split this up func (l *CheckMount) Check(ctx context.Context, _ *Agent, check *CheckData, _ []Argument) (*CheckResult, error) { - if len(l.mountPoint) > 1 { - l.mountPoint = strings.TrimSuffix(l.mountPoint, string(os.PathSeparator)) + if check.output != OutputInventory && len(l.mountPoints) == 0 && l.expectOptions == "" && l.expectFSType == "" { + return nil, fmt.Errorf("must specify at least one of mount/options/fstype") } + + for idx := range l.mountPoints { + l.mountPoints[idx] = trimTrailingSeparator(l.mountPoints[idx]) + } + partitionMap := map[string]bool{} partitions, err := l.getDrives(ctx, partitionMap) if err != nil { @@ -121,16 +133,18 @@ func (l *CheckMount) Check(ctx context.Context, _ *Agent, check *CheckData, _ [] } // check if a mountpoint was supplied but not yet found - if l.mountPoint != "" { - if _, ok := partitionMap[l.mountPoint]; !ok { - entry := map[string]string{ - "mount": l.mountPoint, - "device": "", - "fstype": "", - "options": "", - "issues": "not mounted", + if len(l.mountPoints) > 0 { + for _, mountPoint := range l.mountPoints { + if _, ok := partitionMap[mountPoint]; !ok { + entry := map[string]string{ + "mount": mountPoint, + "device": "", + "fstype": "", + "options": "", + "issues": "not mounted", + } + check.listData = append(check.listData, entry) } - check.listData = append(check.listData, entry) } } @@ -147,42 +161,43 @@ func (l *CheckMount) getDrives(ctx context.Context, partitionMap map[string]bool for i := range partitions { partition := partitions[i] - partitionMap[partition.Mountpoint] = true - if l.mountPoint != "" { - if partition.Mountpoint != l.mountPoint { - log.Tracef("skipped mountpoint: %s - not matching mount argument", partition.Mountpoint) + mountpoint := trimTrailingSeparator(partition.Mountpoint) + partitionMap[mountpoint] = true + if len(l.mountPoints) > 0 { + if !slices.Contains(l.mountPoints, mountpoint) { + log.Tracef("skipped mountpoint: %s - not matching mount argument", mountpoint) continue } } else { // skip internal filesystems if slices.Contains(excludes, partition.Fstype) { - log.Tracef("skipped mountpoint: %s - fstype %s is excluded", partition.Mountpoint, partition.Fstype) + log.Tracef("skipped mountpoint: %s - fstype %s is excluded", mountpoint, partition.Fstype) continue } // skip some know internal locations switch { - case strings.HasPrefix(partition.Mountpoint, "/run"), - strings.HasPrefix(partition.Mountpoint, "/proc"), - strings.HasPrefix(partition.Mountpoint, "/sys"), - strings.HasPrefix(partition.Mountpoint, "/dev"): + case strings.HasPrefix(mountpoint, "/run"), + strings.HasPrefix(mountpoint, "/proc"), + strings.HasPrefix(mountpoint, "/sys"), + strings.HasPrefix(mountpoint, "/dev"): - log.Tracef("skipped mountpoint: %s - prefix matched internal system mounts", partition.Mountpoint) + log.Tracef("skipped mountpoint: %s - prefix matched internal system mounts", mountpoint) continue } } - if partition.Fstype == "" && partition.Device == "" && partition.Mountpoint == "" { - log.Tracef("skipped mountpoint: %s - empty device, fstype and mountpoint", partition.Mountpoint) + if partition.Fstype == "" && partition.Device == "" && mountpoint == "" { + log.Tracef("skipped mountpoint: %s - empty device, fstype and mountpoint", mountpoint) continue } device := utils.ReplaceCommonPasswordPattern(partition.Device) entry := map[string]string{ - "mount": partition.Mountpoint, + "mount": mountpoint, "device": device, "fstype": partition.Fstype, "options": strings.Join(partition.Opts, ","), @@ -193,3 +208,12 @@ func (l *CheckMount) getDrives(ctx context.Context, partitionMap map[string]bool return drives, nil } + +// trimTrailingSeparator removes a trailing path separator, but keeps a single-character mountpoint like "/" or "C:" intact so that it stays a valid root path. +func trimTrailingSeparator(path string) string { + if len(path) > 1 { + return strings.TrimSuffix(path, string(os.PathSeparator)) + } + + return path +} diff --git a/pkg/snclient/check_mount_test.go b/pkg/snclient/check_mount_test.go index 69c50976..49b01ff5 100644 --- a/pkg/snclient/check_mount_test.go +++ b/pkg/snclient/check_mount_test.go @@ -21,14 +21,77 @@ func TestMount(t *testing.T) { res = snc.RunCheck("check_mount", []string{"mount=/"}) } assert.Equalf(t, CheckExitOK, res.State, "state OK") - assert.Contains(t, string(res.BuildPluginOutput()), "OK - mounts are as expected", "output matches") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - 1 mount(s) found", "output matches") inv, err := snc.getInventoryEntry(t.Context(), "check_mount") require.NoError(t, err) require.NotEmptyf(t, inv, "expected mounts list to be non-empty") res = snc.RunCheck("check_mount", []string{"mount=" + inv[0]["mount"], "options=" + inv[0]["options"], "fstype=" + inv[0]["fstype"]}) assert.Equalf(t, CheckExitOK, res.State, "state OK") - assert.Contains(t, string(res.BuildPluginOutput()), "OK - mounts are as expected", "output matches") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - 1 mount(s) found", "output matches") + + StopTestAgent(t, snc) +} + +func TestMountNoMountArgument(t *testing.T) { + snc := StartTestAgent(t, "") + + // mount= left empty means all mounts are checked + res := snc.RunCheck("check_mount", []string{}) + assert.Equalf(t, CheckExitUnknown, res.State, "state UNKNOWN") + assert.Equalf(t, "UNKNOWN - must specify at least one of mount/options/fstype", string(res.BuildPluginOutput()), "output matches") + + StopTestAgent(t, snc) +} + +func TestMountMultipleMounts(t *testing.T) { + snc := StartTestAgent(t, "") + + inv, err := snc.getInventoryEntry(t.Context(), "check_mount") + require.NoError(t, err) + require.NotEmptyf(t, inv, "expected mounts list to be non-empty") + + realMounts := []string{} + for _, entry := range inv { + if entry["mount"] != "" { + realMounts = append(realMounts, entry["mount"]) + } + } + require.NotEmptyf(t, realMounts, "expected at least one mount") + + // checking multiple existing mounts at once must be ok + args := make([]string, 0, min(len(realMounts), 3)+1) + for _, mount := range realMounts[:min(len(realMounts), 3)] { + args = append(args, "mount="+mount) + } + res := snc.RunCheck("check_mount", args) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + assert.Regexp(t, `OK - [\d]+ mount\(s\) found`, string(res.BuildPluginOutput()), "output matches") + + // if one of them is missing, the whole check must raise critical + missing := "not_mounted_xyz" + if runtime.GOOS == "windows" { + missing = `\\?\Volume{ffffffff-ffff-ffff-ffff-ffffffffffff}` + } + res = snc.RunCheck("check_mount", append(args, "mount="+missing)) + assert.Equalf(t, CheckExitCritical, res.State, "state Critical") + assert.Contains(t, string(res.BuildPluginOutput()), "mount "+missing+" not mounted", "output matches") + + StopTestAgent(t, snc) +} + +func TestMountSingleSpecifiedMount(t *testing.T) { + snc := StartTestAgent(t, "") + + if runtime.GOOS == "windows" { + res := snc.RunCheck("check_mount", []string{"mount=C:\\"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - 1 mount(s) found", "output matches") + } else { + res := snc.RunCheck("check_mount", []string{"mount=/"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - 1 mount(s) found", "output matches") + } StopTestAgent(t, snc) } diff --git a/pkg/snclient/check_mount_windows.go b/pkg/snclient/check_mount_windows.go index bc205369..20966736 100644 --- a/pkg/snclient/check_mount_windows.go +++ b/pkg/snclient/check_mount_windows.go @@ -2,9 +2,7 @@ package snclient import ( "context" - "os" "slices" - "strings" ) // getVolumes retrieves volumes and their details, excluding specified partitions, and returns a list of drives and any potential errors. @@ -26,16 +24,19 @@ func (l *CheckMount) getVolumes(ctx context.Context, check *CheckData, partition continue } } - mountPoint := strings.TrimSuffix(partition["drive_or_id"], string(os.PathSeparator)) + + mountPoint := trimTrailingSeparator(partition["drive_or_name"]) if _, ok := partitionMap[mountPoint]; ok { continue } + partitionMap[mountPoint] = true - if l.mountPoint != "" && mountPoint != l.mountPoint { + if len(l.mountPoints) > 0 && !slices.Contains(l.mountPoints, mountPoint) { log.Tracef("skipped mountpoint: %s - not matching mount argument", mountPoint) continue } + // skip internal filesystems if slices.Contains(excludes, partition["fstype"]) { log.Tracef("skipped mountpoint: %s - fstype %s is excluded", mountPoint, partition["fstype"]) diff --git a/pkg/snclient/check_mount_windows_test.go b/pkg/snclient/check_mount_windows_test.go new file mode 100644 index 00000000..24877aac --- /dev/null +++ b/pkg/snclient/check_mount_windows_test.go @@ -0,0 +1,38 @@ +//go:build windows + +package snclient + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMountWindowsNoDuplicateEntries makes sure a drive that is reported by both +// the partition discovery (getDrives) and +// the volume discovery (getVolumes) +// is only listed once in the output. +func TestMountWindowsNoDuplicateEntries(t *testing.T) { + snc := StartTestAgent(t, "") + + // force every entry into the problem list so all mounts end up in the output + res := snc.RunCheck("check_mount", []string{"detail-syntax=mount=${mount}", "fstype=NTFS", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + output := string(res.BuildPluginOutput()) + + mountRe := regexp.MustCompile(`mount=(\S+)`) + mountSeen := map[string]int{} + for _, match := range mountRe.FindAllStringSubmatch(output, -1) { + mountSeen[match[1]]++ + } + + require.NotEmptyf(t, mountSeen, "expected at least one mount in output") + + for mount, count := range mountSeen { + assert.Equalf(t, 1, count, "mount %s reported %d times, expected exactly once", mount, count) + } + + StopTestAgent(t, snc) +}