From 7f8f3e41e70ff5b8ab3d8b1205be3aca5c430788 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 14 Jul 2026 14:11:37 +0200 Subject: [PATCH 1/7] check_drivesize: improve detection of drives out of the drive argument that user can type add tests that check both the status line and perdata labels using regex --- pkg/snclient/check_drivesize_windows.go | 52 ++++++++++++++++++-- pkg/snclient/check_drivesize_windows_test.go | 37 ++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index 724b8182..46e72fb9 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -513,17 +513,55 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map } } - // match a drive, ex: "c" or "c:" + // organize the custom path, so that if it refers to a drive it matches the results from disk discovery + // the form to use looks like this: C:\ + var possibleDrivePath = path + checkAsDrive := true switch len(path) { - case 1, 2: - path = strings.TrimSuffix(path, ":") + ":" + case 1: + if !unicode.IsLetter(rune(path[0])) { + log.Tracef("Custom path has length 1 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) + checkAsDrive = false + } + possibleDrivePath = strings.ToUpper(path) + ":" + "\\" + path += ":" // add a colon to the path, similarly to the nsclient + case 2: + if !unicode.IsLetter(rune(path[0])) { + log.Tracef("Custom path has length 2 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) + checkAsDrive = false + } + if path[1] != ':' { + log.Tracef("Custom path has length 2 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) + checkAsDrive = false + } + possibleDrivePath = strings.ToUpper(path) + "\\" + case 3: + if !unicode.IsLetter(rune(path[0])) { + log.Tracef("Custom path has length 3 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) + checkAsDrive = false + } + if path[1] != ':' { + log.Tracef("Custom path has length 3 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) + checkAsDrive = false + } + if path[2] != '\\' { + log.Tracef("Custom path has length 3 and likely refers to a drive, but third rune is not backslash, path: '%s' ", path) + checkAsDrive = false + } + possibleDrivePath = strings.ToUpper(path) + } + + if checkAsDrive { + log.Tracef("Custom path likely refers to a drive, checking drives, path: '%s' , possibleDrivePath: '%s' ", path, possibleDrivePath) + availDisks := map[string]map[string]string{} err = l.setDrives(availDisks) for driveOrID := range availDisks { - if strings.EqualFold(driveOrID, path+"\\") { + if strings.EqualFold(driveOrID, possibleDrivePath) { requiredDisks[path] = utils.CloneStringMap(availDisks[driveOrID]) requiredDisks[path]["drive"] = path // use name from attributes requiredDisks[path]["drive_or_name"] = path // used in default detail syntax + requiredDisks[path]["drive_or_name_or_id"] = path return nil } @@ -536,7 +574,9 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map } } - // make sure path exists + // at this point, the path is checked if its a drive or a network path. + // if its neither of those, try to open the file/directory + // check for volumes that include the path in their drive _, err = os.Stat(path) if err != nil && os.IsNotExist(err) { log.Debugf("%s: %s", path, err.Error()) @@ -575,6 +615,8 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map if match != nil { requiredDisks[path] = utils.CloneStringMap(*match) requiredDisks[path]["drive"] = path + requiredDisks[path]["drive_or_name"] = path + requiredDisks[path]["drive_or_name_or_id"] = path return nil } diff --git a/pkg/snclient/check_drivesize_windows_test.go b/pkg/snclient/check_drivesize_windows_test.go index 37b7300d..3defb6f6 100644 --- a/pkg/snclient/check_drivesize_windows_test.go +++ b/pkg/snclient/check_drivesize_windows_test.go @@ -43,12 +43,43 @@ func TestCheckDrivesize(t *testing.T) { 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"}) + + // rules for status line and perfdata label: + // use the string user gave as drive, do not change uppercase and lowercase, + // add a colon if its missing + // flip the slash to be a backwards slash + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - c: .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - c: .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - c:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:/", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - c:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:"}) + assert.Regexpf(t, `^OK - C: .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\"}) + assert.Regexpf(t, `^OK - C: .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:\\", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:/", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") // must not match res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\Windows"}) From 19d5687f0f0500b75243569e707fa0a33222a23c Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 14 Jul 2026 14:27:52 +0200 Subject: [PATCH 2/7] citest fixes --- pkg/snclient/check_drivesize_windows.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index 46e72fb9..33af7b8f 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -484,7 +484,7 @@ func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, v // This is used if folders are given // c:/, d:/volume, f:/folder/with/slash // -//nolint:funlen // can not split this function up +//nolint:funlen,gocyclo // can not split this function up, it has to check if its network drive, normal drive, a custom path under a volume etc. func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) { path = strings.ReplaceAll(path, "/", "\\") // if its a network share path, discover existing shares and match it with a drive[remote_path] @@ -515,7 +515,7 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map // organize the custom path, so that if it refers to a drive it matches the results from disk discovery // the form to use looks like this: C:\ - var possibleDrivePath = path + possibleDrivePath := path checkAsDrive := true switch len(path) { case 1: @@ -557,15 +557,18 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map availDisks := map[string]map[string]string{} err = l.setDrives(availDisks) for driveOrID := range availDisks { - if strings.EqualFold(driveOrID, possibleDrivePath) { - requiredDisks[path] = utils.CloneStringMap(availDisks[driveOrID]) - requiredDisks[path]["drive"] = path // use name from attributes - requiredDisks[path]["drive_or_name"] = path // used in default detail syntax - requiredDisks[path]["drive_or_name_or_id"] = path - - return nil + if !strings.EqualFold(driveOrID, possibleDrivePath) { + continue } + + requiredDisks[path] = utils.CloneStringMap(availDisks[driveOrID]) + requiredDisks[path]["drive"] = path // use name from attributes + requiredDisks[path]["drive_or_name"] = path + requiredDisks[path]["drive_or_name_or_id"] = path + + return nil } + if err != nil { // if setDisks had a problem (e.g. bitlocker locked drive) and did not return // the required drive, then pass any possible error on to the caller. otherwise From 5290cbe799e7a94ac39a4220c95874fc2b6e1a7a Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Wed, 15 Jul 2026 16:44:18 +0200 Subject: [PATCH 3/7] check_drivesize: adjust expected output line and perfdata labels on windows and improve functions overall output line always uses the format 'C:\' if its a drive. this includes uppercase conversion, adding colon if necessary, adding backslashes if necessarry, converting slashes to backslashes and compacting slashes perfdata labels append colon if necessary, change existing slashes to backslashes and compact them. perfdata labels are not kept the same due to compatibility reasons these are generated in separate functions now called cleanupPathString and getPerflabelPrefix improvements: windows CheckDrivesize.setDrives , CheckDrivesize.setVolumes , CheckDrivesize.setShares all try to populate the entry data for 'id' , 'drive' , 'drive_or_id' , 'drive_or_name' , 'drive_or_name_or_id' improved some error logs, they were not easily understandable if format strings were empty revise the windows volume discovery, it was causing check to fail due to RDP shared folder --- pkg/snclient/check_drivesize.go | 9 +- pkg/snclient/check_drivesize_windows.go | 311 +++++++++++++------ pkg/snclient/check_drivesize_windows_test.go | 30 +- 3 files changed, 241 insertions(+), 109 deletions(-) diff --git a/pkg/snclient/check_drivesize.go b/pkg/snclient/check_drivesize.go index f1cc5bbe..5282266c 100644 --- a/pkg/snclient/check_drivesize.go +++ b/pkg/snclient/check_drivesize.go @@ -506,13 +506,16 @@ func (l *CheckDrivesize) addDriveSizeDetails(check *CheckData, drive map[string] return } - // volumes without an assigned drive letter have empty drive["drive"] - // use the volume name or id as fallback l.transformDrivePctMetrics(drive["drive_or_name_or_id"], check) l.disableGenerallizedConditionsForDrive(drive["drive_or_name_or_id"], drive, check) - l.addMetrics(drive["drive_or_name_or_id"], drive, check, usage, magic) + metricPrefix := drive["drive_or_name_or_id"] + if perfdataPrefix, perfdataPrefixOk := drive["perflabel_prefix"]; perfdataPrefixOk { + metricPrefix = perfdataPrefix + } + + l.addMetrics(metricPrefix, drive, check, usage, magic) } func (l *CheckDrivesize) getFlagNames(drive map[string]string) []string { diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index 33af7b8f..f196c27e 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" "syscall" "unicode" @@ -52,10 +53,11 @@ func (l *CheckDrivesize) getExample() string { // Volume: A logical abstraction of a storage, formatted with a file system. It can be a virtual file, a RAID disk or one partition -> \\?\Volume{GUID}\ // Drive: This term is not defined well. Here, it means a logical access point with an assigned drive letter. -// For a given path, it may correspond to a mounted disk, like a \\SERVER\SHARENAME being added as K: drive +// For a given path, it may correspond to a mounted disk, like a normal C:\ drive +// Or it could be a network drive like \\SERVER\SHARENAME being mounted to K:\ drive // Checks the given path, resolves the alias and calls the corresponding function that adds the details func (l *CheckDrivesize) getRequiredDrives(paths []string, parentFallback bool) (requiredDrives map[string]map[string]string, err error) { - // create map of required disks/volmes/network_shares with "drive_or_id" as primary key + // create map of required disks/volmes/network_shares with "drive_or_name_or_id" as primary key requiredDrives = map[string]map[string]string{} @@ -265,7 +267,7 @@ func (l *CheckDrivesize) setMediaType(drive map[string]string) error { func (l *CheckDrivesize) setDeviceInfo(drive map[string]string) { driveType, err := GetDriveType(drive["drive_or_id"]) if err != nil { - log.Warnf("Error when getting the drive type of drive %s: %s", drive["drive_or_id"], err.Error()) + log.Warnf("Error when getting the drive type of drive, drive: '%s' , error: %s", drive["drive_or_id"], err.Error()) return } @@ -377,8 +379,10 @@ func (l *CheckDrivesize) setDrives(requiredDrives map[string]map[string]string) entry = make(map[string]string) } entry["drive"] = logicalDrive + entry["name"] = logicalDrive entry["drive_or_id"] = logicalDrive entry["drive_or_name"] = logicalDrive + entry["drive_or_name_or_id"] = logicalDrive entry["letter"] = fmt.Sprintf("%c", logicalDrive[0]) entry["fstype"] = partition.Fstype requiredDrives[logicalDrive] = entry @@ -389,6 +393,7 @@ func (l *CheckDrivesize) setDrives(requiredDrives map[string]map[string]string) // adds all logical volumes to the requiredDisks // network shares are not listed in volumes +// this function populates requiredDrives map func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) { volumeGUIDPaths := []string{} bufLen, err := convert.UInt32E(windows.MAX_PATH + 1) @@ -414,13 +419,15 @@ func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) for { err := windows.FindNextVolume(hndl, &volumeGUIDPathBuffer[0], bufLen) if err != nil { - log.Tracef("FindNextVolume: %s", err.Error()) + log.Tracef("FindNextVolume error, likely iterated every volume, error: %s", err.Error()) break } volumeGUIDPaths = append(volumeGUIDPaths, syscall.UTF16ToString(volumeGUIDPathBuffer)) } + log.Tracef("Found volume GUID paths: %s", strings.Join(volumeGUIDPaths, ", ")) + // Windows syscall findFirstVolume, findNextVolume... give GUID paths to volumes e.g: // "\\\\?\\Volume{a6b8f57e-dac6-4bac-8dc2-fac22cd740cf}\\" // They need to be translated @@ -430,8 +437,9 @@ func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) } } -// this function is used to further process the volume GUID path returned from API call -// takes a GUID path of a volume, finds its path name. it may or may not be mounted directly on a drive letter +// this function is used to further process the volume GUID path returned from Windows API calls +// takes a GUID path of a volume, then finds its path name. +// it may or may not be mounted directly on a drive letter func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, volumeGUIDPath string, buffer []uint16) { volPtr, err := syscall.UTF16PtrFromString(volumeGUIDPath) if err != nil { @@ -440,44 +448,169 @@ func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, v return } returnLen := uint32(0) + bufferLen, err := convert.UInt32E(len(buffer)) if err != nil { log.Warnf("convert.UInt32E: %s", err.Error()) return } + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumepathnamesforvolumenamew // SMB does not support volume management functions // uses GetVolumePathNamesForVolumeNameW err = windows.GetVolumePathNamesForVolumeName(volPtr, &buffer[0], bufferLen, &returnLen) if err != nil { - log.Warnf("Error when calling GetVolumePathNamesForVolumeName: %s: %s", volumeGUIDPath, err.Error()) + log.Warnf("Error when calling GetVolumePathNamesForVolumeName, volumeGUIDPath: '%s' , error: %s", volumeGUIDPath, err.Error()) return } - names := syscall.UTF16ToString(buffer) - // The path name is given with uppercase, convert it. - // names = strings.ToLower(names) - driveOrID := names + volumePathName := syscall.UTF16ToString(buffer) + + if volumePathName == "" { + volumePathName = volumeGUIDPath // if there is no name attached to it, use this as name + } + + // entry attributes + volumeID := volumeGUIDPath + + name := volumePathName + + drive, isDrive, _ := cleanupPathString(volumePathName) + if !isDrive { + drive = "" + } + + driveOrID := drive if driveOrID == "" { - driveOrID = volumeGUIDPath + driveOrID = volumeID + } + + driveOrName := drive + if driveOrName == "" { + driveOrName = name + } + + driveOrNameOrID := driveOrName + if driveOrNameOrID == "" { + driveOrNameOrID = volumeID + } + + letter := "" + if drive != "" { + letter = fmt.Sprintf("%c", drive[0]) + } + + mounted := "0" + if letter != "" { + mounted = "1" } - // only add it if it does not exists - entry, ok := requiredDisks[driveOrID] - if !ok { + + skip := "0" + if mounted == "0" { + skip = "1" + } + + // skip unmounted volumes + // these are generally weird types of items shown in Explorer, some examples: + // RDP session shared folder + // UPnP/DLNA media server + if skip == "1" { + return + } + + // check if there exists an entry + // this prevents adding the same drive multiple times + entry, idAlreadyAdded := requiredDisks[volumeID] + if !idAlreadyAdded { entry = make(map[string]string) } - entry["id"] = volumeGUIDPath - entry["drive"] = names + + entry["id"] = volumeID + entry["drive"] = drive + entry["name"] = name entry["drive_or_id"] = driveOrID - entry["drive_or_name"] = names - entry["letter"] = "" - if names != "" { - entry["letter"] = fmt.Sprintf("%c", names[0]) - } else { - entry["mounted"] = "0" + entry["drive_or_name"] = driveOrName + entry["drive_or_name_or_id"] = driveOrNameOrID + entry["letter"] = letter + entry["mounted"] = mounted + entry["skip"] = skip + + requiredDisks[volumeID] = entry +} + +// The perflabel prefix situation it complicated due to compatibility reasons +func getPerflabelPrefix(path string) (perflabelPrefix string, err error) { + if path == "" { + return "", fmt.Errorf("path to get perflabel for is empty") + } + + perflabelPrefix = filepath.FromSlash(path) + + sep := string(os.PathSeparator) + for strings.Contains(perflabelPrefix, sep+sep) { + perflabelPrefix = strings.ReplaceAll(perflabelPrefix, sep+sep, sep) + } + + if len(perflabelPrefix) == 1 { + perflabelPrefix += ":" + } + + return perflabelPrefix, nil +} + +// cleans up an absolute path +// changes all slashes to be backwards +// compacts slashes that come one after another +// changes the drive letter to be uppercase, if present +// adds a colon after the drive letter if its not present +// adds a backward slash after colon if not present +func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error) { + if path == "" { + return "", false, fmt.Errorf("path to cleanup is empty") + } + + cleanedPath = filepath.FromSlash(path) + isDrive = true + + sep := string(os.PathSeparator) + for strings.Contains(cleanedPath, sep+sep) { + cleanedPath = strings.ReplaceAll(cleanedPath, sep+sep, sep) + } + + if len(cleanedPath) >= 1 && !unicode.IsLetter(rune(path[0])) { + log.Tracef("Custom path has length 1 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) + isDrive = false + } + + if len(cleanedPath) >= 1 && unicode.IsLetter(rune(path[0])) { + cleanedPath = strings.ToUpper(string(cleanedPath[0])) + cleanedPath[1:] + } + + if len(cleanedPath) == 1 { + cleanedPath += ":" + } + + if len(cleanedPath) >= 2 && cleanedPath[1] != ':' { + log.Tracef("Custom path has length 2 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) + isDrive = false + } + + if len(cleanedPath) == 2 { + cleanedPath += "\\" + } + + if len(cleanedPath) >= 3 && cleanedPath[2] != '\\' { + log.Tracef("Custom path has length 3 and likely refers to a drive, but third rune is not backslash, path: '%s' ", path) + isDrive = false } - requiredDisks[driveOrID] = entry + + if len(cleanedPath) != 3 || (len(cleanedPath) >= 1 && !unicode.IsLetter(rune(path[0]))) || + (len(cleanedPath) >= 2 && cleanedPath[1] != ':') || (len(cleanedPath) >= 3 && cleanedPath[2] != '\\') { + isDrive = false + } + + return cleanedPath, isDrive, nil } // This function is called when a custom path needs to be added @@ -486,7 +619,8 @@ func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, v // //nolint:funlen,gocyclo // can not split this function up, it has to check if its network drive, normal drive, a custom path under a volume etc. func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) { - path = strings.ReplaceAll(path, "/", "\\") + // --------- Option 1 : Network share path + // if its a network share path, discover existing shares and match it with a drive[remote_path] // then we replace path argument in-place, replacing the network path with the logical drive it is assigned to if isNetworkSharePath(path) { @@ -494,16 +628,16 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map l.setShares(discoveredNetworkShares) for key := range discoveredNetworkShares { - drive := discoveredNetworkShares[key] - remoteName, hasRemoteName := drive["remote_name"] + networkShare := discoveredNetworkShares[key] + remoteName, hasRemoteName := networkShare["remote_name"] if hasRemoteName && strings.HasPrefix(path, remoteName) { requiredDisks[key] = utils.CloneStringMap(discoveredNetworkShares[key]) + // drive["remote_name"] = \\SERVER\SHARENAME // drive["drive"] = x: // pathExample1 = \\SERVER\SHARENAME -> x: // pathExample2 = \\SERVER\SHARENAME\FOO\BAR -> x:\FOO\BAR - pathReplaced := strings.Replace(path, drive["remote_name"], drive["drive"], 1) - requiredDisks[key]["drive"] = pathReplaced + pathReplaced := strings.Replace(path, networkShare["remote_name"], networkShare["drive"], 1) // It is better to let users set their own detailSyntax or okSyntax, we give them the attributes for it // requiredDisks[key]["drive_or_name"] = fmt.Sprintf("%s - (%s)", path, pathReplaced) requiredDisks[key]["localised_remote_path"] = pathReplaced @@ -513,104 +647,80 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map } } - // organize the custom path, so that if it refers to a drive it matches the results from disk discovery - // the form to use looks like this: C:\ - possibleDrivePath := path - checkAsDrive := true - switch len(path) { - case 1: - if !unicode.IsLetter(rune(path[0])) { - log.Tracef("Custom path has length 1 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) - checkAsDrive = false - } - possibleDrivePath = strings.ToUpper(path) + ":" + "\\" - path += ":" // add a colon to the path, similarly to the nsclient - case 2: - if !unicode.IsLetter(rune(path[0])) { - log.Tracef("Custom path has length 2 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) - checkAsDrive = false - } - if path[1] != ':' { - log.Tracef("Custom path has length 2 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) - checkAsDrive = false - } - possibleDrivePath = strings.ToUpper(path) + "\\" - case 3: - if !unicode.IsLetter(rune(path[0])) { - log.Tracef("Custom path has length 3 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) - checkAsDrive = false - } - if path[1] != ':' { - log.Tracef("Custom path has length 3 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) - checkAsDrive = false - } - if path[2] != '\\' { - log.Tracef("Custom path has length 3 and likely refers to a drive, but third rune is not backslash, path: '%s' ", path) - checkAsDrive = false - } - possibleDrivePath = strings.ToUpper(path) + // Important: UNC network paths have slashes next to each other e.g: \\ServerName\SharedFolder\ResourcePath + // This gets cleaned up using cleanupPathString , as it is meant for absolute paths inside a drive. + // Not cleaning up the path beforehand is intentional + cleanedPath, isDrivePath, err := cleanupPathString(path) + if err != nil { + return fmt.Errorf("error when cleaning up path: %w", err) } - if checkAsDrive { - log.Tracef("Custom path likely refers to a drive, checking drives, path: '%s' , possibleDrivePath: '%s' ", path, possibleDrivePath) + // --------- Option 2 : Drive path + if isDrivePath { + log.Tracef("Custom path likely refers to a drive, checking drives, path: '%s', cleanedPath: '%s' ", path, cleanedPath) availDisks := map[string]map[string]string{} err = l.setDrives(availDisks) - for driveOrID := range availDisks { - if !strings.EqualFold(driveOrID, possibleDrivePath) { + if err != nil { + // if setDisks had a problem (e.g. bitlocker locked drive) and did not return + // the required drive, then pass any possible error on to the caller. + return err + } + + for key := range availDisks { + if !strings.EqualFold(key, cleanedPath) { continue } - requiredDisks[path] = utils.CloneStringMap(availDisks[driveOrID]) - requiredDisks[path]["drive"] = path // use name from attributes - requiredDisks[path]["drive_or_name"] = path - requiredDisks[path]["drive_or_name_or_id"] = path + requiredDisks[path] = utils.CloneStringMap(availDisks[key]) + requiredDisks[path]["id"] = key + requiredDisks[path]["drive"] = cleanedPath + requiredDisks[path]["drive_or_id"] = cleanedPath + requiredDisks[path]["drive_or_name"] = cleanedPath + requiredDisks[path]["drive_or_name_or_id"] = cleanedPath + requiredDisks[path]["perflabel_prefix"], _ = getPerflabelPrefix(path) return nil } - - if err != nil { - // if setDisks had a problem (e.g. bitlocker locked drive) and did not return - // the required drive, then pass any possible error on to the caller. otherwise - // we got what we want and already returned nil above. - return err - } } + // --------- Option 3: Path under a volume // at this point, the path is checked if its a drive or a network path. // if its neither of those, try to open the file/directory // check for volumes that include the path in their drive - _, err = os.Stat(path) + + _, err = os.Stat(cleanedPath) if err != nil && os.IsNotExist(err) { - log.Debugf("%s: %s", path, err.Error()) + log.Debugf("%s: %s", cleanedPath, err.Error()) - return &PartitionNotFoundError{Path: path, err: err} + return &PartitionNotFoundError{Path: cleanedPath, err: err} } - // try to find closes matching volume + // try to find closest matching volume availVolumes := map[string]map[string]string{} l.setVolumes(availVolumes) - testDrive := strings.TrimSuffix(path, "\\") + + testPath := strings.TrimSuffix(cleanedPath, "\\") + "\\" // make first character uppercase because drives are uppercase in the volume list - switch { - case len(testDrive) > 1: - r := []rune(testDrive) - testDrive = string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...)) - case len(testDrive) == 1: - r := []rune(testDrive) - testDrive = string(unicode.ToUpper(r[0])) + if len(testPath) > 1 { + testPath = strings.ToUpper(testPath[0:1]) + testPath[1:] } + var match *map[string]string for i := range availVolumes { - vol := availVolumes[i] - if parentFallback && vol["drive"] != "" && strings.HasPrefix(strings.ToUpper(testDrive+"\\"), strings.ToUpper(vol["drive"])) { - if match == nil || len((*match)["drive"]) < len(vol["drive"]) { - match = &vol + volume := availVolumes[i] + + // parent fallback means parent folders of a drive are valid as well + if parentFallback && volume["drive"] != "" && + strings.HasPrefix(strings.ToUpper(testPath), strings.ToUpper(volume["drive"])) { + if match == nil || len((*match)["drive"]) < len(volume["drive"]) { + match = &volume } } - if strings.EqualFold(testDrive+"\\", vol["drive"]) { - match = &vol + + if strings.EqualFold(testPath, volume["drive"]) { + match = &volume break } @@ -618,6 +728,7 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map if match != nil { requiredDisks[path] = utils.CloneStringMap(*match) requiredDisks[path]["drive"] = path + requiredDisks[path]["name"] = path requiredDisks[path]["drive_or_name"] = path requiredDisks[path]["drive_or_name_or_id"] = path @@ -651,6 +762,7 @@ func (l *CheckDrivesize) setShares(requiredDisks map[string]map[string]string) { continue } + if driveType == DriveRemote { remoteName, err := NetGetConnection(logicalDrive[0 : len(logicalDrive)-1]) if err != nil { @@ -668,8 +780,9 @@ func (l *CheckDrivesize) setShares(requiredDisks map[string]map[string]string) { drive["id"] = remoteName drive["drive"] = logicalDrive drive["drive_or_id"] = logicalDrive - // It is better to let users set their own detailSyntax or okSyntax, we give them the attributes for it drive["drive_or_name"] = logicalDrive + drive["drive_or_name_or_id"] = logicalDrive + drive["letter"] = fmt.Sprintf("%c", logicalDrive[0]) drive["remote_name"] = remoteName if isNetworkDrivePersistent(logicalDrive) { diff --git a/pkg/snclient/check_drivesize_windows_test.go b/pkg/snclient/check_drivesize_windows_test.go index 3defb6f6..d7de6188 100644 --- a/pkg/snclient/check_drivesize_windows_test.go +++ b/pkg/snclient/check_drivesize_windows_test.go @@ -16,7 +16,7 @@ func TestCheckDrivesize(t *testing.T) { assert.Equalf(t, CheckExitCritical, res.State, "state critical") assert.Regexpf( t, - `^CRITICAL - c: .*?\/.*? \(\d+\.\d+%\) \|'c: free'=.*?B;0;0;0;.*? 'c: free %'=.*?%;0;0;0;100`, + `^CRITICAL - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c: free'=.*?B;0;0;0;.*? 'c: free %'=.*?%;0;0;0;100`, string(res.BuildPluginOutput()), "output matches", ) @@ -51,36 +51,52 @@ func TestCheckDrivesize(t *testing.T) { res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - c: .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - c: .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c: used'=.*?B;(\d+);(\d+);0;(\d+) 'c: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - c:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:\\\\\\", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:/", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - c:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=c:///////", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'c:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'c:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - C: .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, `^OK - C: .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C: used'=.*?B;(\d+);(\d+);0;(\d+) 'C: used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:\\", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:\\\\\\", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:/", "show-all"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + res = snc.RunCheck("check_drivesize", []string{"warn=used>100%", "crit=used>100%", "drive=C:///", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf(t, `^OK - C:\\ .*?\/.*? \(\d+\.\d+%\) \|'C:\\ used'=.*?B;(\d+);(\d+);0;(\d+) 'C:\\ used %'=.*?%;100;100;0;100`, string(res.BuildPluginOutput()), "output matches") + // 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") From 4714c74133fd9cb899b4cce9ce072acc9da25449 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Wed, 15 Jul 2026 16:54:52 +0200 Subject: [PATCH 4/7] check_drivesize: address citest issues --- pkg/snclient/check_drivesize_windows.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index f196c27e..f59064f6 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -440,6 +440,8 @@ func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) // this function is used to further process the volume GUID path returned from Windows API calls // takes a GUID path of a volume, then finds its path name. // it may or may not be mounted directly on a drive letter +// +//nolint:funlen // there are a lot of entry attributes func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, volumeGUIDPath string, buffer []uint16) { volPtr, err := syscall.UTF16PtrFromString(volumeGUIDPath) if err != nil { @@ -617,7 +619,7 @@ func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error // This is used if folders are given // c:/, d:/volume, f:/folder/with/slash // -//nolint:funlen,gocyclo // can not split this function up, it has to check if its network drive, normal drive, a custom path under a volume etc. +//nolint:funlen // can not split this function up, it has to check if its network drive, normal drive, a custom path under a volume etc. func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) { // --------- Option 1 : Network share path From 95c752522f219487c2e31446840e22ea452003be Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Wed, 15 Jul 2026 17:27:34 +0200 Subject: [PATCH 5/7] check_drivesize: ai-pass fix some of the concerns fix extra space when loading 'WNetGetLastErrorA ' library, there was a trailing space. Likely never encountered before add windows build tag to check_drivesize_windows_test.go fix wrong trace messages in cleanupPathString make discovery functions use consitent naming: now they all use requiredDrives fix other comments --- pkg/snclient/check_drivesize_windows.go | 60 +++++++++---------- pkg/snclient/check_drivesize_windows_test.go | 2 + .../check_drivesize_windows_win32api.go | 2 +- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index f59064f6..ca38a872 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -57,8 +57,8 @@ func (l *CheckDrivesize) getExample() string { // Or it could be a network drive like \\SERVER\SHARENAME being mounted to K:\ drive // Checks the given path, resolves the alias and calls the corresponding function that adds the details func (l *CheckDrivesize) getRequiredDrives(paths []string, parentFallback bool) (requiredDrives map[string]map[string]string, err error) { - // create map of required disks/volmes/network_shares with "drive_or_name_or_id" as primary key - + // create map of required disks/volmes/network_shares + // this map will be populated piece by piece requiredDrives = map[string]map[string]string{} // if there are multiple drive= arguments, these functions may be called multiple times. @@ -391,7 +391,7 @@ func (l *CheckDrivesize) setDrives(requiredDrives map[string]map[string]string) return nil } -// adds all logical volumes to the requiredDisks +// adds all logical volumes to the requiredDrives // network shares are not listed in volumes // this function populates requiredDrives map func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) { @@ -442,7 +442,7 @@ func (l *CheckDrivesize) setVolumes(requiredDrives map[string]map[string]string) // it may or may not be mounted directly on a drive letter // //nolint:funlen // there are a lot of entry attributes -func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, volumeGUIDPath string, buffer []uint16) { +func (l *CheckDrivesize) setVolume(requiredDrives map[string]map[string]string, volumeGUIDPath string, buffer []uint16) { volPtr, err := syscall.UTF16PtrFromString(volumeGUIDPath) if err != nil { log.Warnf("stringPtr: %s: %s", volumeGUIDPath, err.Error()) @@ -523,7 +523,7 @@ func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, v // check if there exists an entry // this prevents adding the same drive multiple times - entry, idAlreadyAdded := requiredDisks[volumeID] + entry, idAlreadyAdded := requiredDrives[volumeID] if !idAlreadyAdded { entry = make(map[string]string) } @@ -538,7 +538,7 @@ func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, v entry["mounted"] = mounted entry["skip"] = skip - requiredDisks[volumeID] = entry + requiredDrives[volumeID] = entry } // The perflabel prefix situation it complicated due to compatibility reasons @@ -581,7 +581,7 @@ func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error } if len(cleanedPath) >= 1 && !unicode.IsLetter(rune(path[0])) { - log.Tracef("Custom path has length 1 and likely refers to a drive, but first rune is not a letter, path: '%s' ", path) + log.Tracef("Cleaned paths first rune is not a letter, cannot be a drive, path: '%s' ", path) isDrive = false } @@ -594,7 +594,7 @@ func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error } if len(cleanedPath) >= 2 && cleanedPath[1] != ':' { - log.Tracef("Custom path has length 2 and likely refers to a drive, but second rune is not colon, path: '%s' ", path) + log.Tracef("Cleaned paths second rune is not colon, cannot be a drive, path: '%s' ", path) isDrive = false } @@ -603,7 +603,7 @@ func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error } if len(cleanedPath) >= 3 && cleanedPath[2] != '\\' { - log.Tracef("Custom path has length 3 and likely refers to a drive, but third rune is not backslash, path: '%s' ", path) + log.Tracef("Cleaned paths third rune is not backslash, cannot be a drive, path: '%s' ", path) isDrive = false } @@ -620,7 +620,7 @@ func cleanupPathString(path string) (cleanedPath string, isDrive bool, err error // c:/, d:/volume, f:/folder/with/slash // //nolint:funlen // can not split this function up, it has to check if its network drive, normal drive, a custom path under a volume etc. -func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) { +func (l *CheckDrivesize) setCustomPath(path string, requiredDrives map[string]map[string]string, parentFallback bool) (err error) { // --------- Option 1 : Network share path // if its a network share path, discover existing shares and match it with a drive[remote_path] @@ -633,7 +633,7 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map networkShare := discoveredNetworkShares[key] remoteName, hasRemoteName := networkShare["remote_name"] if hasRemoteName && strings.HasPrefix(path, remoteName) { - requiredDisks[key] = utils.CloneStringMap(discoveredNetworkShares[key]) + requiredDrives[key] = utils.CloneStringMap(discoveredNetworkShares[key]) // drive["remote_name"] = \\SERVER\SHARENAME // drive["drive"] = x: @@ -641,8 +641,8 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map // pathExample2 = \\SERVER\SHARENAME\FOO\BAR -> x:\FOO\BAR pathReplaced := strings.Replace(path, networkShare["remote_name"], networkShare["drive"], 1) // It is better to let users set their own detailSyntax or okSyntax, we give them the attributes for it - // requiredDisks[key]["drive_or_name"] = fmt.Sprintf("%s - (%s)", path, pathReplaced) - requiredDisks[key]["localised_remote_path"] = pathReplaced + // requiredDrives[key]["drive_or_name"] = fmt.Sprintf("%s - (%s)", path, pathReplaced) + requiredDrives[key]["localised_remote_path"] = pathReplaced return nil } @@ -674,13 +674,13 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map continue } - requiredDisks[path] = utils.CloneStringMap(availDisks[key]) - requiredDisks[path]["id"] = key - requiredDisks[path]["drive"] = cleanedPath - requiredDisks[path]["drive_or_id"] = cleanedPath - requiredDisks[path]["drive_or_name"] = cleanedPath - requiredDisks[path]["drive_or_name_or_id"] = cleanedPath - requiredDisks[path]["perflabel_prefix"], _ = getPerflabelPrefix(path) + requiredDrives[path] = utils.CloneStringMap(availDisks[key]) + requiredDrives[path]["id"] = key + requiredDrives[path]["drive"] = cleanedPath + requiredDrives[path]["drive_or_id"] = cleanedPath + requiredDrives[path]["drive_or_name"] = cleanedPath + requiredDrives[path]["drive_or_name_or_id"] = cleanedPath + requiredDrives[path]["perflabel_prefix"], _ = getPerflabelPrefix(path) return nil } @@ -728,11 +728,11 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map } } if match != nil { - requiredDisks[path] = utils.CloneStringMap(*match) - requiredDisks[path]["drive"] = path - requiredDisks[path]["name"] = path - requiredDisks[path]["drive_or_name"] = path - requiredDisks[path]["drive_or_name_or_id"] = path + requiredDrives[path] = utils.CloneStringMap(*match) + requiredDrives[path]["drive"] = path + requiredDrives[path]["name"] = path + requiredDrives[path]["drive_or_name"] = path + requiredDrives[path]["drive_or_name_or_id"] = path return nil } @@ -740,13 +740,13 @@ func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map // add anyway to generate an error later with more default values filled in entry := l.driveEntry(path) entry["_error"] = fmt.Sprintf("%s not mounted", path) - requiredDisks[path] = entry + requiredDrives[path] = entry return nil } -// adds all network shares to requiredDisks -func (l *CheckDrivesize) setShares(requiredDisks map[string]map[string]string) { +// adds all network shares to requiredDrives +func (l *CheckDrivesize) setShares(requiredDrives map[string]map[string]string) { partitions, err := disk.Partitions(true) if err != nil { log.Debugf("Error when discovering partitions: %s", err.Error()) @@ -775,7 +775,7 @@ func (l *CheckDrivesize) setShares(requiredDisks map[string]map[string]string) { log.Debugf("Logical Drive: %s, Drive Type: %d, Remote name: %s", logicalDrive, driveType, remoteName) // modify existing drive if its there // if its not, add it new - drive, ok := requiredDisks[logicalDrive] + drive, ok := requiredDrives[logicalDrive] if !ok { drive = make(map[string]string) } @@ -792,7 +792,7 @@ func (l *CheckDrivesize) setShares(requiredDisks map[string]map[string]string) { } else { drive["persistent"] = "0" } - requiredDisks[logicalDrive] = drive + requiredDrives[logicalDrive] = drive } } } diff --git a/pkg/snclient/check_drivesize_windows_test.go b/pkg/snclient/check_drivesize_windows_test.go index d7de6188..95becdc5 100644 --- a/pkg/snclient/check_drivesize_windows_test.go +++ b/pkg/snclient/check_drivesize_windows_test.go @@ -1,3 +1,5 @@ +//go:build windows + package snclient import ( diff --git a/pkg/snclient/check_drivesize_windows_win32api.go b/pkg/snclient/check_drivesize_windows_win32api.go index 0e12cfde..a1840a36 100644 --- a/pkg/snclient/check_drivesize_windows_win32api.go +++ b/pkg/snclient/check_drivesize_windows_win32api.go @@ -132,7 +132,7 @@ func NetGetConnection(lpLocalName string) (lpRemoteName string, err error) { } func handleWNetError(errorCode uintptr, winnetwkDll *windows.LazyDLL) (err error) { - wNetGetLastErrorAFunc := winnetwkDll.NewProc("WNetGetLastErrorA ") + wNetGetLastErrorAFunc := winnetwkDll.NewProc("WNetGetLastErrorA") const lpErrorBufLength = uint32(1024) lpErrorBuf := make([]byte, lpErrorBufLength) const lpNameBufLength = uint32(256) From 0f46ab22706f91eb1dc339d8eaae716c816c37be Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Wed, 15 Jul 2026 17:39:41 +0200 Subject: [PATCH 6/7] check_drivesize: fix drives appearing twice with drive=all argument, once discovered in drive check and once discovered in volume check --- pkg/snclient/check_drivesize_windows.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index ca38a872..61754d93 100644 --- a/pkg/snclient/check_drivesize_windows.go +++ b/pkg/snclient/check_drivesize_windows.go @@ -66,11 +66,11 @@ func (l *CheckDrivesize) getRequiredDrives(paths []string, parentFallback bool) for _, drive := range paths { switch drive { case "*", "all": - l.setVolumes(requiredDrives) err := l.setDrives(requiredDrives) if err != nil { return nil, err } + l.setVolumes(requiredDrives) l.setShares(requiredDrives) case "all-drives": err := l.setDrives(requiredDrives) @@ -521,8 +521,14 @@ func (l *CheckDrivesize) setVolume(requiredDrives map[string]map[string]string, return } + for _, existingDrive := range requiredDrives { + if existingDrive["drive"] == drive { + // drive already added + return + } + } + // check if there exists an entry - // this prevents adding the same drive multiple times entry, idAlreadyAdded := requiredDrives[volumeID] if !idAlreadyAdded { entry = make(map[string]string) From cfd07b7a7481567343eb6055f1388809713e30ac Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Wed, 15 Jul 2026 17:54:29 +0200 Subject: [PATCH 7/7] check_drivesize: ai pass, stop cutting trailing slash on non-windows platforms if drive itself is literally "/" --- pkg/snclient/check_drivesize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/snclient/check_drivesize.go b/pkg/snclient/check_drivesize.go index 5282266c..58ac2146 100644 --- a/pkg/snclient/check_drivesize.go +++ b/pkg/snclient/check_drivesize.go @@ -539,7 +539,7 @@ func (l *CheckDrivesize) getFlagNames(drive map[string]string) []string { func (l *CheckDrivesize) tidyThresholdDriveValues(check *CheckData) { removeTrailingSlashFromDriveValue := func(cond *Condition) (err error) { if cond.keyword == "drive" && runtime.GOOS != "windows" { - if val, castOK := cond.value.(string); castOK { + if val, castOK := cond.value.(string); castOK && val != "/" { // Example: '/tmp/' -> '/tmp' if cut, cutOK := strings.CutSuffix(val, "/"); cutOK { cond.value = cut