Skip to content

Commit

Permalink
check_drivesize: do not use parent folder. Use folder=...if required
Browse files Browse the repository at this point in the history
  • Loading branch information
sni committed Mar 14, 2024
1 parent 4dc9813 commit c99e8ad
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 36 deletions.
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ next:
- improve wmi stability
- add regexp replacement macro post processor
- add if/else conditionals to *-syntax templates
- check_drivesize: do not use parent folder. Use folder=...if required

0.19 Wed Feb 28 00:09:39 CET 2024
- write startup errors to default logfile
Expand Down
8 changes: 7 additions & 1 deletion docs/checks/commands/check_drivesize.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ Check drive including inodes:
check_drivesize drive=/ warn="used > 90%" "crit=used > 95%" "warn=inodes > 90%" "crit=inodes > 95%"
OK - All 1 drive(s) are ok |'/ used'=307515822080B;440613398938;465091921101;0;489570443264 '/ used %'=62.8%;90;95;0;100 '/ inodes'=12.1%;90;95;0;100

Check folder, no matter if its a mountpoint itself or not:

check_drivesize folder=/tmp show-all
OK - /tmp 280.155 GiB/455.948 GiB (64.7%) |...

### Example using NRPE and Naemon

Naemon Config
Expand Down Expand Up @@ -61,8 +66,9 @@ Naemon Config

