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
|
||||
mkdir -p ${WRKDIR}
|
||||
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
|
||||
|
||||
install: build
|
||||
install -d -m 0755 ${DESTDIR}${bindir}
|
||||
install -d -m 0755 ${DESTDIR}${sysconfdir}
|
||||
install -m 0755 ${WRKDIR}/moncheck ${DESTDIR}${bindir}
|
||||
install -m 0755 ${WRKDIR}/monfront ${DESTDIR}${bindir}
|
||||
install -m 0755 ${WRKDIR}/monwork ${DESTDIR}${bindir}
|
||||
install -m 0644 moncheck.conf.example ${DESTDIR}${sysconfdir}
|
||||
install -m 0644 monfront.conf.example ${DESTDIR}${sysconfdir}
|
||||
install -m 0644 monwork.conf.example ${DESTDIR}${sysconfdir}
|
||||
|
||||
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