diff --git a/app/main.go b/app/main.go index de65b74..d3df2ae 100644 --- a/app/main.go +++ b/app/main.go @@ -5,16 +5,14 @@ import ( "flag" "fmt" "io" + "io/ioutil" "log" "os" "os/exec" - "path/filepath" "regexp" "strings" "time" - "gopkg.in/yaml.v2" - "jobwatch/state" "jobwatch/types" ) @@ -71,47 +69,12 @@ func evalOutput(job types.Job, output []string) (types.CMKState, error) { 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") - } +func runJob(job types.Job) (state.State, error) { - 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) { + st := state.StateFromJob(job) + st.LastExitCode = -1 + st.LastState = int(types.UNKNOWN) + st.LastRun = time.Now().Format(time.RFC3339) cmd := exec.Command(job.Cmd, append(job.Args, job.InstArgs...)...) @@ -124,43 +87,48 @@ func runJob(job types.Job) (types.CMKState, error) { cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf, &stdcombinedBuf) } - var state = 0 + var stcode = 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() + stcode = exitError.ProcessState.ExitCode() } else { // cmd not found etc - return types.UNKNOWN, err + return st, err } - if state == 0 { // Exec failed, but exitcode, be sure to get error! - state = int(types.UNKNOWN) + if stcode == 0 { // Exec failed, but exitcode, be sure to get error! + stcode = int(types.UNKNOWN) } } else { - state = cmd.ProcessState.ExitCode() + stcode = cmd.ProcessState.ExitCode() } - log.Printf("- ExitCode of Command: %v", state) + var exitCode = stcode + log.Printf("- ExitCode of Command: %v", stcode) 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 (stcode == int(em.From)) || (em.From == -1 && stcode > 0) { + stcode = int(em.To) } } - if state > int(types.UNKNOWN) { - state = int(types.CRIT) + if stcode > int(types.UNKNOWN) { + stcode = int(types.CRIT) } - log.Printf("- State after ExitCodeMapping: %v", state) + log.Printf("- State after ExitCodeMapping: %v", stcode) outText := string(stdcombinedBuf.Bytes()) log_state, err := evalOutput(job, strings.Split(outText, "\n")) - if int(log_state) > state { - state = int(log_state) + if int(log_state) > stcode { + stcode = int(log_state) } - log.Printf("- State Output-evaluation: %v", state) + log.Printf("- State after Output-evaluation: %v", stcode) - return types.CMKState(state), err + st.LastExitCode = exitCode + st.LastState = stcode + st.LastRun = time.Now().Format(time.RFC3339) + + return st, err } func setup() (*types.Job, error) { @@ -178,17 +146,26 @@ func setup() (*types.Job, error) { if debug { log.SetOutput(os.Stderr) } else { - log.SetOutput(nil) + log.SetOutput(ioutil.Discard) } - if !regexp.MustCompile("^[a-z0-9]*$").MatchString(job_instance) { - return nil, fmt.Errorf("-i invalid chars") + if jobdir == "" && jobid == "" && job_instance == "" { + // No params -> Show mode + return nil, nil + } + + if !regexp.MustCompile("^[a-z0-9\\-]*$").MatchString(jobid) { + return nil, fmt.Errorf("-j: invalid chars") + } + + 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 { + if job, err := types.LoadJob(jobdir, jobid); err == nil { job.InstArgs = flag.Args() job.InstId = job_instance return job, nil @@ -202,17 +179,26 @@ func main() { 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)) + if job == nil && err == nil { + fmt.Println("<<>>") + state.ShowAll() } else { - os.Exit(int(res)) + var res state.State + if err == nil { + res, err = runJob(*job) + } + + res.ReEval() + res.SaveState() + + if err != nil { + fmt.Printf("! Error running job %+v\n", err) + os.Exit(int(types.UNKNOWN)) + } else { + os.Exit(int(res.LastState)) + } + + state.WriteLog(*job) } - state.WriteLog(*job) } diff --git a/app/state/state.go b/app/state/state.go index 8ae5e40..18803fc 100644 --- a/app/state/state.go +++ b/app/state/state.go @@ -1,9 +1,16 @@ package state import ( + "bufio" + "encoding/json" "fmt" - "jobwatch/timespec" + "io/ioutil" "jobwatch/types" + "log" + "os" + "path/filepath" + "strings" + "time" ) /* @@ -12,16 +19,197 @@ import ( */ 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"` + JobId string `json:"job_id"` + JobInstanceId string `json:"job_inst_id"` + LastRun string `json:"last_run"` + LastRunAge int `json:"last_run_age_seconds"` + LastRunAgeState int `json:"last_run_age_state"` + LastRunWarnAt types.TimeSpec `json:"last_run_warn_at"` + LastRunCritAt types.TimeSpec `json:"last_run_crit_at"` + LastExitCode int `json:"last_exit_code"` + LastState int `json:"last_state"` +} + +type summaryEntry struct { + State State `json:"state"` + LogEntries []string `json:"log_entries"` +} + +type summaryPerUser struct { + UserId string `json:"userd_id"` + Path string `json:"-"` + Entries []summaryEntry `json:"entries"` +} + +type summary struct { + Entries []summaryPerUser `json:"all"` +} + +func getStateDir() (string, error) { + var dir string + dir = "/var/lib/jobwatch/states" + + if os.Getuid() > 0 { + if od, err := os.UserHomeDir(); err == nil { + dir = od + "/.jobwatch/states" + } + } + + err := os.MkdirAll(dir, 0770) + + return dir, err +} + +func findAllStateDirs() []summaryPerUser { + var dirs []summaryPerUser + var res []summaryPerUser + + var su summaryPerUser + su.UserId = "root" + su.Path = "/var/lib/jobwatch/states" + + dirs = append(dirs, su) + f, err := os.Open("/etc/passwd") + if err == nil { + defer f.Close() + + rdr := bufio.NewReader(f) + for { + line, err := rdr.ReadString(10) + parts := strings.Split(line, ":") + + if len(parts) > 6 { + hd := parts[5] + var su summaryPerUser + su.UserId = parts[0] + su.Path = filepath.FromSlash(hd + "/.jobwatch/states") + dirs = append(dirs, su) + } + if err != nil { + break + } + } + + for _, d := range dirs { + if _, err := os.Stat(d.Path); err == nil { + res = append(res, d) + } + } + } + return res +} + +func LoadState(path string) (*State, error) { + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var res State + err = json.Unmarshal(data, &res) + if err != nil { + return nil, fmt.Errorf("Error unmashaling state: %v", err) + } + + return &res, nil } func WriteLog(job types.Job, line ...string) { fmt.Println("B") } + +func (state *State) ReEval() error { + lr, err := time.Parse(time.RFC3339, state.LastRun) + if err != nil { + return err + } + + age := time.Now().Sub(lr) + state.LastRunAge = int(age.Seconds()) + if age.Seconds() > float64(state.LastRunCritAt.AsSeconds()) { + state.LastRunAgeState = int(types.CRIT) + } else if age.Seconds() > float64(state.LastRunWarnAt.AsSeconds()) { + state.LastRunAgeState = int(types.WARN) + } else { + state.LastRunAgeState = int(types.OK) + } + + return nil +} + +func StateFromJob(job types.Job) State { + var res State + res.JobId = job.Id + res.JobInstanceId = job.InstId + res.LastRunCritAt = job.LastRunCrit + res.LastRunWarnAt = job.LastRunWarn + + return res +} + +func (st State) SaveState() error { + dir, err := getStateDir() + if err != nil { + return err + } + + data, err := json.MarshalIndent(st, " ", " ") + if err != nil { + return fmt.Errorf("Error mashaling state: %v", err) + } + + if st.JobInstanceId == "" { + dir = filepath.FromSlash(fmt.Sprintf("%v/%v.state.json", dir, st.JobId)) + } else { + dir = filepath.FromSlash(fmt.Sprintf("%v/%v_%v.state.json", dir, st.JobId, st.JobInstanceId)) + } + + if err := os.WriteFile(dir, data, 0600); err != nil { + return fmt.Errorf("Error writing state file %v: %v", dir, err) + } + + return nil +} + +func ShowAll() error { + dirs := findAllStateDirs() + + for i, d := range dirs { + fsi, err := ioutil.ReadDir(d.Path) + log.Printf("Scanning path %v", d.Path) + + if err != nil { + continue + } + + for _, e := range fsi { + p := filepath.FromSlash(d.Path + "/" + e.Name()) + log.Printf("Processing state file %v", p) + + st, err := LoadState(p) + if err == nil { + var se summaryEntry + se.State = *st + if err := se.State.ReEval(); err != nil { + log.Printf("Ignoring error while reeval: %v", err) + } + d.Entries = append(d.Entries, se) + dirs[i] = d + } else { + log.Printf("Ignoring error processing state file %v: %v", p, err) + } + } + } + + if len(dirs) > 0 { + bytes, err := json.MarshalIndent(dirs, " ", " ") + if err != nil { + return err + } + + fmt.Printf("%v", string(bytes)) + } + return nil + +} diff --git a/app/timespec/timespec.go b/app/types/timespec.go similarity index 84% rename from app/timespec/timespec.go rename to app/types/timespec.go index f4db043..8d7d740 100644 --- a/app/timespec/timespec.go +++ b/app/types/timespec.go @@ -1,4 +1,4 @@ -package timespec +package types type TimeUnit string @@ -10,8 +10,8 @@ const ( ) type TimeSpec struct { - Value int `yaml:"val"` - Unit TimeUnit `yaml:"unit"` + Value int `yaml:"val" json:"val"` + Unit TimeUnit `yaml:"unit" json:"unit"` } func (e TimeSpec) HasValidUnit() bool { diff --git a/app/types/types.go b/app/types/types.go index 1b32850..95208f3 100644 --- a/app/types/types.go +++ b/app/types/types.go @@ -1,6 +1,13 @@ package types -import "jobwatch/timespec" +import ( + "fmt" + "log" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) type CMKState int32 @@ -23,6 +30,7 @@ type ExistCodeMapEntry struct { } type Job struct { + Id string Cmd string `yaml:"cmd"` Args []string `yaml:"args"` OutputLog string @@ -31,6 +39,47 @@ type Job struct { LogMatches []LogMatch `yaml:"log_matches"` InstArgs []string InstId string - LastRunWarn timespec.TimeSpec `yaml:"last_run_warn"` - LastRunCrit timespec.TimeSpec `yaml:"last_run_crit"` + LastRunWarn TimeSpec `yaml:"last_run_warn"` + LastRunCrit TimeSpec `yaml:"last_run_crit"` +} + +func LoadJob(jobdir string, jobid string) (*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 := 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) + } + job.Id = jobid + return &job, nil }