Code

Make sure to not use a client connection multiple times in parallel.
[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         // Templates:
65         main    *template.Template
66         results map[string]*template.Template
68         // Static content:
69         static http.Handler
70 }
72 // New constructs a new SysDB web server using the specified configuration.
73 func New(cfg Config) (*Server, error) {
74         if len(cfg.Conns) == 0 {
75                 return nil, errors.New("need at least one client connection")
76         }
78         s := &Server{
79                 conns:   make(chan *client.Conn, len(cfg.Conns)),
80                 results: make(map[string]*template.Template),
81         }
82         for _, c := range cfg.Conns {
83                 s.conns <- c
84         }
86         var err error
87         s.main, err = cfg.parse("main.tmpl")
88         if err != nil {
89                 return nil, err
90         }
92         types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
93         for _, t := range types {
94                 s.results[t], err = cfg.parse(t + ".tmpl")
95                 if err != nil {
96                         return nil, err
97                 }
98         }
100         s.static = http.FileServer(http.Dir(cfg.StaticPath))
101         return s, nil
104 func (cfg Config) parse(name string) (*template.Template, error) {
105         t := template.New(filepath.Base(name))
106         return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
109 type request struct {
110         r    *http.Request
111         cmd  string
112         args []string
115 var handlers = map[string]func(request, *Server) (template.HTML, error){
116         "": index,
118         // Queries
119         "host":     fetch,
120         "service":  fetch,
121         "metric":   fetch,
122         "hosts":    listAll,
123         "services": listAll,
124         "metrics":  listAll,
125         "lookup":   lookup,
128 // ServeHTTP implements the http.Handler interface and serves
129 // the SysDB user interface.
130 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
131         path := r.URL.Path
132         if len(path) > 0 && path[0] == '/' {
133                 path = path[1:]
134         }
135         fields := strings.Split(path, "/")
137         if fields[0] == "style" || fields[0] == "images" {
138                 s.static.ServeHTTP(w, r)
139                 return
140         }
142         req := request{
143                 r:   r,
144                 cmd: fields[0],
145         }
146         if len(fields) > 1 {
147                 if fields[len(fields)-1] == "" {
148                         // Slash at the end of the URL
149                         fields = fields[:len(fields)-1]
150                 }
151                 if len(fields) > 1 {
152                         req.args = fields[1:]
153                 }
154         }
156         f, ok := handlers[req.cmd]
157         if !ok {
158                 s.notfound(w, r)
159                 return
160         }
161         r.ParseForm()
162         content, err := f(req, s)
163         if err != nil {
164                 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
165                 return
166         }
168         page := struct {
169                 Title   string
170                 Query   string
171                 Content template.HTML
172         }{
173                 Title:   "SysDB - The System Database",
174                 Query:   r.FormValue("query"),
175                 Content: content,
176         }
178         var buf bytes.Buffer
179         err = s.main.Execute(&buf, &page)
180         if err != nil {
181                 s.internal(w, err)
182                 return
183         }
185         w.WriteHeader(http.StatusOK)
186         io.Copy(w, &buf)
189 // Content handlers.
191 func index(_ request, s *Server) (template.HTML, error) {
192         return "<section><h1>Welcome to the System Database.</h1></section>", nil
195 func listAll(req request, s *Server) (template.HTML, error) {
196         if len(req.args) != 0 {
197                 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
198         }
200         res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
201         if err != nil {
202                 return "", err
203         }
204         // the template *must* exist
205         return tmpl(s.results[req.cmd], res)
208 func lookup(req request, s *Server) (template.HTML, error) {
209         if req.r.Method != "POST" {
210                 return "", errors.New("Method not allowed")
211         }
212         q := proto.EscapeString(req.r.FormValue("query"))
213         if q == "''" {
214                 return "", errors.New("Empty query")
215         }
217         res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
218         if err != nil {
219                 return "", err
220         }
221         return tmpl(s.results["hosts"], res)
224 func fetch(req request, s *Server) (template.HTML, error) {
225         if len(req.args) == 0 {
226                 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
227         }
229         var q string
230         switch req.cmd {
231         case "host":
232                 if len(req.args) != 1 {
233                         return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
234                 }
235                 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
236         case "service", "metric":
237                 if len(req.args) < 2 {
238                         return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
239                 }
240                 host := proto.EscapeString(req.args[0])
241                 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
242                 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
243         default:
244                 panic("Unknown request: fetch(" + req.cmd + ")")
245         }
247         res, err := s.query(q)
248         if err != nil {
249                 return "", err
250         }
251         return tmpl(s.results[req.cmd], res)
254 func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
255         var buf bytes.Buffer
256         if err := t.Execute(&buf, data); err != nil {
257                 return "", fmt.Errorf("Template error: %v", err)
258         }
259         return template.HTML(buf.String()), nil
262 func html(s string) template.HTML {
263         return template.HTML(template.HTMLEscapeString(s))
266 func (s *Server) query(cmd string) (interface{}, error) {
267         c := <-s.conns
268         defer func() { s.conns <- c }()
270         m := &proto.Message{
271                 Type: proto.ConnectionQuery,
272                 Raw:  []byte(cmd),
273         }
274         if err := c.Send(m); err != nil {
275                 return nil, fmt.Errorf("Query %q: %v", cmd, err)
276         }
278         for {
279                 m, err := c.Receive()
280                 if err != nil {
281                         return nil, fmt.Errorf("Failed to receive server response: %v", err)
282                 }
283                 if m.Type == proto.ConnectionLog {
284                         log.Println(string(m.Raw[4:]))
285                         continue
286                 } else if m.Type == proto.ConnectionError {
287                         return nil, errors.New(string(m.Raw))
288                 }
290                 t, err := m.DataType()
291                 if err != nil {
292                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
293                 }
295                 var res interface{}
296                 switch t {
297                 case proto.HostList:
298                         var hosts []sysdb.Host
299                         err = proto.Unmarshal(m, &hosts)
300                         res = hosts
301                 case proto.Host:
302                         var host sysdb.Host
303                         err = proto.Unmarshal(m, &host)
304                         res = host
305                 default:
306                         return nil, fmt.Errorf("Unsupported data type %d", t)
307                 }
308                 if err != nil {
309                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
310                 }
311                 return res, nil
312         }
315 // vim: set tw=78 sw=4 sw=4 noexpandtab :