diff --git a/Makefile b/Makefile index 2a41f7f..f06f270 100644 --- a/Makefile +++ b/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} diff --git a/cmd/monfront/main.go b/cmd/monfront/main.go deleted file mode 100644 index 57904a5..0000000 --- a/cmd/monfront/main.go +++ /dev/null @@ -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": ` - - - {{ .Title }} - - - - - - - - {{ if .Error }}
{{ .Error }}
{{ end }}`, - "footer": ``, - "checklist": `{{ template "header" . }} -
-
- - - - - - {{ $current := "" }} - {{ $mapping := .Mappings }} - {{ range .Checks }} - - - - - - - - {{ end }} - -
hoststatusnext checkmessage
{{ if ne $current .NodeName }}{{ $current = .NodeName }}{{ .NodeName }}{{ end }}{{ if ne .Notify true }}{{ end }}{{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }}{{ .NextTime.Format "2006.01.02 15:04:05" }} - in {{ in .NextTime }}{{ .Msg }}
-
- - {{ template "footer" . }}`, - "checkformheader": `
-
- `, - "checkformfooter": `
`, - "grouplist": `{{ template "header" . }} - {{ template "checkformheader" . }} - - - - - {{ $current := "" }} - {{ $mapping := .Mappings }} - {{ range .Groups }} - - - - - - - {{ end }} - -
grouphostworst state
{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}{{ .NodeName }}{{ (index $mapping .MappingId .State).Title }}
-
- {{ template "checkformfooter" . }} - {{ template "footer" . }}`, - "check": `{{ template "header" . }} - {{ template "checkformheader" . }} - - {{ $mapping := .Mappings }} - {{ with .CheckDetails }} - -
-

check

-
current state{{ (index $mapping .MappingId (index .States 0) ).Title }}
-
current notice{{ if .Notice }}{{ .Notice.String }}{{ end }}
-
Message{{ .Message }}
-
enabled{{ .Enabled }}
-
updated{{ .Updated.Format "2006.01.02 15:04:05" }}
-
next check{{ .NextTime.Format "2006.01.02 15:04:05" }}
-
last refresh{{ .LastRefresh.Format "2006.01.02 15:04:05" }}
-
mapping{{ .MappingId }}
-
- -
-

command {{ .CommandName }}

-
Message{{ .CommandMessage }}
-
command line{{ join .CommandLine " " }}
-
-
-

notifications

- - - - {{ range .Notifications -}} - - - - - - - - {{ end -}} - -
notifierstatecreatedsentoutput
{{ .NotifierName }}{{ (index $mapping .MappingId .State).Title }}{{ .Inserted.Format "2006.01.02 15:04:05" }}{{ if .Sent.Valid }}{{ .Sent.Time.Format "2006.01.02 15:04:05" }}{{ end }}{{ .Output }}
-
- {{ end }} -
- {{ template "checkformfooter" . }} - {{ template "footer" . }}`, - } - Static = map[string]string{ - "icon-mute": `Check is muted`, - "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) }, - } -) diff --git a/monfront.conf.example b/monfront.conf.example deleted file mode 100644 index 6a79e13..0000000 --- a/monfront.conf.example +++ /dev/null @@ -1,4 +0,0 @@ -{ - "db": "dbname=monzero", - "listen": ":9292" -}