diff options
| author | Paul Buetow <paul@buetow.org> | 2023-04-18 23:15:01 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2023-04-18 23:15:01 +0300 |
| commit | 88c7597a3bed54002b27570f7f36b945cffd47ad (patch) | |
| tree | b8b2b7ead6fbc7740f565ecb96e2c6bc00638085 /internal | |
| parent | 3658638e3bb3edb0f266c65fe0e42a1cd53a5f83 (diff) | |
split into main and internal package"
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/check.go | 48 | ||||
| -rw-r--r-- | internal/config.go | 58 | ||||
| -rw-r--r-- | internal/execute.go | 59 | ||||
| -rw-r--r-- | internal/nagioscode.go | 23 | ||||
| -rw-r--r-- | internal/notify.go | 33 | ||||
| -rw-r--r-- | internal/state.go | 140 |
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 +} |
