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
110 }
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))
115 }
117 type request struct {
118 r *http.Request
119 cmd string
120 args []string
121 }
123 type handler func(http.ResponseWriter, request)
125 // Content generators for HTML pages.
126 var content = map[string]func(request, *Server) (template.HTML, error){
127 "": index,
129 // Queries
130 "host": fetch,
131 "service": fetch,
132 "metric": fetch,
133 "hosts": listAll,
134 "services": listAll,
135 "metrics": listAll,
136 "lookup": lookup,
137 }
139 // ServeHTTP implements the http.Handler interface and serves
140 // the SysDB user interface.
141 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
142 path := r.URL.Path
143 if len(path) > 0 && path[0] == '/' {
144 path = path[1:]
145 }
146 fields := strings.Split(path, "/")
148 req := request{
149 r: r,
150 cmd: fields[0],
151 }
152 if len(fields) > 1 {
153 if fields[len(fields)-1] == "" {
154 // Slash at the end of the URL
155 fields = fields[:len(fields)-1]
156 }
157 if len(fields) > 1 {
158 req.args = fields[1:]
159 }
160 }
162 if h := s.mux[fields[0]]; h != nil {
163 h(w, req)
164 return
165 }
167 f, ok := content[req.cmd]
168 if !ok {
169 s.notfound(w, r)
170 return
171 }
172 r.ParseForm()
173 content, err := f(req, s)
174 if err != nil {
175 s.err(w, http.StatusBadRequest, fmt.Errorf("Error: %v", err))
176 return
177 }
179 page := struct {
180 Title string
181 Query string
182 Content template.HTML
183 }{
184 Title: "SysDB - The System Database",
185 Query: r.FormValue("query"),
186 Content: content,
187 }
189 var buf bytes.Buffer
190 err = s.main.Execute(&buf, &page)
191 if err != nil {
192 s.internal(w, err)
193 return
194 }
196 w.WriteHeader(http.StatusOK)
197 io.Copy(w, &buf)
198 }
200 // static serves static content.
201 func (s *Server) static(w http.ResponseWriter, req request) {
202 http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
203 }
205 // Content handlers.
207 func index(_ request, s *Server) (template.HTML, error) {
208 return "<section><h1>Welcome to the System Database.</h1></section>", nil
209 }
211 func listAll(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 res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
217 if err != nil {
218 return "", err
219 }
220 // the template *must* exist
221 return tmpl(s.results[req.cmd], res)
222 }
224 func lookup(req request, s *Server) (template.HTML, error) {
225 if req.r.Method != "POST" {
226 return "", errors.New("Method not allowed")
227 }
228 q := proto.EscapeString(req.r.FormValue("query"))
229 if q == "''" {
230 return "", errors.New("Empty query")
231 }
233 res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
234 if err != nil {
235 return "", err
236 }
237 return tmpl(s.results["hosts"], res)
238 }
240 func fetch(req request, s *Server) (template.HTML, error) {
241 if len(req.args) == 0 {
242 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
243 }
245 var q string
246 switch req.cmd {
247 case "host":
248 if len(req.args) != 1 {
249 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
250 }
251 q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
252 case "service", "metric":
253 if len(req.args) < 2 {
254 return "", fmt.Errorf("%s not found", strings.Title(req.cmd))
255 }
256 host := proto.EscapeString(req.args[0])
257 name := proto.EscapeString(strings.Join(req.args[1:], "/"))
258 q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
259 default:
260 panic("Unknown request: fetch(" + req.cmd + ")")
261 }
263 res, err := s.query(q)
264 if err != nil {
265 return "", err
266 }
267 return tmpl(s.results[req.cmd], res)
268 }
270 func tmpl(t *template.Template, data interface{}) (template.HTML, error) {
271 var buf bytes.Buffer
272 if err := t.Execute(&buf, data); err != nil {
273 return "", fmt.Errorf("Template error: %v", err)
274 }
275 return template.HTML(buf.String()), nil
276 }
278 func html(s string) template.HTML {
279 return template.HTML(template.HTMLEscapeString(s))
280 }
282 func (s *Server) query(cmd string) (interface{}, error) {
283 c := <-s.conns
284 defer func() { s.conns <- c }()
286 m := &proto.Message{
287 Type: proto.ConnectionQuery,
288 Raw: []byte(cmd),
289 }
290 if err := c.Send(m); err != nil {
291 return nil, fmt.Errorf("Query %q: %v", cmd, err)
292 }
294 for {
295 m, err := c.Receive()
296 if err != nil {
297 return nil, fmt.Errorf("Failed to receive server response: %v", err)
298 }
299 if m.Type == proto.ConnectionLog {
300 log.Println(string(m.Raw[4:]))
301 continue
302 } else if m.Type == proto.ConnectionError {
303 return nil, errors.New(string(m.Raw))
304 }
306 t, err := m.DataType()
307 if err != nil {
308 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
309 }
311 var res interface{}
312 switch t {
313 case proto.HostList:
314 var hosts []sysdb.Host
315 err = proto.Unmarshal(m, &hosts)
316 res = hosts
317 case proto.Host:
318 var host sysdb.Host
319 err = proto.Unmarshal(m, &host)
320 res = host
321 case proto.Timeseries:
322 var ts sysdb.Timeseries
323 err = proto.Unmarshal(m, &ts)
324 res = ts
325 default:
326 return nil, fmt.Errorf("Unsupported data type %d", t)
327 }
328 if err != nil {
329 return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
330 }
331 return res, nil
332 }
333 }
335 // vim: set tw=78 sw=4 sw=4 noexpandtab :