icinga

Icinga2 API client in Go
git clone https://git.olowe.co/icinga
Log | Files | Refs | README | LICENSE

commit 6c821dbc0890acc0ce1ff7c6a98523aa7fca0573
parent 410b374583e5172b215020f0ad3db35a8635fa59
Author: Oliver Lowe <o@olowe.co>
Date:   Sat, 11 Mar 2023 17:36:38 +1100

cmd/checkweb: start experimental basic web app for icinga2

No relational databases or server-side dependencies required.
Requires docs!

Diffstat:
Acmd/checkweb/base.tmpl | 42++++++++++++++++++++++++++++++++++++++++++
Acmd/checkweb/checkweb.go | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/checkweb/host.tmpl | 28++++++++++++++++++++++++++++
Acmd/checkweb/hosts.tmpl | 11+++++++++++
Acmd/checkweb/service.tmpl | 26++++++++++++++++++++++++++
Acmd/checkweb/services.tmpl | 11+++++++++++
6 files changed, 286 insertions(+), 0 deletions(-)

diff --git a/cmd/checkweb/base.tmpl b/cmd/checkweb/base.tmpl @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="stylesheet" href="https://the.missing.style"> + <link href="https://fonts.bunny.net/css?family=source-sans-3:400,700|m-plus-code-latin:400,700" rel="stylesheet"> + <title>{{block "title" .}}checkweb{{end}}</title> + + <style> +.unknown { + --box-bg: lavender; + --accent: rebeccapurple; +} + </style> +</head> +<body> +<header class="navbar"> +<nav class="contents"> +<a href="/">checkweb</a> +<a href="/objects/services">Services</a> +<a href="/objects/hosts">Hosts</a> +<form class="f-row" action="/search"> + <input type="search" placeholder="Search" name="filter"> + <select name="type"> + <option value="service">Services</option> + <option value="servicegroup">Service Groups</option> + <option value="host">Hosts</option> + <option value="hostgroup">Host Groups</option> + </select> +</form> + </li> +</ul> +</nav> +</header> +<main> + {{block "content" .}}Hello, checkweb!{{end}} +</main> +<footer> +</footer> +</body> +</html> diff --git a/cmd/checkweb/checkweb.go b/cmd/checkweb/checkweb.go @@ -0,0 +1,168 @@ +// Command checkweb is a web application for... +package main + +import ( + "crypto/tls" + "errors" + "fmt" + "html/template" + "log" + "net/http" + "net/url" + "path" + + "olowe.co/icinga" +) + +type server struct { + tmpl struct { + services *template.Template + service *template.Template + hosts *template.Template + host *template.Template + root *template.Template + } + client *icinga.Client +} + +func (srv *server) servicesHandler(w http.ResponseWriter, req *http.Request) { + filter := req.URL.Query().Get("filter") + services, err := srv.client.Services(filter) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Print(err) + return + } + if err := srv.tmpl.services.Execute(w, services); err != nil { + log.Println("render services:", err) + } +} + +func (srv *server) hostsHandler(w http.ResponseWriter, req *http.Request) { + filter := req.URL.Query().Get("filter") + hosts, err := srv.client.Hosts(filter) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Print(err) + return + } + if err := srv.tmpl.hosts.Execute(w, hosts); err != nil { + log.Println("render hosts:", err) + } +} + +func (srv *server) hostHandler(w http.ResponseWriter, req *http.Request) { + base := path.Base(req.URL.Path) + name, err := url.PathUnescape(base) + if err != nil { + msg := fmt.Sprintf("escape object name: %v", err) + log.Println(err) + http.Error(w, msg, http.StatusBadRequest) + return + } + host, err := srv.client.LookupHost(name) + if err != nil { + msg := fmt.Sprintf("lookup %s: %v", name, err) + code := http.StatusInternalServerError + if errors.Is(err, icinga.ErrNotExist) { + code = http.StatusNotFound + } + http.Error(w, msg, code) + return + } + + if err := srv.tmpl.host.Execute(w, host); err != nil { + log.Println("render hosts:", err) + } +} + +func (srv *server) serviceHandler(w http.ResponseWriter, req *http.Request) { + p := req.URL.Path + if req.URL.RawPath != "" { + p = req.URL.RawPath + } + base := path.Base(p) + name, err := url.PathUnescape(base) + if err != nil { + msg := fmt.Sprintf("escape object name: %v", err) + log.Println(err) + http.Error(w, msg, http.StatusBadRequest) + return + } + fmt.Println("looking up", name) + service, err := srv.client.LookupService(name) + if err != nil { + msg := fmt.Sprintf("lookup %s: %v", name, err) + code := http.StatusInternalServerError + if errors.Is(err, icinga.ErrNotExist) { + code = http.StatusNotFound + } + http.Error(w, msg, code) + return + } + + if err := srv.tmpl.service.Execute(w, service); err != nil { + log.Println("render service:", err) + } +} + +func (srv *server) searchHandler(w http.ResponseWriter, req *http.Request) { + var u url.URL + typ := req.URL.Query().Get("type") + switch typ { + case "host": + u.Path = "/objects/hosts" + case "service": + u.Path = "/objects/services" + } + v := url.Values{} + v.Set("filter", req.URL.Query().Get("filter")) + u.RawQuery = v.Encode() + http.Redirect(w, req, u.String(), http.StatusFound) +} + +func (srv *server) rootHandler(w http.ResponseWriter, req *http.Request) { + if err := srv.tmpl.root.Execute(w, nil); err != nil { + log.Println(err) + } +} + +var tFuncMap = template.FuncMap{ + "pathescape": url.PathEscape, +} + +func newTemplate(root *template.Template, files ...string) (*template.Template, error) { + t, err := root.Clone() + if err != nil { + return nil, err + } + return t.ParseFiles(files...) +} + +func main() { + t := http.DefaultTransport.(*http.Transport) + t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + c := http.DefaultClient + c.Transport = t + client, err := icinga.Dial("127.0.0.1:5665", "", "", c) + if err != nil { + log.Fatal(err) + } + srv := &server{ + client: client, + } + + srv.tmpl.root = template.Must(template.ParseFiles("base.tmpl")).Funcs(tFuncMap) + srv.tmpl.services = template.Must(newTemplate(srv.tmpl.root, "services.tmpl")) + srv.tmpl.service = template.Must(newTemplate(srv.tmpl.root, "service.tmpl")) + srv.tmpl.hosts = template.Must(newTemplate(srv.tmpl.root, "hosts.tmpl")) + srv.tmpl.host = template.Must(newTemplate(srv.tmpl.root, "host.tmpl")) + + http.HandleFunc("/objects/services", srv.servicesHandler) + http.HandleFunc("/objects/services/", srv.serviceHandler) + http.HandleFunc("/objects/hosts", srv.hostsHandler) + http.HandleFunc("/objects/hosts/", srv.hostHandler) + http.HandleFunc("/search", srv.searchHandler) + http.HandleFunc("/", srv.rootHandler) + log.Fatal(http.ListenAndServe(":6969", nil)) +} diff --git a/cmd/checkweb/host.tmpl b/cmd/checkweb/host.tmpl @@ -0,0 +1,27 @@ +{{define "content"}} +<h1> + {{.DisplayName}} + <sub-title>{{.Name}}</sub-title> +</h1> + +<p>{{.Notes}} +{{if .NotesURL}}<a href="{{.NotesURL}}">More info</a>{{end}} +</p> + +<table> + <tr><th>Last check</th><td><time>{{.LastCheck}}</time></td></tr> + <tr> + <th>State</th> + <td> + <chip class="{{if eq .State 0}}ok{{else}}bad{{end}}">{{.State}}</chip> + {{if .Acknowledgement}}<chip class="info">Acknowledged</chip>{{end}} + </td> + </tr> + <tr><th>CheckCommand</th><td>{{.CheckCommand}}</td></tr> + <tr><th>Raw command</th><td><code>{{.LastCheckResult.RawCommand}}</code></td></tr> +</table> + +<figure> + <pre>{{.LastCheckResult.Output}}</pre> +</figure> +{{end}} +\ No newline at end of file diff --git a/cmd/checkweb/hosts.tmpl b/cmd/checkweb/hosts.tmpl @@ -0,0 +1,10 @@ +{{define "title"}}Hosts{{end}} + +{{define "content"}} +{{range .}} + <div class="f-row justify-content:space-between"> + <a href="/objects/hosts/{{.Name}}">{{.DisplayName}} ({{.Name}})</a> + <chip class="{{if eq .State 0}}ok{{else}}bad{{end}}">{{.State}}</chip> + </div> +{{end}} +{{end}} +\ No newline at end of file diff --git a/cmd/checkweb/service.tmpl b/cmd/checkweb/service.tmpl @@ -0,0 +1,25 @@ +{{define "title"}}{{.Name}}{{end}} + +{{define "content"}} +<h1> + {{.DisplayName}} + <sub-title>{{.Name}}</sub-title> +</h1> +<p>{{.Notes}}</p> +<table> + <tr><th>Last check</th><td><time>{{.LastCheck}}</time></td></tr> + <tr> + <th>State</th> + <td> + <chip class="{{if eq .State 0}}ok{{else if eq .State 1}}warn{{else if eq .State 2}}bad{{else}}unknown{{end}}">{{.State}}</chip> + {{if .Acknowledgement}}<chip class="info">Acknowledged</chip>{{end}} + </td> + </tr> + <tr><th>CheckCommand</th><td>{{.CheckCommand}}</td></tr> + <tr><th>Raw command</th><td><code>{{.LastCheckResult.RawCommand}}</code></td></tr> +</table> + +<figure> + <pre>{{.LastCheckResult.Output}}</pre> +</figure> +{{end}} +\ No newline at end of file diff --git a/cmd/checkweb/services.tmpl b/cmd/checkweb/services.tmpl @@ -0,0 +1,10 @@ +{{define "title"}}Services{{end}} + +{{define "content"}} +{{range .}} + <div class="f-row justify-content:space-between"> + <a href="/objects/services/{{pathescape .Name}}">{{.DisplayName}} ({{.Name}})</a> + <chip class="{{if eq .State 0}}ok{{else if eq .State 1}}warn{{else if eq .State 2}}bad{{else}}unknown{{end}}">{{.State}}</chip> + </div> +{{end}} +{{end}} +\ No newline at end of file