Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.idea
\#*
.\#*

vendor/
coverage.html
coverage.out
dist/

check_sms3status
60 changes: 60 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
version: "2"
run:
tests: false
linters:
default: all
enable:
- wsl_v5
- gomodguard_v2
disable:
- wsl
- gomodguard
- cyclop
- depguard
- err113
- exhaustruct
- forbidigo
- forcetypeassert
- gochecknoglobals
- gochecknoinits
- godot
- godox
- lll
- mnd
- musttag
- nakedret
- nlreturn
- nolintlint
- nonamedreturns
- tagliatelle
- varnamelen
- wrapcheck
- funlen
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: true
branch-max-lines: 2
disable:
- err
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.PHONY: test coverage lint vet

build:
CGO_ENABLED=0 go build
lint:
go fmt $(go list ./... | grep -v /vendor/)
vet:
go vet $(go list ./... | grep -v /vendor/)
test:
go test -v -cover ./...
coverage:
go test -v -cover -coverprofile=coverage.out ./... &&\
go tool cover -html=coverage.out -o coverage.html
51 changes: 23 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
check_sms3status
================
# check_sms3status

This plugin checks the status of an SMS modem using the regular_run functionality
provided in smstools3
A check plugin for SMS modem using the regular_run functionality provided by [smstools3](https://smstools3.kekekasvi.com/)

It does not directly access the modem, but instead reads a status file generated
by smstools3.
It does not directly access the modem, instead it reads the status file generated by smstools3.

In order to work the following options need to be set in smsd.conf
In order to work the following options need to be set in `smsd.conf`:

regular_run_interval = 60
regular_run_cmd = AT+CREG?;+CSQ;+COPS?
regular_run_statfile = F<status_file>
```
regular_run_interval = 60
regular_run_cmd = AT+CREG?;+CSQ;+COPS?
regular_run_statfile = F<status_file>
```

## Usage

### Requirements
```
Flags:
-a, --age int The maximum age of the file in seconds (default 300) (default 300)
-c, --critical string Critical threshold for signal strength in percent (default "20:")
-h, --help help for check_sms3status
-s, --statusfile string Path to the status file
-v, --version version for check_sms3status
-w, --warning string Warning threshold for signal strength in percent (default "40:")
```

* Perl library: `Nagios::Plugins utils.pm`

### Usage

check_sms3status [options] status_file

--warning
warning level for percentage signal strength (default 40)

--critical
critical level for percentage signal strength (default 20)

--timeout
how long to wait for the file (default 30)

--age
the maximum age of the file in seconds (default 300)
## Examples

```bash
check_sms3status --warning 30 --critical 50 --statusfile /path/to/statusfile
```
195 changes: 195 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package cmd

import (
"fmt"
"math"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/NETWAYS/go-check"
"github.com/NETWAYS/go-check/result"
"github.com/spf13/cobra"
)

var warning, critical, statusfilePath string

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put those variables in a config struct so it obvious that they belong together and share a purpose.

var maxFileAge int64

var csqRe = regexp.MustCompile(`\+CSQ:\s*(\d+),(\d+)`)
var copsReQuoted = regexp.MustCompile(`\+COPS:\s*\d+,\d+,"([^"]*)"`)
var copsReUnquoted = regexp.MustCompile(`\+COPS:\s*\d+,\d+,([^"]+)`)
var cregRe = regexp.MustCompile(`\+CREG:\s*(\d,(?:1|5))`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could those be constants?


var rootCmd = &cobra.Command{
Use: "check_sms3status",
Short: "This plugin checks the status of an SMS modem using the regular_run functionality provided in smstools3",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some prefix for "plugin" would be good. What kind of "plugin" is it? For what?

Assume someone might find this without a big Icinga/Nagios/Naemon background.

Long: `This plugin checks the status of an SMS modem using the regular_run functionality provided in smstools3a. It does not directly access the modem, but instead reads a status file generated by smstools3.`,
Example: `
check_sms3status --warning 30 --critical 50 --statusfile /path/to/statusfile
`,
Run: func(_ *cobra.Command, _ []string) {
// Parse the thresholds add exit if there's an issue
warnThreshold, warningThresholdErr := check.ParseThreshold(warning)

if warningThresholdErr != nil {
check.ExitError(warningThresholdErr)
}

critThreshold, critThresholdErr := check.ParseThreshold(critical)

if critThresholdErr != nil {
check.ExitError(critThresholdErr)
}

// Get the file content and info
content, fileInfo, contentErr := getFileContentAndInfo(statusfilePath)

if contentErr != nil {
check.ExitError(contentErr)
}

var overall result.Overall

overall.AddSubcheck(checkFileAge(fileInfo))
overall.AddSubcheck(checkContent(string(content), *warnThreshold, *critThreshold))

check.Exit(overall.GetStatus(), overall.GetOutput())
},
}

func Execute(version string) {
defer check.CatchPanic()

rootCmd.Version = version
rootCmd.VersionTemplate()

err := rootCmd.Execute()
if err != nil {
check.ExitError(err)
}
}

func init() {
rootCmd.Flags().StringVarP(&statusfilePath, "statusfile", "s", "", "Path to the status file")
rootCmd.Flags().StringVarP(&warning, "warning", "w", "40:", "Warning threshold for signal strength in percent")
rootCmd.Flags().StringVarP(&critical, "critical", "c", "20:", "Critical threshold for signal strength in percent")
rootCmd.Flags().Int64VarP(&maxFileAge, "age", "a", 300, "The maximum age of the file in seconds (default 300)")

_ = rootCmd.MarkFlagRequired("statusfile")
}

func Usage(cmd *cobra.Command, _ []string) {
_ = cmd.Usage()

os.Exit(3)
}

// getFileContentAndInfo returns the files content and file info
func getFileContentAndInfo(filePath string) ([]byte, os.FileInfo, error) {
content, contentErr := os.ReadFile(filePath)

var fileInfo os.FileInfo

if contentErr != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error check should be directly below the statement that might cause the error.

return content, fileInfo, contentErr
}

fileInfo, fileInfoErr := os.Stat(filePath)
if fileInfoErr != nil {
return content, fileInfo, fileInfoErr
}

return content, fileInfo, nil
}

// checkFileAge compares the given file into with the requested maximum file age
func checkFileAge(fileInfo os.FileInfo) *result.PartialResult {
nowSec := time.Now().Unix()
fileSec := fileInfo.ModTime().Unix()
fileAge := nowSec - fileSec

tmpResult := &result.PartialResult{
Output: fmt.Sprintf("Status file was last updated %d seconds ago", fileAge),
}

tmpResult.SetState(check.OK)

if fileAge > maxFileAge {
tmpResult.SetState(check.Critical)
}

return tmpResult
}

// checkContent compares the file content against the given thresholds
func checkContent(fileContent string, warningThreshold check.Threshold, criticalThreshold check.Threshold) *result.PartialResult {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should return an unknown if the file content is not a statfile

echo "foobar" > /tmp/status 

go run main.go  --statusfile /tmp/status
[CRITICAL] - Registered on network '' with signal strength 0%
\_ [OK] Status file was last updated 6 seconds ago
\_ [CRITICAL] Registered on network '' with signal strength 0%
|dbm=-113 signal=0%;40:;20: bit_error_rate=0

var signal, sigber int

var network string

isRegistered := true

lines := strings.SplitSeq(fileContent, "\n")

for line := range lines {
if matches := csqRe.FindStringSubmatch(line); matches != nil {
val, err := strconv.Atoi(matches[1])
if err == nil {
signal = val
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no match for the regex, signal is just zero. Is that correct?


val, err = strconv.Atoi(matches[2])
if err == nil {
sigber = val
}
} else if matches := copsReQuoted.FindStringSubmatch(line); matches != nil {
network = matches[1]
} else if matches := copsReUnquoted.FindStringSubmatch(line); matches != nil {
network = matches[1]
}

if strings.Contains(line, "+CREG:") && !cregRe.MatchString(line) {
isRegistered = false
}
}

sigdb := (2 * signal) - 113
sigproc := float64(signal*100) / 31.0
result := result.NewPartialResult()

if !isRegistered {
result.Output = "Modem not registered on network"
result.SetState(check.Critical)

return result
} else if signal > 31 {
result.Output = "Signal strength returned an invalid value"
result.SetState(check.Unknown)
}

result.Perfdata.Add(&check.Perfdata{Label: "dbm", Value: sigdb, Uom: "", Warn: nil, Crit: nil, Min: nil, Max: nil})
result.Perfdata.Add(&check.Perfdata{Label: "signal", Value: math.Round(sigproc), Uom: "%", Warn: &warningThreshold, Crit: &criticalThreshold})

msg := fmt.Sprintf("Registered on network '%s' with signal strength %0.f%%", network, sigproc)

result.Output = msg

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just result.Output = fmt.Sprinf....?


// bit error rate 99 means unknown
if sigber != 99 {
result.Perfdata.Add(&check.Perfdata{Label: "bit_error_rate", Value: sigber})
}

//nolint: gocritic
if criticalThreshold.DoesViolate(sigproc) {
result.SetState(check.Critical)
} else if warningThreshold.DoesViolate(sigproc) {
result.SetState(check.Warning)
result.Output = msg
} else {
result.SetState(check.OK)
}

return result
}
Loading