Code

Improved error reporting in graph(): correctly report bad requests.
[sysdb/webui.git] / server / server.go
index 995f2558c7bd118854ba6db28e0f9c5fb63db2f3..6b3f8d07a57e5ae715fb828df75cb1f4c556332c 100644 (file)
@@ -32,20 +32,21 @@ import (
        "fmt"
        "html/template"
        "io"
-       "log"
        "net/http"
+       "net/url"
        "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
+       // 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
@@ -56,19 +57,32 @@ type Config struct {
 
 // A Server implements an http.Handler that serves the SysDB user interface.
 type Server struct {
-       c *client.Conn
+       conns chan *client.Conn
+
+       // Request multiplexer
+       mux map[string]handler
 
        // Templates:
        main    *template.Template
        results map[string]*template.Template
 
-       // Static content:
-       static http.Handler
+       // Base directory of static files.
+       basedir string
 }
 
 // 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)}
+       if len(cfg.Conns) == 0 {
+               return nil, errors.New("need at least one client connection")
+       }
+
+       s := &Server{
+               conns:   make(chan *client.Conn, len(cfg.Conns)),
+               results: make(map[string]*template.Template),
+       }
+       for _, c := range cfg.Conns {
+               s.conns <- c
+       }
 
        var err error
        s.main, err = cfg.parse("main.tmpl")
@@ -84,7 +98,12 @@ func New(cfg Config) (*Server, error) {
                }
        }
 
-       s.static = http.FileServer(http.Dir(cfg.StaticPath))
+       s.basedir = cfg.StaticPath
+       s.mux = map[string]handler{
+               "images": s.static,
+               "style":  s.static,
+               "graph":  s.graph,
+       }
        return s, nil
 }
 
@@ -99,7 +118,16 @@ type request struct {
        args []string
 }
 
-var handlers = map[string]func(request, *Server) (template.HTML, error){
+type handler func(http.ResponseWriter, request)
+
+type page struct {
+       Title   string
+       Query   string
+       Content template.HTML
+}
+
+// Content generators for HTML pages.
+var content = map[string]func(request, *Server) (*page, error){
        "": index,
 
        // Queries
@@ -115,15 +143,18 @@ var handlers = map[string]func(request, *Server) (template.HTML, error){
 // 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
+       path := r.RequestURI
        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
+       var fields []string
+       for _, f := range strings.Split(path, "/") {
+               f, err := url.QueryUnescape(f)
+               if err != nil {
+                       s.badrequest(w, fmt.Errorf("Error: %v", err))
+                       return
+               }
+               fields = append(fields, f)
        }
 
        req := request{
@@ -140,30 +171,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                }
        }
 
-       f, ok := handlers[req.cmd]
+       if h := s.mux[fields[0]]; h != nil {
+               h(w, req)
+               return
+       }
+
+       f, ok := content[req.cmd]
        if !ok {
                s.notfound(w, r)
                return
        }
        r.ParseForm()
-       content, err := f(req, s)
+       page, err := f(req, s)
        if err != nil {
-               s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
+               s.badrequest(w, 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,
+       page.Query = r.FormValue("query")
+       if page.Title == "" {
+               page.Title = "SysDB - The System Database"
        }
 
        var buf bytes.Buffer
-       err = s.main.Execute(&buf, &page)
+       err = s.main.Execute(&buf, page)
        if err != nil {
                s.internal(w, err)
                return
@@ -173,127 +204,25 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        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)
+// static serves static content.
+func (s *Server) static(w http.ResponseWriter, req request) {
+       http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
 }
 
-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 index(_ request, s *Server) (*page, error) {
+       return &page{Content: "<section><h1>Welcome to the System Database.</h1></section>"}, nil
 }
 
-func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
+func tmpl(t *template.Template, data interface{}) (*page, error) {
        var buf bytes.Buffer
        if err := t.Execute(&buf, data); err != nil {
-               return "", fmt.Errorf("Template error: %v", err)
+               return nil, fmt.Errorf("Template error: %v", err)
        }
-       return template.HTML(buf.String()), nil
+       return &page{Content: 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 :