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 },
}
)