Gibheer
887e8ea089
After the notifications were extracted the mute option didn't work anymore. With this fix, they are back to working.
408 lines
11 KiB
Go
408 lines
11 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
|
|
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":
|
|
setTable = "checks_notify"
|
|
setClause = "enabled = false"
|
|
case "unmute":
|
|
setTable = "checks_notify"
|
|
setClause = "enabled = 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" || setTable == "checks_notify" {
|
|
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.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.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.mapping_id, ac.states[1] as state,
|
|
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>
|
|
<script>
|
|
setTimeout(function() { location.reload(true) }, 30000)
|
|
</script>
|
|
</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 },
|
|
}
|
|
)
|