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
102 }
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))
107 }
109 type request struct {
110 r *http.Request
111 cmd string
112 args []string
113 }
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,
126 }
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)
187 }
189 // Content handlers.
191 func index(_ request, s *Server) (template.HTML, error) {
192 return "<section><h1>Welcome to the System Database.</h1></section>", nil
193 }
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)
206 }
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)
222 }
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)
252 }
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
260 }
262 func html(s string) template.HTML {
263 return template.HTML(template.HTMLEscapeString(s))
264 }
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 }
313 }
315 // vim: set tw=78 sw=4 sw=4 noexpandtab :