monzero/cmd/monfront/main.go
Gibheer 6c2bea5365 monfront - make fake groups in frontend
It would have been nice to use rowgrouping for the node name, but
somehow it wasn't easy to get the rowcount. So this should do for now.
2018-12-10 09:34:45 +01:00

344 lines
9.1 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"html"
"html/template"
"io/ioutil"
"log"
"net/http"
"strings"
"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.HandleFunc("/unhandled/checks", showChecks)
http.HandleFunc("/unhandled/hosts", showUnhandledHosts)
http.HandleFunc("/unhandled/groups", showUnhandledGroups)
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
}
ref, found := r.Header["Referrer"]
if found {
w.Header()["Location"] = ref
} else {
w.Header()["Location"] = []string{"/"}
}
w.WriteHeader(http.StatusSeeOther)
return
}
func showChecks(w http.ResponseWriter, r *http.Request) {
query := SQLShowChecks
if strings.HasPrefix(r.URL.Path, "/unhandled") {
query = SQLShowUnhandledChecks
}
rows, err := DB.Query(query)
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 {
NodeName 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.NodeName, &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 := template.New("checklist")
tmpl.Funcs(Funcs)
tmpl, err = tmpl.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
}
func showUnhandledHosts(w http.ResponseWriter, r *http.Request) {
}
func showUnhandledGroups(w http.ResponseWriter, r *http.Request) {
rows, err := DB.Query(SQLShowUnhandledGroups)
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 {
GroupName string
NodeName string
State int
}
checks := []check{}
for rows.Next() {
c := check{}
err := rows.Scan(&c.GroupName, &c.NodeName, &c.State)
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(TmplUnhandledGroups)
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;`
SQLShowUnhandledChecks = `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
where ac.states[1] > 0
order by n.name, co.name;`
SQLShowUnhandledGroups = `select g.name, n.name, max(ac.state[1])
from groups g
join nodes_groups ng on g.id = ng.group_id
join nodes n on ng.node_id = n.id
join checks c on n.id = c.node_id
join active_checks ac on c.id = ac.check_id
where ac.states[1] > 0
group by g.name, n.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 nav { order: 1; }
form content { order: 2; flex-grow: 1; }
form nav { display: flex; flex-direction: column; }
form nav > * { 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>
<nav id="mainmenu">
<ul>
<li><a href="/">home</a></li>
<li><a href="/unhandled/checks">unhandled checks</a></li>
<li><a href="/unhandled/hosts">unhandled hosts</a></li>
<li><a href="/unhandled/groups">unhandled groups</a></li>
</ul>
</nav>
<form method="post" action="/action">
<section>
<nav>
<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>
</nav>
<content>
<table>
<thead><tr><th></th><th>host</th><th>status</th><th>next check</th><th>message</th></tr></thead>
<tbody>
{{ $current := "" }}
{{ range . }}
<tr>
<td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td>
<td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}{{ .NodeName }}{{ end }}</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>`
TmplUnhandledGroups = `TODO`
Funcs = template.FuncMap{
"sub": func(base, amount int) int { return base - amount },
}
)