diff --git a/pkg/snclient/check_drivesize.go b/pkg/snclient/check_drivesize.go index f1cc5bbe..58ac2146 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 { @@ -536,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 diff --git a/pkg/snclient/check_drivesize_windows.go b/pkg/snclient/check_drivesize_windows.go index 724b8182..61754d93 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,11 +53,12 @@ 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 + // 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. @@ -64,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) @@ -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 @@ -387,8 +391,9 @@ 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) { 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,9 +437,12 @@ 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 -func (l *CheckDrivesize) setVolume(requiredDisks map[string]map[string]string, volumeGUIDPath string, buffer []uint16) { +// 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(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()) @@ -440,53 +450,185 @@ 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 } - // only add it if it does not exists - entry, ok := requiredDisks[driveOrID] - if !ok { + + driveOrNameOrID := driveOrName + if driveOrNameOrID == "" { + driveOrNameOrID = volumeID + } + + letter := "" + if drive != "" { + letter = fmt.Sprintf("%c", drive[0]) + } + + mounted := "0" + if letter != "" { + mounted = "1" + } + + 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 + } + + for _, existingDrive := range requiredDrives { + if existingDrive["drive"] == drive { + // drive already added + return + } + } + + // check if there exists an entry + entry, idAlreadyAdded := requiredDrives[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 + + requiredDrives[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 += ":" } - requiredDisks[driveOrID] = entry + + 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("Cleaned paths first rune is not a letter, cannot be a drive, 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("Cleaned paths second rune is not colon, cannot be a drive, path: '%s' ", path) + isDrive = false + } + + if len(cleanedPath) == 2 { + cleanedPath += "\\" + } + + if len(cleanedPath) >= 3 && cleanedPath[2] != '\\' { + log.Tracef("Cleaned paths third rune is not backslash, cannot be a drive, path: '%s' ", path) + isDrive = false + } + + 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 // This is used if folders are given // c:/, d:/volume, f:/folder/with/slash // -//nolint:funlen // can not split this function up -func (l *CheckDrivesize) setCustomPath(path string, requiredDisks map[string]map[string]string, parentFallback bool) (err error) { - path = strings.ReplaceAll(path, "/", "\\") +//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, 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] // then we replace path argument in-place, replacing the network path with the logical drive it is assigned to if isNetworkSharePath(path) { @@ -494,87 +636,109 @@ 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]) + requiredDrives[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 + // requiredDrives[key]["drive_or_name"] = fmt.Sprintf("%s - (%s)", path, pathReplaced) + requiredDrives[key]["localised_remote_path"] = pathReplaced return nil } } } - // match a drive, ex: "c" or "c:" - switch len(path) { - case 1, 2: - path = strings.TrimSuffix(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) + } + + // --------- 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, path+"\\") { - 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 - - 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. + // the required drive, then pass any possible error on to the caller. return err } + + for key := range availDisks { + if !strings.EqualFold(key, cleanedPath) { + continue + } + + 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 + } } - // make sure path exists - _, err = os.Stat(path) + // --------- 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(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 } } if match != nil { - requiredDisks[path] = utils.CloneStringMap(*match) - requiredDisks[path]["drive"] = 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 } @@ -582,13 +746,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()) @@ -606,6 +770,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 { @@ -616,15 +781,16 @@ 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) } 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) { @@ -632,7 +798,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 37b7300d..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 ( @@ -16,7 +18,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", ) @@ -43,12 +45,59 @@ 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") - 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") - 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") + + 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"}) 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)