monzero/cmd/monfront/main.go
Gibheer dece1ac2dc add level mappings
This allows to map the command exit codes to any other output level
which can then be reported by the notification plugin.
With the provided colors, the frontend will show them accordingly.
2018-12-11 12:37:30 +01:00

404 lines
10 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"`
}
Context struct {
Mappings map[int]map[int]MapEntry
Checks []check
}
MapEntry struct {
Title string
Color string
}
check struct {
NodeName string
CommandName string
CheckID int64
MappingId int
State int
Notify bool
Enabled bool
Notice sql.NullString
NextTime time.Time
Msg string
}
)
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
}
checks := []check{}
for rows.Next() {
c := check{}
err := rows.Scan(&c.CheckID, &c.NodeName, &c.CommandName, &c.MappingId, &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
}
con := Context{
Checks: checks,
}
if err := loadMappings(&con); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with the mappings"))
log.Printf("could not load mappings: %s", err)
return
}
w.Header()["Content-Type"] = []string{"text/html"}
if err := tmpl.Execute(w, con); 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
}
func loadMappings(c *Context) error {
c.Mappings = map[int]map[int]MapEntry{}
rows, err := DB.Query(SQLShowMappings)
if err != nil {
return err
}
for rows.Next() {
if rows.Err() != nil {
return rows.Err()
}
var (
mapId int
target int
title string
color string
)
if err := rows.Scan(&mapId, &target, &title, &color); err != nil {
return err
}
ma, found := c.Mappings[mapId]
if !found {
ma = map[int]MapEntry{}
c.Mappings[mapId] = ma
}
ma[target] = MapEntry{Title: title, Color: color}
}
return nil
}
var (
SQLShowMappings = `select mapping_id, target, title, color
from mapping_level`
SQLShowChecks = `select c.id, n.name, co.name, ac.mapping_id, 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; }
{{ range $mapId, $mapping := .Mappings }}
{{ range $target, $val := $mapping }}
td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
{{ end }}
{{ end }}
/* 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 := "" }}
{{ $mapping := .Mappings }}
{{ range .Checks }}
<tr>
<td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td>
<td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}{{ .NodeName }}{{ end }}</td>
<td class="state-{{ .MappingId }}-{{ .State }}">{{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }}</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 },
}
)