Gibheer
6c2bea5365
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.
344 lines
9.1 KiB
Go
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 },
|
|
}
|
|
)
|