Code

Add support for variable root mount points.
[sysdb/webui.git] / server / server.go
index 309cdb6b35f86541dc528b6f6df742cf30d51f39..fb5ba225a194dc3f5c94cebe6010994047961ae4 100644 (file)
@@ -28,10 +28,10 @@ package server
 
 import (
        "bytes"
-       "errors"
        "fmt"
        "html/template"
        "io"
+       "log"
        "net/http"
        "net/url"
        "path/filepath"
@@ -42,22 +42,19 @@ import (
 
 // A Config specifies configuration values for a SysDB web server.
 type Config struct {
-       // Conns is a slice of connections to a SysDB server instance. The number of
-       // elements specifies the maximum number of parallel queries to the backend.
-       // Note that a client connection is not thread-safe but multiple idle
-       // connections don't impose any load on the server.
-       Conns []*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
+
+       // Root mount point of the server.
+       Root string
 }
 
 // A Server implements an http.Handler that serves the SysDB user interface.
 type Server struct {
-       conns chan *client.Conn
+       c *client.Client
 
        // Request multiplexer
        mux map[string]handler
@@ -68,37 +65,41 @@ type Server struct {
 
        // Base directory of static files.
        basedir string
+
+       // Root mount point.
+       root string
 }
 
 // New constructs a new SysDB web server using the specified configuration.
-func New(cfg Config) (*Server, error) {
-       if len(cfg.Conns) == 0 {
-               return nil, errors.New("need at least one client connection")
-       }
-
+func New(addr, user string, cfg Config) (*Server, error) {
        s := &Server{
-               conns:   make(chan *client.Conn, len(cfg.Conns)),
                results: make(map[string]*template.Template),
+               basedir: cfg.StaticPath,
+               root:    cfg.Root,
        }
-       for _, c := range cfg.Conns {
-               s.conns <- c
+       if s.root == "" {
+               s.root = "/"
        }
 
        var err error
-       s.main, err = cfg.parse("main.tmpl")
-       if err != nil {
+       if s.c, err = client.Connect(addr, user); err != nil {
                return nil, err
        }
+       if major, minor, patch, extra, err := s.c.ServerVersion(); err == nil {
+               log.Printf("Connected to SysDB %d.%d.%d%s.", major, minor, patch, extra)
+       }
 
-       types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
+       if s.main, err = cfg.parse(s, "main.tmpl"); err != nil {
+               return nil, err
+       }
+       types := []string{"graphs", "host", "hosts", "service", "services", "metric", "metrics"}
        for _, t := range types {
-               s.results[t], err = cfg.parse(t + ".tmpl")
+               s.results[t], err = cfg.parse(s, t+".tmpl")
                if err != nil {
                        return nil, err
                }
        }
 
-       s.basedir = cfg.StaticPath
        s.mux = map[string]handler{
                "images": s.static,
                "style":  s.static,
@@ -107,8 +108,10 @@ func New(cfg Config) (*Server, error) {
        return s, nil
 }
 
-func (cfg Config) parse(name string) (*template.Template, error) {
-       t := template.New(filepath.Base(name))
+func (cfg Config) parse(s *Server, name string) (*template.Template, error) {
+       t := template.New(filepath.Base(name)).Funcs(template.FuncMap{
+               "root": s.Root,
+       })
        return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
 }
 
@@ -131,6 +134,7 @@ var content = map[string]func(request, *Server) (*page, error){
        "": index,
 
        // Queries
+       "graphs":   graphs,
        "host":     fetch,
        "service":  fetch,
        "metric":   fetch,
@@ -144,14 +148,24 @@ var content = map[string]func(request, *Server) (*page, error){
 // the SysDB user interface.
 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        path := r.RequestURI
+       if !strings.HasPrefix(path, s.root) {
+               s.notfound(w, r)
+               return
+       }
+       path = strings.TrimPrefix(path, s.root)
+       r.URL.Path = strings.TrimPrefix(r.URL.Path, s.root)
+
        if len(path) > 0 && path[0] == '/' {
                path = path[1:]
        }
+       if idx := strings.Index(path, "?"); idx != -1 {
+               path = path[:idx]
+       }
        var fields []string
        for _, f := range strings.Split(path, "/") {
                f, err := url.QueryUnescape(f)
                if err != nil {
-                       s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
+                       s.badrequest(w, fmt.Errorf("Error: %v", err))
                        return
                }
                fields = append(fields, f)
@@ -182,19 +196,22 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                return
        }
        r.ParseForm()
-       page, err := f(req, s)
+       p, err := f(req, s)
        if err != nil {
-               s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
-               return
+               p = &page{
+                       Content: "<section class=\"error\">" +
+                               html(fmt.Sprintf("Error: %v", err)) +
+                               "</section>",
+               }
        }
 
-       page.Query = r.FormValue("query")
-       if page.Title == "" {
-               page.Title = "SysDB - The System Database"
+       p.Query = r.FormValue("query")
+       if p.Title == "" {
+               p.Title = "SysDB - The System Database"
        }
 
        var buf bytes.Buffer
-       err = s.main.Execute(&buf, page)
+       err = s.main.Execute(&buf, p)
        if err != nil {
                s.internal(w, err)
                return
@@ -209,8 +226,26 @@ func (s *Server) static(w http.ResponseWriter, req request) {
        http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
 }
 
+// Root returns the root mount point of the server suitable for use as a path
+// prefix.
+func (s *Server) Root() string {
+       if s.root[len(s.root)-1] == '/' {
+               return s.root
+       }
+       return s.root + "/"
+}
+
 func index(_ request, s *Server) (*page, error) {
-       return &page{Content: "<section><h1>Welcome to the System Database.</h1></section>"}, nil
+       major, minor, patch, extra, err := s.c.ServerVersion()
+       if err != nil {
+               return nil, err
+       }
+
+       content := fmt.Sprintf("<section>"+
+               "<h1>Welcome to the System Database.</h1>"+
+               "<p>Connected to SysDB %d.%d.%d%s</p>"+
+               "</section>", major, minor, patch, html(extra))
+       return &page{Content: template.HTML(content)}, nil
 }
 
 func tmpl(t *template.Template, data interface{}) (*page, error) {