Code

b7cd37a3c5ed3e3dc209cbd4a6010d0b7f2da622
[sysdb/webui.git] / server / server.go
1 //
2 // Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 // Package server implements the core of the SysDB web server.
27 package server
29 import (
30         "bytes"
31         "errors"
32         "fmt"
33         "html/template"
34         "io"
35         "log"
36         "net/http"
37         "path/filepath"
38         "strings"
40         "github.com/sysdb/go/client"
41         "github.com/sysdb/go/proto"
42         "github.com/sysdb/go/sysdb"
43 )
45 // A Config specifies configuration values for a SysDB web server.
46 type Config struct {
47         // Conns is a slice of connections to a SysDB server instance. The number of
48         // elements specifies the maximum number of parallel queries to the backend.
49         // Note that a client connection is not thread-safe but multiple idle
50         // connections don't impose any load on the server.
51         Conns []*client.Conn
53         // TemplatePath specifies the relative or absolute location of template files.
54         TemplatePath string
56         // StaticPath specifies the relative or absolute location of static files.
57         StaticPath string
58 }
60 // A Server implements an http.Handler that serves the SysDB user interface.
61 type Server struct {
62         conns chan *client.Conn
64         // Request multiplexer
65         mux map[string]handler
67         // Templates:
68         main    *template.Template
69         results map[string]*template.Template
71         // Base directory of static files.
72         basedir string
73 }
75 // New constructs a new SysDB web server using the specified configuration.
76 func New(cfg Config) (*Server, error) {
77         if len(cfg.Conns) == 0 {
78                 return nil, errors.New("need at least one client connection")
79         }
81         s := &Server{
82                 conns:   make(chan *client.Conn, len(cfg.Conns)),
83                 results: make(map[string]*template.Template),
84         }
85         for _, c := range cfg.Conns {
86                 s.conns <- c
87         }
89         var err error
90         s.main, err = cfg.parse("main.tmpl")
91         if err != nil {
92                 return nil, err
93         }
95         types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
96         for _, t := range types {
97                 s.results[t], err = cfg.parse(t + ".tmpl")
98                 if err != nil {
99                         return nil, err
100                 }
101         }
103         s.basedir = cfg.StaticPath
104         s.mux = map[string]handler{
105                 "images": s.static,
106                 "style":  s.static,
107                 "graph":  s.graph,
108         }
109         return s, nil
112 func (cfg Config) parse(name string) (*template.Template, error) {
113         t := template.New(filepath.Base(name))
114         return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
117 type request struct {
118         r    *http.Request
119         cmd  string
120         args []string
123 type handler func(http.ResponseWriter, request)
125 type page struct {
126         Title   string
127         Query   string
128         Content template.HTML
131 // Content generators for HTML pages.
132 var content = map[string]func(request, *Server) (*page, error){
133         "": index,
135         // Queries
136         "host":     fetch,
137         "service":  fetch,
138         "metric":   fetch,
139         "hosts":    listAll,
140         "services": listAll,
141         "metrics":  listAll,
142         "lookup":   lookup,
145 // ServeHTTP implements the http.Handler interface and serves
146 // the SysDB user interface.
147 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
148         path := r.URL.Path
149         if len(path) > 0 && path[0] == '/' {
150                 path = path[1:]
151         }
152         fields := strings.Split(path, "/")
154         req := request{
155                 r:   r,
156                 cmd: fields[0],
157         }
158         if len(fields) > 1 {
159                 if fields[len(fields)-1] == "" {
160                         // Slash at the end of the URL
161                         fields = fields[:len(fields)-1]
162                 }
163                 if len(fields) > 1 {
164                         req.args = fields[1:]
165                 }
166         }
168         if h := s.mux[fields[0]]; h != nil {
169                 h(w, req)
170                 return
171         }
173         f, ok := content[req.cmd]
174         if !ok {
175                 s.notfound(w, r)
176                 return
177         }
178         r.ParseForm()
179         page, err := f(req, s)
180         if err != nil {
181                 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
182                 return
183         }
185         page.Query = r.FormValue("query")
186         if page.Title == "" {
187                 page.Title = "SysDB - The System Database"
188         }
190         var buf bytes.Buffer
191         err = s.main.Execute(&buf, page)
192         if err != nil {
193                 s.internal(w, err)
194                 return
195         }
197         w.WriteHeader(http.StatusOK)
198         io.Copy(w, &buf)
201 // static serves static content.
202 func (s *Server) static(w http.ResponseWriter, req request) {
203         http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
206 // Content handlers.
208 func index(_ request, s *Server) (*page, error) {
209         return &page{Content: "<section><h1>Welcome to the System Database.</h1></section>"}, nil
212 func listAll(req request, s *Server) (*page, error) {
213         if len(req.args) != 0 {
214                 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
215         }
217         res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
218         if err != nil {
219                 return nil, err
220         }
221         // the template *must* exist
222         return tmpl(s.results[req.cmd], res)
225 func lookup(req request, s *Server) (*page, error) {
226         if req.r.Method != "POST" {
227                 return nil, errors.New("Method not allowed")
228         }
229         q := proto.EscapeString(req.r.FormValue("query"))
230         if q == "''" {
231                 return nil, errors.New("Empty query")
232         }
234         res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
235         if err != nil {
236                 return nil, err
237         }
238         return tmpl(s.results["hosts"], res)
241 func fetch(req request, s *Server) (*page, error) {
242         if len(req.args) == 0 {
243                 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
244         }
246         var q string
247         switch req.cmd {
248         case "host":
249                 if len(req.args) != 1 {
250                         return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
251                 }
252                 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
253         case "service", "metric":
254                 if len(req.args) < 2 {
255                         return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
256                 }
257                 host := proto.EscapeString(req.args[0])
258                 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
259                 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
260         default:
261                 panic("Unknown request: fetch(" + req.cmd + ")")
262         }
264         res, err := s.query(q)
265         if err != nil {
266                 return nil, err
267         }
268         return tmpl(s.results[req.cmd], res)
271 func tmpl(t *template.Template, data interface{}) (*page, error) {
272         var buf bytes.Buffer
273         if err := t.Execute(&buf, data); err != nil {
274                 return nil, fmt.Errorf("Template error: %v", err)
275         }
276         return &page{Content: template.HTML(buf.String())}, nil
279 func html(s string) template.HTML {
280         return template.HTML(template.HTMLEscapeString(s))
283 func (s *Server) query(cmd string) (interface{}, error) {
284         c := <-s.conns
285         defer func() { s.conns <- c }()
287         m := &proto.Message{
288                 Type: proto.ConnectionQuery,
289                 Raw:  []byte(cmd),
290         }
291         if err := c.Send(m); err != nil {
292                 return nil, fmt.Errorf("Query %q: %v", cmd, err)
293         }
295         for {
296                 m, err := c.Receive()
297                 if err != nil {
298                         return nil, fmt.Errorf("Failed to receive server response: %v", err)
299                 }
300                 if m.Type == proto.ConnectionLog {
301                         log.Println(string(m.Raw[4:]))
302                         continue
303                 } else if m.Type == proto.ConnectionError {
304                         return nil, errors.New(string(m.Raw))
305                 }
307                 t, err := m.DataType()
308                 if err != nil {
309                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
310                 }
312                 var res interface{}
313                 switch t {
314                 case proto.HostList:
315                         var hosts []sysdb.Host
316                         err = proto.Unmarshal(m, &hosts)
317                         res = hosts
318                 case proto.Host:
319                         var host sysdb.Host
320                         err = proto.Unmarshal(m, &host)
321                         res = host
322                 case proto.Timeseries:
323                         var ts sysdb.Timeseries
324                         err = proto.Unmarshal(m, &ts)
325                         res = ts
326                 default:
327                         return nil, fmt.Errorf("Unsupported data type %d", t)
328                 }
329                 if err != nil {
330                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
331                 }
332                 return res, nil
333         }
336 // vim: set tw=78 sw=4 sw=4 noexpandtab :