b7cd37a3c5ed3e3dc209cbd4a6010d0b7f2da622
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 type page struct {
126 Title string
127 Query string
128 Content template.HTML
129 }
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,
143 }
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)
199 }
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)))
204 }
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
210 }
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)
223 }
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)
239 }
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)
269 }
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
277 }
279 func html(s string) template.HTML {
280 return template.HTML(template.HTMLEscapeString(s))
281 }
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 }
334 }
336 // vim: set tw=78 sw=4 sw=4 noexpandtab :