diff options
-rw-r--r-- | cmd/monfront/main.go | 266 |
1 files changed, 220 insertions, 46 deletions
diff --git a/cmd/monfront/main.go b/cmd/monfront/main.go index fd3d3ec..a3fd50d 100644 --- a/cmd/monfront/main.go +++ b/cmd/monfront/main.go @@ -29,10 +29,13 @@ type ( } Context struct { - Title string - Mappings map[int]map[int]MapEntry - Checks []check - Groups []group + Title string + CurrentPath string + Error string + Mappings map[int]map[int]MapEntry + Checks []check + CheckDetails *checkDetails + Groups []group } MapEntry struct { @@ -54,6 +57,42 @@ type ( 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 + States []int + Output string + Inserted time.Time + Sent time.Time + } + group struct { GroupId int Name string @@ -90,7 +129,10 @@ func main() { Tmpl = tmpl http.HandleFunc("/", showChecks) + http.HandleFunc("/static/", showStatic) http.HandleFunc("/check", showCheck) + http.HandleFunc("/group", showGroup) + http.HandleFunc("/node", showNode) http.HandleFunc("/checks", showChecks) http.HandleFunc("/groups", showGroups) http.HandleFunc("/action", checkAction) @@ -100,6 +142,7 @@ func main() { } 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")) @@ -157,8 +200,8 @@ func checkAction(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusSeeOther) return default: - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "unknown action %s", action) + con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0]) + returnError(http.StatusNotFound, con, w) return } whereColumn := "id" @@ -194,7 +237,7 @@ func showChecks(w http.ResponseWriter, r *http.Request) { left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id` where := []string{} if strings.HasPrefix(r.URL.Path, "/unhandled") { - where = append(where, `ac.states[1] > 0 and ac.acknowledged = false`) + where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`) } idx := 0 params := []interface{}{} @@ -252,6 +295,7 @@ func showChecks(w http.ResponseWriter, r *http.Request) { 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) @@ -267,10 +311,6 @@ func showChecks(w http.ResponseWriter, r *http.Request) { return } -// showCheck loads shows the notifications for a specific check. -func showCheck(w http.ResponseWriter, r *http.Request) { -} - func showGroups(w http.ResponseWriter, r *http.Request) { query := `select groupid, groupname, nodeid, nodename, mapping_id, state from ( @@ -326,6 +366,73 @@ func showGroups(w http.ResponseWriter, r *http.Request) { 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 + from checks c + join active_checks ac on c.id = ac.check_id + join nodes n on c.node_id = n.id + join commands co on c.command_id = co.id + join mappings m on ac.mapping_id = m.id + where c.id = $1::bigint` + err := DB.QueryRow(query, id[0]).Scan(&cd.Id, &cd.Message, &cd.Enabled, &cd.Updated, &cd.LastRefresh, + &cd.MappingId, &cd.MappingName, &cd.NodeId, &cd.NodeName, &cd.NodeMessage, + &cd.CommandId, &cd.CommandName, &cd.CommandMessage, + pq.Array(&cd.CommandLine), pq.Array(&cd.States), &cd.Notice) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problems with the database")) + log.Printf("could not get check details for check id %s: %s", id[0], err) + return + } + + // TODO load the last couple notifications + + if err := loadMappings(&con); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problem with the mappings")) + log.Printf("could not load mappings: %s", err) + return + } + + w.Header()["Content-Type"] = []string{"text/html"} + if err := Tmpl.ExecuteTemplate(w, "check", con); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problem with a template")) + log.Printf("could not execute template: %s", err) + return + } +} +func showGroup(w http.ResponseWriter, r *http.Request) { + // TODO implement showing all nodes only from one group and its message? + return +} +func showNode(w http.ResponseWriter, r *http.Request) { + // TODO implement showing all checks from one node and its message? + return +} + +func returnError(status int, con Context, w http.ResponseWriter) { + w.Header()["Content-Type"] = []string{"text/html"} + w.WriteHeader(status) + if err := Tmpl.ExecuteTemplate(w, "error", con); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("problem with a template")) + log.Printf("could not execute template: %s", err) + } + +} + func loadMappings(c *Context) error { c.Mappings = map[int]map[int]MapEntry{} rows, err := DB.Query(SQLShowMappings) @@ -356,6 +463,20 @@ func loadMappings(c *Context) error { 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` @@ -367,6 +488,7 @@ var ( <html> <head> <title>{{ .Title }}</title> + <link rel="shortcut icon" href="/static/favicon" /> <style type="text/css"> * { font-size: 100%; } body { display: flex; flex-direction: column; padding: 0; margin: 0; } @@ -409,14 +531,26 @@ var ( table tr:hover { background: #dfdfdf; } table th { background: #cccccc; color: #3a5f78; } table td, table th { text-align: center; } - table pre { font-size: 75%; } + table code { font-size: 75%; } table td.disabled { text-decoration: line-through; } - .icon { height: 1em; margin: 0; width: 1em; vertical-align: bottom; margin-right: 0.5em;} - {{ range $mapId, $mapping := .Mappings }} - {{ range $target, $val := $mapping }} - td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; } - {{ end }} - {{ end }} + .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); + } + /* 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) @@ -448,7 +582,8 @@ var ( </li> <li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li> </ul> - </nav>`, + </nav> + {{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`, "footer": `</body></html>`, "checklist": `{{ template "header" . }} <form method="post" action="/action"> @@ -482,9 +617,9 @@ var ( <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 }}{{ template "icon-mute" . }}{{ end }}{{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }}</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><pre>{{ .Msg }}</pre></td> + <td><code>{{ .Msg }}</code></td> </tr> {{ end }} </tbody> @@ -492,8 +627,7 @@ var ( </content> </form> {{ template "footer" . }}`, - "grouplist": `{{ template "header" . }} - <form method="post" action="/action"> + "checkformheader": `<form method="post" action="/action"> <section> <nav> <div class="option"> @@ -513,32 +647,72 @@ var ( <input name="comment" /> </div> <button type="submit">submit</button> - </nav> - <content> - <table> - <thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead> - <tbody> - {{ $current := "" }} - {{ $mapping := .Mappings }} - {{ range .Groups }} - <tr> - <td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td> - <td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td> - <td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td> - <td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td> - </tr> - {{ end }} - </tbody> - </table> - </content> - </form> + </nav>`, + "checkformfooter": `</section></form>`, + "grouplist": `{{ template "header" . }} + {{ template "checkformheader" . }} + <content> + <table> + <thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead> + <tbody> + {{ $current := "" }} + {{ $mapping := .Mappings }} + {{ range .Groups }} + <tr> + <td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td> + <td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td> + <td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td> + <td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td> + </tr> + {{ end }} + </tbody> + </table> + </content> + {{ template "checkformfooter" . }} {{ template "footer" . }}`, - "icon-mute": `<svg class="icon" width="100" height="100" 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>`, + "check": `{{ template "header" . }} + {{ template "checkformheader" . }} + <content> + {{ $mapping := .Mappings }} + {{ with .CheckDetails }} + <input type="hidden" name="check_id" value="{{ .Id }}" /> + <article> + <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> + <h1>node {{ .NodeName }}</h1> + <div><span class="label">Message</span><span class="value">{{ .NodeMessage }}</span></div> + </article> + <article> + <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">{{ .CommandLine }}</span></div> + </article> + States []int64 + Notifiers []notifier + Notifications []notification + {{ end }} + </content> + {{ template "checkformfooter" . }} + {{ template "footer" . }}`, + } + Static = map[string]string{ + "icon-mute": `<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35.3 35.3" version="1.1"><title>Check is muted</title><style>.s0{fill:#191919;}</style><g transform="translate(0,-261.72223)"><path d="m17.6 261.7v35.3L5.3 284.7H0v-10.6l5.3 0zM30.2 273.1l-3.7 3.7-3.7-3.7-2.5 2.5 3.7 3.7-3.7 3.7 2.5 2.5 3.7-3.7 3.7 3.7 2.5-2.5-3.7-3.7 3.7-3.7z" fill="#191919"/></g></svg>`, + "error": `{{ template "header" . }}{{ template "footer" . }}`, } TmplUnhandledGroups = `TODO` Funcs = template.FuncMap{ - "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() }, + "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) }, } ) |