Code

995f2558c7bd118854ba6db28e0f9c5fb63db2f3
[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         // Conn specifies a connection to a SysDB server instance.
48         Conn *client.Conn
50         // TemplatePath specifies the relative or absolute location of template files.
51         TemplatePath string
53         // StaticPath specifies the relative or absolute location of static files.
54         StaticPath string
55 }
57 // A Server implements an http.Handler that serves the SysDB user interface.
58 type Server struct {
59         c *client.Conn
61         // Templates:
62         main    *template.Template
63         results map[string]*template.Template
65         // Static content:
66         static http.Handler
67 }
69 // New constructs a new SysDB web server using the specified configuration.
70 func New(cfg Config) (*Server, error) {
71         s := &Server{c: cfg.Conn, results: make(map[string]*template.Template)}
73         var err error
74         s.main, err = cfg.parse("main.tmpl")
75         if err != nil {
76                 return nil, err
77         }
79         types := []string{"host", "hosts", "service", "services", "metric", "metrics"}
80         for _, t := range types {
81                 s.results[t], err = cfg.parse(t + ".tmpl")
82                 if err != nil {
83                         return nil, err
84                 }
85         }
87         s.static = http.FileServer(http.Dir(cfg.StaticPath))
88         return s, nil
89 }
91 func (cfg Config) parse(name string) (*template.Template, error) {
92         t := template.New(filepath.Base(name))
93         return t.ParseFiles(filepath.Join(cfg.TemplatePath, name))
94 }
96 type request struct {
97         r    *http.Request
98         cmd  string
99         args []string
102 var handlers = map[string]func(request, *Server) (template.HTML, error){
103         "": index,
105         // Queries
106         "host":     fetch,
107         "service":  fetch,
108         "metric":   fetch,
109         "hosts":    listAll,
110         "services": listAll,
111         "metrics":  listAll,
112         "lookup":   lookup,
115 // ServeHTTP implements the http.Handler interface and serves
116 // the SysDB user interface.
117 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118         path := r.URL.Path
119         if len(path) > 0 && path[0] == '/' {
120                 path = path[1:]
121         }
122         fields := strings.Split(path, "/")
124         if fields[0] == "style" || fields[0] == "images" {
125                 s.static.ServeHTTP(w, r)
126                 return
127         }
129         req := request{
130                 r:   r,
131                 cmd: fields[0],
132         }
133         if len(fields) > 1 {
134                 if fields[len(fields)-1] == "" {
135                         // Slash at the end of the URL
136                         fields = fields[:len(fields)-1]
137                 }
138                 if len(fields) > 1 {
139                         req.args = fields[1:]
140                 }
141         }
143         f, ok := handlers[req.cmd]
144         if !ok {
145                 s.notfound(w, r)
146                 return
147         }
148         r.ParseForm()
149         content, err := f(req, s)
150         if err != nil {
151                 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
152                 return
153         }
155         page := struct {
156                 Title   string
157                 Query   string
158                 Content template.HTML
159         }{
160                 Title:   "SysDB - The System Database",
161                 Query:   r.FormValue("query"),
162                 Content: content,
163         }
165         var buf bytes.Buffer
166         err = s.main.Execute(&buf, &page)
167         if err != nil {
168                 s.internal(w, err)
169                 return
170         }
172         w.WriteHeader(http.StatusOK)
173         io.Copy(w, &buf)
176 // Content handlers.
178 func index(_ request, s *Server) (template.HTML, error) {
179         return "<section><h1>Welcome to the System Database.</h1></section>", nil
182 func listAll(req request, s *Server) (template.HTML, error) {
183         if len(req.args) != 0 {
184                 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
185         }
187         res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
188         if err != nil {
189                 return "", err
190         }
191         // the template *must* exist
192         return tmpl(s.results[req.cmd], res)
195 func lookup(req request, s *Server) (template.HTML, error) {
196         if req.r.Method != "POST" {
197                 return "", errors.New("Method not allowed")
198         }
199         q := proto.EscapeString(req.r.FormValue("query"))
200         if q == "''" {
201                 return "", errors.New("Empty query")
202         }
204         res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
205         if err != nil {
206                 return "", err
207         }
208         return tmpl(s.results["hosts"], res)
211 func fetch(req request, s *Server) (template.HTML, error) {
212         if len(req.args) == 0 {
213                 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
214         }
216         var q string
217         switch req.cmd {
218         case "host":
219                 if len(req.args) != 1 {
220                         return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
221                 }
222                 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
223         case "service", "metric":
224                 if len(req.args) < 2 {
225                         return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
226                 }
227                 host := proto.EscapeString(req.args[0])
228                 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
229                 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
230         default:
231                 panic("Unknown request: fetch(" + req.cmd + ")")
232         }
234         res, err := s.query(q)
235         if err != nil {
236                 return "", err
237         }
238         return tmpl(s.results[req.cmd], res)
241 func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
242         var buf bytes.Buffer
243         if err := t.Execute(&buf, data); err != nil {
244                 return "", fmt.Errorf("Template error: %v", err)
245         }
246         return template.HTML(buf.String()), nil
249 func html(s string) template.HTML {
250         return template.HTML(template.HTMLEscapeString(s))
253 func (s *Server) query(cmd string) (interface{}, error) {
254         m := &proto.Message{
255                 Type: proto.ConnectionQuery,
256                 Raw:  []byte(cmd),
257         }
258         if err := s.c.Send(m); err != nil {
259                 return nil, fmt.Errorf("Query %q: %v", cmd, err)
260         }
262         for {
263                 m, err := s.c.Receive()
264                 if err != nil {
265                         return nil, fmt.Errorf("Failed to receive server response: %v", err)
266                 }
267                 if m.Type == proto.ConnectionLog {
268                         log.Println(string(m.Raw[4:]))
269                         continue
270                 } else if m.Type == proto.ConnectionError {
271                         return nil, errors.New(string(m.Raw))
272                 }
274                 t, err := m.DataType()
275                 if err != nil {
276                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
277                 }
279                 var res interface{}
280                 switch t {
281                 case proto.HostList:
282                         var hosts []sysdb.Host
283                         err = proto.Unmarshal(m, &hosts)
284                         res = hosts
285                 case proto.Host:
286                         var host sysdb.Host
287                         err = proto.Unmarshal(m, &host)
288                         res = host
289                 default:
290                         return nil, fmt.Errorf("Unsupported data type %d", t)
291                 }
292                 if err != nil {
293                         return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
294                 }
295                 return res, nil
296         }
299 // vim: set tw=78 sw=4 sw=4 noexpandtab :