From 9d51740b056aee0b0aa2c4e6ed52716c210ee9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20H=C3=B6=C3=9F?= Date: Sat, 19 Feb 2022 21:40:02 +0100 Subject: [PATCH] Initial --- app/go.mod | 5 + app/go.sum | 3 + app/main.go | 218 +++++++++++++++++++++++++++++++++++++++ app/sample.job.yml | 23 +++++ app/state/state.go | 27 +++++ app/timespec/timespec.go | 34 ++++++ app/types/types.go | 36 +++++++ 7 files changed, 346 insertions(+) create mode 100644 app/go.mod create mode 100644 app/go.sum create mode 100644 app/main.go create mode 100644 app/sample.job.yml create mode 100644 app/state/state.go create mode 100644 app/timespec/timespec.go create mode 100644 app/types/types.go diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..633de7c --- /dev/null +++ b/app/go.mod @@ -0,0 +1,5 @@ +module jobwatch + +go 1.18 + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..7534661 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..de65b74 --- /dev/null +++ b/app/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "gopkg.in/yaml.v2" + + "jobwatch/state" + "jobwatch/types" +) + +// Jobs root-idfile + +func evalOutput(job types.Job, output []string) (types.CMKState, error) { + var res types.CMKState = types.OK + var logLines []string + var rcs []*regexp.Regexp + + for _, m := range job.LogMatches { + r, err := regexp.Compile(m.Regex) + if err != nil { + return 0, err + } else { + rcs = append(rcs, r) + } + } + + // Determine state and wanted loglines + for _, v := range output { + for idx, m := range job.LogMatches { + if rcs[idx].Match([]byte(v)) { + if m.State > res { + res = m.State + } + + if m.AltMsg != "" { + v = fmt.Sprintf(m.AltMsg, v) + } + + var p = "" + switch s := m.State; s { + case types.OK: + p = "O" + case types.WARN: + p = "W" + case types.CRIT: + p = "C" + case types.UNKNOWN: + p = "U" + } + p += ": " + logLines = append(logLines, time.Now().Format("2006-01-2 15:04:05")+" "+p+v) + + } + } + } + for _, oln := range logLines { + log.Printf("- Matched LogLine %v\n", oln) + } + + return res, nil +} + +func loadJob(jobdir string, jobid string) (*types.Job, error) { + // Find jobfile + var dirs []string + if jobdir != "" { + dirs = append(dirs, jobdir) + } else { + if hd, err := os.UserHomeDir(); err == nil { + dirs = append(dirs, filepath.FromSlash(hd)+"/jobwatch.d") + } + dirs = append(dirs, "/etc/jobwatch.d") + } + + var jobfile = "" + for _, d := range dirs { + jobfile = filepath.FromSlash(d + "/" + jobid + ".job.yml") + if _, err := os.Stat(jobfile); err == nil { + break + } else { + jobfile = "" + continue + } + } + + if jobfile == "" { + return nil, fmt.Errorf("JobFile not found") + } + + // Load job from jobfile + job := types.Job{} + bytes, _ := os.ReadFile(jobfile) + err := yaml.Unmarshal(bytes, &job) + + if err == nil { + log.Printf("%+v\n", job) + } else { + return nil, fmt.Errorf("! Error reading job %v: %+v\n", jobfile, err) + } + return &job, nil +} + +func runJob(job types.Job) (types.CMKState, error) { + + cmd := exec.Command(job.Cmd, append(job.Args, job.InstArgs...)...) + + var stdoutBuf, stderrBuf, stdcombinedBuf bytes.Buffer + if job.HideOutput { + cmd.Stdout = io.MultiWriter(&stdoutBuf, &stdcombinedBuf) + cmd.Stderr = io.MultiWriter(&stderrBuf, &stdcombinedBuf) + } else { + cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf, &stdcombinedBuf) + cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf, &stdcombinedBuf) + } + + var state = 0 + if err := cmd.Run(); err != nil { + // cmd executed, but failed w/ non-zero + if exitError, ok := err.(*exec.ExitError); ok { + state = exitError.ProcessState.ExitCode() + } else { + // cmd not found etc + return types.UNKNOWN, err + } + if state == 0 { // Exec failed, but exitcode, be sure to get error! + state = int(types.UNKNOWN) + } + } else { + state = cmd.ProcessState.ExitCode() + } + log.Printf("- ExitCode of Command: %v", state) + + for _, em := range job.ExitCodeMap { + // From == -1 -> Alle non-zero-states mappen + if (state == int(em.From)) || (em.From == -1 && state > 0) { + state = int(em.To) + } + } + if state > int(types.UNKNOWN) { + state = int(types.CRIT) + } + log.Printf("- State after ExitCodeMapping: %v", state) + + outText := string(stdcombinedBuf.Bytes()) + log_state, err := evalOutput(job, strings.Split(outText, "\n")) + if int(log_state) > state { + state = int(log_state) + } + + log.Printf("- State Output-evaluation: %v", state) + + return types.CMKState(state), err +} + +func setup() (*types.Job, error) { + var jobdir string + var jobid string + var job_instance string + var debug bool + + flag.StringVar(&jobdir, "jd", "", "JobDir. Default: $HOME/etc/jobwatch.d /etc/jobwatch.d'") + flag.StringVar(&jobid, "j", "", "JobId. reads $jobDir/$job.job.yml Default ''") + flag.StringVar(&job_instance, "i", "", "JobId Instance. Default ''") + flag.BoolVar(&debug, "d", false, "Debug") + flag.Parse() + + if debug { + log.SetOutput(os.Stderr) + } else { + log.SetOutput(nil) + } + + if !regexp.MustCompile("^[a-z0-9]*$").MatchString(job_instance) { + return nil, fmt.Errorf("-i invalid chars") + } + + log.Printf(". Raw args : %+v\n", flag.Args()) + log.Printf(". Job onstance id : %+v\n", job_instance) + + if job, err := loadJob(jobdir, jobid); err == nil { + job.InstArgs = flag.Args() + job.InstId = job_instance + return job, nil + } else { + return nil, err + } +} + +func main() { + fmt.Println("Jobwatch 0.1") + + job, err := setup() + + var res = types.UNKNOWN + if err == nil { + res, err = runJob(*job) + } + + if err != nil { + fmt.Printf("! Error running job %+v\n", err) + os.Exit(int(types.UNKNOWN)) + } else { + os.Exit(int(res)) + } + + state.WriteLog(*job) +} diff --git a/app/sample.job.yml b/app/sample.job.yml new file mode 100644 index 0000000..3ed19ea --- /dev/null +++ b/app/sample.job.yml @@ -0,0 +1,23 @@ +cmd: /usr/bin/w +args: + - -i +exitcode_map: + - from: 23 + to: 3 + - from: -1 + to: 1 + - from: 0 + to: 1 +log_matches: + - regex: .*192.168.*.* + state: 1 + - regex: "-" + state: 2 + alt_msg: "User logged in at console (%v)" +hide_output: True +last_run_warn: + val: 8 + unit: "h" +last_run_crit: + val: 16 + unit: "h" diff --git a/app/state/state.go b/app/state/state.go new file mode 100644 index 0000000..8ae5e40 --- /dev/null +++ b/app/state/state.go @@ -0,0 +1,27 @@ +package state + +import ( + "fmt" + "jobwatch/timespec" + "jobwatch/types" +) + +/* + + + + */ +type State struct { + JobId string `json:"job_id"` + JobInstanceId string `json:"job_inst_id"` + LastRun string `json:"last_run"` + LastRunAge int `json:"last_run_age"` + LastRunAgeState int `json:"last_run_age_state"` + LastRunWarnAt timespec.TimeSpec `json:"last_run_warn_at"` + LastRunCritAt timespec.TimeSpec `json:"last_run_crit_at"` + LastExitCode int `json:"last_exit_code"` +} + +func WriteLog(job types.Job, line ...string) { + fmt.Println("B") +} diff --git a/app/timespec/timespec.go b/app/timespec/timespec.go new file mode 100644 index 0000000..f4db043 --- /dev/null +++ b/app/timespec/timespec.go @@ -0,0 +1,34 @@ +package timespec + +type TimeUnit string + +const ( + Sec = "s" + Min = "m" + Hour = "h" + Day = "d" +) + +type TimeSpec struct { + Value int `yaml:"val"` + Unit TimeUnit `yaml:"unit"` +} + +func (e TimeSpec) HasValidUnit() bool { + return (e.Unit == "" || + e.Unit == Sec || + e.Unit == Min || + e.Unit == Hour || + e.Unit == Day) +} + +func (e TimeSpec) AsSeconds() int { + if e.Unit == "" || e.Unit == Min { + return e.Value * 60 + } else if e.Unit == "" || e.Unit == Hour { + return e.Value * 60 * 60 + } else if e.Unit == "" || e.Unit == Day { + return e.Value * 60 * 60 * 24 + } + return e.Value +} diff --git a/app/types/types.go b/app/types/types.go new file mode 100644 index 0000000..1b32850 --- /dev/null +++ b/app/types/types.go @@ -0,0 +1,36 @@ +package types + +import "jobwatch/timespec" + +type CMKState int32 + +const ( + OK CMKState = iota + WARN + CRIT + UNKNOWN +) + +type LogMatch struct { + Regex string `yaml:"regex"` + State CMKState `yaml:"state"` + AltMsg string `yaml:"alt_msg"` +} + +type ExistCodeMapEntry struct { + From int32 `yaml:"from"` + To int32 `yaml:"to"` +} + +type Job struct { + Cmd string `yaml:"cmd"` + Args []string `yaml:"args"` + OutputLog string + ExitCodeMap []ExistCodeMapEntry `yaml:"exitcode_map"` + HideOutput bool `yaml:"hide_output"` + LogMatches []LogMatch `yaml:"log_matches"` + InstArgs []string + InstId string + LastRunWarn timespec.TimeSpec `yaml:"last_run_warn"` + LastRunCrit timespec.TimeSpec `yaml:"last_run_crit"` +}