package main import ( "database/sql" "encoding/json" "flag" "fmt" "html" "html/template" "io/ioutil" "log" "net/http" "strings" "time" "github.com/lib/pq" ) var ( configPath = flag.String("config", "monfront.conf", "path to the config file") DB *sql.DB Tmpl *template.Template ) type ( Config struct { DB string `json:"db"` Listen string `json:"listen"` } Context struct { Title string Mappings map[int]map[int]MapEntry Checks []check Groups []group } MapEntry struct { Title string Color string } check struct { NodeId int NodeName string CommandName string CheckID int64 MappingId int State int Enabled bool Notice sql.NullString NextTime time.Time Msg string } 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)) } Tmpl = tmpl http.HandleFunc("/", showChecks) http.HandleFunc("/checks", showChecks) http.HandleFunc("/hosts", showHosts) http.HandleFunc("/groups", showGroups) http.HandleFunc("/action", checkAction) http.HandleFunc("/unhandled/checks", showChecks) http.HandleFunc("/unhandled/hosts", showHosts) http.HandleFunc("/unhandled/groups", showGroups) http.ListenAndServe(config.Listen, nil) } func checkAction(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("method is not supported")) return } if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "could not parse parameters: %s", err) return } checks := r.PostForm["checks"] action := r.PostForm["action"] if len(action) == 0 || action[0] == "" || len(checks) == 0 { w.Header()["Location"] = []string{"/"} w.WriteHeader(http.StatusSeeOther) return } setTable := "checks" setClause := "" switch action[0] { case "mute": setTable = "checks_notify" setClause = "enabled = false" case "unmute": setTable = "checks_notify" setClause = "enabled = true" case "enable": setClause = "enabled = true" case "disable": setClause = "enabled = false" case "reschedule": setClause = "next_time = now()" setTable = "active_checks" case "ack": setClause = "acknowledged = true" setTable = "active_checks" case "comment": if len(r.PostForm["comment"]) == 0 { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "no comment sent") return } comment := r.PostForm["comment"][0] _, err := DB.Exec( "update active_checks set notice = $2 where check_id = any ($1::int[]);", pq.Array(&checks), html.EscapeString(comment)) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "could not store changes") log.Printf("could not adjust checks %#v: %s", checks, err) return } w.Header()["Location"] = []string{"/"} w.WriteHeader(http.StatusSeeOther) return default: w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "unknown action %s", action) return } whereColumn := "id" if setTable == "active_checks" || 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 } ref, found := r.Header["Referrer"] if found { w.Header()["Location"] = ref } else { w.Header()["Location"] = []string{"/"} } w.WriteHeader(http.StatusSeeOther) return } func showChecks(w http.ResponseWriter, r *http.Request) { query := `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 from active_checks ac join checks c on ac.check_id = c.id join nodes n on c.node_id = n.id join commands co on c.command_id = co.id` where := []string{} if strings.HasPrefix(r.URL.Path, "/unhandled") { where = append(where, `ac.states[1] > 0`) } idx := 0 params := []interface{}{} 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 ") } 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) 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 := Context{ Checks: checks, } if err := loadMappings(&con); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("problem with the mappings")) log.Printf("could not load mappings: %s", err) return } w.Header()["Content-Type"] = []string{"text/html"} if err := Tmpl.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 showHosts(w http.ResponseWriter, r *http.Request) { } 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) 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 := Context{ 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 } func loadMappings(c *Context) error { c.Mappings = map[int]map[int]MapEntry{} rows, err := DB.Query(SQLShowMappings) if err != nil { return err } for rows.Next() { if rows.Err() != nil { return rows.Err() } var ( mapId int target int title string color string ) if err := rows.Scan(&mapId, &target, &title, &color); err != nil { return err } ma, found := c.Mappings[mapId] if !found { ma = map[int]MapEntry{} c.Mappings[mapId] = ma } ma[target] = MapEntry{Title: title, Color: color} } return nil } var ( SQLShowMappings = `select mapping_id, target, title, color from mapping_level` ) var ( Templates = map[string]string{ "header": ` {{ .Title }} `, "footer": ``, "checklist": `{{ template "header" . }}
{{ $current := "" }} {{ $mapping := .Mappings }} {{ range .Checks }} {{ end }}
hoststatusnext checkmessage
{{ if ne $current .NodeName }}{{ $current = .NodeName }}{{ .NodeName }}{{ end }} {{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }} {{ .NextTime.Format "2006.01.02 15:04:05" }}
{{ .Msg }}
{{ template "footer" . }}`, "grouplist": `{{ template "header" . }}
{{ $current := "" }} {{ $mapping := .Mappings }} {{ range .Groups }} {{ end }}
grouphostworst state
{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }} {{ .NodeName }} {{ (index $mapping .MappingId .State).Title }}
{{ template "footer" . }}`, } TmplUnhandledGroups = `TODO` Funcs = template.FuncMap{ "sub": func(base, amount int) int { return base - amount }, } )