diff --git a/server/server.go b/server/server.go
index 706dac2492a86ab2882b1714d991571fef9f4094..8d0d72dc191770f6810896306ea3f0f3beca64b5 100644 (file)
--- a/server/server.go
+++ b/server/server.go
import (
"bytes"
- "errors"
"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 {
- // 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
// 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
// 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) {
- 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{results: make(map[string]*template.Template)}
- s := &Server{
- conns: make(chan *client.Conn, len(cfg.Conns)),
- results: make(map[string]*template.Template),
+ var err error
+ if s.c, err = client.Connect(addr, user); err != nil {
+ return nil, err
}
- for _, c := range cfg.Conns {
- s.conns <- c
+ if major, minor, patch, extra, err := s.c.ServerVersion(); err == nil {
+ log.Printf("Connected to SysDB %d.%d.%d%s.", major, minor, patch, extra)
}
- var err error
- s.main, err = cfg.parse("main.tmpl")
- if err != nil {
+ if s.main, err = cfg.parse("main.tmpl"); 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")
}
}
- 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
}
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
// 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
+ 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.badrequest(w, fmt.Errorf("Error: %v", err))
+ return
+ }
+ fields = append(fields, f)
}
req := 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)
+ 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 := struct {
- Title string
- Query string
- Content template.HTML
- }{
- Title: "SysDB - The System Database",
- Query: r.FormValue("query"),
- Content: content,
+ 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
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
+// 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 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))
+func index(_ request, s *Server) (*page, error) {
+ major, minor, patch, extra, err := s.c.ServerVersion()
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 + ")")
+ return nil, err
}
- res, err := s.query(q)
- if err != nil {
- return "", err
- }
- return tmpl(s.results[req.cmd], res)
+ 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{}) (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) {
- c := <-s.conns
- defer func() { s.conns <- c }()
-
- m := &proto.Message{
- Type: proto.ConnectionQuery,
- Raw: []byte(cmd),
- }
- if err := c.Send(m); err != nil {
- return nil, fmt.Errorf("Query %q: %v", cmd, err)
- }
-
- for {
- m, err := 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 :