diff options
Diffstat (limited to 'cmd/monfront')
-rw-r--r-- | cmd/monfront/main.go | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/cmd/monfront/main.go b/cmd/monfront/main.go new file mode 100644 index 0000000..544d4f3 --- /dev/null +++ b/cmd/monfront/main.go @@ -0,0 +1,254 @@ +package main + +import ( + "database/sql" + "encoding/json" + "flag" + "fmt" + "html" + "html/template" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/lib/pq" +) + +var ( + configPath = flag.String("config", "monfront.conf", "path to the config file") + DB *sql.DB +) + +type ( + Config struct { + DB string `json:"db"` + Listen string `json:"listen"` + } +) + +func main() { + flag.Parse() + + raw, err := ioutil.ReadFile(*configPath) + if err != nil { + log.Fatalf("could not read config: %s", err) + } + config := Config{} + if err := json.Unmarshal(raw, &config); err != nil { + log.Fatalf("could not parse config: %s", err) + } + + db, err := sql.Open("postgres", config.DB) + if err != nil { + log.Fatalf("could not open database connection: %s", err) + } + DB = db + + http.HandleFunc("/", showChecks) + http.HandleFunc("/action", checkAction) + http.ListenAndServe(config.Listen, nil) +} + +func checkAction(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("method is not supported")) + return + } + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "could not parse parameters: %s", err) + return + } + checks := r.PostForm["checks"] + action := r.PostForm["action"] + if len(action) == 0 || action[0] == "" || len(checks) == 0 { + w.Header()["Location"] = []string{"/"} + w.WriteHeader(http.StatusSeeOther) + return + } + setTable := "checks" + setClause := "" + switch action[0] { + case "mute": + setClause = "notify = false" + case "unmute": + setClause = "notify = true" + case "enable": + setClause = "enabled = true" + case "disable": + setClause = "enabled = false" + case "reschedule": + setClause = "next_time = now()" + setTable = "active_checks" + case "ack": + setClause = "acknowledged = true" + setTable = "active_checks" + case "comment": + if len(r.PostForm["comment"]) == 0 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "no comment sent") + return + } + comment := r.PostForm["comment"][0] + _, err := DB.Exec( + "update active_checks set notice = $2 where check_id = any ($1::int[]);", + pq.Array(&checks), + html.EscapeString(comment)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "could not store changes") + log.Printf("could not adjust checks %#v: %s", checks, err) + return + } + w.Header()["Location"] = []string{"/"} + w.WriteHeader(http.StatusSeeOther) + return + default: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "unknown action %s", action) + return + } + whereColumn := "id" + if setTable == "active_checks" { + whereColumn = "check_id" + } + + _, err := DB.Exec("update "+setTable+" set "+setClause+" where "+whereColumn+" = any ($1::int[]);", pq.Array(&checks)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "could not store changes") + log.Printf("could not adjust checks %#v: %s", checks, err) + return + } + w.Header()["Location"] = []string{"/"} + w.WriteHeader(http.StatusSeeOther) + return +} + +func showChecks(w http.ResponseWriter, r *http.Request) { + rows, err := DB.Query(SQLShowChecks) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problems with the database")) + log.Printf("could not get check list: %s", err) + return + } + + type check struct { + Name string + CommandName string + CheckID int64 + State int + Notify bool + Enabled bool + Notice sql.NullString + NextTime time.Time + Msg string + } + checks := []check{} + for rows.Next() { + c := check{} + err := rows.Scan(&c.CheckID, &c.Name, &c.CommandName, &c.State, &c.Notify, &c.Enabled, &c.Notice, &c.NextTime, &c.Msg) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problems with the database")) + log.Printf("could not get check list: %s", err) + return + } + checks = append(checks, c) + } + tmpl, err := template.New("checklist").Parse(TmplCheckList) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problems with a template")) + log.Printf("could not parse template: %s", err) + return + } + w.Header()["Content-Type"] = []string{"text/html"} + if err := tmpl.Execute(w, checks); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problem with a template")) + log.Printf("could not execute template: %s", err) + return + } + return +} + +var ( + SQLShowChecks = `select c.id, n.name, co.name, ac.states[1] as state, ac.notify, + ac.enabled, ac.notice, ac.next_time, ac.msg + from active_checks ac + join checks c on ac.check_id = c.id + join nodes n on c.node_id = n.id + join commands co on c.command_id = co.id + order by n.name, co.name;` +) + +var ( + TmplCheckList = `<doctype html> +<html> + <head> + <title>check list</title> + <style type="text/css"> + * { font-size: 100%; } + form section { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + } + form menu { order: 1; } + form content { order: 2; flex-grow: 1; } + form menu { display: flex; flex-direction: column; } + form menu > * { margin: 0.5em; } + table td, table th { padding: 0.5em; } + td.state-0 { background: green; } + td.state-1 { background: orange; } + td.state-2 { background: red; } + td.state-99 { background: gray; } + </style> + </head> + <body> + <form method="post" action="/action"> + <section> + <menu> + <div class="option"> + <label for="action">Action</label> + <select name="action"> + <option value="reschedule">run now</option> + <option value="mute">mute</option> + <option value="unmute">unmute</option> + <option value="ack">acknowledge</option> + <option value="disable">disable</option> + <option value="enable">enable</option> + <option value="comment">comment</option> + </select> + </div> + <div class="option"> + <label for="comment">comment</label> + <input name="comment" /> + </div> + <button type="submit">submit</button> + </menu> + <content> + <table> + <thead><tr><th></th><th>host</th><th>status</th><th>next check</th><th>message</th></tr></thead> + <tbody> + {{ range . }} + <tr> + <td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td> + <td>{{ .Name }}</td> + <td class="state-{{ .State }}">{{ .CommandName }} - {{ .State }}</td> + <td>{{ .NextTime.Format "2006.01.02 15:04:05" }}</td> + <td><pre>{{ .Msg }}</pre></td> + </tr> + {{ end }} + </tbody> + </table> + </content> + </form> + </body> +</html>` +) |