1 #!/usr/bin/perl
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>
22 use strict;
23 use warnings;
24 use utf8;
25 use vars (qw($BASE_DIR));
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 }
39 use Carp (qw(cluck confess));
40 use CGI (':cgi');
41 use CGI::Carp ('fatalsToBrowser');
42 use HTML::Entities ('encode_entities');
44 use Data::Dumper;
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 ();
53 our $TimeSpans =
54 {
55 Hour => 3600,
56 Day => 86400,
57 Week => 7 * 86400,
58 Month => 31 * 86400,
59 Year => 366 * 86400
60 };
62 my %Actions =
63 (
64 list_hosts => \&action_list_hosts,
65 show_selection => \&action_show_selection
66 );
68 sub base_dir
69 {
70 if (defined $::BASE_DIR)
71 {
72 return ($::BASE_DIR);
73 }
75 if (!defined ($ENV{'SCRIPT_FILENAME'}))
76 {
77 return;
78 }
80 if ($ENV{'SCRIPT_FILENAME'} =~ m{^(/.+)/bin/[^/]+$})
81 {
82 $::BASE_DIR = $1;
83 return ($::BASE_DIR);
84 }
86 return;
87 }
89 sub lib_dir
90 {
91 my $base = base_dir ();
93 if ($base)
94 {
95 return "$base/lib";
96 }
97 else
98 {
99 return "../lib";
100 }
101 }
103 sub sysconf_dir
104 {
105 my $base = base_dir ();
107 if ($base)
108 {
109 return "$base/etc";
110 }
111 else
112 {
113 return "../etc";
114 }
115 }
117 sub init
118 {
119 my $lib_dir = lib_dir ();
120 my $sysconf_dir = sysconf_dir ();
122 if (!grep { $lib_dir eq $_ } (@::INC))
123 {
124 unshift (@::INC, $lib_dir);
125 }
127 gc_read_config ("$sysconf_dir/collection.conf");
128 }
130 sub main
131 {
132 my $Debug = param ('debug') ? 1 : 0;
133 my $action = param ('action') || 'list_hosts';
135 if (!exists ($Actions{$action}))
136 {
137 print STDERR "No such action: $action\n";
138 return (1);
139 }
141 init ();
143 $Actions{$action}->();
144 return (1);
145 } # sub main
147 sub can_handle_xhtml
148 {
149 my %types = ();
151 if (!defined $ENV{'HTTP_ACCEPT'})
152 {
153 return;
154 }
156 for (split (',', $ENV{'HTTP_ACCEPT'}))
157 {
158 my $type = lc ($_);
159 my $q = 1.0;
161 if ($type =~ m#^([^;]+);q=([0-9\.]+)$#)
162 {
163 $type = $1;
164 $q = 0.0 + $2;
165 }
166 $types{$type} = $q;
167 }
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
187 my $html_started;
188 sub start_html
189 {
190 return if ($html_started);
192 my $end;
193 my $begin;
194 my $timespan;
196 $end = time ();
197 $timespan = get_timespan_selection ();
198 $begin = $end - $timespan;
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 }
234 sub end_html
235 {
236 print <<HTML;
237 </body>
238 </html>
239 HTML
240 $html_started = 0;
241 }
243 sub contains_invalid_chars
244 {
245 my $str = shift;
247 for (split (m//, $str))
248 {
249 my $n = ord ($_);
251 # Whitespace is allowed.
252 if (($n >= 9) && ($n <= 13))
253 {
254 next;
255 }
256 elsif ($n < 32)
257 {
258 return (1);
259 }
260 }
262 return;
263 }
265 sub show_selector
266 {
267 my $timespan_selection = get_timespan_selection ();
268 my $host_selection = get_host_selection ();
269 my $plugin_selection = get_plugin_selection ();
271 print <<HTML;
272 <form action="${\script_name ()}" method="get">
273 <fieldset>
274 <legend>Data selection</legend>
275 <select name="hostname" multiple="multiple" size="15">
276 HTML
277 for (sort (keys %$host_selection))
278 {
279 next if contains_invalid_chars ($_);
280 my $host = encode_entities ($_);
281 my $selected = $host_selection->{$_}
282 ? ' selected="selected"'
283 : '';
284 print qq# <option value="$host"$selected>$host</option>\n#;
285 }
286 print <<HTML;
287 </select>
288 <select name="plugin" multiple="multiple" size="15">
289 HTML
290 for (sort (keys %$plugin_selection))
291 {
292 next if contains_invalid_chars ($_);
293 my $plugin = encode_entities ($_);
294 my $selected = $plugin_selection->{$_}
295 ? ' selected="selected"'
296 : '';
297 print qq# <option value="$plugin"$selected>$plugin</option>\n#;
298 }
299 print <<HTML;
300 </select>
301 <select name="timespan">
302 HTML
303 for (sort { $TimeSpans->{$a} <=> $TimeSpans->{$b} } (keys (%$TimeSpans)))
304 {
305 next if contains_invalid_chars ($_);
306 my $name = encode_entities ($_);
307 my $value = $TimeSpans->{$_};
308 my $selected = ($value == $timespan_selection)
309 ? ' selected="selected"'
310 : '';
311 print qq# <option value="$value"$selected>$name</option>\n#;
312 }
313 print <<HTML;
314 </select>
315 <input type="hidden" name="action" value="show_selection" />
316 <input type="submit" name="ok_button" value="OK" />
317 </fieldset>
318 </form>
319 HTML
320 } # show_selector
322 sub action_list_hosts
323 {
324 start_html ();
325 show_selector ();
327 my @hosts = get_all_hosts ();
328 print " <ul>\n";
329 for (sort @hosts)
330 {
331 my $url = encode_entities (script_name () . "?action=show_selection;hostname=$_");
332 next if contains_invalid_chars ($_);
333 my $name = encode_entities ($_);
334 print qq# <li><a href="$url">$name</a></li>\n#;
335 }
336 print " </ul>\n";
338 end_html ();
339 } # action_list_hosts
341 =head1 MODULE LOADING
343 This script makes use of the various B<Collectd::Graph::Type::*> modules. If a
344 file like C<foo.rrd> is encountered it tries to load the
345 B<Collectd::Graph::Type::Foo> module and, if that fails, falls back to the
346 B<Collectd::Graph::Type> base class.
348 If you want to create a specialized graph for a certain type, you have to
349 create a new module which inherits from the B<Collectd::Graph::Type> base
350 class. A description of provided (and used) methods can be found in the inline
351 documentation of the B<Collectd::Graph::Type> module.
353 There are other, more specialized, "abstract" classes that possibly better fit
354 your need. Unfortunately they are not yet documented.
356 =over 4
358 =item B<Collectd::Graph::Type::GenericStacked>
360 Specialized class that groups files by their plugin instance and stacks them on
361 top of each other. Example types that inherit from this class are
362 B<Collectd::Graph::Type::Cpu> and B<Collectd::Graph::Type::Memory>.
364 =item B<Collectd::Graph::Type::GenericIO>
366 Specialized class for input/output graphs. This class can only handle files
367 with exactly two data sources, input and output. Example types that inherit
368 from this class are B<Collectd::Graph::Type::DiskOctets> and
369 B<Collectd::Graph::Type::IfOctets>.
371 =back
373 =cut
375 sub action_show_selection
376 {
377 start_html ();
378 show_selector ();
380 my $all_files;
381 my $timespan;
383 my $types = {};
385 my $id_counter = 0;
387 $all_files = get_selected_files ();
388 $timespan = get_timespan_selection ();
390 if (param ('debug'))
391 {
392 print "<pre>", Data::Dumper->Dump ([$all_files], ['all_files']), "</pre>\n";
393 }
395 # Send FLUSH command to the daemon if necessary and possible.
396 flush_files ($all_files,
397 begin => time () - $timespan,
398 end => time (),
399 addr => gc_get_scalar ('UnixSockAddr', undef),
400 interval => gc_get_scalar ('Interval', 10));
402 for (@$all_files)
403 {
404 my $file = $_;
405 my $type = ucfirst (lc ($file->{'type'}));
407 $type =~ s/[^A-Za-z0-9_]//g;
408 $type =~ s/_([A-Za-z0-9])/\U$1\E/g;
410 if (!defined ($types->{$type}))
411 {
412 $types->{$type} = tl_load_type ($file->{'type'});
413 if (!$types->{$type})
414 {
415 warn ("tl_load_type (" . $file->{'type'} . ") failed");
416 next;
417 }
418 }
420 $types->{$type}->addFiles ($file);
421 }
422 #print STDOUT Data::Dumper->Dump ([$types], ['types']);
424 print qq# <table>\n#;
425 for (sort (keys %$types))
426 {
427 my $type = $_;
429 if (!defined ($types->{$type}))
430 {
431 next;
432 }
434 my $graphs_num = $types->{$type}->getGraphsNum ();
436 for (my $i = 0; $i < $graphs_num; $i++)
437 {
438 my $args = $types->{$type}->getGraphArgs ($i);
439 my $url = encode_entities ("graph.cgi?$args;begin=-$timespan");
440 my $id = sprintf ("graph%04i", $id_counter++);
442 print " <tr>\n";
443 print " <td rowspan=\"$graphs_num\">$type</td>\n" if ($i == 0);
444 print <<EOF;
445 <td>
446 <div class="graph_canvas">
447 <div class="graph_float">
448 <img id="${id}" class="graph_image"
449 alt="A graph"
450 src="$url" />
451 <div class="controls zoom">
452 <div title="Earlier"
453 onclick="nav_move_earlier ('${id}');">←</div>
454 <div title="Zoom out"
455 onclick="nav_zoom_out ('${id}');">-</div>
456 <div title="Zoom in"
457 onclick="nav_zoom_in ('${id}');">+</div>
458 <div title="Later"
459 onclick="nav_move_later ('${id}');">→</div>
460 </div>
461 <div class="controls preset">
462 <div title="Show current hour"
463 onclick="nav_time_reset ('${id}', 3600);">H</div>
464 <div title="Show current day"
465 onclick="nav_time_reset ('${id}', 86400);">D</div>
466 <div title="Show current week"
467 onclick="nav_time_reset ('${id}', 7 * 86400);">W</div>
468 <div title="Show current month"
469 onclick="nav_time_reset ('${id}', 31 * 86400);">M</div>
470 <div title="Show current year"
471 onclick="nav_time_reset ('${id}', 366 * 86400);">Y</div>
472 <div title="Set all images to this timespan"
473 onclick="nav_set_reference ('${id}');">!</div>
474 </div>
475 </div>
476 </div>
477 </td>
478 EOF
479 # print qq# <td><img src="$url" /></td>\n#;
480 print " </tr>\n";
481 }
482 }
484 print " </table>\n";
485 end_html ();
486 }
488 main ();
490 =head1 SEE ALSO
492 L<Collectd::Graph::Type>
494 =head1 AUTHOR AND LICENSE
496 Copyright (c) 2008 by Florian Forster
497 E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
498 General Public License, VersionE<nbsp>2 (GPLv2).
500 =cut
502 # vim: set shiftwidth=2 softtabstop=2 tabstop=8 :