aboutsummaryrefslogblamecommitdiff
path: root/cmd/monfront/main.go
blob: 989759ab003482311ff53d1c884c09c676e47efe (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                       
                 







                                                                                      
                                     






                                             

                        
                               

                                                 
                                







                            
                               




                                  




                                          








                                



















                                                                         






                                                     
                                        
                                            
                                              
                                              
                                               
                                                        
                                                        
























                                                                     

                                             
                      

                                            



































                                                                                                 
                                                                       









                                                                                                                              





                                                      




                                                         






                                                                                           
                                                        
                                                         
         

                                 










                                                                                               



















                                                                           






                                                               


                            
                                                                                                                                                        







                                                                       








                                                              
                                                          
                                                                         







                                                                 

                                                                
 
 
















                                                                                                   






                                                               
                           
                         

                                                                                                     





                                                                       
                                          
         



                                                  
                                                             

                                                              


                                                          
                                                                         







                                                                 





























                                                                                  
     

                                                                  


     
                                      
                                          
      

                                           

                                              
























                                                                                                 





                                                        
                                              
                                                                                                   

                                                                           





                                                                         




                                                                                             
                        
                        
                                                                               
                         

               


                                                             



















                                                                                               
                             


                                                       

                                                     
                                     
















                                                                                        
                                      



                                                                                                                                                
                                                                    

                                                                           

                                                                                                                               
                                                                                                                                                                                             
                                                                                                                                                                                







                                                                                                             










































                                                                                                                                                           
                                    


                                                                           
 
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("/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) {
	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 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 ")
	}
	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
}

// 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 (
		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": `<!doctype html>
<html>
	<head>
		<title>{{ .Title }}</title>
		<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; }
			#mainmenu form input { }
			#mainmenu form button { }
			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:hover { background: #dfdfdf; }
			table th { background: #cccccc; color: #3a5f78; }
			table td, table th { text-align: center; }
			table pre { font-size: 75%; }
			{{ range $mapId, $mapping := .Mappings }}
			{{ range $target, $val := $mapping }}
			td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
			{{ end }}
			{{ end }}
		</style>
		<script>
			setTimeout(function() { location.reload(true) }, 30000)
		</script>
	</head>
	<body>
		<nav id="mainmenu">
			<ul>
				<li><a href="/">home</a></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>
			</ul>
		</nav>`,
		"footer": `</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></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 }}">{{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }}</td>
							<td>{{ .NextTime.Format "2006.01.02 15:04:05" }}</td>
							<td><pre>{{ .Msg }}</pre></td>
						</tr>
						{{ end }}
						</tbody>
					</table>
				</content>
		</form>
		{{ template "footer" . }}`,
		"grouplist": `{{ 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></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>
		{{ template "footer" . }}`,
	}
	TmplUnhandledGroups = `TODO`
	Funcs               = template.FuncMap{
		"sub": func(base, amount int) int { return base - amount },
	}
)