diff --git a/.golangci.yml b/.golangci.yml index d0f65ba..f1de1e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,3 +34,6 @@ linters-settings: - standard - default - localmodule + gomoddirectives: + replace-allow-list: + - github.com/karrick/golf diff --git a/README.md b/README.md index 4af0721..243f153 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ supports the following command-line flags: ### DNS zone serials - The `check_zone_serial` plugin can be used to check that the version of a - zone served by a DNS is up-to-date compared to the same zone served by - another, "reference" DNS. It supports the following command-line flags: +The `check_zone_serial` plugin can be used to check that the version of a +zone served by a DNS is up-to-date compared to the same zone served by +another, "reference" DNS. It supports the following command-line flags: * `-H name`/`--hostname name`: the host name or address of the server to check. @@ -49,6 +49,48 @@ supports the following command-line flags: * `-p port`/`--rs-port port`: the port to use on the reference server (defaults to 53). +### Generic text match counter + +The `check_output_matches` plugin can be used to count occurrences of strings +in a program's output or in a text file, and compute its final status based on +that. + +It supports the following general command line flags: + +* `-f` / `--is-file` indicates that the plugin will be reading from a text file + instead of running another program; +* `-s` / `--source` is either the name of the file to read, or the command to + execute. The command may include multiple arguments separated by single + spaces; it does not support any form of quoting. +* `-T` / `--timeout` can set a timeout for the command. It is disabled by + default. +* `-S` / `--strict` determines how unmatched lines are handled. By default they + are ignored, but setting this flag will cause the plugin to enter `CRITICAL` + mode if unmatched lines are found. + +Other flags are available in order to configure the matches. The main flag is +`-m` / `--match`, which adds a new match string to the set of checks to run. +The checks are influenced by the following additional flags, which apply to all +subsequent matches. + +* `-r` / `--regexp` indicates that new matches will be based on regular + expressions instead of substrings. +* `-R` / `--no-regexp` switches back to substring-based matches. +* `-w` / `--warn` can be used to set a warning range. It must be followed by + a Nagios range specification. +* `-W` / `--no-warn` clears the warning range. +* `-c` / `--critical` can be used to set the critical range. It must be followed + by a Nagios range specification. +* `-C` / `--no-critical` clears the critical range. + +For example, the command below: + + gomonop check_output_matches -s 'find /some/place' \ + -w 4 -r -m '^.*\.warn$' \ + -W -c 0 -R -m fatal + +configures a warning if more than 4 files ending in `.warn` are found, and a +critical state if any file with `fatal` in its name is found. Building from source -------------------- diff --git a/cmd/matches/cmdline.go b/cmd/matches/cmdline.go new file mode 100644 index 0000000..fbf9e3b --- /dev/null +++ b/cmd/matches/cmdline.go @@ -0,0 +1,95 @@ +package matches // import nocternity.net/gomonop/cmd/matches + +import ( + "context" + "regexp" + "strings" + "time" + + "github.com/karrick/golf" + + "nocternity.net/gomonop/pkg/perfdata" +) + +// pluginFlags represent command line flags that have been parsed. +type pluginFlags struct { + isFile bool // Are we reading from a file? + dataSource string // The file or command to read from + timeout time.Duration // A timeout for the command, or 0 to disable + matches []matchConfig // Configuration for the matches to check + strict bool // Reject lines that don't match anything +} + +// matchConfig is the configuration for a single match to check. +type matchConfig struct { + isRegexp bool // Are we checking against a regular expression? + matchString string // The string or regexp to match + compiledRe *regexp.Regexp // The compiled regexp + warn *perfdata.Range // Warning range + crit *perfdata.Range // Critical range +} + +// parseArguments parses command line arguments for the plugin. +func (p *pluginFlags) parseArguments() { + golf.BoolVarP(&p.isFile, 'f', "is-file", false, "Are we reading from a file?") + golf.StringVarP(&p.dataSource, 's', "source", "", "The file or command to read from") + golf.DurationVarP(&p.timeout, 'T', "timeout", 0, "A timeout for the command, or 0 to disable") + golf.BoolVarP(&p.strict, 'S', "strict", false, "Reject lines that do not match anything") + + isRegexp := golf.BoolP('R', "no-regexp", true, "Following match argument will be a basic string") + golf.BoolVarP(isRegexp, 'r', "regexp", false, "Following match argument will be a regexp") + + var wRange *perfdata.Range + golf.StringFuncP('w', "warn", "", "Warning range, in Nagios-compatible format", func(s string) error { + locRange, err := perfdata.ParseRange(s) + if err == nil { + wRange = locRange + } + return err + }) + golf.BoolFuncP('W', "no-warn", false, "Clear warning range", func(bool) error { + wRange = nil + return nil + }) + + var cRange *perfdata.Range + golf.StringFuncP('c', "critical", "", "Critical range, in Nagios-compatible format", func(s string) error { + locRange, err := perfdata.ParseRange(s) + if err == nil { + cRange = locRange + } + return err + }) + golf.BoolFuncP('C', "no-critical", false, "Clear warning range", func(bool) error { + cRange = nil + return nil + }) + + golf.StringFuncP('m', "match", "", "Match string", func(s string) error { + p.matches = append(p.matches, matchConfig{ + isRegexp: *isRegexp, + matchString: s, + warn: wRange, + crit: cRange, + }) + return nil + }) + + golf.Parse() +} + +// makeContext generates a context based on the timeout, if one is set. +func (p *pluginFlags) makeContext() (context.Context, context.CancelFunc) { + if p.timeout == 0 { + return context.Background(), func() {} + } + return context.WithTimeout(context.Background(), p.timeout) +} + +// matches check if the specified string matches a configuration. +func (p *matchConfig) matches(s string) bool { + if p.isRegexp { + return p.compiledRe.MatchString(s) + } + return strings.Contains(s, p.matchString) +} diff --git a/cmd/matches/extprogram.go b/cmd/matches/extprogram.go new file mode 100644 index 0000000..aa45dee --- /dev/null +++ b/cmd/matches/extprogram.go @@ -0,0 +1,118 @@ +package matches // import nocternity.net/gomonop/pkg/matches + +import ( + "bufio" + "context" + "io" + "os/exec" + "strings" +) + +// streamObtainer refers to functions that may return either an input stream or +// an error. +type streamObtainer func() (io.ReadCloser, error) + +// pipeCopy encapsulates the structures that are used to transfer data from a +// single stream to the reader. +type pipeCopy struct { + reader io.ReadCloser // The input stream being read from + dataPipe chan string // Channel that receives the data we read + aborter chan struct{} // Channel that causes the copy to abort + readResult chan error // Channel that returns results from reading +} + +// startPipeCopy starts a goroutine that copies data from the reader to the +// dataPipe. +func startPipeCopy(obtainer streamObtainer, pipes *readerPipes) *pipeCopy { + stream, err := obtainer() + if err != nil { + pipes.done <- err + return nil + } + + pipeCopyData := &pipeCopy{ + reader: stream, + dataPipe: pipes.data, + aborter: make(chan struct{}), + readResult: make(chan error), + } + + go pipeCopyData.run() + + return pipeCopyData +} + +// run runs the copy operation from the input stream to the data pipe. It is +// meant to be executed as a goroutine. +func (pc *pipeCopy) run() { + scanner := bufio.NewScanner(pc.reader) + for scanner.Scan() { + select { + case <-pc.aborter: + return + case pc.dataPipe <- scanner.Text(): + } + } + pc.readResult <- scanner.Err() +} + +// close closes the stream and channels used by a copy pipe. +func (pc *pipeCopy) close() { + pc.abort() + _ = pc.reader.Close() + close(pc.aborter) + close(pc.readResult) +} + +// abort causes the pipe copy to be aborted. +func (pc *pipeCopy) abort() { + pc.aborter <- struct{}{} +} + +// readFromProgram starts a goroutine that controls the program, sending lines +// from both stderr and stdout to the dataPipe. +func (pluginInst *matchesPlugin) readFromProgram(ctx context.Context, pipes *readerPipes) { + go func() { + args := strings.Split(pluginInst.dataSource, " ") + cmd := exec.Command(args[0], args[1:]...) //nolint:gosec // Command is in fact user-provided + + outs := startPipeCopy(func() (io.ReadCloser, error) { return cmd.StdoutPipe() }, pipes) + defer outs.close() + errs := startPipeCopy(func() (io.ReadCloser, error) { return cmd.StderrPipe() }, pipes) + defer errs.close() + + if err := cmd.Start(); err != nil { + pipes.done <- err + return + } + + abort := func(err error) { + _ = cmd.Process.Kill() + pipes.done <- err + } + + errComplete := false + outComplete := false + for !(errComplete && outComplete) { + select { + case <-ctx.Done(): + abort(ctx.Err()) + return + case err := <-outs.readResult: + if err != nil { + abort(err) + return + } + outComplete = true + case err := <-errs.readResult: + if err != nil { + abort(err) + return + } + errComplete = true + } + } + + pipes.done <- cmd.Wait() + }() +} diff --git a/cmd/matches/plugin.go b/cmd/matches/plugin.go new file mode 100644 index 0000000..8f56a30 --- /dev/null +++ b/cmd/matches/plugin.go @@ -0,0 +1,76 @@ +// The matches package contains the implementation of the `check_output_matches` +// plugin, which can be used to run an arbitrary command and check its output +// against various patterns. It can also be configured to do the same on an +// arbitrary file. +package matches // import nocternity.net/gomonop/cmd/matches + +import ( + "regexp" + + "nocternity.net/gomonop/pkg/plugin" + "nocternity.net/gomonop/pkg/results" + "nocternity.net/gomonop/pkg/status" +) + +// Program data for the string matches plugin. +type matchesPlugin struct { + pluginFlags // Flags from the command line + results *results.Results // Plugin output state + counters []int // Counters for each match + unmatchedLines int // Number of lines that didn't match anything +} + +// Initialise the plugin. +func NewPlugin() plugin.Plugin { + pluginInst := &matchesPlugin{ + results: results.New("String matches counter"), + } + pluginInst.parseArguments() + return pluginInst +} + +// Return the program's output value. +func (pluginInst *matchesPlugin) Results() *results.Results { + return pluginInst.results +} + +// Check the values that were specified from the command line. Returns true +// if the arguments made sense. +func (pluginInst *matchesPlugin) CheckArguments() bool { + if pluginInst.dataSource == "" { + pluginInst.results.SetState(status.StatusUnknown, "no data source specified") + return false + } + + if !pluginInst.strict && len(pluginInst.matches) == 0 { + pluginInst.results.SetState(status.StatusUnknown, "would match anything") + return false + } + + for index := range pluginInst.matches { + if pluginInst.matches[index].matchString == "" { + pluginInst.results.SetState(status.StatusUnknown, "empty match string") + pluginInst.results.AddLinef("(At match %d)", index+1) + return false + } + if pluginInst.matches[index].isRegexp { + rexp, err := regexp.Compile(pluginInst.matches[index].matchString) + if err != nil { + pluginInst.results.SetState(status.StatusUnknown, "Invalid regular expression") + pluginInst.results.AddLine(err.Error()) + pluginInst.results.AddLinef("(At match %d)", index+1) + return false + } + pluginInst.matches[index].compiledRe = rexp + } + } + + return true +} + +// Run the check. +func (pluginInst *matchesPlugin) RunCheck() { + pluginInst.counters = make([]int, len(pluginInst.matches)) + err := pluginInst.processData() + pluginInst.checkResults(err) +} diff --git a/cmd/matches/reader.go b/cmd/matches/reader.go new file mode 100644 index 0000000..87d15b1 --- /dev/null +++ b/cmd/matches/reader.go @@ -0,0 +1,95 @@ +package matches // import nocternity.net/gomonop/pkg/matches + +import ( + "bufio" + "context" + "os" +) + +// readerPipes encapsulates the two channels used to communicate data and state +// from the functions that acquire the data. +type readerPipes struct { + done chan error + data chan string +} + +// makeReaderPipes initializes the channels. +func makeReaderPipes() *readerPipes { + return &readerPipes{ + done: make(chan error), + data: make(chan string), + } +} + +// close closes the channels. +func (rdp *readerPipes) close() { + close(rdp.done) + close(rdp.data) +} + +// readFromFile starts a goroutine that reads from the file and sends each line +// to the dataPipe. +func (pluginInst *matchesPlugin) readFromFile(ctx context.Context, pipes *readerPipes) { + go func() { + file, err := os.Open(pluginInst.dataSource) + if err != nil { + pipes.done <- err + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + select { + case <-ctx.Done(): + pipes.done <- ctx.Err() + return + case pipes.data <- scanner.Text(): + } + } + pipes.done <- scanner.Err() + }() +} + +// startReading starts reading data from either a file or a program. +func (pluginInst *matchesPlugin) startReading(ctx context.Context, pipes *readerPipes) { + if pluginInst.isFile { + pluginInst.readFromFile(ctx, pipes) + } else { + pluginInst.readFromProgram(ctx, pipes) + } +} + +// processLine processes a single line obtained from the program or file. +func (pluginInst *matchesPlugin) processLine(line string) { + hadMatch := false + for index := range pluginInst.matches { + if pluginInst.matches[index].matches(line) { + pluginInst.counters[index]++ + hadMatch = true + } + } + if !hadMatch { + pluginInst.unmatchedLines++ + } +} + +// processData processes data from either the program or the file. +func (pluginInst *matchesPlugin) processData() error { + ctx, cancel := pluginInst.makeContext() + defer cancel() + + pipes := makeReaderPipes() + defer pipes.close() + + pluginInst.startReading(ctx, pipes) + + for { + select { + case line := <-pipes.data: + pluginInst.processLine(line) + case err := <-pipes.done: + return err + } + } +} diff --git a/cmd/matches/results.go b/cmd/matches/results.go new file mode 100644 index 0000000..57cf4d3 --- /dev/null +++ b/cmd/matches/results.go @@ -0,0 +1,125 @@ +package matches // import nocternity.net/gomonop/cmd/matches + +import ( + "fmt" + "strconv" + "strings" + + "nocternity.net/gomonop/pkg/perfdata" + "nocternity.net/gomonop/pkg/status" +) + +// resultsInfo contains information about results being processed. +type resultsInfo struct { + nWarns, nCrits, nUnknowns uint +} + +// updateResults updates the result counters based on the specified status. +func (info *resultsInfo) updateFrom(itemStatus status.Status) { + switch itemStatus { + case status.StatusCritical: + info.nCrits++ + case status.StatusWarning: + info.nWarns++ + case status.StatusUnknown: + info.nUnknowns++ + case status.StatusOK: + // do nothing + } +} + +// toProblems generate the list of problems found while running the plugin, +// as a string which can be returned to the monitoring server. +func (info *resultsInfo) toProblems() string { + problems := []string{} + if info.nCrits > 0 { + problems = append(problems, + fmt.Sprintf("%d value(s) critical", info.nCrits), + ) + } + if info.nWarns > 0 { + problems = append(problems, + fmt.Sprintf("%d value(s) warning", info.nWarns), + ) + } + if info.nUnknowns > 0 { + problems = append(problems, + fmt.Sprintf("%d value(s) unknown", info.nUnknowns), + ) + } + if len(problems) == 0 { + problems = append(problems, "No match errors") + } + return strings.Join(problems, ", ") +} + +// toStatus computes the final status of the plugin based on the status +// counters. +func (info *resultsInfo) toStatus() status.Status { + switch { + case info.nCrits > 0: + return status.StatusCritical + case info.nWarns > 0: + return status.StatusWarning + case info.nUnknowns > 0: + return status.StatusUnknown + default: + return status.StatusOK + } +} + +// checkCounters checks the match counters against their configured thresholds, +// if any, and adds the corresponding perf data to the result. It initializes a +// resultsInfo structure containing the counts of critical, warning and unknown +// statuses gathered so far. +func (pluginInst *matchesPlugin) checkCounters() resultsInfo { + info := resultsInfo{} + + for index := range pluginInst.counters { + config := &pluginInst.matches[index] + + var nature string + if config.isRegexp { + nature = "regexp" + } else { + nature = "substring" + } + label := fmt.Sprintf("#%2d : %s %s", index+1, nature, config.matchString) + value := strconv.Itoa(pluginInst.counters[index]) + + pdat := perfdata.New(label, perfdata.UomNone, value) + pdat.SetWarn(config.warn) + pdat.SetCrit(config.crit) + pluginInst.results.AddPerfData(pdat) + + info.updateFrom(pdat.GetStatus()) + } + + return info +} + +// checkUnmatched checks the unmatched lines counters, applying a limit if +// strict mode is enabled. The corresponding performance data is added to the +// result. +func (pluginInst *matchesPlugin) checkUnmatched(info *resultsInfo) { + umlPdat := perfdata.New("unmatched", perfdata.UomNone, strconv.Itoa(pluginInst.unmatchedLines)) + if pluginInst.strict { + umlPdat.SetCrit(perfdata.RangeMinMax("~", "0")) + } + info.updateFrom(umlPdat.GetStatus()) + pluginInst.results.AddPerfData(umlPdat) +} + +// checkResults checks the various counters against their configured thresholds, +// and the strict mode failure count. +func (pluginInst *matchesPlugin) checkResults(readError error) { + info := pluginInst.checkCounters() + pluginInst.checkUnmatched(&info) + + if readError != nil { + pluginInst.results.AddLinef("Error while reading data: %s", readError.Error()) + info.nUnknowns++ + } + + pluginInst.results.SetState(info.toStatus(), info.toProblems()) +} diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index eb7b2f1..87f1a34 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -17,6 +17,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" "nocternity.net/gomonop/pkg/results" + "nocternity.net/gomonop/pkg/status" ) //-------------------------------------------------------------------------------------------------------- @@ -224,20 +225,20 @@ func (program *checkProgram) Results() *results.Results { // if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(results.StatusUnknown, "no hostname specified") + program.plugin.SetState(status.StatusUnknown, "no hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(results.StatusUnknown, "invalid or missing port number") + program.plugin.SetState(status.StatusUnknown, "invalid or missing port number") return false } if program.warn != -1 && program.crit != -1 && program.warn <= program.crit { - program.plugin.SetState(results.StatusUnknown, "nonsensical thresholds") + program.plugin.SetState(status.StatusUnknown, "nonsensical thresholds") return false } if _, ok := certGetters[program.startTLS]; !ok { errstr := "unsupported StartTLS protocol " + program.startTLS - program.plugin.SetState(results.StatusUnknown, errstr) + program.plugin.SetState(status.StatusUnknown, errstr) return false } program.hostname = strings.ToLower(program.hostname) @@ -262,13 +263,13 @@ func (program *checkProgram) getCertificate() error { // matches the requested host name. func (program *checkProgram) checkSANlessCertificate() bool { if !program.ignoreCnOnly || len(program.extraNames) != 0 { - program.plugin.SetState(results.StatusWarning, + program.plugin.SetState(status.StatusWarning, "certificate doesn't have SAN domain names") return false } dn := strings.ToLower(program.certificate.Subject.String()) if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", program.hostname)) { - program.plugin.SetState(results.StatusCritical, "incorrect certificate CN") + program.plugin.SetState(status.StatusCritical, "incorrect certificate CN") return false } return true @@ -298,7 +299,7 @@ func (program *checkProgram) checkNames() bool { certificateIsOk = program.checkHostName(name) && certificateIsOk } if !certificateIsOk { - program.plugin.SetState(results.StatusCritical, "names missing from SAN domain names") + program.plugin.SetState(status.StatusCritical, "names missing from SAN domain names") } return certificateIsOk } @@ -306,26 +307,26 @@ func (program *checkProgram) checkNames() bool { // Check a certificate's time to expiry against the warning and critical // thresholds, returning a status code and description based on these // values. -func (program *checkProgram) checkCertificateExpiry(tlDays int) (results.Status, string) { +func (program *checkProgram) checkCertificateExpiry(tlDays int) (status.Status, string) { if tlDays <= 0 { - return results.StatusCritical, "certificate expired" + return status.StatusCritical, "certificate expired" } var limitStr string - var state results.Status + var state status.Status switch { case program.crit > 0 && tlDays <= program.crit: limitStr = fmt.Sprintf(" (<= %d)", program.crit) - state = results.StatusCritical + state = status.StatusCritical case program.warn > 0 && tlDays <= program.warn: limitStr = fmt.Sprintf(" (<= %d)", program.warn) - state = results.StatusWarning + state = status.StatusWarning default: limitStr = "" - state = results.StatusOK + state = status.StatusOK } statusString := fmt.Sprintf("certificate will expire in %d days%s", @@ -351,7 +352,7 @@ func (program *checkProgram) setPerfData(tlDays int) { func (program *checkProgram) RunCheck() { err := program.getCertificate() if err != nil { - program.plugin.SetState(results.StatusUnknown, err.Error()) + program.plugin.SetState(status.StatusUnknown, err.Error()) } else if program.checkNames() { timeLeft := time.Until(program.certificate.NotAfter) tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour)) diff --git a/cmd/zoneserial/main.go b/cmd/zoneserial/main.go index f5a1f25..b73965c 100644 --- a/cmd/zoneserial/main.go +++ b/cmd/zoneserial/main.go @@ -15,6 +15,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" "nocternity.net/gomonop/pkg/results" + "nocternity.net/gomonop/pkg/status" ) //------------------------------------------------------------------------------------------------------- @@ -93,23 +94,23 @@ func (program *checkProgram) Results() *results.Results { // Check the values that were specified from the command line. Returns true if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(results.StatusUnknown, "no DNS hostname specified") + program.plugin.SetState(status.StatusUnknown, "no DNS hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(results.StatusUnknown, "invalid DNS port number") + program.plugin.SetState(status.StatusUnknown, "invalid DNS port number") return false } if program.zone == "" { - program.plugin.SetState(results.StatusUnknown, "no DNS zone specified") + program.plugin.SetState(status.StatusUnknown, "no DNS zone specified") return false } if program.rsHostname == "" { - program.plugin.SetState(results.StatusUnknown, "no reference DNS hostname specified") + program.plugin.SetState(status.StatusUnknown, "no reference DNS hostname specified") return false } if program.rsPort < 1 || program.rsPort > 65535 { - program.plugin.SetState(results.StatusUnknown, "invalid reference DNS port number") + program.plugin.SetState(status.StatusUnknown, "invalid reference DNS port number") return false } program.hostname = strings.ToLower(program.hostname) @@ -176,12 +177,12 @@ func (program *checkProgram) RunCheck() { cOk, cSerial := program.getSerial("checked", checkResponse) rOk, rSerial := program.getSerial("reference", refResponse) if !(cOk && rOk) { - program.plugin.SetState(results.StatusUnknown, "could not read serials") + program.plugin.SetState(status.StatusUnknown, "could not read serials") return } if cSerial == rSerial { - program.plugin.SetState(results.StatusOK, "serials match") + program.plugin.SetState(status.StatusOK, "serials match") } else { - program.plugin.SetState(results.StatusCritical, "serials mismatch") + program.plugin.SetState(status.StatusCritical, "serials mismatch") } } diff --git a/go.mod b/go.mod index 111d2ca..6b14533 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,16 @@ go 1.22 require ( github.com/karrick/golf v1.4.0 github.com/miekg/dns v1.1.40 + github.com/stretchr/testify v1.9.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/karrick/golf v1.4.0 => github.com/tseeker/golf v0.0.0-20240720130627-ce082c3b50d5 diff --git a/go.sum b/go.sum index 37c20f7..d53fb3f 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/karrick/golf v1.4.0 h1:9i9HnUh7uCyUFJhIqg311HBibw4f2pbGldi0ZM2FhaQ= -github.com/karrick/golf v1.4.0/go.mod h1:qGN0IhcEL+IEgCXp00RvH32UP59vtwc8w5YcIdArNRk= github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tseeker/golf v0.0.0-20240720130627-ce082c3b50d5 h1:IreFtDkjJg81heDw+xIEkVZXySV4zQzwKbpenJBQHqk= +github.com/tseeker/golf v0.0.0-20240720130627-ce082c3b50d5/go.mod h1:qGN0IhcEL+IEgCXp00RvH32UP59vtwc8w5YcIdArNRk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -25,6 +25,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index d535144..cf84934 100644 --- a/main.go +++ b/main.go @@ -5,15 +5,17 @@ import ( "os" "path/filepath" + "nocternity.net/gomonop/cmd/matches" "nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/zoneserial" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/results" + "nocternity.net/gomonop/pkg/status" "nocternity.net/gomonop/pkg/version" ) var ( plugins map[string]plugin.Builder = map[string]plugin.Builder{ + "check_output_matches": matches.NewPlugin, "check_ssl_certificate": sslcert.NewProgram, "check_zone_serial": zoneserial.NewProgram, } @@ -28,7 +30,7 @@ func getPlugin() plugin.Plugin { if len(os.Args) < 2 { fmt.Printf("Syntax: %s [arguments]\n", ownName) - fmt.Printf(" %s --plugin|-p\n", ownName) + fmt.Printf(" %s --plugins|-p\n", ownName) fmt.Printf(" %s --version|-v\n", ownName) os.Exit(1) } @@ -61,7 +63,7 @@ func main() { output := runPlugin.Results() defer func() { if r := recover(); r != nil { - output.SetState(results.StatusUnknown, "Internal error") + output.SetState(status.StatusUnknown, "Internal error") output.AddLinef("Error info: %v", r) } fmt.Println(output.String()) diff --git a/pkg/perfdata/perfdata.go b/pkg/perfdata/perfdata.go index fc1914e..f551c78 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -3,7 +3,10 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( + "strconv" "strings" + + "nocternity.net/gomonop/pkg/status" ) // Performance data, including a label, units, a value, warning/critical @@ -35,14 +38,22 @@ func New(label string, units UnitOfMeasurement, value string) *PerfData { // Set the warning range for the performance data record. func (d *PerfData) SetWarn(r *Range) { - d.warn = *r - d.bits |= PDatWarn + if r == nil { + d.bits &^= PDatWarn + } else { + d.warn = *r + d.bits |= PDatWarn + } } // Set the critical range for the performance data record. func (d *PerfData) SetCrit(r *Range) { - d.crit = *r - d.bits |= PDatCrit + if r == nil { + d.bits &^= PDatCrit + } else { + d.crit = *r + d.bits |= PDatCrit + } } // Set the performance data's minimal value. @@ -63,6 +74,23 @@ func (d *PerfData) SetMax(max string) { d.bits |= PDatMax } +// Check the performance data's value against its configured ranges. +func (d *PerfData) GetStatus() status.Status { + value, err := strconv.ParseFloat(d.value, 64) + if err != nil { + return status.StatusUnknown + } + + if d.bits&PDatCrit != 0 && d.crit.Contains(value) { + return status.StatusCritical + } + if d.bits&PDatWarn != 0 && d.warn.Contains(value) { + return status.StatusWarning + } + + return status.StatusOK +} + // Converts performance data to a string which may be read by the monitoring // system. func (d *PerfData) String() string { diff --git a/pkg/perfdata/perfdata_test.go b/pkg/perfdata/perfdata_test.go index 894077c..ac3f3e9 100644 --- a/pkg/perfdata/perfdata_test.go +++ b/pkg/perfdata/perfdata_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "nocternity.net/gomonop/pkg/status" ) func TestNewNoValue(t *testing.T) { @@ -50,6 +51,12 @@ func TestSetWarn(t *testing.T) { assert.Equal(t, rangeStr, data.warn.String()) } +func TestSetWarnNil(t *testing.T) { + data := PerfData{} + data.SetWarn(nil) + assert.Equal(t, perfDataBits(0), data.bits&PDatWarn) +} + func TestSetWarnTwice(t *testing.T) { range1Value := Range{start: "A", end: "B"} range2Value := Range{start: "C", end: "D"} @@ -64,6 +71,16 @@ func TestSetWarnTwice(t *testing.T) { assert.Equal(t, range2Str, data.warn.String()) } +func TestSetWarnClear(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + + data := PerfData{} + data.SetWarn(&range1Value) + data.SetWarn(nil) + + assert.Equal(t, perfDataBits(0), data.bits&PDatWarn) +} + func TestSetCrit(t *testing.T) { rangeValue := Range{start: "A", end: "B"} rangeStr := rangeValue.String() @@ -75,6 +92,12 @@ func TestSetCrit(t *testing.T) { assert.Equal(t, rangeStr, data.crit.String()) } +func TestSetCritNil(t *testing.T) { + data := PerfData{} + data.SetCrit(nil) + assert.Equal(t, perfDataBits(0), data.bits&PDatCrit) +} + func TestSetCritTwice(t *testing.T) { range1Value := Range{start: "A", end: "B"} range2Value := Range{start: "C", end: "D"} @@ -89,6 +112,16 @@ func TestSetCritTwice(t *testing.T) { assert.Equal(t, range2Str, data.crit.String()) } +func TestSetCritClear(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + + data := PerfData{} + data.SetCrit(&range1Value) + data.SetCrit(nil) + + assert.Equal(t, perfDataBits(0), data.bits&PDatCrit) +} + func TestSetMin(t *testing.T) { const min = "100" @@ -135,6 +168,35 @@ func TestSetMaxTwice(t *testing.T) { assert.True(t, data.bits&PDatMax != 0) } +func TestCheck(t *testing.T) { + warnRange := RangeMinMax("0", "10").Inside() + critRange := RangeMinMax("1", "9").Inside() + + type Test struct { + in string + out status.Status + } + + tests := []Test{ + {in: "moo", out: status.StatusUnknown}, + {in: "-1", out: status.StatusOK}, + {in: "0", out: status.StatusWarning}, + {in: "1", out: status.StatusCritical}, + {in: "9", out: status.StatusCritical}, + {in: "10", out: status.StatusWarning}, + {in: "11", out: status.StatusOK}, + } + for _, test := range tests { + pdat := PerfData{ + value: test.in, + bits: PDatCrit | PDatWarn, + warn: *warnRange, + crit: *critRange, + } + assert.Equal(t, test.out, pdat.GetStatus()) + } +} + func TestString(t *testing.T) { type Test struct { PerfData diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go index e4ad2c7..7defac8 100644 --- a/pkg/perfdata/range.go +++ b/pkg/perfdata/range.go @@ -1,5 +1,10 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata +import ( + "fmt" + "strconv" +) + // Performance data range. type Range struct { start string @@ -29,6 +34,9 @@ func RangeMinMax(min, max string) *Range { panic("invalid performance data range minimum value") } pdRange := &Range{} + if min == "~" { + min = "" + } pdRange.start = min pdRange.end = max return pdRange @@ -60,3 +68,26 @@ func (r *Range) String() string { return inside + start + ":" + r.end } + +// Contains checks whether a numeric value is within the range. +func (r *Range) Contains(value float64) bool { + var inStart, inEnd bool + + if r.start != "" { + startValue, err := strconv.ParseFloat(r.start, 64) + if err != nil { + panic(fmt.Sprintf("invalid performance data range start value: %v", err)) + } + inStart = value < startValue + } + + if r.end != "" { + endValue, err := strconv.ParseFloat(r.end, 64) + if err != nil { + panic(fmt.Sprintf("invalid performance data range end value: %v", err)) + } + inEnd = value > endValue + } + + return (inStart || inEnd) != r.inside +} diff --git a/pkg/perfdata/range_test.go b/pkg/perfdata/range_test.go index 26d1f21..9873395 100644 --- a/pkg/perfdata/range_test.go +++ b/pkg/perfdata/range_test.go @@ -1,6 +1,7 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -40,6 +41,14 @@ func TestRangeMinMax(t *testing.T) { assert.False(t, pdr.inside, "Inside flag should not be set") } +func TestRangeMinMaxOpen(t *testing.T) { + min, max := "~", "456" + pdr := RangeMinMax(min, max) + assert.Equal(t, "", pdr.start, "Min value not clear in PerfDataRange") + assert.Equal(t, max, pdr.end, "Max value not copied to PerfDataRange") + assert.False(t, pdr.inside, "Inside flag should not be set") +} + func TestRangeInside(t *testing.T) { pdr := &Range{} pdr = pdr.Inside() @@ -65,3 +74,53 @@ func TestRangeString(t *testing.T) { assert.Equal(t, test.out, result, "Expected '%s', got '%s'", test.out, result) } } + +func TestRangeContains(t *testing.T) { + type Test struct { + pdr Range + value float64 + result bool + } + + tests := []Test{ + {pdr: Range{start: "0", end: "10"}, value: 0, result: false}, + {pdr: Range{start: "0", end: "10"}, value: 10, result: false}, + {pdr: Range{start: "0", end: "10"}, value: -1, result: true}, + {pdr: Range{start: "0", end: "10"}, value: 11, result: true}, + {pdr: Range{start: "", end: "10"}, value: -1000, result: false}, + {pdr: Range{start: "", end: "10"}, value: 10, result: false}, + {pdr: Range{start: "", end: "10"}, value: 11, result: true}, + {pdr: Range{start: "10", end: ""}, value: -1000, result: true}, + {pdr: Range{start: "10", end: ""}, value: 9, result: true}, + {pdr: Range{start: "10", end: ""}, value: 10, result: false}, + {pdr: Range{start: "10", end: "20"}, value: 9, result: true}, + {pdr: Range{start: "10", end: "20"}, value: 10, result: false}, + {pdr: Range{start: "10", end: "20"}, value: 20, result: false}, + {pdr: Range{start: "10", end: "20"}, value: 21, result: true}, + } + + // Test cases with the inside flag set and the opposite result + n := len(tests) + for i := range n { + tests = append(tests, Test{ + pdr: Range{ + start: tests[i].pdr.start, + end: tests[i].pdr.end, + inside: !tests[i].pdr.inside, + }, + value: tests[i].value, + result: !tests[i].result, + }) + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%v", test), func(t *testing.T) { + result := test.pdr.Contains(test.value) + assert.Equal( + t, test.result, result, + "Expected '%v', got '%v' for value '%f' and range '%s'", + test.result, result, test.value, test.pdr.String(), + ) + }) + } +} diff --git a/pkg/perfdata/rangeparser.go b/pkg/perfdata/rangeparser.go new file mode 100644 index 0000000..c9ea0c1 --- /dev/null +++ b/pkg/perfdata/rangeparser.go @@ -0,0 +1,196 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "fmt" + "strings" +) + +// A state of the range parser. +type rangeParserState int + +const ( + rpsInit rangeParserState = iota // Initial state + rpsExpectStart // Expect the start of the range + rpsInStart // Reading the start of the range + rpsExpectColon // Expect the colon that separates the start and end + rpsExpectEnd // Expect the end of the range + rpsInEnd // Reading the end of the range +) + +// An error emitted by the range parser. +type rangeParserError struct { + input string + position int + message string +} + +// parseError creates a new range parser error. +func parseError(input string, position int, message string) *rangeParserError { + return &rangeParserError{ + input: input, + position: position, + message: message, + } +} + +// Error implements the error interface for the range parser error type. +func (rpe *rangeParserError) Error() string { + return fmt.Sprintf("in `%s' at position %d: %s", rpe.input, rpe.position, rpe.message) +} + +// The full state of the range parser. +type rangeParser struct { + runes []rune // The runes being parsed + index int // The read position + state rangeParserState // The FSM state + startOfValue int // The position of the start of the value being read + strBuilder strings.Builder // An accumulator for values + output Range // The output being generated +} + +// initRangeParser initializes the range parser from the specified input. +func initRangeParser(input string) *rangeParser { + return &rangeParser{ + runes: []rune(input), + state: rpsInit, + } +} + +// handleInit handles the parser's rpsInit state. It can accept a '@' or skip +// to rpsExpectStart. +func (rp *rangeParser) handleInit(current rune) { + if current == '@' { + rp.output.inside = true + rp.index++ + } + rp.state = rpsExpectStart +} + +// handleExpectStart handles the parser's rpsExpectStart state. It may accept +// a colon, which will move the parser to the rpsExpectEnd state, a '~' which +// will move it to rpsExpectColon, or any other rune which will cause the +// accumulation of the "start" value to begin, and the state to be moved to +// handleInStart. +func (rp *rangeParser) handleExpectStart(current rune) { + switch current { + case ':': + rp.output.start = "0" + rp.state = rpsExpectEnd + rp.index++ + case '~': + rp.state = rpsExpectColon + rp.index++ + default: + rp.startOfValue = rp.index + rp.state = rpsInStart + } +} + +// handleInStart handles the parser's rpsInStart state, which corresponds to the +// reading of the "start" value. If a colon is found, the value will be written +// to the range structure and validated, before switching to the rpsExpectEnd +// state. Otherwise it will simply accumulate runes. +func (rp *rangeParser) handleInStart(current rune) error { + switch current { + case ':': + rp.output.start = rp.strBuilder.String() + if !valueCheck.MatchString(rp.output.start) { + return parseError(string(rp.runes), rp.startOfValue, "invalid start value") + } + rp.strBuilder.Reset() + rp.state = rpsExpectEnd + default: + rp.strBuilder.WriteRune(current) + } + + rp.index++ + return nil +} + +// handleExpectColon handles the parser's rpsExpectColon state. A colon MUST be +// read, otherwise an error will be returned. The parser will then switch to the +// rpsExpectEnd state. +func (rp *rangeParser) handleExpectColon(current rune) error { + switch current { + case ':': + rp.state = rpsExpectEnd + rp.index++ + default: + return parseError(string(rp.runes), rp.index, "expected ':'") + } + + return nil +} + +// handleExpectEnd handles the parser's rpsExpectEnd state, which sets the +// position of the beginning of the "end" value, and jumps to the rpsInEnd +// state. +func (rp *rangeParser) handleExpectEnd() { + rp.startOfValue = rp.index + rp.state = rpsInEnd +} + +// handleInEnd handles the parser's rpsInEnd state, which accumulates runes for +// the "end" value. +func (rp *rangeParser) handleInEnd(current rune) { + rp.strBuilder.WriteRune(current) + rp.index++ +} + +// consumeInput is the parser's state machine. +func (rp *rangeParser) consumeInput() error { + for rp.index < len(rp.runes) { + var err error + curRune := rp.runes[rp.index] + + switch rp.state { + case rpsInit: + rp.handleInit(curRune) + + case rpsExpectStart: + rp.handleExpectStart(curRune) + + case rpsInStart: + err = rp.handleInStart(curRune) + + case rpsExpectColon: + err = rp.handleExpectColon(curRune) + + case rpsExpectEnd: + rp.handleExpectEnd() + + case rpsInEnd: + rp.handleInEnd(curRune) + } + + if err != nil { + return err + } + } + + return nil +} + +// Try to parse a string into a performance data range. +func ParseRange(input string) (*Range, error) { + parser := initRangeParser(input) + if err := parser.consumeInput(); err != nil { + return nil, err + } + + if parser.state == rpsInStart { + // The range was a single value, so that's the upper bound. + parser.output.start = "0" + parser.state = rpsInEnd + } + if parser.state != rpsInEnd { + return nil, parseError(input, parser.index, "unexpected end of input") + } + + parser.output.end = parser.strBuilder.String() + if !valueCheck.MatchString(parser.output.end) { + return nil, parseError(input, parser.startOfValue, "invalid end value") + } + + return &parser.output, nil +} diff --git a/pkg/perfdata/rangeparser_test.go b/pkg/perfdata/rangeparser_test.go new file mode 100644 index 0000000..8ff5271 --- /dev/null +++ b/pkg/perfdata/rangeparser_test.go @@ -0,0 +1,66 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRangeParserOk(t *testing.T) { + type Test struct { + in string + out Range + } + tests := []Test{ + {in: ":0", out: Range{start: "0", end: "0"}}, + {in: "0:0", out: Range{start: "0", end: "0"}}, + {in: "~:0", out: Range{start: "", end: "0"}}, + {in: ":123", out: Range{start: "0", end: "123"}}, + {in: "0:123", out: Range{start: "0", end: "123"}}, + {in: "~:123", out: Range{start: "", end: "123"}}, + {in: "1", out: Range{start: "0", end: "1"}}, + {in: "@:0", out: Range{start: "0", end: "0", inside: true}}, + {in: "@0:0", out: Range{start: "0", end: "0", inside: true}}, + {in: "@~:0", out: Range{start: "", end: "0", inside: true}}, + {in: "@:123", out: Range{start: "0", end: "123", inside: true}}, + {in: "@0:123", out: Range{start: "0", end: "123", inside: true}}, + {in: "@~:123", out: Range{start: "", end: "123", inside: true}}, + {in: "@1", out: Range{start: "0", end: "1", inside: true}}, + } + + for _, test := range tests { + result, err := ParseRange(test.in) + require.NoError(t, err, "Expected no error, got '%v'", err) + assert.Equal(t, test.out, *result, "Expected '%v', got '%v'", test.out, *result) + } +} + +func TestRangeParserError(t *testing.T) { + type Test struct { + in string + errPos int + } + tests := []Test{ + {in: "", errPos: 0}, + {in: ":", errPos: 1}, + {in: "x:1", errPos: 0}, + {in: ":~", errPos: 1}, + {in: "@", errPos: 1}, + {in: "@:", errPos: 2}, + {in: "@x:1", errPos: 1}, + {in: "@:~", errPos: 2}, + } + + for _, test := range tests { + result, err := ParseRange(test.in) + require.Error(t, err, "Expected error, got '%v'", err) + assert.Nil(t, result, "Expected nil result, got '%v'", result) + assert.True( + t, strings.Contains(err.Error(), fmt.Sprintf("at position %d", test.errPos)), + "Expected error to contain '%s', got '%s'", fmt.Sprintf("at position %d", test.errPos), err, + ) + } +} diff --git a/pkg/results/results.go b/pkg/results/results.go index 3118071..bc34c13 100644 --- a/pkg/results/results.go +++ b/pkg/results/results.go @@ -9,6 +9,7 @@ import ( "strings" "nocternity.net/gomonop/pkg/perfdata" + "nocternity.net/gomonop/pkg/status" ) // Results represents the monitoring plugin's results, including its name, @@ -16,7 +17,7 @@ import ( // data to be encoded in the output. type Results struct { name string - status Status + status status.Status message string extraText *list.List perfData map[string]*perfdata.PerfData @@ -26,7 +27,7 @@ type Results struct { func New(name string) *Results { p := new(Results) p.name = name - p.status = StatusUnknown + p.status = status.StatusUnknown p.message = "no status set" p.perfData = make(map[string]*perfdata.PerfData) return p @@ -34,7 +35,7 @@ func New(name string) *Results { // SetState sets the plugin's output code to `status` and its message to // the specified `message`. -func (p *Results) SetState(status Status, message string) { +func (p *Results) SetState(status status.Status, message string) { p.status = status p.message = message } diff --git a/pkg/results/results_test.go b/pkg/results/results_test.go index 32ddacf..b6566fb 100644 --- a/pkg/results/results_test.go +++ b/pkg/results/results_test.go @@ -5,14 +5,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "nocternity.net/gomonop/pkg/perfdata" + "nocternity.net/gomonop/pkg/status" ) func TestNew(t *testing.T) { p := New("test") assert.Equal(t, p.name, "test") - assert.Equal(t, p.status, StatusUnknown) + assert.Equal(t, p.status, status.StatusUnknown) assert.Equal(t, p.message, "no status set") assert.Nil(t, p.extraText) assert.NotNil(t, p.perfData) @@ -21,9 +23,9 @@ func TestNew(t *testing.T) { func TestSetState(t *testing.T) { p := Results{} - p.SetState(StatusWarning, "test") + p.SetState(status.StatusWarning, "test") - assert.Equal(t, p.status, StatusWarning) + assert.Equal(t, p.status, status.StatusWarning) assert.Equal(t, p.message, "test") } @@ -96,7 +98,7 @@ func TestString(t *testing.T) { { Results{ name: "test", - status: StatusWarning, + status: status.StatusWarning, message: "test", perfData: make(map[string]*perfdata.PerfData), }, @@ -106,7 +108,7 @@ func TestString(t *testing.T) { func() Results { p := Results{ name: "test", - status: StatusWarning, + status: status.StatusWarning, message: "test", perfData: make(map[string]*perfdata.PerfData), extraText: list.New(), @@ -121,7 +123,7 @@ func TestString(t *testing.T) { func() Results { p := Results{ name: "test", - status: StatusWarning, + status: status.StatusWarning, message: "test", perfData: make(map[string]*perfdata.PerfData), } @@ -136,7 +138,7 @@ func TestString(t *testing.T) { func() Results { p := Results{ name: "test", - status: StatusWarning, + status: status.StatusWarning, message: "test", perfData: make(map[string]*perfdata.PerfData), extraText: list.New(), @@ -161,15 +163,15 @@ func TestString(t *testing.T) { func TestExitCode(t *testing.T) { p := Results{} - p.status = StatusOK - assert.Equal(t, int(StatusOK), p.ExitCode()) + p.status = status.StatusOK + assert.Equal(t, int(status.StatusOK), p.ExitCode()) - p.status = StatusWarning - assert.Equal(t, int(StatusWarning), p.ExitCode()) + p.status = status.StatusWarning + assert.Equal(t, int(status.StatusWarning), p.ExitCode()) - p.status = StatusCritical - assert.Equal(t, int(StatusCritical), p.ExitCode()) + p.status = status.StatusCritical + assert.Equal(t, int(status.StatusCritical), p.ExitCode()) - p.status = StatusUnknown - assert.Equal(t, int(StatusUnknown), p.ExitCode()) + p.status = status.StatusUnknown + assert.Equal(t, int(status.StatusUnknown), p.ExitCode()) } diff --git a/pkg/results/status.go b/pkg/status/status.go similarity index 74% rename from pkg/results/status.go rename to pkg/status/status.go index 6238a20..f34382e 100644 --- a/pkg/results/status.go +++ b/pkg/status/status.go @@ -1,4 +1,6 @@ -package results // import nocternity.net/gomonop/pkg/results +// The status package contains the datatype that corresponds to monitoring +// plugin status values. +package status // import nocternity.net/gomonop/pkg/status // Status represents the return status of the monitoring plugin. The // corresponding integer value will be used as the program's exit code, diff --git a/pkg/results/status_test.go b/pkg/status/status_test.go similarity index 90% rename from pkg/results/status_test.go rename to pkg/status/status_test.go index 8f81f43..0e3de96 100644 --- a/pkg/results/status_test.go +++ b/pkg/status/status_test.go @@ -1,4 +1,4 @@ -package results // import nocternity.net/gomonop/pkg/results +package status // import nocternity.net/gomonop/pkg/status import ( "testing"