aboutsummaryrefslogblamecommitdiff
path: root/cmd/monfront/main.go
blob: e128ed67fdac2999fd258d00c056b58a60bb72a9 (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
)

type (
	Config struct {
		DB     string `json:"db"`
		Listen string `json:"listen"`
	}

	Context struct {
		Mappings map[int]map[int]MapEntry
		Checks   []check
	}

	MapEntry struct {
		Title string
		Color string
	}

	check struct {
		NodeName    string
		CommandName string
		CheckID     int64
		MappingId   int
		State       int
		Notify      bool
		Enabled     bool
		Notice      sql.NullString
		NextTime    time.Time
		Msg         string
	}
)

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

	http.HandleFunc("/", showChecks)
	http.HandleFunc("/action", checkAction)
	http.HandleFunc("/unhandled/checks", showChecks)
	http.HandleFunc("/unhandled/hosts", showUnhandledHosts)
	http.HandleFunc("/unhandled/groups", showUnhandledGroups)
	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":
		setClause = "notify = false"
	case "unmute":
		setClause = "notify = 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" {
		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 := SQLShowChecks
	if strings.HasPrefix(r.URL.Path, "/unhandled") {
		query = SQLShowUnhandledChecks
	}
	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
	}

	checks := []check{}
	for rows.Next() {
		c := check{}
		err := rows.Scan(&c.CheckID, &c.NodeName, &c.CommandName, &c.MappingId, &c.State, &c.Notify, &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)
	}
	tmpl := template.New("checklist")
	tmpl.Funcs(Funcs)
	tmpl, err = tmpl.Parse(TmplCheckList)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("problems with a template"))
		log.Printf("could not parse template: %s", err)
		return
	}
	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.Execute(w, 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 showUnhandledHosts(w http.ResponseWriter, r *http.Request) {
}

func showUnhandledGroups(w http.ResponseWriter, r *http.Request) {
	rows, err := DB.Query(SQLShowUnhandledGroups)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("problems with the database"))
		log.Printf("could not get check list: %s", err)
		return
	}

	type check struct {
		GroupName string
		NodeName  string
		State     int
	}
	checks := []check{}
	for rows.Next() {
		c := check{}
		err := rows.Scan(&c.GroupName, &c.NodeName, &c.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
		}
		checks = append(checks, c)
	}
	tmpl, err := template.New("checklist").Parse(TmplUnhandledGroups)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("problems with a template"))
		log.Printf("could not parse template: %s", err)
		return
	}
	w.Header()["Content-Type"] = []string{"text/html"}
	if err := tmpl.Execute(w, checks); 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`
	SQLShowChecks = `select c.id, n.name, co.name, ac.mapping_id, ac.states[1] as state, ac.notify,
	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
	order by n.name, co.name;`
	SQLShowUnhandledChecks = `select c.id, n.name, co.name, ac.mapping_id, ac.states[1] as state, ac.notify,
	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 ac.states[1] > 0
	order by n.name, co.name;`
	SQLShowUnhandledGroups = `select g.name, n.name, max(ac.state[1])
	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
	where ac.states[1] > 0
	group by g.name, n.name;`
)

var (
	TmplCheckList = `<doctype html>
<html>
  <head>
		<title>check list</title>
		<style type="text/css">
			* { font-size: 100%; }
			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; }
			form nav { display: flex; flex-direction: column; }
			form nav > * { margin: 0.5em; }
			table td, table th { padding: 0.5em; }
			{{ range $mapId, $mapping := .Mappings }}
			{{ range $target, $val := $mapping }}
			td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
			{{ end }}
			{{ end }}
		 /* td.state-0 { background: green; }
		  td.state-1 { background: orange; }
		  td.state-2 { background: red; }
		  td.state-99 { background: gray; } */
		</style>
		<script>
		setTimeout(function() { location.reload(true) }, 30000)
		</script>
	</head>
	<body>
		<nav id="mainmenu">
			<ul>
				<li><a href="/">home</a></li>
				<li><a href="/unhandled/checks">unhandled checks</a></li>
				<li><a href="/unhandled/hosts">unhandled hosts</a></li>
				<li><a href="/unhandled/groups">unhandled groups</a></li>
			</ul>
		</nav>
		<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 }}{{ .NodeName }}{{ 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>
  </body>
</html>`
	TmplUnhandledGroups = `TODO`
	Funcs               = template.FuncMap{
		"sub": func(base, amount int) int { return base - amount },
	}
)