author | Sebastian Harl <sh@tokkee.org> | |
Thu, 20 Nov 2014 22:19:18 +0000 (23:19 +0100) | ||
committer | Sebastian Harl <sh@tokkee.org> | |
Thu, 20 Nov 2014 22:19:18 +0000 (23:19 +0100) |
This is the initial version of a web-based client programm for querying SysDB
(https://sysdb.io/). For now, it supports browsing, querying, and displaying
basic information about hosts, services, and metrics.
(https://sysdb.io/). For now, it supports browsing, querying, and displaying
basic information about hosts, services, and metrics.
15 files changed:
COPYING | [new file with mode: 0644] | patch | blob |
README | [new file with mode: 0644] | patch | blob |
main.go | [new file with mode: 0644] | patch | blob |
server/error.go | [new file with mode: 0644] | patch | blob |
server/server.go | [new file with mode: 0644] | patch | blob |
static/images/favicon.png | [new file with mode: 0644] | patch | blob |
static/images/owl.png | [new file with mode: 0644] | patch | blob |
static/style/main.css | [new file with mode: 0644] | patch | blob |
templates/host.tmpl | [new file with mode: 0644] | patch | blob |
templates/hosts.tmpl | [new file with mode: 0644] | patch | blob |
templates/main.tmpl | [new file with mode: 0644] | patch | blob |
templates/metric.tmpl | [new file with mode: 0644] | patch | blob |
templates/metrics.tmpl | [new file with mode: 0644] | patch | blob |
templates/service.tmpl | [new file with mode: 0644] | patch | blob |
templates/services.tmpl | [new file with mode: 0644] | patch | blob |
diff --git a/COPYING b/COPYING
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,24 @@
+Copyright (c) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/README b/README
--- /dev/null
+++ b/README
@@ -0,0 +1,99 @@
+ Web User-Interface for SysDB
+ ==============================
+
+ The SysDB web user-interface provides a graphical client application for
+ browsing and querying the system database.
+
+What is SysDB?
+--------------
+
+ “System DataBase” (SysDB) is a multi-backend system management and inventory
+ collection service. It stores system and inventory information about
+ hardware and software systems. This information is (continuously) collected
+ from various configurable backends (inventory services, monitoring services,
+ etc.) and stored in a graph-like hierarchy of generic objects. The store may
+ be queried through a generic interface independent of the active backends.
+ Object names are canonicalized before they are added to the store to ensure
+ a consistent view of your infrastructure.
+
+ The central object type is a host, which generally represents a physical or
+ virtual machine or any other type of physical resource. Hosts, in turn, may
+ reference a list of services which represent any kind of logical resource
+ like a software system. Both, hosts and services, may reference a list of
+ attributes which represent further information about the respective host or
+ service object. For example, attributes may specify static information like
+ a host's architecture or the software version. A host may also reference a
+ list of metrics which are references to performance data stored about the
+ host. SysDB supports querying the respective time-series from a backend's
+ data store.
+
+ SysDB is free and open source software, licensed under the 2-clause BSD
+ license. See COPYING for details.
+
+ <https://sysdb.io/>
+
+Install the web-interface
+-------------------------
+
+ The SysDB webui is written in Go. It can be installed along with all of its
+ dependencies as easy as running the following command:
+
+ go get github.com/sysdb/webui/...
+
+ This will download and install the packages and all of their dependencies
+ into GOPATH. See ‘go help get’ for more details.
+
+Running the web-interface
+-------------------------
+
+ The SysDB webui is a standalone web application. It can run all on its own
+ but it can also be put behind a reverse proxy (e.g. using Apache or nginx).
+ It is composed of a dynamic application and a set of static files (images
+ and style-sheets) which are shipped along with the source distribution. You
+ can start the application using the following command, using the --address
+ option to point it at a running SysDB daemon:
+
+ ./webui \
+ --address=/var/run/sysdbd.sock \
+ --listen=:8080 \
+ --static-path=$GOPATH/src/github.com/sysdb/webui/static \
+ --template-path=$GOPATH/src/github.com/sysdb/webui/templates
+
+ You can then access the interface by pointing your browser at
+ http://localhost:8080
+
+Packages
+--------
+
+ While the webui is a standalone application, most of its functionality is
+ implemented in a reusable library allowing to integrate it into other
+ software written in Go.
+
+ * github.com/sysdb/webui/server: The core of the SysDB web server.
+
+ It makes use of the following packages:
+
+ * github.com/sysdb/go/client (and related packages): A SysDB client
+ implementation.
+
+Documentation
+-------------
+
+ The documentation for all Go packages is included with the source code in a
+ way compatible with the godoc tool. As such, it can be viewed and browsed
+ online at <https://godoc.org/github.com/sysdb/webui>.
+
+Getting Help
+------------
+
+ Various channels for asynchronous and real-time communication with
+ developers and users are available. See <https://sysdb.io/contact/> for
+ details about the mailing list, IRC channel, and social media.
+
+Author
+------
+
+ Sebastian “tokkee” Harl <sh@tokkee.org>
+
+ Want to contribute? Check out the website <https://sysdb.io> for details.
+
diff --git a/main.go b/main.go
--- /dev/null
+++ b/main.go
@@ -0,0 +1,81 @@
+//
+// Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// webui is a web-based user-interface for SysDB.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/sysdb/go/client"
+ "github.com/sysdb/webui/server"
+)
+
+var (
+ addr = flag.String("address", "/var/run/sysdbd.sock", "SysDB server address")
+ user = flag.String("user", "sysdb", "SysDB user name")
+
+ listen = flag.String("listen", ":8080", "address to listen for incoming connections")
+ tmpl = flag.String("template-path", "templates", "location of template files")
+ static = flag.String("static-path", "static", "location of static files")
+)
+
+func main() {
+ flag.Parse()
+
+ log.Printf("Connecting to SysDB at %s.", *addr)
+ conn, err := client.Connect(*addr, *user)
+ if err != nil {
+ fatalf("Failed to connect to SysDB at %q: %v", *addr, err)
+ }
+
+ srv, err := server.New(server.Config{
+ Conn: conn,
+ TemplatePath: *tmpl,
+ StaticPath: *static,
+ })
+ if err != nil {
+ fatalf("Failed to construct web-server: %v", err)
+ }
+
+ log.Printf("Listening on %s.", *listen)
+ http.Handle("/", srv)
+ err = http.ListenAndServe(*listen, nil)
+ if err != nil {
+ fatalf("Failed to set up HTTP server on address %q: %v", *listen, err)
+ }
+}
+
+func fatalf(format string, a ...interface{}) {
+ fmt.Fprintf(os.Stderr, format, a...)
+ fmt.Fprintln(os.Stderr)
+ os.Exit(1)
+}
+
+// vim: set tw=78 sw=4 sw=4 noexpandtab :
diff --git a/server/error.go b/server/error.go
--- /dev/null
+++ b/server/error.go
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package server
+
+// Error handlers for the SysDB web interface.
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+)
+
+func (s *Server) notfound(w http.ResponseWriter, r *http.Request) {
+ s.err(w, http.StatusNotFound, fmt.Errorf("%s not found", r.RequestURI))
+}
+
+func (s *Server) internal(w http.ResponseWriter, err error) {
+ s.err(w, http.StatusInternalServerError, err)
+}
+
+func (s *Server) err(w http.ResponseWriter, status int, err error) {
+ log.Printf("%s: %v", http.StatusText(status), err)
+
+ page := struct {
+ Title string
+ Query string
+ Content template.HTML
+ }{
+ Title: "SysDB - Error",
+ Content: "<section class=\"error\">" + html(err.Error()) + "</section>",
+ }
+
+ var buf bytes.Buffer
+ err = s.main.Execute(&buf, &page)
+ if err != nil {
+ // Nothing more we can do about this.
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(status)
+ io.Copy(w, &buf)
+}
+
+// vim: set tw=78 sw=4 sw=4 noexpandtab :
diff --git a/server/server.go b/server/server.go
--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,299 @@
+//
+// Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Package server implements the core of the SysDB web server.
+package server
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/sysdb/go/client"
+ "github.com/sysdb/go/proto"
+ "github.com/sysdb/go/sysdb"
+)
+
+// A Config specifies configuration values for a SysDB web server.
+type Config struct {
+ // Conn specifies a connection to a SysDB server instance.
+ Conn *client.Conn
+
+ // TemplatePath specifies the relative or absolute location of template files.
+ TemplatePath string
+
+ // StaticPath specifies the relative or absolute location of static files.
+ StaticPath string
+}
+
+// A Server implements an http.Handler that serves the SysDB user interface.
+type Server struct {
+ c *client.Conn
+
+ // Templates:
+ main *template.Template
+ results map[string]*template.Template
+
+ // Static content:
+ static http.Handler
+}
+
+// New constructs a new SysDB web server using the specified configuration.
+func New(cfg Config) (*Server, error) {
+ s := &Server{c: cfg.Conn, results: make(map[string]*template.Template)}
+
+ var err error
+ s.main, err = cfg.parse("main.tmpl")
+ if err != nil {
+ return nil, err
+ }
+
+ types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
+ for _, t := range types {
+ s.results[t], err = cfg.parse(t + ".tmpl")
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ s.static = http.FileServer(http.Dir(cfg.StaticPath))
+ return s, nil
+}
+
+func (cfg Config) parse(name string) (*template.Template, error) {
+ t := template.New(filepath.Base(name))
+ return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
+}
+
+type request struct {
+ r *http.Request
+ cmd string
+ args []string
+}
+
+var handlers = map[string]func(request, *Server) (template.HTML, error){
+ "": index,
+
+ // Queries
+ "host": fetch,
+ "service": fetch,
+ "metric": fetch,
+ "hosts": listAll,
+ "services": listAll,
+ "metrics": listAll,
+ "lookup": lookup,
+}
+
+// ServeHTTP implements the http.Handler interface and serves
+// the SysDB user interface.
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+ if len(path) > 0 && path[0] == '/' {
+ path = path[1:]
+ }
+ fields := strings.Split(path, "/")
+
+ if fields[0] == "style" || fields[0] == "images" {
+ s.static.ServeHTTP(w, r)
+ return
+ }
+
+ req := request{
+ r: r,
+ cmd: fields[0],
+ }
+ if len(fields) > 1 {
+ if fields[len(fields)-1] == "" {
+ // Slash at the end of the URL
+ fields = fields[:len(fields)-1]
+ }
+ if len(fields) > 1 {
+ req.args = fields[1:]
+ }
+ }
+
+ f, ok := handlers[req.cmd]
+ if !ok {
+ s.notfound(w, r)
+ return
+ }
+ r.ParseForm()
+ content, err := f(req, s)
+ if err != nil {
+ s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
+ return
+ }
+
+ page := struct {
+ Title string
+ Query string
+ Content template.HTML
+ }{
+ Title: "SysDB - The System Database",
+ Query: r.FormValue("query"),
+ Content: content,
+ }
+
+ var buf bytes.Buffer
+ err = s.main.Execute(&buf, &page)
+ if err != nil {
+ s.internal(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ io.Copy(w, &buf)
+}
+
+// Content handlers.
+
+func index(_ request, s *Server) (template.HTML, error) {
+ return "<section><h1>Welcome to the System Database.</h1></section>", nil
+}
+
+func listAll(req request, s *Server) (template.HTML, error) {
+ if len(req.args) != 0 {
+ return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
+ }
+
+ res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
+ if err != nil {
+ return "", err
+ }
+ // the template *must* exist
+ return tmpl(s.results[req.cmd], res)
+}
+
+func lookup(req request, s *Server) (template.HTML, error) {
+ if req.r.Method != "POST" {
+ return "", errors.New("Method not allowed")
+ }
+ q := proto.EscapeString(req.r.FormValue("query"))
+ if q == "''" {
+ return "", errors.New("Empty query")
+ }
+
+ res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
+ if err != nil {
+ return "", err
+ }
+ return tmpl(s.results["hosts"], res)
+}
+
+func fetch(req request, s *Server) (template.HTML, error) {
+ if len(req.args) == 0 {
+ return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
+ }
+
+ var q string
+ switch req.cmd {
+ case "host":
+ if len(req.args) != 1 {
+ return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
+ }
+ q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
+ case "service", "metric":
+ if len(req.args) < 2 {
+ return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
+ }
+ host := proto.EscapeString(req.args[0])
+ name := proto.EscapeString(strings.Join(req.args[1:], "/"))
+ q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
+ default:
+ panic("Unknown request: fetch(" + req.cmd + ")")
+ }
+
+ res, err := s.query(q)
+ if err != nil {
+ return "", err
+ }
+ return tmpl(s.results[req.cmd], res)
+}
+
+func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
+ var buf bytes.Buffer
+ if err := t.Execute(&buf, data); err != nil {
+ return "", fmt.Errorf("Template error: %v", err)
+ }
+ return template.HTML(buf.String()), nil
+}
+
+func html(s string) template.HTML {
+ return template.HTML(template.HTMLEscapeString(s))
+}
+
+func (s *Server) query(cmd string) (interface{}, error) {
+ m := &proto.Message{
+ Type: proto.ConnectionQuery,
+ Raw: []byte(cmd),
+ }
+ if err := s.c.Send(m); err != nil {
+ return nil, fmt.Errorf("Query %q: %v", cmd, err)
+ }
+
+ for {
+ m, err := s.c.Receive()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to receive server response: %v", err)
+ }
+ if m.Type == proto.ConnectionLog {
+ log.Println(string(m.Raw[4:]))
+ continue
+ } else if m.Type == proto.ConnectionError {
+ return nil, errors.New(string(m.Raw))
+ }
+
+ t, err := m.DataType()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
+ }
+
+ var res interface{}
+ switch t {
+ case proto.HostList:
+ var hosts []sysdb.Host
+ err = proto.Unmarshal(m, &hosts)
+ res = hosts
+ case proto.Host:
+ var host sysdb.Host
+ err = proto.Unmarshal(m, &host)
+ res = host
+ default:
+ return nil, fmt.Errorf("Unsupported data type %d", t)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
+ }
+ return res, nil
+ }
+}
+
+// vim: set tw=78 sw=4 sw=4 noexpandtab :
diff --git a/static/images/favicon.png b/static/images/favicon.png
new file mode 100644 (file)
index 0000000..f56ba49
Binary files /dev/null and b/static/images/favicon.png differ
index 0000000..f56ba49
Binary files /dev/null and b/static/images/favicon.png differ
diff --git a/static/images/owl.png b/static/images/owl.png
new file mode 100644 (file)
index 0000000..bb54e88
Binary files /dev/null and b/static/images/owl.png differ
index 0000000..bb54e88
Binary files /dev/null and b/static/images/owl.png differ
diff --git a/static/style/main.css b/static/style/main.css
--- /dev/null
+++ b/static/style/main.css
@@ -0,0 +1,160 @@
+body {
+ font-family: sans-serif;
+ background-color: #f0f0f0;
+ margin: 0px;
+ padding: 0px;
+}
+
+a, a:link, a:active, a:visited, a:hover, a:focus, a:active {
+ text-decoration: none;
+ background-color: transparent;
+}
+
+iframe {
+ border: none;
+ overflow: hidden;
+}
+
+img {
+ border: none;
+}
+
+header {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+}
+
+div.topmenu {
+ background-color: #000;
+ color: #fff;
+ font-size: small;
+ height: 1em;
+ vertical-align: middle;
+ text-align: right;
+ margin: 0px;
+ padding: .7em 3em;
+}
+
+div.topmenu a, div.topmenu a:link, div.topmenu a:active, div.topmenu a:visited {
+ background-color: transparent;
+ color: #fff;
+ padding: 2px;
+}
+
+div.topmenu a:hover, div.topmenu a:focus, div.topmenu a:active {
+ background-color: #454545;
+ color: #fff;
+ padding: 2px;
+}
+
+div.searchbar {
+ background-color: #1e466d;
+ font-size: small;
+ color: #fff;
+ margin: 0px;
+ padding: 0px;
+ border-bottom: 1px solid #000;
+}
+
+div.logo {
+ padding: 5px 0px 5px 25px;
+ margin-top: -2.4em;
+ vertical-align: middle;
+ display: inline-block;
+ float: left;
+}
+
+div.searchbox {
+ margin-left: 152px;
+ padding: 0px 5px;
+ overflow: hidden;
+}
+
+div.searchbox input[type=text] {
+ width: 440px;
+ height: 25px;
+ border: 0px;
+ margin: 10px 0px;
+ padding: 0px 3px;
+}
+
+div.searchbox button {
+ background-color: #000;
+ color: #fff;
+ border: 0px;
+ width: 50px;
+ height: 25px;
+ margin: 0px;
+ padding: 0px;
+}
+
+div.main {
+ margin: 0px;
+ padding: 0px;
+ width: 100%;
+ overflow: hidden;
+}
+
+div.content {
+ margin-left: 152px;
+ padding: 10px 10px 0px 0px;
+ overflow: hidden;
+}
+
+div.content section {
+ background-color: #fff;
+ margin: 10px 0px;
+ border: 1px solid #000;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+ padding: 1em;
+}
+
+div.content section.error {
+ border: 1px solid #f00;
+}
+
+aside {
+ width: 142px;
+ float: left;
+ padding: 0px;
+ margin: 0x;
+}
+
+nav {
+ line-height: 1.5em;
+ padding: 25px 10px;
+}
+
+nav a, nav a:link, nav a:active, nav a:visited {
+ color: #000;
+ background-color: #cdcdcd;
+ padding: 3px;
+ margin: 5px 0px;
+ display: block;
+}
+
+nav a:hover, nav a:focus, nav a:active {
+ color: #000;
+ background-color: #ababab;
+ padding: 3px;
+ margin: 5px 0px;
+ display: block;
+}
+
+section h1 {
+ margin-top: 0px;
+ border-bottom: 1px solid #000;
+}
+
+table.results {
+ border-collapse: collapse;
+}
+
+table.results th, table.results td {
+ border: 1px solid #1e466d;
+ padding: 3px;
+ vertical-align: top;
+}
diff --git a/templates/host.tmpl b/templates/host.tmpl
--- /dev/null
+++ b/templates/host.tmpl
@@ -0,0 +1,33 @@
+<section>
+ <h1>Host {{.Name}}</h1>
+ <table class="results">
+ <tr><td><b>Last update</b></td><td>{{.LastUpdate}}</td></tr>
+ <tr><td><b>Update interval</b></td><td>{{.UpdateInterval}}</td></tr>
+ <tr><td><b>Backends</b></td><td>{{.Backends}}</td></tr>
+{{if len .Attributes}}
+ <tr><th colspan="2">Attributes</th></tr>
+ {{range .Attributes}}
+ <tr><td>{{.Name}}</td><td>{{.Value}}</td></tr>
+ {{end}}
+{{else}}
+ <tr><th colspan="2">No attributes</th></tr>
+{{end}}
+{{if len .Services}}
+ <tr><th colspan="2">Services</th></tr>
+ {{range .Services}}
+ <tr><td colspan="2"><a href="/service/{{urlquery $.Name}}/{{urlquery .Name}}">{{.Name}}</a></td></tr>
+ {{end}}
+{{else}}
+ <tr><th colspan="2">No services</th></tr>
+{{end}}
+{{if len .Metrics}}
+ <tr><th colspan="2">Metrics</th></tr>
+ {{range .Metrics}}
+ <tr><td colspan="2"><a href="/metric/{{urlquery $.Name}}/{{urlquery .Name}}">{{.Name}}</a></td></tr>
+ {{end}}
+{{else}}
+ <tr><th colspan="2">No Metrics</th></tr>
+{{end}}
+ </table>
+ <p> </p>
+</section>
diff --git a/templates/hosts.tmpl b/templates/hosts.tmpl
--- /dev/null
+++ b/templates/hosts.tmpl
@@ -0,0 +1,14 @@
+<section>
+ <h1>Hosts</h1>
+{{if len .}}
+ <table class="results">
+ <tr><th>Host</th><th>Last update</th></tr>
+ {{range .}}
+ <tr><td><a href="/host/{{urlquery .Name}}">{{.Name}}</a></td><td>{{.LastUpdate}}</td></tr>
+ {{end}}
+ </table>
+{{else}}
+ <p>No results found.</p>
+{{end}}
+ <p> </p>
+</section>
diff --git a/templates/main.tmpl b/templates/main.tmpl
--- /dev/null
+++ b/templates/main.tmpl
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ itemscope itemtype="http://schema.org/Product">
+<head>
+ <title>{{.Title}}</title>
+
+ <meta name="author" content="Copyright (C) 2014 Sebastian ‘tokkee’ Harl" />
+ <meta itemprop="name" content="SysDB">
+ <meta itemprop="description" content="The System Database">
+
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+
+ <link rel="stylesheet" href="/style/main.css" type="text/css" />
+ <link rel="icon" href="/images/favicon.png" type="images/png" />
+</head>
+
+<body>
+ <header>
+ <div class="topmenu">
+ <a href="https://sysdb.io">SysDB</a>
+ </div>
+ <div class="searchbar">
+ <div class="logo">
+ <a href="/"><img src="/images/owl.png" alt="[SysDB]" class="logo" /></a>
+ </div>
+
+ <div class="searchbox">
+ <form action="/lookup" method="POST">
+ <input type="text" name="query" value="{{.Query}}" placeholder="Search hosts"
+ required /><button type="submit">GO</button>
+ </form>
+ </div>
+ </div>
+ </header>
+
+ <div class="main">
+ <aside><nav>
+ <a href="/hosts">Hosts</a>
+ <a href="/services">Services</a>
+ <a href="/metrics">Metrics</a>
+ </nav></aside>
+
+ <div class="content">
+{{.Content}}
+ </div>
+ </div>
+</body>
+</html>
diff --git a/templates/metric.tmpl b/templates/metric.tmpl
--- /dev/null
+++ b/templates/metric.tmpl
@@ -0,0 +1,18 @@
+<section>{{$m := index .Metrics 0}}
+ <h1>Metric {{$.Name}} — {{$m.Name}}</h1>
+ <table class="results">
+ <tr><td><b>Host</b></td><td><a href="/host/{{urlquery $.Name}}">{{$.Name}}</a></td></tr>
+ <tr><td><b>Last update</b></td><td>{{$m.LastUpdate}}</td></tr>
+ <tr><td><b>Update interval</b></td><td>{{$m.UpdateInterval}}</td></tr>
+ <tr><td><b>Backends</b></td><td>{{$m.Backends}}</td></tr>
+{{if len $m.Attributes}}
+ <tr><th colspan="2">Attributes</th></tr>
+ {{range $m.Attributes}}
+ <tr><td>{{.Name}}</td><td>{{.Value}}</td></tr>
+ {{end}}
+{{else}}
+ <tr><th colspan="2">No attributes</th></tr>
+{{end}}
+ </table>
+ <p> </p>
+</section>
diff --git a/templates/metrics.tmpl b/templates/metrics.tmpl
--- /dev/null
+++ b/templates/metrics.tmpl
@@ -0,0 +1,18 @@
+<section>
+ <h1>Metrics</h1>
+{{if len .}}
+ <table class="results">
+ <tr><th>Host</th><th>Metric</th><th>Last update</th></tr>
+ {{range $h := .}}
+ {{range $i, $m := $h.Metrics}}
+ {{if not $i}}
+ <tr><td rowspan="{{len $h.Metrics}}"><a href="/host/{{urlquery $h.Name}}">{{$h.Name}}</a></td><td><a href="/metric/{{urlquery $h.Name}}/{{urlquery $m.Name}}">{{$m.Name}}</a></td><td>{{$m.LastUpdate}}</td>
+ {{else}}
+ <tr><td><a href="/metric/{{urlquery $h.Name}}/{{urlquery $m.Name}}">{{$m.Name}}</a></td><td>{{$m.LastUpdate}}</td></tr>
+ {{end}}{{end}}{{end}}
+ </table>
+{{else}}
+ <p>No results found.</p>
+{{end}}
+ <p> </p>
+</section>
diff --git a/templates/service.tmpl b/templates/service.tmpl
--- /dev/null
+++ b/templates/service.tmpl
@@ -0,0 +1,18 @@
+<section>{{$s := index .Services 0}}
+ <h1>Service {{$.Name}} — {{$s.Name}}</h1>
+ <table class="results">
+ <tr><td><b>Host</b></td><td><a href="/host/{{urlquery $.Name}}">{{$.Name}}</a></td></tr>
+ <tr><td><b>Last update</b></td><td>{{$s.LastUpdate}}</td></tr>
+ <tr><td><b>Update interval</b></td><td>{{$s.UpdateInterval}}</td></tr>
+ <tr><td><b>Backends</b></td><td>{{$s.Backends}}</td></tr>
+{{if len $s.Attributes}}
+ <tr><th colspan="2">Attributes</th></tr>
+ {{range $s.Attributes}}
+ <tr><td>{{.Name}}</td><td>{{.Value}}</td></tr>
+ {{end}}
+{{else}}
+ <tr><th colspan="2">No attributes</th></tr>
+{{end}}
+ </table>
+ <p> </p>
+</section>
diff --git a/templates/services.tmpl b/templates/services.tmpl
--- /dev/null
+++ b/templates/services.tmpl
@@ -0,0 +1,18 @@
+<section>
+ <h1>Services</h1>
+{{if len .}}
+ <table class="results">
+ <tr><th>Host</th><th>Service</th><th>Last update</th></tr>
+ {{range $h := .}}
+ {{range $i, $s := $h.Services}}
+ {{if not $i}}
+ <tr><td rowspan="{{len $h.Services}}"><a href="/host/{{urlquery $h.Name}}">{{$h.Name}}</a></td><td><a href="/service/{{urlquery $h.Name}}/{{urlquery $s.Name}}">{{$s.Name}}</a></td><td>{{$s.LastUpdate}}</td>
+ {{else}}
+ <tr><td><a href="/service/{{urlquery $h.Name}}/{{urlquery $s.Name}}">{{$s.Name}}</a></td><td>{{$s.LastUpdate}}</td></tr>
+ {{end}}{{end}}{{end}}
+ </table>
+{{else}}
+ <p>No results found.</p>
+{{end}}
+ <p> </p>
+</section>