Gibheer
c646179fa9
This is the first step to view the group status. The templates were split into multipe parts, to make reuse easier.
482 lines
13 KiB
Go
482 lines
13 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
|
|
Tmpl *template.Template
|
|
)
|
|
|
|
type (
|
|
Config struct {
|
|
DB string `json:"db"`
|
|
Listen string `json:"listen"`
|
|
}
|
|
|
|
Context struct {
|
|
Title string
|
|
Mappings map[int]map[int]MapEntry
|
|
Checks []check
|
|
Groups []group
|
|
}
|
|
|
|
MapEntry struct {
|
|
Title string
|
|
Color string
|
|
}
|
|
|
|
check struct {
|
|
NodeId int
|
|
NodeName string
|
|
CommandName string
|
|
CheckID int64
|
|
MappingId int
|
|
State int
|
|
Enabled bool
|
|
Notice sql.NullString
|
|
NextTime time.Time
|
|
Msg string
|
|
}
|
|
|
|
group struct {
|
|
GroupId int
|
|
Name string
|
|
NodeId int
|
|
NodeName string
|
|
State int
|
|
MappingId int
|
|
}
|
|
)
|
|
|
|
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
|
|
|
|
tmpl := template.New("main")
|
|
tmpl.Funcs(Funcs)
|
|
for k, val := range Templates {
|
|
template.Must(tmpl.New(k).Parse(val))
|
|
}
|
|
Tmpl = tmpl
|
|
|
|
http.HandleFunc("/", showChecks)
|
|
http.HandleFunc("/checks", showChecks)
|
|
http.HandleFunc("/hosts", showHosts)
|
|
http.HandleFunc("/groups", showGroups)
|
|
http.HandleFunc("/action", checkAction)
|
|
http.HandleFunc("/unhandled/checks", showChecks)
|
|
http.HandleFunc("/unhandled/hosts", showHosts)
|
|
http.HandleFunc("/unhandled/groups", showGroups)
|
|
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 := `select c.id, n.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 := []string{}
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
where = append(where, `ac.states[1] > 0`)
|
|
}
|
|
idx := 0
|
|
params := []interface{}{}
|
|
if id, found := r.URL.Query()["node_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("n.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if id, found := r.URL.Query()["command_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("co.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if id, found := r.URL.Query()["check_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("c.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if len(where) > 0 {
|
|
query += " where " + strings.Join(where, " and ")
|
|
}
|
|
query += ` order by n.name, co.name`
|
|
rows, err := DB.Query(query, params...)
|
|
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.NodeId, &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)
|
|
}
|
|
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.ExecuteTemplate(w, "checklist", 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 showHosts(w http.ResponseWriter, r *http.Request) {
|
|
}
|
|
|
|
func showGroups(w http.ResponseWriter, r *http.Request) {
|
|
query := `select groupid, groupname, nodeid, nodename, mapping_id, state
|
|
from (
|
|
select g.id groupid, g.name groupname, n.id nodeid, n.name nodename, ac.mapping_id,
|
|
ac.states[1] state, max(ac.states[1]) over (partition by c.node_id) maxstate
|
|
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
|
|
join mapping_level ml on ac.mapping_id = ml.mapping_id and ac.states[1] = ml.target
|
|
) s
|
|
where state = maxstate`
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
query += ` and state > 0`
|
|
}
|
|
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
|
|
}
|
|
|
|
groups := []group{}
|
|
for rows.Next() {
|
|
g := group{}
|
|
err := rows.Scan(&g.GroupId, &g.Name, &g.NodeId, &g.NodeName, &g.MappingId, &g.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
|
|
}
|
|
groups = append(groups, g)
|
|
}
|
|
con := Context{
|
|
Groups: groups,
|
|
}
|
|
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.ExecuteTemplate(w, "grouplist", 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 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`
|
|
)
|
|
|
|
var (
|
|
Templates = map[string]string{
|
|
"header": `<doctype html>
|
|
<html>
|
|
<head>
|
|
<title>{{ .Title }}</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>`,
|
|
"footer": `</body></html>`,
|
|
"checklist": `{{ template "header" . }}
|
|
<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 }}<a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a>{{ 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>
|
|
{{ template "footer" . }}`,
|
|
"grouplist": `{{ template "header" . }}
|
|
<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>group</th><th>host</th><th>worst state</th></tr></thead>
|
|
<tbody>
|
|
{{ $current := "" }}
|
|
{{ $mapping := .Mappings }}
|
|
{{ range .Groups }}
|
|
<tr>
|
|
<td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td>
|
|
<td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td>
|
|
<td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td>
|
|
<td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</content>
|
|
</form>
|
|
{{ template "footer" . }}`,
|
|
}
|
|
TmplUnhandledGroups = `TODO`
|
|
Funcs = template.FuncMap{
|
|
"sub": func(base, amount int) int { return base - amount },
|
|
}
|
|
)
|