summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2023-04-18 23:15:01 +0300
committerPaul Buetow <paul@buetow.org>2023-04-18 23:15:01 +0300
commit88c7597a3bed54002b27570f7f36b945cffd47ad (patch)
treeb8b2b7ead6fbc7740f565ecb96e2c6bc00638085 /internal
parent3658638e3bb3edb0f266c65fe0e42a1cd53a5f83 (diff)
split into main and internal package"
Diffstat (limited to 'internal')
-rw-r--r--internal/check.go48
-rw-r--r--internal/config.go58
-rw-r--r--internal/execute.go59
-rw-r--r--internal/nagioscode.go23
-rw-r--r--internal/notify.go33
-rw-r--r--internal/state.go140
6 files changed, 361 insertions, 0 deletions
diff --git a/internal/check.go b/internal/check.go
new file mode 100644
index 0000000..44e878d
--- /dev/null
+++ b/internal/check.go
@@ -0,0 +1,48 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "os/exec"
+ "strings"
+)
+
+type check struct {
+ Plugin string
+ Args []string
+}
+
+type namedCheck struct {
+ check
+ name string
+}
+
+type checkResult struct {
+ name string
+ output string
+ status nagiosCode
+}
+
+func (c check) execute(ctx context.Context, name string) checkResult {
+ cmd := exec.CommandContext(ctx, c.Plugin, c.Args...)
+
+ var bytes bytes.Buffer
+ cmd.Stdout = &bytes
+ cmd.Stderr = &bytes
+
+ if err := cmd.Run(); err != nil {
+ if ctx.Err() == context.DeadlineExceeded {
+ return checkResult{name, "Check command timed out", critical}
+ }
+ }
+
+ // Remove Nagios perf data from output and trim whitespaces
+ parts := strings.Split(bytes.String(), "|")
+ output := strings.TrimSpace(parts[0])
+
+ return checkResult{name, output, nagiosCode(cmd.ProcessState.ExitCode())}
+}
+
+func (c namedCheck) execute(ctx context.Context) checkResult {
+ return c.check.execute(ctx, c.name)
+}
diff --git a/internal/config.go b/internal/config.go
new file mode 100644
index 0000000..39e2209
--- /dev/null
+++ b/internal/config.go
@@ -0,0 +1,58 @@
+package internal
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+)
+
+type config struct {
+ EmailTo string
+ EmailFrom string
+ SMTPServer string `json:"omitempty"`
+ StateDir string `json:"omitempty"`
+ CheckTimeoutS int
+ CheckConcurrency int
+ Checks map[string]check
+}
+
+func newConfig(configFile string) (config, error) {
+ var config config
+
+ // Open the file
+ file, err := os.Open(configFile)
+ if err != nil {
+ return config, err
+ }
+ defer file.Close()
+
+ // Read the file content
+ bytes, err := ioutil.ReadAll(file)
+ if err != nil {
+ return config, err
+ }
+
+ // Parse the JSON content
+ err = json.Unmarshal(bytes, &config)
+ if err != nil {
+ return config, err
+ }
+
+ if config.SMTPServer == "" {
+ hostname, err := os.Hostname()
+ if err != nil {
+ panic(err)
+ }
+ config.SMTPServer = fmt.Sprintf("%s:25", hostname)
+ log.Println("Set SMTPServer to " + config.SMTPServer)
+ }
+
+ if config.StateDir == "" {
+ config.StateDir = "."
+ log.Println("Set StateDir to " + config.StateDir)
+ }
+
+ return config, nil
+}
diff --git a/internal/execute.go b/internal/execute.go
new file mode 100644
index 0000000..d8f426f
--- /dev/null
+++ b/internal/execute.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "context"
+ "log"
+ "sync"
+ "time"
+)
+
+func execute(state state, config config) state {
+ limiterCh := make(chan struct{}, config.CheckConcurrency)
+ inputCh := make(chan namedCheck)
+ outputCh := make(chan checkResult)
+
+ go func() {
+ for name, check := range config.Checks {
+ inputCh <- namedCheck{check, name}
+ }
+ close(inputCh)
+ }()
+
+ var outputWg sync.WaitGroup
+ outputWg.Add(1)
+
+ go func() {
+ for checkResult := range outputCh {
+ state.update(checkResult)
+ }
+ outputWg.Done()
+ }()
+
+ var inputWg sync.WaitGroup
+ inputWg.Add(len(config.Checks))
+
+ for check := range inputCh {
+ go func(check namedCheck) {
+ limiterCh <- struct{}{}
+ defer func() {
+ <-limiterCh
+ inputWg.Done()
+ }()
+
+ ctx, cancel := context.WithTimeout(context.Background(),
+ time.Duration(config.CheckTimeoutS)*time.Second)
+ defer cancel()
+
+ outputCh <- check.execute(ctx)
+ }(check)
+ }
+
+ inputWg.Wait()
+ log.Println("All checks completed!")
+ close(outputCh)
+
+ outputWg.Wait()
+ log.Println("All outputs collected!")
+
+ return state
+}
diff --git a/internal/nagioscode.go b/internal/nagioscode.go
new file mode 100644
index 0000000..b5dc892
--- /dev/null
+++ b/internal/nagioscode.go
@@ -0,0 +1,23 @@
+package internal
+
+type nagiosCode int
+
+const (
+ ok nagiosCode = 0
+ warning nagiosCode = 1
+ critical nagiosCode = 2
+ unknown nagiosCode = 3
+)
+
+func (n nagiosCode) Str() string {
+ switch n {
+ case 0:
+ return "OK"
+ case 1:
+ return "WARNING"
+ case 2:
+ return "CRITICAL"
+ default:
+ return "UNKNOWN"
+ }
+}
diff --git a/internal/notify.go b/internal/notify.go
new file mode 100644
index 0000000..7d8fc5a
--- /dev/null
+++ b/internal/notify.go
@@ -0,0 +1,33 @@
+package internal
+
+import (
+ "fmt"
+ "log"
+ "net/smtp"
+)
+
+func notify(config config, subject, body string) error {
+ log.Println("notify", subject, body)
+
+ headers := make(map[string]string)
+ headers["From"] = config.EmailFrom
+ headers["To"] = config.EmailTo
+ headers["Subject"] = subject
+ headers["MIME-Version"] = "1.0"
+ headers["Content-Type"] = "text/plain; charset=\"utf-8\""
+
+ header := ""
+ for k, v := range headers {
+ header += fmt.Sprintf("%s: %s\r\n", k, v)
+ }
+
+ message := header + "\r\n" + body
+ log.Println("Using SMTP server", config.SMTPServer)
+
+ return smtp.SendMail(config.SMTPServer, nil, config.EmailFrom,
+ []string{config.EmailTo}, []byte(message))
+}
+
+func notifyError(config config, err error) error {
+ return notify(config, fmt.Sprintf("GOGIOS: An error occured: %v", err), err.Error())
+}
diff --git a/internal/state.go b/internal/state.go
new file mode 100644
index 0000000..012bdf5
--- /dev/null
+++ b/internal/state.go
@@ -0,0 +1,140 @@
+package internal
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+)
+
+type checkState struct {
+ Status nagiosCode
+ PrevStatus nagiosCode
+ output string
+}
+
+type state struct {
+ stateFile string
+ checks map[string]checkState
+}
+
+func readState(config config) (state, error) {
+ s := state{
+ stateFile: fmt.Sprintf("%s/state.json", config.StateDir),
+ checks: make(map[string]checkState),
+ }
+
+ if _, err := os.Stat(s.stateFile); err != nil {
+ // OK, may be first run with no state yet.
+ return s, nil
+ }
+
+ file, err := os.Open(s.stateFile)
+ if err != nil {
+ return s, err
+ }
+ defer file.Close()
+
+ bytes, err := ioutil.ReadAll(file)
+ if err != nil {
+ return s, err
+ }
+
+ if err := json.Unmarshal(bytes, &s.checks); err != nil {
+ return s, err
+ }
+
+ var obsolete []string
+ for name := range s.checks {
+ if _, ok := config.Checks[name]; !ok {
+ obsolete = append(obsolete, name)
+ }
+ }
+
+ for _, name := range obsolete {
+ delete(s.checks, name)
+ log.Printf("State of %s is obsolete (removed)", name)
+ }
+
+ return s, nil
+}
+
+func (s state) update(result checkResult) {
+ prevStatus := unknown
+ prevState, ok := s.checks[result.name]
+ if ok {
+ prevStatus = prevState.Status
+ }
+
+ checkState := checkState{result.status, prevStatus, result.output}
+ s.checks[result.name] = checkState
+ log.Println(result.name, checkState)
+}
+
+func (s state) persist() error {
+ jsonData, err := json.Marshal(s.checks)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(s.stateFile, jsonData, os.ModePerm)
+}
+
+func (s state) report() (string, string, bool) {
+ var sb strings.Builder
+ var changed bool
+
+ f := func(filter func(n nagiosCode) bool) int {
+ var count int
+ for name, checkState := range s.checks {
+ if !filter(checkState.Status) {
+ continue
+ }
+ count++
+
+ if checkState.Status != checkState.PrevStatus {
+ sb.WriteString(nagiosCode(checkState.PrevStatus).Str())
+ sb.WriteString("->")
+ changed = true
+ }
+
+ sb.WriteString(nagiosCode(checkState.Status).Str())
+ sb.WriteString(": ")
+ sb.WriteString(name)
+ sb.WriteString(" ==>> ")
+ sb.WriteString(checkState.output)
+ sb.WriteString("\n")
+ }
+
+ return count
+ }
+
+ sb.WriteString("This is the recent Gogios report!\n\n")
+
+ numCriticals := f(func(n nagiosCode) bool { return n == 2 })
+ if numCriticals > 0 {
+ sb.WriteString("\n")
+ }
+
+ numWarnings := f(func(n nagiosCode) bool { return n == 1 })
+ if numWarnings > 0 {
+ sb.WriteString("\n")
+ }
+
+ numUnknowns := f(func(n nagiosCode) bool { return n > 2 })
+ if numUnknowns > 0 {
+ sb.WriteString("\n")
+ }
+
+ numOks := f(func(n nagiosCode) bool { return n == 0 })
+ if numOks > 0 {
+ sb.WriteString("\n")
+ }
+
+ sb.WriteString("Have a nice day!\n")
+ subject := fmt.Sprintf("GOGIOS Report [C:%d W:%d U:%d OK:%d]",
+ numCriticals, numWarnings, numUnknowns, numOks)
+
+ return subject, sb.String(), changed
+}