Continue implmenting base features

This commit is contained in:
Michael Höß 2022-02-20 21:19:18 +01:00
parent 9d51740b05
commit f27366251c
4 changed files with 311 additions and 88 deletions

View File

@ -5,16 +5,14 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v2"
"jobwatch/state" "jobwatch/state"
"jobwatch/types" "jobwatch/types"
) )
@ -71,47 +69,12 @@ func evalOutput(job types.Job, output []string) (types.CMKState, error) {
return res, nil return res, nil
} }
func loadJob(jobdir string, jobid string) (*types.Job, error) { func runJob(job types.Job) (state.State, 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 = "" st := state.StateFromJob(job)
for _, d := range dirs { st.LastExitCode = -1
jobfile = filepath.FromSlash(d + "/" + jobid + ".job.yml") st.LastState = int(types.UNKNOWN)
if _, err := os.Stat(jobfile); err == nil { st.LastRun = time.Now().Format(time.RFC3339)
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...)...) 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) cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf, &stdcombinedBuf)
} }
var state = 0 var stcode = 0
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
// cmd executed, but failed w/ non-zero // cmd executed, but failed w/ non-zero
if exitError, ok := err.(*exec.ExitError); ok { if exitError, ok := err.(*exec.ExitError); ok {
state = exitError.ProcessState.ExitCode() stcode = exitError.ProcessState.ExitCode()
} else { } else {
// cmd not found etc // cmd not found etc
return types.UNKNOWN, err return st, err
} }
if state == 0 { // Exec failed, but exitcode, be sure to get error! if stcode == 0 { // Exec failed, but exitcode, be sure to get error!
state = int(types.UNKNOWN) stcode = int(types.UNKNOWN)
} }
} else { } 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 { for _, em := range job.ExitCodeMap {
// From == -1 -> Alle non-zero-states mappen // From == -1 -> Alle non-zero-states mappen
if (state == int(em.From)) || (em.From == -1 && state > 0) { if (stcode == int(em.From)) || (em.From == -1 && stcode > 0) {
state = int(em.To) stcode = int(em.To)
} }
} }
if state > int(types.UNKNOWN) { if stcode > int(types.UNKNOWN) {
state = int(types.CRIT) stcode = int(types.CRIT)
} }
log.Printf("- State after ExitCodeMapping: %v", state) log.Printf("- State after ExitCodeMapping: %v", stcode)
outText := string(stdcombinedBuf.Bytes()) outText := string(stdcombinedBuf.Bytes())
log_state, err := evalOutput(job, strings.Split(outText, "\n")) log_state, err := evalOutput(job, strings.Split(outText, "\n"))
if int(log_state) > state { if int(log_state) > stcode {
state = int(log_state) 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) { func setup() (*types.Job, error) {
@ -178,17 +146,26 @@ func setup() (*types.Job, error) {
if debug { if debug {
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
} else { } else {
log.SetOutput(nil) log.SetOutput(ioutil.Discard)
} }
if !regexp.MustCompile("^[a-z0-9]*$").MatchString(job_instance) { if jobdir == "" && jobid == "" && job_instance == "" {
return nil, fmt.Errorf("-i invalid chars") // 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(". Raw args : %+v\n", flag.Args())
log.Printf(". Job onstance id : %+v\n", job_instance) 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.InstArgs = flag.Args()
job.InstId = job_instance job.InstId = job_instance
return job, nil return job, nil
@ -202,17 +179,26 @@ func main() {
job, err := setup() job, err := setup()
var res = types.UNKNOWN if job == nil && err == nil {
fmt.Println("<<<jobwatch>>>")
state.ShowAll()
} else {
var res state.State
if err == nil { if err == nil {
res, err = runJob(*job) res, err = runJob(*job)
} }
res.ReEval()
res.SaveState()
if err != nil { if err != nil {
fmt.Printf("! Error running job %+v\n", err) fmt.Printf("! Error running job %+v\n", err)
os.Exit(int(types.UNKNOWN)) os.Exit(int(types.UNKNOWN))
} else { } else {
os.Exit(int(res)) os.Exit(int(res.LastState))
} }
state.WriteLog(*job) state.WriteLog(*job)
}
} }

View File

@ -1,9 +1,16 @@
package state package state
import ( import (
"bufio"
"encoding/json"
"fmt" "fmt"
"jobwatch/timespec" "io/ioutil"
"jobwatch/types" "jobwatch/types"
"log"
"os"
"path/filepath"
"strings"
"time"
) )
/* /*
@ -15,13 +22,194 @@ type State struct {
JobId string `json:"job_id"` JobId string `json:"job_id"`
JobInstanceId string `json:"job_inst_id"` JobInstanceId string `json:"job_inst_id"`
LastRun string `json:"last_run"` LastRun string `json:"last_run"`
LastRunAge int `json:"last_run_age"` LastRunAge int `json:"last_run_age_seconds"`
LastRunAgeState int `json:"last_run_age_state"` LastRunAgeState int `json:"last_run_age_state"`
LastRunWarnAt timespec.TimeSpec `json:"last_run_warn_at"` LastRunWarnAt types.TimeSpec `json:"last_run_warn_at"`
LastRunCritAt timespec.TimeSpec `json:"last_run_crit_at"` LastRunCritAt types.TimeSpec `json:"last_run_crit_at"`
LastExitCode int `json:"last_exit_code"` 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) { func WriteLog(job types.Job, line ...string) {
fmt.Println("B") 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
}

View File

@ -1,4 +1,4 @@
package timespec package types
type TimeUnit string type TimeUnit string
@ -10,8 +10,8 @@ const (
) )
type TimeSpec struct { type TimeSpec struct {
Value int `yaml:"val"` Value int `yaml:"val" json:"val"`
Unit TimeUnit `yaml:"unit"` Unit TimeUnit `yaml:"unit" json:"unit"`
} }
func (e TimeSpec) HasValidUnit() bool { func (e TimeSpec) HasValidUnit() bool {

View File

@ -1,6 +1,13 @@
package types package types
import "jobwatch/timespec" import (
"fmt"
"log"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
type CMKState int32 type CMKState int32
@ -23,6 +30,7 @@ type ExistCodeMapEntry struct {
} }
type Job struct { type Job struct {
Id string
Cmd string `yaml:"cmd"` Cmd string `yaml:"cmd"`
Args []string `yaml:"args"` Args []string `yaml:"args"`
OutputLog string OutputLog string
@ -31,6 +39,47 @@ type Job struct {
LogMatches []LogMatch `yaml:"log_matches"` LogMatches []LogMatch `yaml:"log_matches"`
InstArgs []string InstArgs []string
InstId string InstId string
LastRunWarn timespec.TimeSpec `yaml:"last_run_warn"` LastRunWarn TimeSpec `yaml:"last_run_warn"`
LastRunCrit timespec.TimeSpec `yaml:"last_run_crit"` 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
} }