Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/checks/commands/check_mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) as expected

check_mount mount=/ options=rw,relatime fstype=ext4
OK - 1 mount(s) as expected

check_mount mount=X:
CRITICAL - mount X: not mounted

### Example using NRPE and Naemon

Expand All @@ -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) as expected |
| detail-syntax | mount \${mount} \${issues} |

## Check Specific Arguments
Expand Down
78 changes: 49 additions & 29 deletions pkg/snclient/check_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func init() {
}

type CheckMount struct {
mountPoint string
mountPoints []string
expectOptions string
expectFSType string
}
Expand All @@ -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) as expected",
topSyntax: "${status} - ${problem_list}",
defaultWarning: "issues != ''",
defaultCritical: "issues like 'not mounted'",
Expand All @@ -54,17 +54,25 @@ 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) as expected

check_mount mount=/ options=rw,relatime fstype=ext4
OK - 1 mount(s) as expected

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))
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 {
Expand Down Expand Up @@ -121,16 +129,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)
}
}

Expand All @@ -147,42 +157,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, ","),
Expand All @@ -193,3 +204,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
}
67 changes: 65 additions & 2 deletions pkg/snclient/check_mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) as expected", "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) as expected", "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, CheckExitOK, res.State, "state OK")
assert.Regexp(t, `OK - [\d]+ mount\(s\) as expected`, 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\) as expected`, 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) as expected", "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) as expected", "output matches")
}

StopTestAgent(t, snc)
}
9 changes: 5 additions & 4 deletions pkg/snclient/check_mount_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"])
Expand Down
38 changes: 38 additions & 0 deletions pkg/snclient/check_mount_windows_test.go
Original file line number Diff line number Diff line change
@@ -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}", "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)
}