move monfront to separate repository
Monfront was forked into its own repository to make it easier to work on.
This commit is contained in:
parent
361ca94ca3
commit
e5ac5a4e53
3
Makefile
3
Makefile
@ -25,17 +25,14 @@ clean:
|
|||||||
build: clean
|
build: clean
|
||||||
mkdir -p ${WRKDIR}
|
mkdir -p ${WRKDIR}
|
||||||
GOOS=${GOOS} go build -ldflags="${LDFLAGS}" -o ${WRKDIR}/moncheck ${PKGNAME}/cmd/moncheck
|
GOOS=${GOOS} go build -ldflags="${LDFLAGS}" -o ${WRKDIR}/moncheck ${PKGNAME}/cmd/moncheck
|
||||||
GOOS=${GOOS} go build -ldflags="${LDFLAGS}" -o ${WRKDIR}/monfront ${PKGNAME}/cmd/monfront
|
|
||||||
GOOS=${GOOS} go build -ldflags="${LDFLAGS}" -o ${WRKDIR}/monwork ${PKGNAME}/cmd/monwork
|
GOOS=${GOOS} go build -ldflags="${LDFLAGS}" -o ${WRKDIR}/monwork ${PKGNAME}/cmd/monwork
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
install -d -m 0755 ${DESTDIR}${bindir}
|
install -d -m 0755 ${DESTDIR}${bindir}
|
||||||
install -d -m 0755 ${DESTDIR}${sysconfdir}
|
install -d -m 0755 ${DESTDIR}${sysconfdir}
|
||||||
install -m 0755 ${WRKDIR}/moncheck ${DESTDIR}${bindir}
|
install -m 0755 ${WRKDIR}/moncheck ${DESTDIR}${bindir}
|
||||||
install -m 0755 ${WRKDIR}/monfront ${DESTDIR}${bindir}
|
|
||||||
install -m 0755 ${WRKDIR}/monwork ${DESTDIR}${bindir}
|
install -m 0755 ${WRKDIR}/monwork ${DESTDIR}${bindir}
|
||||||
install -m 0644 moncheck.conf.example ${DESTDIR}${sysconfdir}
|
install -m 0644 moncheck.conf.example ${DESTDIR}${sysconfdir}
|
||||||
install -m 0644 monfront.conf.example ${DESTDIR}${sysconfdir}
|
|
||||||
install -m 0644 monwork.conf.example ${DESTDIR}${sysconfdir}
|
install -m 0644 monwork.conf.example ${DESTDIR}${sysconfdir}
|
||||||
|
|
||||||
package: DESTDIR = ${NAME}-${VERSION}
|
package: DESTDIR = ${NAME}-${VERSION}
|
||||||
|
@ -1,874 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"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
|
|
||||||
CurrentPath string
|
|
||||||
Error string
|
|
||||||
Mappings map[int]map[int]MapEntry
|
|
||||||
Checks []check
|
|
||||||
CheckDetails *checkDetails
|
|
||||||
Groups []group
|
|
||||||
Unhandled bool // set this flag when unhandled was called
|
|
||||||
}
|
|
||||||
|
|
||||||
MapEntry struct {
|
|
||||||
Title string
|
|
||||||
Color string
|
|
||||||
}
|
|
||||||
|
|
||||||
check struct {
|
|
||||||
NodeId int
|
|
||||||
NodeName string
|
|
||||||
CommandName string
|
|
||||||
CheckID int64
|
|
||||||
MappingId int
|
|
||||||
State int
|
|
||||||
Enabled bool
|
|
||||||
Notify bool
|
|
||||||
Notice sql.NullString
|
|
||||||
NextTime time.Time
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
State int
|
|
||||||
Output string
|
|
||||||
Inserted time.Time
|
|
||||||
Sent pq.NullTime
|
|
||||||
NotifierName string
|
|
||||||
MappingId int
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
for k, val := range AdminTemplates {
|
|
||||||
template.Must(tmpl.New("admin-" + k).Parse(val))
|
|
||||||
}
|
|
||||||
Tmpl = tmpl
|
|
||||||
|
|
||||||
http.HandleFunc("/", showChecks)
|
|
||||||
http.HandleFunc("/admin/", showAdmin)
|
|
||||||
http.HandleFunc("/admin/action", adminAction)
|
|
||||||
http.HandleFunc("/static/", showStatic)
|
|
||||||
http.HandleFunc("/check", showCheck)
|
|
||||||
http.HandleFunc("/checks", showChecks)
|
|
||||||
http.HandleFunc("/groups", showGroups)
|
|
||||||
http.HandleFunc("/action", checkAction)
|
|
||||||
http.HandleFunc("/unhandled/checks", showChecks)
|
|
||||||
http.HandleFunc("/unhandled/groups", showGroups)
|
|
||||||
http.ListenAndServe(config.Listen, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAction(w http.ResponseWriter, r *http.Request) {
|
|
||||||
con := Context{}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
ref, found := r.Header["Referer"]
|
|
||||||
if found {
|
|
||||||
w.Header()["Location"] = ref
|
|
||||||
} else {
|
|
||||||
w.Header()["Location"] = []string{"/"}
|
|
||||||
}
|
|
||||||
checks := r.PostForm["checks"]
|
|
||||||
action := r.PostForm["action"]
|
|
||||||
if len(action) == 0 || action[0] == "" || len(checks) == 0 {
|
|
||||||
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, updated = now()"
|
|
||||||
case "disable":
|
|
||||||
setClause = "enabled = false, updated = now()"
|
|
||||||
case "reschedule":
|
|
||||||
setClause = "next_time = now()"
|
|
||||||
setTable = "active_checks"
|
|
||||||
case "ack":
|
|
||||||
setClause = "acknowledged = true"
|
|
||||||
setTable = "active_checks"
|
|
||||||
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not resolve hostname: %s", err)
|
|
||||||
con.Error = "could not resolve hostname"
|
|
||||||
returnError(http.StatusInternalServerError, con, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := DB.Exec(`insert into notifications(check_id, states, output, mapping_id, notifier_id, check_host)
|
|
||||||
select ac.check_id, 0 || states[1:array_length(states, 1)], 'check acknowledged', ac.mapping_id,
|
|
||||||
cn.notifier_id, $2
|
|
||||||
from checks_notify cn
|
|
||||||
join active_checks ac on cn.check_id = ac.check_id
|
|
||||||
where cn.check_id = any ($1::int[])`, pq.Array(&checks), &hostname); err != nil {
|
|
||||||
log.Printf("could not acknowledge check: %s", err)
|
|
||||||
con.Error = "could not acknowledge check"
|
|
||||||
returnError(http.StatusInternalServerError, con, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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.WriteHeader(http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0])
|
|
||||||
returnError(http.StatusNotFound, con, w)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func showChecks(w http.ResponseWriter, r *http.Request) {
|
|
||||||
con := Context{}
|
|
||||||
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,
|
|
||||||
case when cn.check_id is null then false else true end as notify_enabled
|
|
||||||
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
|
|
||||||
left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id`
|
|
||||||
where := []string{}
|
|
||||||
idx := 0
|
|
||||||
params := []interface{}{}
|
|
||||||
if id, found := r.URL.Query()["group_id"]; found {
|
|
||||||
query += ` join nodes_groups ng on n.id = ng.node_id`
|
|
||||||
idx += 1
|
|
||||||
where = append(where, fmt.Sprintf(`ng.group_id = $%d::int`, idx))
|
|
||||||
params = append(params, id[0])
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
||||||
where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`)
|
|
||||||
con.Unhandled = true
|
|
||||||
}
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
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 ")
|
|
||||||
}
|
|
||||||
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`
|
|
||||||
}
|
|
||||||
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, &c.Notify)
|
|
||||||
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.Checks = checks
|
|
||||||
if err := loadMappings(&con); err != nil {
|
|
||||||
con.Error = "could not load mapping data"
|
|
||||||
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 showGroups(w http.ResponseWriter, r *http.Request) {
|
|
||||||
con := Context{}
|
|
||||||
query := `select
|
|
||||||
group_id,
|
|
||||||
group_name,
|
|
||||||
node_id,
|
|
||||||
node_name,
|
|
||||||
mapping_id,
|
|
||||||
state
|
|
||||||
from (
|
|
||||||
select
|
|
||||||
g.id group_id,
|
|
||||||
g.name group_name,
|
|
||||||
n.id node_id,
|
|
||||||
n.name node_name,
|
|
||||||
ac.states[1] state,
|
|
||||||
ac.mapping_id,
|
|
||||||
ac.acknowledged,
|
|
||||||
row_number() over (partition by c.node_id order by ac.states[1] desc) 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
|
|
||||||
%s
|
|
||||||
order by g.name, n.name
|
|
||||||
) groups
|
|
||||||
where maxstate = 1`
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
||||||
query = fmt.Sprintf(query, `where ac.states[1] != 0 and acknowledged = false`)
|
|
||||||
con.Unhandled = true
|
|
||||||
} else {
|
|
||||||
query = fmt.Sprintf(query, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, ac.next_time
|
|
||||||
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, &cd.NextTime)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
query = `select n.id, states[1], output, inserted, sent, no.name, n.mapping_id
|
|
||||||
from notifications n
|
|
||||||
join notifier no on n.notifier_id = no.id
|
|
||||||
where check_id = $1::bigint
|
|
||||||
order by inserted desc
|
|
||||||
limit 500`
|
|
||||||
rows, err := DB.Query(query, cd.Id)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not load notifications: %s", err)
|
|
||||||
con.Error = "could not load notification information"
|
|
||||||
returnError(http.StatusInternalServerError, con, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cd.Notifications = []notification{}
|
|
||||||
for rows.Next() {
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
log.Printf("could not load notifications: %s", err)
|
|
||||||
con.Error = "could not load notification information"
|
|
||||||
returnError(http.StatusInternalServerError, con, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
no := notification{}
|
|
||||||
if err := rows.Scan(&no.Id, &no.State, &no.Output, &no.Inserted,
|
|
||||||
&no.Sent, &no.NotifierName, &no.MappingId); err != nil {
|
|
||||||
log.Printf("could not scan notifications: %s", err)
|
|
||||||
con.Error = "could not load notification information"
|
|
||||||
returnError(http.StatusInternalServerError, con, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cd.Notifications = append(cd.Notifications, no)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 returnError(status int, con interface{}, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
SQLShowMappings = `select mapping_id, target, title, color
|
|
||||||
from mapping_level`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Templates = map[string]string{
|
|
||||||
"header": `<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{ .Title }}</title>
|
|
||||||
<meta name="referrer" content="same-origin">
|
|
||||||
<link rel="shortcut icon" href="/static/favicon" />
|
|
||||||
<style type="text/css">
|
|
||||||
* { font-size: 100%; }
|
|
||||||
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; }
|
|
||||||
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; border-left: 0.15em solid #dddddd; }
|
|
||||||
form nav { display: flex; flex-direction: column; }
|
|
||||||
form nav > * { margin: 0.5em; }
|
|
||||||
table { width: 100%; }
|
|
||||||
table tr:nth-child(odd) { background: #eeeeee; }
|
|
||||||
table tr.selected:nth-child(odd) { background: #afbfd4; }
|
|
||||||
table tr.selected:nth-child(even) { background: #cddbec; }
|
|
||||||
table tr:hover { background: #dfdfdf; }
|
|
||||||
table th { background: #cccccc; color: #3a5f78; }
|
|
||||||
table td, table th { text-align: center; }
|
|
||||||
table code { font-size: 75%; }
|
|
||||||
table td.disabled { text-decoration: line-through; }
|
|
||||||
.icon {
|
|
||||||
display: inline-block;
|
|
||||||
height: 1em;
|
|
||||||
margin: 0;
|
|
||||||
width: 1em;
|
|
||||||
vertical-align: bottom;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
.mute { background-image: url(/static/icon-mute); }
|
|
||||||
.detail > div { display: grid; grid-template-columns: 25% auto; }
|
|
||||||
.detail > div:hover { background: #dfdfdf; }
|
|
||||||
.error { padding: 0.5em; background: #ffc6c6; border: 1px solid red; }
|
|
||||||
/* state background colors */
|
|
||||||
{{ range $mapId, $mapping := .Mappings -}}
|
|
||||||
{{ range $target, $val := $mapping -}}
|
|
||||||
.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
|
|
||||||
{{ end -}}
|
|
||||||
{{ end -}}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000)
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav id="mainmenu">
|
|
||||||
<ul>
|
|
||||||
<li class="submenu">
|
|
||||||
<span class="header">main</span>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/">home</a></li>
|
|
||||||
<li><a href="/admin">admin</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<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>
|
|
||||||
<li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`,
|
|
||||||
"footer": `<script>
|
|
||||||
function row_head_click_event(event) {
|
|
||||||
check = false;
|
|
||||||
current = event.target;
|
|
||||||
while (current != null) {
|
|
||||||
if (current.nodeName == 'TABLE') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (current.nodeName == 'TR') {
|
|
||||||
check = !current.children[0].children[0].checked;
|
|
||||||
current.children[0].children[0].checked = check;
|
|
||||||
}
|
|
||||||
current = current.parentNode;
|
|
||||||
}
|
|
||||||
lines = current.children[1].children
|
|
||||||
for (i = 0; i < lines.length; i++) {
|
|
||||||
select_row(event, lines[i], lines[i].children[0].children[0], check);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function row_click_event(event) {
|
|
||||||
if (event.target.nodeName == 'INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
current = event.target;
|
|
||||||
while (current = current.parentNode) {
|
|
||||||
if (current.nodeName == 'BODY') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (current.nodeName != 'TR') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
e = current.children[0].children[0];
|
|
||||||
check = !e.checked;
|
|
||||||
select_row(event, current, e, check);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function select_row(event, row, input, check) {
|
|
||||||
if (input != event.target) {
|
|
||||||
input.checked = check;
|
|
||||||
}
|
|
||||||
if (input.checked) {
|
|
||||||
row.classList.add("selected");
|
|
||||||
} else {
|
|
||||||
row.classList.remove("selected");
|
|
||||||
}
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (selector of ['thead > tr', 'thead input']) {
|
|
||||||
els = document.querySelectorAll(selector);
|
|
||||||
for (i = 0; i < els.length; i++) {
|
|
||||||
els[i].addEventListener('click', {handleEvent: row_head_click_event});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (selector of ['tbody > tr', 'tbody input']) {
|
|
||||||
els = document.querySelectorAll(selector);
|
|
||||||
for (i = 0; i < els.length; i++) {
|
|
||||||
els[i].addEventListener('click', {handleEvent: row_click_event});
|
|
||||||
}
|
|
||||||
}</script></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><input type="checkbox" title="select all" /></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 }}">{{ if ne .Notify true }}<span class="icon mute"></span>{{ end }}<a href="/check?check_id={{ .CheckID }}">{{ .CommandName }}</a> - {{ (index $mapping .MappingId .State).Title }}</td>
|
|
||||||
<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>
|
|
||||||
<td><code>{{ .Msg }}</code></td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</content>
|
|
||||||
</form>
|
|
||||||
{{ template "footer" . }}`,
|
|
||||||
"checkformheader": `<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>`,
|
|
||||||
"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 }}<a href="{{ if $.Unhandled }}/unhandled{{ end }}/checks?group_id={{ .GroupId }}">{{ .Name }}</a>{{ 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" . }}
|
|
||||||
{{ template "footer" . }}`,
|
|
||||||
"check": `{{ template "header" . }}
|
|
||||||
{{ template "checkformheader" . }}
|
|
||||||
<content class="details">
|
|
||||||
{{ $mapping := .Mappings }}
|
|
||||||
{{ with .CheckDetails }}
|
|
||||||
<input type="hidden" name="checks" value="{{ .Id }}" />
|
|
||||||
<article class="detail">
|
|
||||||
<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>
|
|
||||||
<article class="detail">
|
|
||||||
<h1>node <a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></h1>
|
|
||||||
<div><span class="label">Message</span><span class="value">{{ .NodeMessage }}</span></div>
|
|
||||||
</article>
|
|
||||||
<article class="detail">
|
|
||||||
<h1>command {{ .CommandName }}</h1>
|
|
||||||
<div><span class="label">Message</span><span class="value">{{ .CommandMessage }}</span></div>
|
|
||||||
<div><span class="label">command line</span><span class="value"><code>{{ join .CommandLine " " }}</code></span></div>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h1>notifications</h1>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>notifier</th><th>state</th><th>created</th><th>sent</th><th>output</th></thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range .Notifications -}}
|
|
||||||
<tr>
|
|
||||||
<td>{{ .NotifierName }}</td>
|
|
||||||
<td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td>
|
|
||||||
<td>{{ .Inserted.Format "2006.01.02 15:04:05" }}</td>
|
|
||||||
<td>{{ if .Sent.Valid }}{{ .Sent.Time.Format "2006.01.02 15:04:05" }}{{ end }}</td>
|
|
||||||
<td>{{ .Output }}</td>
|
|
||||||
</tr>
|
|
||||||
{{ end -}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</article>
|
|
||||||
{{ 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" . }}`,
|
|
||||||
}
|
|
||||||
TmplUnhandledGroups = `TODO`
|
|
||||||
Funcs = template.FuncMap{
|
|
||||||
"int": func(in int64) int { return int(in) },
|
|
||||||
"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) },
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"db": "dbname=monzero",
|
|
||||||
"listen": ":9292"
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user