| Argument | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| drive | The drives to check |
| drive | The drives to check, ex.: c: or / |
| exclude | List of drives to exclude from check |
| folder | The folders to check (parent mountpoint) |
| freespace-ignore-reserved | Don't account root-reserved blocks into freespace, default: true |
| ignore-unreadable | Deprecated, use filter instead |
| magic | Magic number for use with scaling drive sizes. Note there is also a more generic magic factor in the perf-config option. |
Expand Down
36 changes: 33 additions & 3 deletions pkg/snclient/check_drivesize.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package snclient
import (
"context"
"fmt"
"maps"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -56,6 +57,7 @@ func defaultExcludedFsTypes() []string {

type CheckDrivesize struct {
drives []string
folders []string
excludes []string
total bool
magic float64
Expand All @@ -68,7 +70,8 @@ type CheckDrivesize struct {
func NewCheckDrivesize() CheckHandler {
return &CheckDrivesize{
magic: 1,
drives: []string{"all"},
drives: []string{},
folders: []string{},
freespaceIgnoreReserved: true,
}
}
Expand All @@ -83,7 +86,8 @@ func (l *CheckDrivesize) Build() *CheckData {
State: CheckExitOK,
},
args: map[string]CheckArgument{
"drive": {value: &l.drives, isFilter: true, description: "The drives to check"},
"drive": {value: &l.drives, isFilter: true, description: "The drives to check, ex.: c: or /"},
"folder": {value: &l.folders, isFilter: true, description: "The folders to check (parent mountpoint)"},
"exclude": {value: &l.excludes, description: "List of drives to exclude from check"},
"total": {value: &l.total, description: "Include the total of all matching drives"},
"magic": {value: &l.magic, description: "Magic number for use with scaling drive sizes. " +
Expand Down Expand Up @@ -144,10 +148,21 @@ func (l *CheckDrivesize) Check(ctx context.Context, snc *Agent, check *CheckData
check.SetDefaultThresholdUnit("%", []string{"used_pct", "used", "free", "free_pct", "inodes", "inodes_free"})
check.ExpandThresholdUnit([]string{"k", "m", "g", "p", "e", "ki", "mi", "gi", "pi", "ei"}, "B", []string{"used", "free"})

requiredDisks, err := l.getRequiredDisks(l.drives)
if len(l.drives)+len(l.folders) == 0 {
l.drives = []string{"all"}
}
requiredDisks := map[string]map[string]string{}
drives, err := l.getRequiredDisks(l.drives, false)
if err != nil {
return nil, err
}
maps.Copy(requiredDisks, drives)

folders, err := l.getRequiredDisks(l.folders, true)
if err != nil {
return nil, err
}
maps.Copy(requiredDisks, folders)

// sort by drive / id
keys := make([]string, 0, len(requiredDisks))
Expand All @@ -161,6 +176,12 @@ func (l *CheckDrivesize) Check(ctx context.Context, snc *Agent, check *CheckData
if l.isExcluded(drive, l.excludes) {
continue
}
if _, ok := drive["_error"]; ok {
// already failed
check.listData = append(check.listData, drive)

continue
}
l.addDiskDetails(ctx, check, drive, l.magic)
check.listData = append(check.listData, drive)
}
Expand Down Expand Up @@ -191,6 +212,15 @@ func (l *CheckDrivesize) Check(ctx context.Context, snc *Agent, check *CheckData
return check.Finalize()
}

func (l *CheckDrivesize) driveEntry(drive string) map[string]string {
return map[string]string{
"id": "",
"drive": drive,
"drive_or_id": drive,
"drive_or_name": drive,
}
}

func (l *CheckDrivesize) isExcluded(drive map[string]string, excludes []string) bool {
for _, exclude := range excludes {
if strings.EqualFold(exclude, drive["drive"]) {
Expand Down
30 changes: 18 additions & 12 deletions pkg/snclient/check_drivesize_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package snclient
import (
"context"
"fmt"
"os"
"strings"

"pkg/humanize"
Expand All @@ -26,10 +27,15 @@ Check drive including inodes:
check_drivesize drive=/ warn="used > 90%" "crit=used > 95%" "warn=inodes > 90%" "crit=inodes > 95%"
OK - All 1 drive(s) are ok |'/ used'=307515822080B;440613398938;465091921101;0;489570443264 '/ used %'=62.8%;90;95;0;100 '/ inodes'=12.1%;90;95;0;100
Check folder, no matter if its a mountpoint itself or not:
check_drivesize folder=/tmp show-all
OK - /tmp 280.155 GiB/455.948 GiB (64.7%) |...
`
}

func (l *CheckDrivesize) getRequiredDisks(drives []string) (requiredDisks map[string]map[string]string, err error) {
func (l *CheckDrivesize) getRequiredDisks(drives []string, parentFallback bool) (requiredDisks map[string]map[string]string, err error) {
// create map of required disks/volumes with "drive_or_id" as primary key
requiredDisks = map[string]map[string]string{}

Expand All @@ -44,7 +50,7 @@ func (l *CheckDrivesize) getRequiredDisks(drives []string) (requiredDisks map[st
// nothing appropriate on linux
default:
l.hasCustomPath = true
err := l.setCustomPath(drive, requiredDisks)
err := l.setCustomPath(drive, requiredDisks, parentFallback)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -76,12 +82,13 @@ func (l *CheckDrivesize) setDisks(requiredDisks map[string]map[string]string) (e
return
}

func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]map[string]string) (err error) {
func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) {
// make sure path exists
if err = utils.IsFolder(drive); err != nil {
_, err = os.Stat(drive)
if err != nil && os.IsNotExist(err) {
log.Debugf("%s: %s", drive, err.Error())

return fmt.Errorf("failed to find disk partition")
return fmt.Errorf("failed to find disk partition: %s", err.Error())
}

// try to find closest matching mount
Expand All @@ -94,13 +101,15 @@ func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]ma
var match *map[string]string
for i := range availMounts {
vol := availMounts[i]
if vol["drive"] != "" && strings.HasPrefix(drive, vol["drive"]) {
if parentFallback && vol["drive"] != "" && strings.HasPrefix(drive, vol["drive"]) {
if match == nil || len((*match)["drive"]) < len(vol["drive"]) {
match = &vol
}
}
// direct match, no need to search further
if drive == vol["drive"] {
match = &vol

break
}
}
Expand All @@ -112,12 +121,9 @@ func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]ma
}

// add anyway to generate an error later with more default values filled in
requiredDisks[drive] = map[string]string{
"id": "",
"drive": drive,
"drive_or_id": drive,
"drive_or_name": drive,
}
entry := l.driveEntry(drive)
entry["_error"] = fmt.Sprintf("%s not mounted", drive)
requiredDisks[drive] = entry

return nil
}
Expand Down
16 changes: 9 additions & 7 deletions pkg/snclient/check_drivesize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ func TestCheckDrivesize(t *testing.T) {
assert.Equalf(t, CheckExitUnknown, res.State, "state unknown")
assert.Contains(t, string(res.BuildPluginOutput()), "UNKNOWN - failed to find disk partition", "output matches")

res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=/tmp/"})
// must not work, folder is not a mountpoint
tmpFolder := t.TempDir()
res = snc.RunCheck("check_drivesize", []string{"warn=inodes>100%", "crit=inodes>100%", "drive=" + tmpFolder})
assert.Equalf(t, CheckExitUnknown, res.State, "state UNKNOWN")
assert.Contains(t, string(res.BuildPluginOutput()), `not mounted`, "output matches")

// must work with folder argument instead of drive
res = snc.RunCheck("check_drivesize", []string{"warn=inodes>100%", "crit=inodes>100%", "folder=" + tmpFolder})
assert.Equalf(t, CheckExitOK, res.State, "state OK")
assert.Contains(t, string(res.BuildPluginOutput()), `OK - All 1 drive`, "output matches")
assert.Contains(t, string(res.BuildPluginOutput()), `/tmp/ used %`, "output matches")

res = snc.RunCheck("check_drivesize", []string{"warn=inodes>100%", "crit=inodes>100%", "drive=/tmp/"})
assert.Equalf(t, CheckExitOK, res.State, "state OK")
assert.Contains(t, string(res.BuildPluginOutput()), `OK - All 1 drive`, "output matches")
assert.Contains(t, string(res.BuildPluginOutput()), `'/tmp/ inodes'=`, "output matches")
assert.Contains(t, string(res.BuildPluginOutput()), `'`+tmpFolder+` inodes'=`, "output matches")

StopTestAgent(t, snc)
}
32 changes: 19 additions & 13 deletions pkg/snclient/check_drivesize_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package snclient
import (
"context"
"fmt"
"os"
"strings"
"syscall"
"unicode"
Expand Down Expand Up @@ -38,11 +39,14 @@ func (l *CheckDrivesize) getDefaultFilter() string {
func (l *CheckDrivesize) getExample() string {
return `
check_drivesize drive=c: show-all
OK - c: 36.801 GiB/63.075 GiB (58.3%) |...
check_drivesize folder=c:\Temp show-all
OK - c: 36.801 GiB/63.075 GiB (58.3%) |...
`
}

func (l *CheckDrivesize) getRequiredDisks(drives []string) (requiredDisks map[string]map[string]string, err error) {
func (l *CheckDrivesize) getRequiredDisks(drives []string, parentFallback bool) (requiredDisks map[string]map[string]string, err error) {
// create map of required disks/volmes with "drive_or_id" as primary key
requiredDisks = map[string]map[string]string{}

Expand All @@ -66,7 +70,7 @@ func (l *CheckDrivesize) getRequiredDisks(drives []string) (requiredDisks map[st
// "c:\" will use the volume
// "c:\path" will use the best matching volume
l.hasCustomPath = true
err := l.setCustomPath(drive, requiredDisks)
err := l.setCustomPath(drive, requiredDisks, parentFallback)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -395,7 +399,7 @@ func (l *CheckDrivesize) setVolumes(requiredDisks map[string]map[string]string)
}
}

func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]map[string]string) (err error) {
func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) {
// match a drive, ex: "c" or "c:"
switch len(drive) {
case 1, 2:
Expand All @@ -419,10 +423,11 @@ func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]ma
}

// make sure path exists
if err = utils.IsFolder(drive); err != nil {
_, err = os.Stat(drive)
if err != nil && os.IsNotExist(err) {
log.Debugf("%s: %s", drive, err.Error())

return fmt.Errorf("failed to find disk partition")
return fmt.Errorf("failed to find disk partition: %s", err.Error())
}

// try to find closes matching volume
Expand All @@ -441,11 +446,16 @@ func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]ma
var match *map[string]string
for i := range availVolumes {
vol := availVolumes[i]
if vol["drive"] != "" && strings.HasPrefix(testDrive+"\\", vol["drive"]) {
if parentFallback && vol["drive"] != "" && strings.HasPrefix(testDrive+"\\", vol["drive"]) {
if match == nil || len((*match)["drive"]) < len(vol["drive"]) {
match = &vol
}
}
if testDrive+"\\" == vol["drive"] {
match = &vol

break
}
}
if match != nil {
requiredDisks[drive] = utils.CloneStringMap(*match)
Expand All @@ -455,13 +465,9 @@ func (l *CheckDrivesize) setCustomPath(drive string, requiredDisks map[string]ma
}

// add anyway to generate an error later with more default values filled in
requiredDisks[drive] = map[string]string{
"id": "",
"drive": drive,
"drive_or_id": drive,
"drive_or_name": drive,
"letter": "",
}
entry := l.driveEntry(drive)
entry["_error"] = fmt.Sprintf("%s not mounted", drive)
requiredDisks[drive] = entry

return nil
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/snclient/check_drivesize_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,20 @@ func TestCheckDrivesize(t *testing.T) {
assert.Contains(t, string(res.BuildPluginOutput()), "OK - C:\\ ", "output matches")
assert.Contains(t, string(res.BuildPluginOutput()), ";99;99.5;0;100", "output matches")

// test all variants of drive names
res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c"})
assert.Equalf(t, CheckExitOK, res.State, "state ok")
res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:"})
assert.Equalf(t, CheckExitOK, res.State, "state ok")
res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\"})
assert.Equalf(t, CheckExitOK, res.State, "state ok")

// must not match
res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\Windows"})
assert.Equalf(t, CheckExitUnknown, res.State, "state UNKNOWN")
assert.Contains(t, string(res.BuildPluginOutput()), `not mounted`, "output matches")

res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "folder=c:\\Windows"})
assert.Equalf(t, CheckExitOK, res.State, "state OK")
assert.Contains(t, string(res.BuildPluginOutput()), `OK - All 1 drive`, "output matches")
assert.Contains(t, string(res.BuildPluginOutput()), `c:\Windows used %`, "output matches")
Expand Down

0 comments on commit c99e8ad

Please sign in to comment.