feat: add the check_output_matches
plugin #5
22 changed files with 1063 additions and 55 deletions
|
@ -34,3 +34,6 @@ linters-settings:
|
||||||
- standard
|
- standard
|
||||||
- default
|
- default
|
||||||
- localmodule
|
- localmodule
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- github.com/karrick/golf
|
||||||
|
|
48
README.md
48
README.md
|
@ -35,9 +35,9 @@ supports the following command-line flags:
|
||||||
|
|
||||||
### DNS zone serials
|
### DNS zone serials
|
||||||
|
|
||||||
The `check_zone_serial` plugin can be used to check that the version of a
|
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
|
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:
|
another, "reference" DNS. It supports the following command-line flags:
|
||||||
|
|
||||||
* `-H name`/`--hostname name`: the host name or address of the server to
|
* `-H name`/`--hostname name`: the host name or address of the server to
|
||||||
check.
|
check.
|
||||||
|
@ -49,6 +49,48 @@ supports the following command-line flags:
|
||||||
* `-p port`/`--rs-port port`: the port to use on the reference server
|
* `-p port`/`--rs-port port`: the port to use on the reference server
|
||||||
(defaults to 53).
|
(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
|
Building from source
|
||||||
--------------------
|
--------------------
|
||||||
|
|
95
cmd/matches/cmdline.go
Normal file
95
cmd/matches/cmdline.go
Normal file
|
@ -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)
|
||||||
|
}
|
118
cmd/matches/extprogram.go
Normal file
118
cmd/matches/extprogram.go
Normal file
|
@ -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()
|
||||||
|
}()
|
||||||
|
}
|
76
cmd/matches/plugin.go
Normal file
76
cmd/matches/plugin.go
Normal file
|
@ -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)
|
||||||
|
}
|
95
cmd/matches/reader.go
Normal file
95
cmd/matches/reader.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
cmd/matches/results.go
Normal file
125
cmd/matches/results.go
Normal file
|
@ -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())
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"nocternity.net/gomonop/pkg/perfdata"
|
"nocternity.net/gomonop/pkg/perfdata"
|
||||||
"nocternity.net/gomonop/pkg/plugin"
|
"nocternity.net/gomonop/pkg/plugin"
|
||||||
"nocternity.net/gomonop/pkg/results"
|
"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.
|
// if the arguments made sense.
|
||||||
func (program *checkProgram) CheckArguments() bool {
|
func (program *checkProgram) CheckArguments() bool {
|
||||||
if program.hostname == "" {
|
if program.hostname == "" {
|
||||||
program.plugin.SetState(results.StatusUnknown, "no hostname specified")
|
program.plugin.SetState(status.StatusUnknown, "no hostname specified")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if program.port < 1 || program.port > 65535 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if program.warn != -1 && program.crit != -1 && program.warn <= program.crit {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if _, ok := certGetters[program.startTLS]; !ok {
|
if _, ok := certGetters[program.startTLS]; !ok {
|
||||||
errstr := "unsupported StartTLS protocol " + program.startTLS
|
errstr := "unsupported StartTLS protocol " + program.startTLS
|
||||||
program.plugin.SetState(results.StatusUnknown, errstr)
|
program.plugin.SetState(status.StatusUnknown, errstr)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
program.hostname = strings.ToLower(program.hostname)
|
program.hostname = strings.ToLower(program.hostname)
|
||||||
|
@ -262,13 +263,13 @@ func (program *checkProgram) getCertificate() error {
|
||||||
// matches the requested host name.
|
// matches the requested host name.
|
||||||
func (program *checkProgram) checkSANlessCertificate() bool {
|
func (program *checkProgram) checkSANlessCertificate() bool {
|
||||||
if !program.ignoreCnOnly || len(program.extraNames) != 0 {
|
if !program.ignoreCnOnly || len(program.extraNames) != 0 {
|
||||||
program.plugin.SetState(results.StatusWarning,
|
program.plugin.SetState(status.StatusWarning,
|
||||||
"certificate doesn't have SAN domain names")
|
"certificate doesn't have SAN domain names")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
dn := strings.ToLower(program.certificate.Subject.String())
|
dn := strings.ToLower(program.certificate.Subject.String())
|
||||||
if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", program.hostname)) {
|
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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -298,7 +299,7 @@ func (program *checkProgram) checkNames() bool {
|
||||||
certificateIsOk = program.checkHostName(name) && certificateIsOk
|
certificateIsOk = program.checkHostName(name) && certificateIsOk
|
||||||
}
|
}
|
||||||
if !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
|
return certificateIsOk
|
||||||
}
|
}
|
||||||
|
@ -306,26 +307,26 @@ func (program *checkProgram) checkNames() bool {
|
||||||
// Check a certificate's time to expiry against the warning and critical
|
// Check a certificate's time to expiry against the warning and critical
|
||||||
// thresholds, returning a status code and description based on these
|
// thresholds, returning a status code and description based on these
|
||||||
// values.
|
// values.
|
||||||
func (program *checkProgram) checkCertificateExpiry(tlDays int) (results.Status, string) {
|
func (program *checkProgram) checkCertificateExpiry(tlDays int) (status.Status, string) {
|
||||||
if tlDays <= 0 {
|
if tlDays <= 0 {
|
||||||
return results.StatusCritical, "certificate expired"
|
return status.StatusCritical, "certificate expired"
|
||||||
}
|
}
|
||||||
|
|
||||||
var limitStr string
|
var limitStr string
|
||||||
var state results.Status
|
var state status.Status
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case program.crit > 0 && tlDays <= program.crit:
|
case program.crit > 0 && tlDays <= program.crit:
|
||||||
limitStr = fmt.Sprintf(" (<= %d)", program.crit)
|
limitStr = fmt.Sprintf(" (<= %d)", program.crit)
|
||||||
state = results.StatusCritical
|
state = status.StatusCritical
|
||||||
|
|
||||||
case program.warn > 0 && tlDays <= program.warn:
|
case program.warn > 0 && tlDays <= program.warn:
|
||||||
limitStr = fmt.Sprintf(" (<= %d)", program.warn)
|
limitStr = fmt.Sprintf(" (<= %d)", program.warn)
|
||||||
state = results.StatusWarning
|
state = status.StatusWarning
|
||||||
|
|
||||||
default:
|
default:
|
||||||
limitStr = ""
|
limitStr = ""
|
||||||
state = results.StatusOK
|
state = status.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
statusString := fmt.Sprintf("certificate will expire in %d days%s",
|
statusString := fmt.Sprintf("certificate will expire in %d days%s",
|
||||||
|
@ -351,7 +352,7 @@ func (program *checkProgram) setPerfData(tlDays int) {
|
||||||
func (program *checkProgram) RunCheck() {
|
func (program *checkProgram) RunCheck() {
|
||||||
err := program.getCertificate()
|
err := program.getCertificate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
program.plugin.SetState(results.StatusUnknown, err.Error())
|
program.plugin.SetState(status.StatusUnknown, err.Error())
|
||||||
} else if program.checkNames() {
|
} else if program.checkNames() {
|
||||||
timeLeft := time.Until(program.certificate.NotAfter)
|
timeLeft := time.Until(program.certificate.NotAfter)
|
||||||
tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour))
|
tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour))
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"nocternity.net/gomonop/pkg/perfdata"
|
"nocternity.net/gomonop/pkg/perfdata"
|
||||||
"nocternity.net/gomonop/pkg/plugin"
|
"nocternity.net/gomonop/pkg/plugin"
|
||||||
"nocternity.net/gomonop/pkg/results"
|
"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.
|
// Check the values that were specified from the command line. Returns true if the arguments made sense.
|
||||||
func (program *checkProgram) CheckArguments() bool {
|
func (program *checkProgram) CheckArguments() bool {
|
||||||
if program.hostname == "" {
|
if program.hostname == "" {
|
||||||
program.plugin.SetState(results.StatusUnknown, "no DNS hostname specified")
|
program.plugin.SetState(status.StatusUnknown, "no DNS hostname specified")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if program.port < 1 || program.port > 65535 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if program.zone == "" {
|
if program.zone == "" {
|
||||||
program.plugin.SetState(results.StatusUnknown, "no DNS zone specified")
|
program.plugin.SetState(status.StatusUnknown, "no DNS zone specified")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if program.rsHostname == "" {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if program.rsPort < 1 || program.rsPort > 65535 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
program.hostname = strings.ToLower(program.hostname)
|
program.hostname = strings.ToLower(program.hostname)
|
||||||
|
@ -176,12 +177,12 @@ func (program *checkProgram) RunCheck() {
|
||||||
cOk, cSerial := program.getSerial("checked", checkResponse)
|
cOk, cSerial := program.getSerial("checked", checkResponse)
|
||||||
rOk, rSerial := program.getSerial("reference", refResponse)
|
rOk, rSerial := program.getSerial("reference", refResponse)
|
||||||
if !(cOk && rOk) {
|
if !(cOk && rOk) {
|
||||||
program.plugin.SetState(results.StatusUnknown, "could not read serials")
|
program.plugin.SetState(status.StatusUnknown, "could not read serials")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cSerial == rSerial {
|
if cSerial == rSerial {
|
||||||
program.plugin.SetState(results.StatusOK, "serials match")
|
program.plugin.SetState(status.StatusOK, "serials match")
|
||||||
} else {
|
} else {
|
||||||
program.plugin.SetState(results.StatusCritical, "serials mismatch")
|
program.plugin.SetState(status.StatusCritical, "serials mismatch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -5,14 +5,16 @@ go 1.22
|
||||||
require (
|
require (
|
||||||
github.com/karrick/golf v1.4.0
|
github.com/karrick/golf v1.4.0
|
||||||
github.com/miekg/dns v1.1.40
|
github.com/miekg/dns v1.1.40
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // 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/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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
|
||||||
|
|
5
go.sum
5
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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-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 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
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/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/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=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
8
main.go
8
main.go
|
@ -5,15 +5,17 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"nocternity.net/gomonop/cmd/matches"
|
||||||
"nocternity.net/gomonop/cmd/sslcert"
|
"nocternity.net/gomonop/cmd/sslcert"
|
||||||
"nocternity.net/gomonop/cmd/zoneserial"
|
"nocternity.net/gomonop/cmd/zoneserial"
|
||||||
"nocternity.net/gomonop/pkg/plugin"
|
"nocternity.net/gomonop/pkg/plugin"
|
||||||
"nocternity.net/gomonop/pkg/results"
|
"nocternity.net/gomonop/pkg/status"
|
||||||
"nocternity.net/gomonop/pkg/version"
|
"nocternity.net/gomonop/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
plugins map[string]plugin.Builder = map[string]plugin.Builder{
|
plugins map[string]plugin.Builder = map[string]plugin.Builder{
|
||||||
|
"check_output_matches": matches.NewPlugin,
|
||||||
"check_ssl_certificate": sslcert.NewProgram,
|
"check_ssl_certificate": sslcert.NewProgram,
|
||||||
"check_zone_serial": zoneserial.NewProgram,
|
"check_zone_serial": zoneserial.NewProgram,
|
||||||
}
|
}
|
||||||
|
@ -28,7 +30,7 @@ func getPlugin() plugin.Plugin {
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Printf("Syntax: %s <plugin> [arguments]\n", ownName)
|
fmt.Printf("Syntax: %s <plugin> [arguments]\n", ownName)
|
||||||
fmt.Printf(" %s --plugin|-p\n", ownName)
|
fmt.Printf(" %s --plugins|-p\n", ownName)
|
||||||
fmt.Printf(" %s --version|-v\n", ownName)
|
fmt.Printf(" %s --version|-v\n", ownName)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +63,7 @@ func main() {
|
||||||
output := runPlugin.Results()
|
output := runPlugin.Results()
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
output.SetState(results.StatusUnknown, "Internal error")
|
output.SetState(status.StatusUnknown, "Internal error")
|
||||||
output.AddLinef("Error info: %v", r)
|
output.AddLinef("Error info: %v", r)
|
||||||
}
|
}
|
||||||
fmt.Println(output.String())
|
fmt.Println(output.String())
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"nocternity.net/gomonop/pkg/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Performance data, including a label, units, a value, warning/critical
|
// 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.
|
// Set the warning range for the performance data record.
|
||||||
func (d *PerfData) SetWarn(r *Range) {
|
func (d *PerfData) SetWarn(r *Range) {
|
||||||
|
if r == nil {
|
||||||
|
d.bits &^= PDatWarn
|
||||||
|
} else {
|
||||||
d.warn = *r
|
d.warn = *r
|
||||||
d.bits |= PDatWarn
|
d.bits |= PDatWarn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the critical range for the performance data record.
|
// Set the critical range for the performance data record.
|
||||||
func (d *PerfData) SetCrit(r *Range) {
|
func (d *PerfData) SetCrit(r *Range) {
|
||||||
|
if r == nil {
|
||||||
|
d.bits &^= PDatCrit
|
||||||
|
} else {
|
||||||
d.crit = *r
|
d.crit = *r
|
||||||
d.bits |= PDatCrit
|
d.bits |= PDatCrit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the performance data's minimal value.
|
// Set the performance data's minimal value.
|
||||||
|
@ -63,6 +74,23 @@ func (d *PerfData) SetMax(max string) {
|
||||||
d.bits |= PDatMax
|
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
|
// Converts performance data to a string which may be read by the monitoring
|
||||||
// system.
|
// system.
|
||||||
func (d *PerfData) String() string {
|
func (d *PerfData) String() string {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"nocternity.net/gomonop/pkg/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewNoValue(t *testing.T) {
|
func TestNewNoValue(t *testing.T) {
|
||||||
|
@ -50,6 +51,12 @@ func TestSetWarn(t *testing.T) {
|
||||||
assert.Equal(t, rangeStr, data.warn.String())
|
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) {
|
func TestSetWarnTwice(t *testing.T) {
|
||||||
range1Value := Range{start: "A", end: "B"}
|
range1Value := Range{start: "A", end: "B"}
|
||||||
range2Value := Range{start: "C", end: "D"}
|
range2Value := Range{start: "C", end: "D"}
|
||||||
|
@ -64,6 +71,16 @@ func TestSetWarnTwice(t *testing.T) {
|
||||||
assert.Equal(t, range2Str, data.warn.String())
|
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) {
|
func TestSetCrit(t *testing.T) {
|
||||||
rangeValue := Range{start: "A", end: "B"}
|
rangeValue := Range{start: "A", end: "B"}
|
||||||
rangeStr := rangeValue.String()
|
rangeStr := rangeValue.String()
|
||||||
|
@ -75,6 +92,12 @@ func TestSetCrit(t *testing.T) {
|
||||||
assert.Equal(t, rangeStr, data.crit.String())
|
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) {
|
func TestSetCritTwice(t *testing.T) {
|
||||||
range1Value := Range{start: "A", end: "B"}
|
range1Value := Range{start: "A", end: "B"}
|
||||||
range2Value := Range{start: "C", end: "D"}
|
range2Value := Range{start: "C", end: "D"}
|
||||||
|
@ -89,6 +112,16 @@ func TestSetCritTwice(t *testing.T) {
|
||||||
assert.Equal(t, range2Str, data.crit.String())
|
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) {
|
func TestSetMin(t *testing.T) {
|
||||||
const min = "100"
|
const min = "100"
|
||||||
|
|
||||||
|
@ -135,6 +168,35 @@ func TestSetMaxTwice(t *testing.T) {
|
||||||
assert.True(t, data.bits&PDatMax != 0)
|
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) {
|
func TestString(t *testing.T) {
|
||||||
type Test struct {
|
type Test struct {
|
||||||
PerfData
|
PerfData
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
// Performance data range.
|
// Performance data range.
|
||||||
type Range struct {
|
type Range struct {
|
||||||
start string
|
start string
|
||||||
|
@ -29,6 +34,9 @@ func RangeMinMax(min, max string) *Range {
|
||||||
panic("invalid performance data range minimum value")
|
panic("invalid performance data range minimum value")
|
||||||
}
|
}
|
||||||
pdRange := &Range{}
|
pdRange := &Range{}
|
||||||
|
if min == "~" {
|
||||||
|
min = ""
|
||||||
|
}
|
||||||
pdRange.start = min
|
pdRange.start = min
|
||||||
pdRange.end = max
|
pdRange.end = max
|
||||||
return pdRange
|
return pdRange
|
||||||
|
@ -60,3 +68,26 @@ func (r *Range) String() string {
|
||||||
|
|
||||||
return inside + start + ":" + r.end
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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")
|
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) {
|
func TestRangeInside(t *testing.T) {
|
||||||
pdr := &Range{}
|
pdr := &Range{}
|
||||||
pdr = pdr.Inside()
|
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)
|
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(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
196
pkg/perfdata/rangeparser.go
Normal file
196
pkg/perfdata/rangeparser.go
Normal file
|
@ -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
|
||||||
|
}
|
66
pkg/perfdata/rangeparser_test.go
Normal file
66
pkg/perfdata/rangeparser_test.go
Normal file
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"nocternity.net/gomonop/pkg/perfdata"
|
"nocternity.net/gomonop/pkg/perfdata"
|
||||||
|
"nocternity.net/gomonop/pkg/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Results represents the monitoring plugin's results, including its name,
|
// Results represents the monitoring plugin's results, including its name,
|
||||||
|
@ -16,7 +17,7 @@ import (
|
||||||
// data to be encoded in the output.
|
// data to be encoded in the output.
|
||||||
type Results struct {
|
type Results struct {
|
||||||
name string
|
name string
|
||||||
status Status
|
status status.Status
|
||||||
message string
|
message string
|
||||||
extraText *list.List
|
extraText *list.List
|
||||||
perfData map[string]*perfdata.PerfData
|
perfData map[string]*perfdata.PerfData
|
||||||
|
@ -26,7 +27,7 @@ type Results struct {
|
||||||
func New(name string) *Results {
|
func New(name string) *Results {
|
||||||
p := new(Results)
|
p := new(Results)
|
||||||
p.name = name
|
p.name = name
|
||||||
p.status = StatusUnknown
|
p.status = status.StatusUnknown
|
||||||
p.message = "no status set"
|
p.message = "no status set"
|
||||||
p.perfData = make(map[string]*perfdata.PerfData)
|
p.perfData = make(map[string]*perfdata.PerfData)
|
||||||
return p
|
return p
|
||||||
|
@ -34,7 +35,7 @@ func New(name string) *Results {
|
||||||
|
|
||||||
// SetState sets the plugin's output code to `status` and its message to
|
// SetState sets the plugin's output code to `status` and its message to
|
||||||
// the specified `message`.
|
// the specified `message`.
|
||||||
func (p *Results) SetState(status Status, message string) {
|
func (p *Results) SetState(status status.Status, message string) {
|
||||||
p.status = status
|
p.status = status
|
||||||
p.message = message
|
p.message = message
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"nocternity.net/gomonop/pkg/perfdata"
|
"nocternity.net/gomonop/pkg/perfdata"
|
||||||
|
"nocternity.net/gomonop/pkg/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
p := New("test")
|
p := New("test")
|
||||||
|
|
||||||
assert.Equal(t, p.name, "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.Equal(t, p.message, "no status set")
|
||||||
assert.Nil(t, p.extraText)
|
assert.Nil(t, p.extraText)
|
||||||
assert.NotNil(t, p.perfData)
|
assert.NotNil(t, p.perfData)
|
||||||
|
@ -21,9 +23,9 @@ func TestNew(t *testing.T) {
|
||||||
func TestSetState(t *testing.T) {
|
func TestSetState(t *testing.T) {
|
||||||
p := Results{}
|
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")
|
assert.Equal(t, p.message, "test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +98,7 @@ func TestString(t *testing.T) {
|
||||||
{
|
{
|
||||||
Results{
|
Results{
|
||||||
name: "test",
|
name: "test",
|
||||||
status: StatusWarning,
|
status: status.StatusWarning,
|
||||||
message: "test",
|
message: "test",
|
||||||
perfData: make(map[string]*perfdata.PerfData),
|
perfData: make(map[string]*perfdata.PerfData),
|
||||||
},
|
},
|
||||||
|
@ -106,7 +108,7 @@ func TestString(t *testing.T) {
|
||||||
func() Results {
|
func() Results {
|
||||||
p := Results{
|
p := Results{
|
||||||
name: "test",
|
name: "test",
|
||||||
status: StatusWarning,
|
status: status.StatusWarning,
|
||||||
message: "test",
|
message: "test",
|
||||||
perfData: make(map[string]*perfdata.PerfData),
|
perfData: make(map[string]*perfdata.PerfData),
|
||||||
extraText: list.New(),
|
extraText: list.New(),
|
||||||
|
@ -121,7 +123,7 @@ func TestString(t *testing.T) {
|
||||||
func() Results {
|
func() Results {
|
||||||
p := Results{
|
p := Results{
|
||||||
name: "test",
|
name: "test",
|
||||||
status: StatusWarning,
|
status: status.StatusWarning,
|
||||||
message: "test",
|
message: "test",
|
||||||
perfData: make(map[string]*perfdata.PerfData),
|
perfData: make(map[string]*perfdata.PerfData),
|
||||||
}
|
}
|
||||||
|
@ -136,7 +138,7 @@ func TestString(t *testing.T) {
|
||||||
func() Results {
|
func() Results {
|
||||||
p := Results{
|
p := Results{
|
||||||
name: "test",
|
name: "test",
|
||||||
status: StatusWarning,
|
status: status.StatusWarning,
|
||||||
message: "test",
|
message: "test",
|
||||||
perfData: make(map[string]*perfdata.PerfData),
|
perfData: make(map[string]*perfdata.PerfData),
|
||||||
extraText: list.New(),
|
extraText: list.New(),
|
||||||
|
@ -161,15 +163,15 @@ func TestString(t *testing.T) {
|
||||||
func TestExitCode(t *testing.T) {
|
func TestExitCode(t *testing.T) {
|
||||||
p := Results{}
|
p := Results{}
|
||||||
|
|
||||||
p.status = StatusOK
|
p.status = status.StatusOK
|
||||||
assert.Equal(t, int(StatusOK), p.ExitCode())
|
assert.Equal(t, int(status.StatusOK), p.ExitCode())
|
||||||
|
|
||||||
p.status = StatusWarning
|
p.status = status.StatusWarning
|
||||||
assert.Equal(t, int(StatusWarning), p.ExitCode())
|
assert.Equal(t, int(status.StatusWarning), p.ExitCode())
|
||||||
|
|
||||||
p.status = StatusCritical
|
p.status = status.StatusCritical
|
||||||
assert.Equal(t, int(StatusCritical), p.ExitCode())
|
assert.Equal(t, int(status.StatusCritical), p.ExitCode())
|
||||||
|
|
||||||
p.status = StatusUnknown
|
p.status = status.StatusUnknown
|
||||||
assert.Equal(t, int(StatusUnknown), p.ExitCode())
|
assert.Equal(t, int(status.StatusUnknown), p.ExitCode())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// Status represents the return status of the monitoring plugin. The
|
||||||
// corresponding integer value will be used as the program's exit code,
|
// corresponding integer value will be used as the program's exit code,
|
|
@ -1,4 +1,4 @@
|
||||||
package results // import nocternity.net/gomonop/pkg/results
|
package status // import nocternity.net/gomonop/pkg/status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
Loading…
Reference in a new issue