diff options
| author | Gibheer <gibheer+git@zero-knowledge.org> | 2018-11-20 21:48:16 +0100 | 
|---|---|---|
| committer | Gibheer <gibheer+git@zero-knowledge.org> | 2018-11-20 21:48:16 +0100 | 
| commit | 3b8e27706b2b0d623d2c162c1cd24f5f5806140a (patch) | |
| tree | ad3f8a7aec63b594b52dd1eb0af34d7a2be2845e | |
| parent | ddc0053ce8eda6001df8de1b30cf2395c625846e (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.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>` +) | 
