commit - 410b374583e5172b215020f0ad3db35a8635fa59
commit + 6c821dbc0890acc0ce1ff7c6a98523aa7fca0573
blob - /dev/null
blob + 568f6b0030d0fc1695d6a5f3ffd8225c5436ad88 (mode 644)
--- /dev/null
+++ cmd/checkweb/base.tmpl
+<!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>
blob - /dev/null
blob + 314d44fcb0e30e753083d3401f3c1a27b8cb03d9 (mode 644)
--- /dev/null
+++ cmd/checkweb/checkweb.go
+// 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))
+}
blob - /dev/null
blob + 3b008cb06f94d3992744c6afc8be31bf29a2df13 (mode 644)
--- /dev/null
+++ cmd/checkweb/host.tmpl
+{{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
blob - /dev/null
blob + a43e5ac905d6484361b1c30eab9d6aa843944c04 (mode 644)
--- /dev/null
+++ cmd/checkweb/hosts.tmpl
+{{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
blob - /dev/null
blob + 9b44c1371627eb15c292e4ca330d0e4782cd8278 (mode 644)
--- /dev/null
+++ cmd/checkweb/service.tmpl
+{{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
blob - /dev/null
blob + fd955a512b993ae7acbeb46f745f15f0074f7ef5 (mode 644)
--- /dev/null
+++ cmd/checkweb/services.tmpl
+{{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