Code

Initial commit of the SysDB web interface.
authorSebastian Harl <sh@tokkee.org>
Thu, 20 Nov 2014 22:19:18 +0000 (23:19 +0100)
committerSebastian 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.

15 files changed:
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
main.go [new file with mode: 0644]
server/error.go [new file with mode: 0644]
server/server.go [new file with mode: 0644]
static/images/favicon.png [new file with mode: 0644]
static/images/owl.png [new file with mode: 0644]
static/style/main.css [new file with mode: 0644]
templates/host.tmpl [new file with mode: 0644]
templates/hosts.tmpl [new file with mode: 0644]
templates/main.tmpl [new file with mode: 0644]
templates/metric.tmpl [new file with mode: 0644]
templates/metrics.tmpl [new file with mode: 0644]
templates/service.tmpl [new file with mode: 0644]
templates/services.tmpl [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..b7a1f1b
--- /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
new file mode 100644 (file)
index 0000000..6a49783
--- /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
new file mode 100644 (file)
index 0000000..9d099da
--- /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
new file mode 100644 (file)
index 0000000..49fec43
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..995f255
--- /dev/null
@@ -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
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
diff --git a/static/style/main.css b/static/style/main.css
new file mode 100644 (file)
index 0000000..d358e9b
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..6b56446
--- /dev/null
@@ -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>&nbsp;</p>
+</section>
diff --git a/templates/hosts.tmpl b/templates/hosts.tmpl
new file mode 100644 (file)
index 0000000..4eabaef
--- /dev/null
@@ -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>&nbsp;</p>
+</section>
diff --git a/templates/main.tmpl b/templates/main.tmpl
new file mode 100644 (file)
index 0000000..c48b223
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..e4a0efb
--- /dev/null
@@ -0,0 +1,18 @@
+<section>{{$m := index .Metrics 0}}
+       <h1>Metric {{$.Name}} &mdash; {{$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>&nbsp;</p>
+</section>
diff --git a/templates/metrics.tmpl b/templates/metrics.tmpl
new file mode 100644 (file)
index 0000000..88fe308
--- /dev/null
@@ -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>&nbsp;</p>
+</section>
diff --git a/templates/service.tmpl b/templates/service.tmpl
new file mode 100644 (file)
index 0000000..c5716da
--- /dev/null
@@ -0,0 +1,18 @@
+<section>{{$s := index .Services 0}}
+       <h1>Service {{$.Name}} &mdash; {{$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>&nbsp;</p>
+</section>
diff --git a/templates/services.tmpl b/templates/services.tmpl
new file mode 100644 (file)
index 0000000..151d239
--- /dev/null
@@ -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>&nbsp;</p>
+</section>