aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGibheer <gibheer+git@zero-knowledge.org>2018-11-20 21:48:16 +0100
committerGibheer <gibheer+git@zero-knowledge.org>2018-11-20 21:48:16 +0100
commit3b8e27706b2b0d623d2c162c1cd24f5f5806140a (patch)
treead3f8a7aec63b594b52dd1eb0af34d7a2be2845e
parentddc0053ce8eda6001df8de1b30cf2395c625846e (diff)
monfront - add initial version
This version can already show the list of checks, their last state and add ways to manipulate the check or active check.
-rw-r--r--cmd/monfront/main.go254
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>`
+)