diff options
| author | Gibheer <gibheer+git@zero-knowledge.org> | 2019-01-09 20:17:49 +0100 | 
|---|---|---|
| committer | Gibheer <gibheer+git@zero-knowledge.org> | 2019-01-09 20:17:49 +0100 | 
| commit | 96d853fad68565257102fd2b722b01636f131852 (patch) | |
| tree | 7ebd0c8a20ba4738293f82910d7bfb270e876ee5 /cmd/monfront/main.go | |
| parent | 3b222e06ed63050cb99020d9d74ca0f1e52959d2 (diff) | |
monfront - add check detail view
This adds a detail view for a single check. The purpose is to view
notifications for this check alone and get the context information on
the node it belongs to, the command and settings.
Diffstat (limited to 'cmd/monfront/main.go')
| -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) },  	}  )  | 
