aboutsummaryrefslogblamecommitdiff
path: root/cmd/monfront/main.go
blob: 544d4f3a5c427418e453e2f750660711b7659849 (plain) (tree)





























































































































































































































































                                                                                                                                                
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>`
)