Merge branch 'master' into sr/pf
[collectd.git] / contrib / collection3 / bin / index.cgi
1 #!/usr/bin/perl
2
3 # Copyright (C) 2008-2011  Florian Forster
4 # Copyright (C) 2011       noris network AG
5 #
6 # This program is free software; you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free Software
8 # Foundation; only version 2 of the License is applicable.
9 #
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU General Public License along with
16 # this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 #
19 # Authors:
20 #   Florian "octo" Forster <octo at collectd.org>
21
22 use strict;
23 use warnings;
24 use utf8;
25 use vars (qw($BASE_DIR));
26
27 BEGIN
28 {
29   if (defined $ENV{'SCRIPT_FILENAME'})
30   {
31     if ($ENV{'SCRIPT_FILENAME'} =~ m{^(/.+)/bin/[^/]+$})
32     {
33       $::BASE_DIR = $1;
34       unshift (@::INC, "$::BASE_DIR/lib");
35     }
36   }
37 }
38
39 use Carp (qw(cluck confess));
40 use CGI (':cgi');
41 use CGI::Carp ('fatalsToBrowser');
42 use HTML::Entities ('encode_entities');
43
44 use Data::Dumper;
45
46 use Collectd::Graph::Config (qw(gc_read_config gc_get_scalar));
47 use Collectd::Graph::TypeLoader (qw(tl_load_type));
48 use Collectd::Graph::Common (qw(get_files_from_directory get_all_hosts
49       get_timespan_selection get_selected_files get_host_selection
50       get_plugin_selection flush_files));
51 use Collectd::Graph::Type ();
52
53 our $TimeSpans =
54 {
55   Hour  =>        3600,
56   Day   =>       86400,
57   Week  =>   7 * 86400,
58   Month =>  31 * 86400,
59   Year  => 366 * 86400
60 };
61
62 my %Actions =
63 (
64   list_hosts => \&action_list_hosts,
65   show_selection => \&action_show_selection
66 );
67
68 sub base_dir
69 {
70   if (defined $::BASE_DIR)
71   {
72     return ($::BASE_DIR);
73   }
74
75   if (!defined ($ENV{'SCRIPT_FILENAME'}))
76   {
77     return;
78   }
79
80   if ($ENV{'SCRIPT_FILENAME'} =~ m{^(/.+)/bin/[^/]+$})
81   {
82     $::BASE_DIR = $1;
83     return ($::BASE_DIR);
84   }
85
86   return;
87 }
88
89 sub lib_dir
90 {
91   my $base = base_dir ();
92
93   if ($base)
94   {
95     return "$base/lib";
96   }
97   else
98   {
99     return "../lib";
100   }
101 }
102
103 sub sysconf_dir
104 {
105   my $base = base_dir ();
106
107   if ($base)
108   {
109     return "$base/etc";
110   }
111   else
112   {
113     return "../etc";
114   }
115 }
116
117 sub init
118 {
119   my $lib_dir = lib_dir ();
120   my $sysconf_dir = sysconf_dir ();
121
122   if (!grep { $lib_dir eq $_ } (@::INC))
123   {
124     unshift (@::INC, $lib_dir);
125   }
126
127   gc_read_config ("$sysconf_dir/collection.conf");
128 }
129
130 sub main
131 {
132   my $Debug = param ('debug') ? 1 : 0;
133   my $action = param ('action') || 'list_hosts';
134
135   if (!exists ($Actions{$action}))
136   {
137     print STDERR "No such action: $action\n";
138     return (1);
139   }
140
141   init ();
142
143   $Actions{$action}->();
144   return (1);
145 } # sub main
146
147 sub can_handle_xhtml
148 {
149   my %types = ();
150
151   if (!defined $ENV{'HTTP_ACCEPT'})
152   {
153     return;
154   }
155
156   for (split (',', $ENV{'HTTP_ACCEPT'}))
157   {
158     my $type = lc ($_);
159     my $q = 1.0;
160
161     if ($type =~ m#^([^;]+);q=([0-9\.]+)$#)
162     {
163       $type = $1;
164       $q = 0.0 + $2;
165     }
166     $types{$type} = $q;
167   }
168
169   if (!defined ($types{'application/xhtml+xml'}))
170   {
171     return;
172   }
173   elsif (!defined ($types{'text/html'}))
174   {
175     return (1);
176   }
177   elsif ($types{'application/xhtml+xml'} < $types{'text/html'})
178   {
179     return;
180   }
181   else
182   {
183     return (1);
184   }
185 } # can_handle_xhtml
186
187 my $html_started;
188 sub start_html
189 {
190   return if ($html_started);
191
192   my $end;
193   my $begin;
194   my $timespan;
195
196   $end = time ();
197   $timespan = get_timespan_selection ();
198   $begin = $end - $timespan;
199
200   if (can_handle_xhtml ())
201   {
202     print header (-Content_Type => 'application/xhtml+xml; charset=UTF-8');
203     print <<HTML;
204 <?xml version="1.0" encoding="UTF-8"?>
205 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
206     "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
207 <html xmlns="http://www.w3.org/1999/xhtml"
208     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
209     xsi:schemaLocation="http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd"
210     xml:lang="en">
211 HTML
212   }
213   else
214   {
215     print header (-Content_Type => 'text/html; charset=UTF-8');
216     print <<HTML;
217 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
218     "http://www.w3.org/TR/html4/strict.dtd">
219 <html>
220 HTML
221   }
222   print <<HTML;
223   <head>
224     <title>collection.cgi, Version 3</title>
225     <link rel="icon" href="../share/shortcut-icon.png" type="image/png" />
226     <link rel="stylesheet" href="../share/style.css" type="text/css" />
227     <script type="text/javascript" src="../share/navigate.js"></script>
228   </head>
229   <body onload="nav_init ($begin, $end);">
230 HTML
231   $html_started = 1;
232 }
233
234 sub end_html
235 {
236   print <<HTML;
237   </body>
238 </html>
239 HTML
240   $html_started = 0;
241 }
242
243 sub contains_invalid_chars
244 {
245   my $str = shift;
246
247   for (split (m//, $str))
248   {
249     my $n = ord ($_);
250
251     # Whitespace is allowed.
252     if (($n >= 9) && ($n <= 13))
253     {
254       next;
255     }
256     elsif ($n < 32)
257     {
258       return (1);
259     }
260   }
261
262   return;
263 }
264
265 sub contains_invalid_chars
266 {
267   my $str = shift;
268
269   for (split (m//, $str))
270   {
271     my $n = ord ($_);
272
273     # Whitespace is allowed.
274     if (($n >= 9) && ($n <= 13))
275     {
276       next;
277     }
278     elsif ($n < 32)
279     {
280       return (1);
281     }
282   }
283
284   return;
285 }
286
287 sub show_selector
288 {
289   my $timespan_selection = get_timespan_selection ();
290   my $host_selection = get_host_selection ();
291   my $plugin_selection = get_plugin_selection ();
292
293   print <<HTML;
294     <form action="${\script_name ()}" method="get">
295       <fieldset>
296         <legend>Data selection</legend>
297         <select name="hostname" multiple="multiple" size="15">
298 HTML
299   for (sort (keys %$host_selection))
300   {
301     next if contains_invalid_chars ($_);
302     my $host = encode_entities ($_);
303     my $selected = $host_selection->{$_}
304     ? ' selected="selected"'
305     : '';
306     print qq#          <option value="$host"$selected>$host</option>\n#;
307   }
308   print <<HTML;
309         </select>
310         <select name="plugin" multiple="multiple" size="15">
311 HTML
312   for (sort (keys %$plugin_selection))
313   {
314     next if contains_invalid_chars ($_);
315     my $plugin = encode_entities ($_);
316     my $selected = $plugin_selection->{$_}
317     ? ' selected="selected"'
318     : '';
319     print qq#          <option value="$plugin"$selected>$plugin</option>\n#;
320   }
321   print <<HTML;
322         </select>
323         <select name="timespan">
324 HTML
325   for (sort { $TimeSpans->{$a} <=> $TimeSpans->{$b} } (keys (%$TimeSpans)))
326   {
327     next if contains_invalid_chars ($_);
328     my $name = encode_entities ($_);
329     my $value = $TimeSpans->{$_};
330     my $selected = ($value == $timespan_selection)
331     ? ' selected="selected"'
332     : '';
333     print qq#          <option value="$value"$selected>$name</option>\n#;
334   }
335   print <<HTML;
336         </select>
337         <input type="hidden" name="action" value="show_selection" />
338         <input type="submit" name="ok_button" value="OK" />
339       </fieldset>
340     </form>
341 HTML
342 } # show_selector
343
344 sub action_list_hosts
345 {
346   start_html ();
347   show_selector ();
348
349   my @hosts = get_all_hosts ();
350   print "    <ul>\n";
351   for (sort @hosts)
352   {
353     my $url = encode_entities (script_name () . "?action=show_selection;hostname=$_");
354     next if contains_invalid_chars ($_);
355     my $name = encode_entities ($_);
356     print qq#      <li><a href="$url">$name</a></li>\n#;
357   }
358   print "    </ul>\n";
359
360   end_html ();
361 } # action_list_hosts
362
363 =head1 MODULE LOADING
364
365 This script makes use of the various B<Collectd::Graph::Type::*> modules. If a
366 file like C<foo.rrd> is encountered it tries to load the
367 B<Collectd::Graph::Type::Foo> module and, if that fails, falls back to the
368 B<Collectd::Graph::Type> base class.
369
370 If you want to create a specialized graph for a certain type, you have to
371 create a new module which inherits from the B<Collectd::Graph::Type> base
372 class. A description of provided (and used) methods can be found in the inline
373 documentation of the B<Collectd::Graph::Type> module.
374
375 There are other, more specialized, "abstract" classes that possibly better fit
376 your need. Unfortunately they are not yet documented.
377
378 =over 4
379
380 =item B<Collectd::Graph::Type::GenericStacked>
381
382 Specialized class that groups files by their plugin instance and stacks them on
383 top of each other. Example types that inherit from this class are
384 B<Collectd::Graph::Type::Cpu> and B<Collectd::Graph::Type::Memory>.
385
386 =item B<Collectd::Graph::Type::GenericIO>
387
388 Specialized class for input/output graphs. This class can only handle files
389 with exactly two data sources, input and output. Example types that inherit
390 from this class are B<Collectd::Graph::Type::DiskOctets> and
391 B<Collectd::Graph::Type::IfOctets>.
392
393 =back
394
395 =cut
396
397 sub action_show_selection
398 {
399   start_html ();
400   show_selector ();
401
402   my $all_files;
403   my $timespan;
404
405   my $types = {};
406
407   my $id_counter = 0;
408
409   $all_files = get_selected_files ();
410   $timespan = get_timespan_selection ();
411
412   if (param ('debug'))
413   {
414     print "<pre>", Data::Dumper->Dump ([$all_files], ['all_files']), "</pre>\n";
415   }
416
417   # Send FLUSH command to the daemon if necessary and possible.
418   flush_files ($all_files,
419       begin => time () - $timespan,
420       end => time (),
421       addr => gc_get_scalar ('UnixSockAddr', undef),
422       interval => gc_get_scalar ('Interval', 10));
423
424   for (@$all_files)
425   {
426     my $file = $_;
427     my $type = ucfirst (lc ($file->{'type'}));
428
429     $type =~ s/[^A-Za-z0-9_]//g;
430     $type =~ s/_([A-Za-z0-9])/\U$1\E/g;
431
432     if (!defined ($types->{$type}))
433     {
434       $types->{$type} = tl_load_type ($file->{'type'});
435       if (!$types->{$type})
436       {
437         warn ("tl_load_type (" . $file->{'type'} . ") failed");
438         next;
439       }
440     }
441
442     $types->{$type}->addFiles ($file);
443   }
444 #print STDOUT Data::Dumper->Dump ([$types], ['types']);
445
446   print qq#    <table>\n#;
447   for (sort (keys %$types))
448   {
449     my $type = $_;
450
451     if (!defined ($types->{$type}))
452     {
453       next;
454     }
455
456     my $graphs_num = $types->{$type}->getGraphsNum ();
457
458     for (my $i = 0; $i < $graphs_num; $i++)
459     {
460       my $args = $types->{$type}->getGraphArgs ($i);
461       my $url = encode_entities ("graph.cgi?$args;begin=-$timespan");
462       my $id = sprintf ("graph%04i", $id_counter++);
463
464       print "      <tr>\n";
465       print "        <td rowspan=\"$graphs_num\">$type</td>\n" if ($i == 0);
466       print <<EOF;
467         <td>
468           <div class="graph_canvas">
469             <div class="graph_float">
470               <img id="${id}" class="graph_image"
471                 alt="A graph"
472                 src="$url" />
473               <div class="controls zoom">
474                 <div title="Earlier"
475                   onclick="nav_move_earlier ('${id}');">&#x2190;</div>
476                 <div title="Zoom out"
477                   onclick="nav_zoom_out ('${id}');">-</div>
478                 <div title="Zoom in"
479                   onclick="nav_zoom_in ('${id}');">+</div>
480                 <div title="Later"
481                   onclick="nav_move_later ('${id}');">&#x2192;</div>
482               </div>
483               <div class="controls preset">
484                 <div title="Show current hour"
485                   onclick="nav_time_reset ('${id}', 3600);">H</div>
486                 <div title="Show current day"
487                   onclick="nav_time_reset ('${id}', 86400);">D</div>
488                 <div title="Show current week"
489                   onclick="nav_time_reset ('${id}', 7 * 86400);">W</div>
490                 <div title="Show current month"
491                   onclick="nav_time_reset ('${id}', 31 * 86400);">M</div>
492                 <div title="Show current year"
493                   onclick="nav_time_reset ('${id}', 366 * 86400);">Y</div>
494                 <div title="Set all images to this timespan"
495                   onclick="nav_set_reference ('${id}');">!</div>
496               </div>
497             </div>
498           </div>
499         </td>
500 EOF
501       # print qq#        <td><img src="$url" /></td>\n#;
502       print "      </tr>\n";
503     }
504   }
505
506   print "    </table>\n";
507   end_html ();
508 }
509
510 main ();
511
512 =head1 SEE ALSO
513
514 L<Collectd::Graph::Type>
515
516 =head1 AUTHOR AND LICENSE
517
518 Copyright (c) 2008 by Florian Forster
519 E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
520 General Public License, VersionE<nbsp>2 (GPLv2).
521
522 =cut
523
524 # vim: set shiftwidth=2 softtabstop=2 tabstop=8 :