2018-11-20 21:48:16 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"html/template"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
2018-11-29 10:56:20 +01:00
|
|
|
"strings"
|
2018-11-20 21:48:16 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/lib/pq"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
configPath = flag.String("config", "monfront.conf", "path to the config file")
|
|
|
|
DB *sql.DB
|
2019-01-02 16:09:04 +01:00
|
|
|
Tmpl *template.Template
|
2018-11-20 21:48:16 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
Config struct {
|
|
|
|
DB string `json:"db"`
|
|
|
|
Listen string `json:"listen"`
|
|
|
|
}
|
2018-12-11 12:37:30 +01:00
|
|
|
|
|
|
|
Context struct {
|
2019-01-09 20:17:49 +01:00
|
|
|
Title string
|
|
|
|
CurrentPath string
|
|
|
|
Error string
|
|
|
|
Mappings map[int]map[int]MapEntry
|
|
|
|
Checks []check
|
|
|
|
CheckDetails *checkDetails
|
|
|
|
Groups []group
|
2018-12-11 12:37:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
MapEntry struct {
|
|
|
|
Title string
|
|
|
|
Color string
|
|
|
|
}
|
|
|
|
|
|
|
|
check struct {
|
2019-01-02 16:09:04 +01:00
|
|
|
NodeId int
|
2018-12-11 12:37:30 +01:00
|
|
|
NodeName string
|
|
|
|
CommandName string
|
|
|
|
CheckID int64
|
|
|
|
MappingId int
|
|
|
|
State int
|
|
|
|
Enabled bool
|
2019-01-03 19:48:17 +01:00
|
|
|
Notify bool
|
2018-12-11 12:37:30 +01:00
|
|
|
Notice sql.NullString
|
|
|
|
NextTime time.Time
|
|
|
|
Msg string
|
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
|
2019-01-09 20:17:49 +01:00
|
|
|
checkDetails struct {
|
|
|
|
Id int64
|
|
|
|
Message string
|
|
|
|
Enabled bool
|
|
|
|
Updated time.Time
|
|
|
|
LastRefresh time.Time
|
|
|
|
NextTime time.Time
|
|
|
|
MappingId int
|
|
|
|
MappingName string
|
|
|
|
NodeId int
|
|
|
|
NodeName string
|
|
|
|
NodeMessage string
|
|
|
|
CommandId int
|
|
|
|
CommandName string
|
|
|
|
CommandLine []string
|
|
|
|
CommandMessage string
|
|
|
|
States []int64
|
|
|
|
Notice sql.NullString
|
|
|
|
Notifiers []notifier
|
|
|
|
Notifications []notification
|
|
|
|
}
|
|
|
|
|
|
|
|
notifier struct {
|
|
|
|
Id int
|
|
|
|
Name string
|
|
|
|
Enabled bool
|
|
|
|
}
|
|
|
|
|
|
|
|
notification struct {
|
|
|
|
Id int64
|
|
|
|
States []int
|
|
|
|
Output string
|
|
|
|
Inserted time.Time
|
|
|
|
Sent time.Time
|
|
|
|
}
|
|
|
|
|
2019-01-02 16:09:04 +01:00
|
|
|
group struct {
|
|
|
|
GroupId int
|
|
|
|
Name string
|
|
|
|
NodeId int
|
|
|
|
NodeName string
|
|
|
|
State int
|
|
|
|
MappingId int
|
|
|
|
}
|
2018-11-20 21:48:16 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-01-02 16:09:04 +01:00
|
|
|
tmpl := template.New("main")
|
|
|
|
tmpl.Funcs(Funcs)
|
|
|
|
for k, val := range Templates {
|
|
|
|
template.Must(tmpl.New(k).Parse(val))
|
|
|
|
}
|
|
|
|
Tmpl = tmpl
|
|
|
|
|
2018-11-20 21:48:16 +01:00
|
|
|
http.HandleFunc("/", showChecks)
|
2019-01-09 20:17:49 +01:00
|
|
|
http.HandleFunc("/static/", showStatic)
|
2019-01-03 13:44:21 +01:00
|
|
|
http.HandleFunc("/check", showCheck)
|
2019-01-09 20:17:49 +01:00
|
|
|
http.HandleFunc("/group", showGroup)
|
|
|
|
http.HandleFunc("/node", showNode)
|
2019-01-02 16:09:04 +01:00
|
|
|
http.HandleFunc("/checks", showChecks)
|
|
|
|
http.HandleFunc("/groups", showGroups)
|
2018-11-20 21:48:16 +01:00
|
|
|
http.HandleFunc("/action", checkAction)
|
2018-11-29 10:56:20 +01:00
|
|
|
http.HandleFunc("/unhandled/checks", showChecks)
|
2019-01-02 16:09:04 +01:00
|
|
|
http.HandleFunc("/unhandled/groups", showGroups)
|
2018-11-20 21:48:16 +01:00
|
|
|
http.ListenAndServe(config.Listen, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkAction(w http.ResponseWriter, r *http.Request) {
|
2019-01-09 20:17:49 +01:00
|
|
|
con := Context{}
|
2018-11-20 21:48:16 +01:00
|
|
|
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":
|
2018-12-14 19:52:21 +01:00
|
|
|
setTable = "checks_notify"
|
|
|
|
setClause = "enabled = false"
|
2018-11-20 21:48:16 +01:00
|
|
|
case "unmute":
|
2018-12-14 19:52:21 +01:00
|
|
|
setTable = "checks_notify"
|
|
|
|
setClause = "enabled = true"
|
2018-11-20 21:48:16 +01:00
|
|
|
case "enable":
|
2019-01-03 19:48:17 +01:00
|
|
|
setClause = "enabled = true, updated = now()"
|
2018-11-20 21:48:16 +01:00
|
|
|
case "disable":
|
2019-01-03 19:48:17 +01:00
|
|
|
setClause = "enabled = false, updated = now()"
|
2018-11-20 21:48:16 +01:00
|
|
|
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:
|
2019-01-09 20:17:49 +01:00
|
|
|
con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0])
|
|
|
|
returnError(http.StatusNotFound, con, w)
|
2018-11-20 21:48:16 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
whereColumn := "id"
|
2018-12-14 19:52:21 +01:00
|
|
|
if setTable == "active_checks" || setTable == "checks_notify" {
|
2018-11-20 21:48:16 +01:00
|
|
|
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
|
|
|
|
}
|
2018-11-29 10:56:20 +01:00
|
|
|
ref, found := r.Header["Referrer"]
|
|
|
|
if found {
|
|
|
|
w.Header()["Location"] = ref
|
|
|
|
} else {
|
|
|
|
w.Header()["Location"] = []string{"/"}
|
|
|
|
}
|
2018-11-20 21:48:16 +01:00
|
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func showChecks(w http.ResponseWriter, r *http.Request) {
|
2019-01-02 16:09:04 +01:00
|
|
|
query := `select c.id, n.id, n.name, co.name, ac.mapping_id, ac.states[1] as state,
|
2019-01-03 19:48:17 +01:00
|
|
|
ac.enabled, ac.notice, ac.next_time, ac.msg,
|
|
|
|
case when cn.check_id is null then false else true end as notify_enabled
|
2019-01-02 16:09:04 +01:00
|
|
|
from active_checks ac
|
|
|
|
join checks c on ac.check_id = c.id
|
|
|
|
join nodes n on c.node_id = n.id
|
2019-01-03 19:48:17 +01:00
|
|
|
join commands co on c.command_id = co.id
|
|
|
|
left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id`
|
2019-01-02 16:09:04 +01:00
|
|
|
where := []string{}
|
2018-11-29 10:56:20 +01:00
|
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
2019-01-09 20:17:49 +01:00
|
|
|
where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`)
|
2018-11-29 10:56:20 +01:00
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
idx := 0
|
|
|
|
params := []interface{}{}
|
2019-01-03 13:44:21 +01:00
|
|
|
if search, found := r.URL.Query()["search"]; found {
|
|
|
|
idx += 1
|
|
|
|
// Add the search for nodes. As hostnames or FQDNs are really weird, the
|
|
|
|
// string needs to be split up by some characters. The input string needs
|
|
|
|
// to be split up too, so all is done here.
|
|
|
|
// TODO move this into a proper index and add more to search.
|
|
|
|
where = append(where, fmt.Sprintf(
|
|
|
|
`to_tsvector('english', regexp_replace(n.name, '[.-/]', ' ', 'g')) @@
|
|
|
|
to_tsquery('english', regexp_replace($%d, '[.-/]', ' & ', 'g'))`, idx))
|
|
|
|
params = append(params, search[0])
|
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
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 ")
|
|
|
|
}
|
2019-01-10 07:10:02 +01:00
|
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
|
|
query += ` order by ac.states[1] desc, n.name, co.name`
|
|
|
|
} else {
|
|
|
|
query += ` order by n.name, co.name`
|
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
rows, err := DB.Query(query, params...)
|
2018-11-20 21:48:16 +01:00
|
|
|
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{}
|
2019-01-03 19:48:17 +01:00
|
|
|
err := rows.Scan(&c.CheckID, &c.NodeId, &c.NodeName, &c.CommandName, &c.MappingId, &c.State, &c.Enabled, &c.Notice, &c.NextTime, &c.Msg, &c.Notify)
|
2018-11-20 21:48:16 +01:00
|
|
|
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)
|
|
|
|
}
|
2018-12-11 12:37:30 +01:00
|
|
|
con := Context{
|
|
|
|
Checks: checks,
|
|
|
|
}
|
|
|
|
if err := loadMappings(&con); err != nil {
|
2019-01-09 20:17:49 +01:00
|
|
|
con.Error = "could not load mapping data"
|
2018-12-11 12:37:30 +01:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problem with the mappings"))
|
|
|
|
log.Printf("could not load mappings: %s", err)
|
|
|
|
return
|
|
|
|
}
|
2018-11-20 21:48:16 +01:00
|
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
2019-01-02 16:09:04 +01:00
|
|
|
if err := Tmpl.ExecuteTemplate(w, "checklist", con); err != nil {
|
2018-11-20 21:48:16 +01:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problem with a template"))
|
|
|
|
log.Printf("could not execute template: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-01-02 16:09:04 +01:00
|
|
|
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)
|
2018-11-29 10:56:20 +01:00
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problems with the database"))
|
|
|
|
log.Printf("could not get check list: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-01-10 07:10:02 +01:00
|
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
|
|
query += ` order by maxstate desc, g.name, n.name`
|
|
|
|
} else {
|
|
|
|
query += ` order by g.name, n.name`
|
|
|
|
}
|
|
|
|
|
2019-01-02 16:09:04 +01:00
|
|
|
groups := []group{}
|
2018-11-29 10:56:20 +01:00
|
|
|
for rows.Next() {
|
2019-01-02 16:09:04 +01:00
|
|
|
g := group{}
|
|
|
|
err := rows.Scan(&g.GroupId, &g.Name, &g.NodeId, &g.NodeName, &g.MappingId, &g.State)
|
2018-11-29 10:56:20 +01:00
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problems with the database"))
|
|
|
|
log.Printf("could not get check list: %s", err)
|
|
|
|
return
|
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
groups = append(groups, g)
|
2018-11-29 10:56:20 +01:00
|
|
|
}
|
2019-01-02 16:09:04 +01:00
|
|
|
con := Context{
|
|
|
|
Groups: groups,
|
|
|
|
}
|
|
|
|
if err := loadMappings(&con); err != nil {
|
2018-11-29 10:56:20 +01:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2019-01-02 16:09:04 +01:00
|
|
|
w.Write([]byte("problem with the mappings"))
|
|
|
|
log.Printf("could not load mappings: %s", err)
|
2018-11-29 10:56:20 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
2019-01-02 16:09:04 +01:00
|
|
|
if err := Tmpl.ExecuteTemplate(w, "grouplist", con); err != nil {
|
2018-11-29 10:56:20 +01:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problem with a template"))
|
|
|
|
log.Printf("could not execute template: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-01-09 20:17:49 +01:00
|
|
|
// showCheck loads shows the notifications for a specific check.
|
|
|
|
func showCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
cd := checkDetails{}
|
|
|
|
con := Context{CheckDetails: &cd}
|
|
|
|
id, found := r.URL.Query()["check_id"]
|
|
|
|
if !found {
|
|
|
|
con.Error = "no check given to view"
|
|
|
|
returnError(http.StatusNotFound, con, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
query := `select c.id, c.message, c.enabled, c.updated, c.last_refresh,
|
|
|
|
m.id, m.name, n.id, n.name, n.message, co.id, co.Name, co.message,
|
|
|
|
ac.cmdline, ac.states, ac.msg
|
|
|
|
from checks c
|
|
|
|
join active_checks ac on c.id = ac.check_id
|
|
|
|
join nodes n on c.node_id = n.id
|
|
|
|
join commands co on c.command_id = co.id
|
|
|
|
join mappings m on ac.mapping_id = m.id
|
|
|
|
where c.id = $1::bigint`
|
|
|
|
err := DB.QueryRow(query, id[0]).Scan(&cd.Id, &cd.Message, &cd.Enabled, &cd.Updated, &cd.LastRefresh,
|
|
|
|
&cd.MappingId, &cd.MappingName, &cd.NodeId, &cd.NodeName, &cd.NodeMessage,
|
|
|
|
&cd.CommandId, &cd.CommandName, &cd.CommandMessage,
|
|
|
|
pq.Array(&cd.CommandLine), pq.Array(&cd.States), &cd.Notice)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problems with the database"))
|
|
|
|
log.Printf("could not get check details for check id %s: %s", id[0], err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO load the last couple notifications
|
|
|
|
|
|
|
|
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, "check", con); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problem with a template"))
|
|
|
|
log.Printf("could not execute template: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func showGroup(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// TODO implement showing all nodes only from one group and its message?
|
|
|
|
return
|
|
|
|
}
|
|
|
|
func showNode(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// TODO implement showing all checks from one node and its message?
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func returnError(status int, con Context, w http.ResponseWriter) {
|
|
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
|
|
|
w.WriteHeader(status)
|
|
|
|
if err := Tmpl.ExecuteTemplate(w, "error", con); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte("problem with a template"))
|
|
|
|
log.Printf("could not execute template: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-12-11 12:37:30 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-01-09 20:17:49 +01:00
|
|
|
func showStatic(w http.ResponseWriter, r *http.Request) {
|
|
|
|
file := strings.TrimPrefix(r.URL.Path, "/static/")
|
|
|
|
raw, found := Static[file]
|
|
|
|
if !found {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
w.Write([]byte("file does not exist"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Header()["Content-Type"] = []string{"image/svg+xml"}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
w.Write([]byte(raw))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-11-20 21:48:16 +01:00
|
|
|
var (
|
2018-12-11 12:37:30 +01:00
|
|
|
SQLShowMappings = `select mapping_id, target, title, color
|
|
|
|
from mapping_level`
|
2018-11-20 21:48:16 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2019-01-02 16:09:04 +01:00
|
|
|
Templates = map[string]string{
|
2019-01-03 13:44:21 +01:00
|
|
|
"header": `<!doctype html>
|
2018-11-20 21:48:16 +01:00
|
|
|
<html>
|
2019-01-02 16:09:04 +01:00
|
|
|
<head>
|
|
|
|
<title>{{ .Title }}</title>
|
2019-01-09 20:17:49 +01:00
|
|
|
<link rel="shortcut icon" href="/static/favicon" />
|
2018-11-20 21:48:16 +01:00
|
|
|
<style type="text/css">
|
|
|
|
* { font-size: 100%; }
|
2019-01-03 13:44:21 +01:00
|
|
|
body { display: flex; flex-direction: column; padding: 0; margin: 0; }
|
|
|
|
#mainmenu { background: #3a5f78; }
|
|
|
|
#mainmenu ul {
|
|
|
|
padding: 0;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
align-items: stretch;
|
|
|
|
align-content: center; }
|
|
|
|
#mainmenu .submenu { border-left: 0.1em solid black; }
|
|
|
|
#mainmenu li { list-style-type: none; }
|
|
|
|
.submenu .header {
|
|
|
|
text-align: center;
|
|
|
|
font-weight: bold;
|
|
|
|
color: #ff9000;
|
|
|
|
padding: 0.5em 0.5em;
|
|
|
|
display: block; }
|
|
|
|
#mainmenu a, #mainmenu a:visited, #mainmenu a:active, #mainmenu a:hover {
|
|
|
|
color: #ff9000;
|
|
|
|
padding: 0.25em 0.5em;
|
|
|
|
display: block; }
|
|
|
|
#mainmenu a:hover, #mainmenu a:active { color: #eeeeee; }
|
|
|
|
#mainmenu ul ul a { margin-left: 0.5em; }
|
|
|
|
#mainmenu form * { display: block; margin: 0.25em 0.5em; }
|
|
|
|
#mainmenu form input { }
|
|
|
|
#mainmenu form button { }
|
2018-11-20 21:48:16 +01:00
|
|
|
form section {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: flex-start;
|
|
|
|
}
|
2018-11-29 10:56:20 +01:00
|
|
|
form nav { order: 1; }
|
2019-01-03 13:44:21 +01:00
|
|
|
form content { order: 2; flex-grow: 1; border-left: 0.15em solid #dddddd; }
|
2018-11-29 10:56:20 +01:00
|
|
|
form nav { display: flex; flex-direction: column; }
|
|
|
|
form nav > * { margin: 0.5em; }
|
2019-01-03 13:44:21 +01:00
|
|
|
table { width: 100%; }
|
|
|
|
table tr:nth-child(odd) { background: #eeeeee; }
|
|
|
|
table tr:hover { background: #dfdfdf; }
|
|
|
|
table th { background: #cccccc; color: #3a5f78; }
|
|
|
|
table td, table th { text-align: center; }
|
2019-01-09 20:17:49 +01:00
|
|
|
table code { font-size: 75%; }
|
2019-01-03 19:48:17 +01:00
|
|
|
table td.disabled { text-decoration: line-through; }
|
2019-01-09 20:17:49 +01:00
|
|
|
.icon {
|
|
|
|
display: inline-block;
|
|
|
|
height: 1em;
|
|
|
|
margin: 0;
|
|
|
|
width: 1em;
|
|
|
|
vertical-align: bottom;
|
|
|
|
margin-right: 0.5em;
|
|
|
|
background-size: contain;
|
|
|
|
}
|
2019-01-10 05:48:25 +01:00
|
|
|
.mute { background-image: url(/static/icon-mute); }
|
|
|
|
.detail > div { display: grid; grid-template-columns: 25% auto; }
|
2019-01-10 05:50:02 +01:00
|
|
|
.detail > div:hover { background: #dfdfdf; }
|
2019-01-09 20:17:49 +01:00
|
|
|
/* state background colors */
|
|
|
|
{{ range $mapId, $mapping := .Mappings -}}
|
|
|
|
{{ range $target, $val := $mapping -}}
|
|
|
|
.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
|
|
|
|
{{ end -}}
|
|
|
|
{{ end -}}
|
2018-11-20 21:48:16 +01:00
|
|
|
</style>
|
2018-12-11 13:25:01 +01:00
|
|
|
<script>
|
2019-01-03 19:48:17 +01:00
|
|
|
setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000)
|
2018-12-11 13:25:01 +01:00
|
|
|
</script>
|
2018-11-20 21:48:16 +01:00
|
|
|
</head>
|
|
|
|
<body>
|
2018-11-29 10:56:20 +01:00
|
|
|
<nav id="mainmenu">
|
|
|
|
<ul>
|
|
|
|
<li><a href="/">home</a></li>
|
2019-01-03 13:44:21 +01:00
|
|
|
<li class="submenu">
|
|
|
|
<span class="header">all</span>
|
|
|
|
<ul>
|
|
|
|
<li><a href="/checks">checks</a></li>
|
|
|
|
<li><a href="/groups">groups</a></li>
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="submenu">
|
|
|
|
<span class="header">unhandled</span>
|
|
|
|
<ul>
|
|
|
|
<li><a href="/unhandled/checks">checks</a></li>
|
|
|
|
<li><a href="/unhandled/groups">groups</a></li>
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="submenu">
|
|
|
|
<form action="/checks" method="get">
|
|
|
|
<input name="search" placeholder="search" />
|
|
|
|
<button type="submit">search</button>
|
|
|
|
</form>
|
|
|
|
</li>
|
2019-01-03 19:48:17 +01:00
|
|
|
<li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li>
|
2018-11-29 10:56:20 +01:00
|
|
|
</ul>
|
2019-01-09 20:17:49 +01:00
|
|
|
</nav>
|
|
|
|
{{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`,
|
2019-01-02 16:09:04 +01:00
|
|
|
"footer": `</body></html>`,
|
|
|
|
"checklist": `{{ template "header" . }}
|
2018-11-20 21:48:16 +01:00
|
|
|
<form method="post" action="/action">
|
|
|
|
<section>
|
2018-11-29 10:56:20 +01:00
|
|
|
<nav>
|
2018-11-20 21:48:16 +01:00
|
|
|
<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>
|
2018-11-29 10:56:20 +01:00
|
|
|
</nav>
|
2018-11-20 21:48:16 +01:00
|
|
|
<content>
|
|
|
|
<table>
|
|
|
|
<thead><tr><th></th><th>host</th><th>status</th><th>next check</th><th>message</th></tr></thead>
|
|
|
|
<tbody>
|
2018-12-10 09:34:45 +01:00
|
|
|
{{ $current := "" }}
|
2018-12-11 12:37:30 +01:00
|
|
|
{{ $mapping := .Mappings }}
|
|
|
|
{{ range .Checks }}
|
2018-11-20 21:48:16 +01:00
|
|
|
<tr>
|
|
|
|
<td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td>
|
2019-01-02 16:09:04 +01:00
|
|
|
<td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}<a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a>{{ end }}</td>
|
2019-01-09 20:17:49 +01:00
|
|
|
<td class="state-{{ .MappingId }}-{{ .State }}">{{ if ne .Notify true }}<span class="icon mute"></span>{{ end }}<a href="/check?check_id={{ .CheckID }}">{{ .CommandName }}</a> - {{ (index $mapping .MappingId .State).Title }}</td>
|
2019-01-03 19:48:17 +01:00
|
|
|
<td {{ if ne .Enabled true }}title="This check is disabled." class="disabled"{{ end }}>{{ .NextTime.Format "2006.01.02 15:04:05" }} - in {{ in .NextTime }}</td>
|
2019-01-09 20:17:49 +01:00
|
|
|
<td><code>{{ .Msg }}</code></td>
|
2018-11-20 21:48:16 +01:00
|
|
|
</tr>
|
|
|
|
{{ end }}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</content>
|
|
|
|
</form>
|
2019-01-02 16:09:04 +01:00
|
|
|
{{ template "footer" . }}`,
|
2019-01-09 20:17:49 +01:00
|
|
|
"checkformheader": `<form method="post" action="/action">
|
2019-01-02 16:09:04 +01:00
|
|
|
<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>
|
2019-01-09 20:17:49 +01:00
|
|
|
</nav>`,
|
|
|
|
"checkformfooter": `</section></form>`,
|
|
|
|
"grouplist": `{{ template "header" . }}
|
|
|
|
{{ template "checkformheader" . }}
|
|
|
|
<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>
|
|
|
|
{{ template "checkformfooter" . }}
|
2019-01-02 16:09:04 +01:00
|
|
|
{{ template "footer" . }}`,
|
2019-01-09 20:17:49 +01:00
|
|
|
"check": `{{ template "header" . }}
|
|
|
|
{{ template "checkformheader" . }}
|
2019-01-10 05:48:25 +01:00
|
|
|
<content class="details">
|
2019-01-09 20:17:49 +01:00
|
|
|
{{ $mapping := .Mappings }}
|
|
|
|
{{ with .CheckDetails }}
|
|
|
|
<input type="hidden" name="check_id" value="{{ .Id }}" />
|
2019-01-10 05:48:25 +01:00
|
|
|
<article class="detail">
|
2019-01-09 20:17:49 +01:00
|
|
|
<h1>check</h1>
|
|
|
|
<div><span class="label">current state</span><span class="value state-{{ .MappingId }}-{{ index .States 0 }}">{{ (index $mapping .MappingId (index .States 0) ).Title }}</span></div>
|
|
|
|
<div><span class="label">current notice</span><span class="value">{{ if .Notice }}{{ .Notice.String }}{{ end }}</span></div>
|
|
|
|
<div><span class="label">Message</span><span class="value">{{ .Message }}</span></div>
|
|
|
|
<div><span class="label">enabled</span><span class="value">{{ .Enabled }}</span></div>
|
|
|
|
<div><span class="label">updated</span><span class="value">{{ .Updated.Format "2006.01.02 15:04:05" }}</span></div>
|
|
|
|
<div><span class="label">next check</span><span class="value">{{ .NextTime.Format "2006.01.02 15:04:05" }}</span></div>
|
|
|
|
<div><span class="label">last refresh</span><span class="value">{{ .LastRefresh.Format "2006.01.02 15:04:05" }}</span></div>
|
|
|
|
<div><span class="label">mapping</span><span class="value">{{ .MappingId }}</span></div>
|
|
|
|
</article>
|
2019-01-10 05:48:25 +01:00
|
|
|
<article class="detail">
|
2019-01-09 20:17:49 +01:00
|
|
|
<h1>node {{ .NodeName }}</h1>
|
|
|
|
<div><span class="label">Message</span><span class="value">{{ .NodeMessage }}</span></div>
|
|
|
|
</article>
|
2019-01-10 05:48:25 +01:00
|
|
|
<article class="detail">
|
2019-01-09 20:17:49 +01:00
|
|
|
<h1>command {{ .CommandName }}</h1>
|
|
|
|
<div><span class="label">Message</span><span class="value">{{ .CommandMessage }}</span></div>
|
2019-01-10 05:48:25 +01:00
|
|
|
<div><span class="label">command line</span><span class="value"><code>{{ join .CommandLine " " }}</code></span></div>
|
2019-01-09 20:17:49 +01:00
|
|
|
</article>
|
|
|
|
States []int64
|
|
|
|
Notifiers []notifier
|
|
|
|
Notifications []notification
|
|
|
|
{{ end }}
|
|
|
|
</content>
|
|
|
|
{{ template "checkformfooter" . }}
|
|
|
|
{{ template "footer" . }}`,
|
|
|
|
}
|
|
|
|
Static = map[string]string{
|
|
|
|
"icon-mute": `<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35.3 35.3" version="1.1"><title>Check is muted</title><style>.s0{fill:#191919;}</style><g transform="translate(0,-261.72223)"><path d="m17.6 261.7v35.3L5.3 284.7H0v-10.6l5.3 0zM30.2 273.1l-3.7 3.7-3.7-3.7-2.5 2.5 3.7 3.7-3.7 3.7 2.5 2.5 3.7-3.7 3.7 3.7 2.5-2.5-3.7-3.7 3.7-3.7z" fill="#191919"/></g></svg>`,
|
|
|
|
"error": `{{ template "header" . }}{{ template "footer" . }}`,
|
2019-01-02 16:09:04 +01:00
|
|
|
}
|
2018-11-29 10:56:20 +01:00
|
|
|
TmplUnhandledGroups = `TODO`
|
2018-12-10 09:34:45 +01:00
|
|
|
Funcs = template.FuncMap{
|
2019-01-09 20:17:49 +01:00
|
|
|
"sub": func(base, amount int) int { return base - amount },
|
|
|
|
"in": func(t time.Time) time.Duration { return t.Sub(time.Now()).Round(1 * time.Second) },
|
|
|
|
"now": func() time.Time { return time.Now() },
|
|
|
|
"join": func(args []string, c string) string { return strings.Join(args, c) },
|
2018-12-10 09:34:45 +01:00
|
|
|
}
|
2018-11-20 21:48:16 +01:00
|
|
|
)
|