--- /dev/null
+ collection3 - Web frontend for collectd
+=========================================
+http://collectd.org/
+
+About
+-----
+
+ collection3 is a graphing front-end for the RRD files created by and filled
+ with collectd. It is written in Perl and should be run as an CGI-script.
+ Graphs are generated on-the-fly, so no cron job or similar is necessary.
+
+Layout
+------
+
+ The files used by collection3 are organized in a typical UNIX fashion: The
+ configuration resides in etc/, executable scripts are in bin/, supplementary
+ Perl modules are in lib/ and static data for displaying the web page are in
+ share/.
+
+ All files in all subdirectories except bin/ should NOT be executable.
+ Ideally, the webserver should not serve them either. Consider using
+ `.htaccess' files or other means to configure the web server to deny access
+ to these directories.
+
+Dependencies
+------------
+
+ collection3 depends on the following Perl modules not included in the Perl
+ distribution itself:
+
+ * Config::General
+ * HTML::Entities
+ * RRDs
+
+Copyright and License
+---------------------
+
+ Copyright (C) 2008 Florian octo Forster <octo at verplant.org>
+
+ collection3 is provided under the terms of the GNU General Public License,
+ version 2 (GPLv2).
+
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use lib ('../lib');
+
+use FindBin ('$RealBin');
+use Carp (qw(confess cluck));
+use CGI (':cgi');
+use RRDs ();
+
+use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type));
+
+use Collectd::Graph::Common (qw(sanitize_type get_selected_files
+ epoch_to_rfc1123));
+use Collectd::Graph::Type ();
+
+our $Debug = param ('debug');
+our $Begin = param ('begin');
+our $End = param ('end');
+
+if ($Debug)
+{
+ print <<HTTP;
+Content-Type: text/plain
+
+HTTP
+}
+
+tl_read_config ("$RealBin/../etc/collection3.conf");
+
+{ # Sanitize begin and end times
+ $End ||= 0;
+ $Begin ||= 0;
+
+ if ($End =~ m/\D/)
+ {
+ $End = 0;
+ }
+
+ if (!$Begin || !($Begin =~ m/^-?([1-9][0-9]*)$/))
+ {
+ $Begin = -86400;
+ }
+
+ if ($Begin < 0)
+ {
+ if ($End)
+ {
+ $Begin = $End + $Begin;
+ }
+ else
+ {
+ $Begin = time () + $Begin;
+ }
+ }
+
+ if ($Begin < 0)
+ {
+ $Begin = time () - 86400;
+ }
+
+ if (($End > 0) && ($Begin > $End))
+ {
+ my $temp = $End;
+ $End = $Begin;
+ $Begin = $temp;
+ }
+}
+
+my $type = param ('type') or die;
+my $obj;
+
+$obj = tl_load_type ($type);
+if (!$obj)
+{
+ confess ("tl_load_type ($type) failed");
+}
+
+$type = ucfirst (lc ($type));
+$type =~ s/_([A-Za-z])/\U$1\E/g;
+$type = sanitize_type ($type);
+
+my $files = get_selected_files ();
+if ($Debug)
+{
+ require Data::Dumper;
+ print STDOUT Data::Dumper->Dump ([$files], ['files']);
+}
+for (@$files)
+{
+ $obj->addFiles ($_);
+}
+
+my $expires = time ();
+if (($End == 0) || (($Begin <= $expires) && ($End >= $expires)))
+{
+ # 400 == width in pixels
+ my $timespan = $expires - $Begin;
+ $expires += int ($timespan / 400);
+}
+elsif (($End > 0) && ($End < $expires))
+{
+ $expires += 366 * 86400;
+}
+elsif ($Begin > $expires)
+{
+ $expires = $Begin;
+}
+
+print STDOUT header (-Content_type => 'image/png',
+ -Last_Modified => epoch_to_rfc1123 ($obj->getLastModified ()),
+ -Expires => epoch_to_rfc1123 ($expires));
+
+my $args = $obj->getRRDArgs (0);
+
+if ($Debug)
+{
+ require Data::Dumper;
+ print STDOUT Data::Dumper->Dump ([$obj], ['obj']);
+ print STDOUT join (",\n", @$args) . "\n";
+ print STDOUT "Last-Modified: " . epoch_to_rfc1123 ($obj->getLastModified ()) . "\n";
+}
+else
+{
+ my @timesel = ();
+
+ if ($End) # $Begin is always true
+ {
+ @timesel = ('-s', $Begin, '-e', $End);
+ }
+ else
+ {
+ @timesel = ('-s', $Begin); # End is implicitely `now'.
+ }
+
+ $| = 1;
+ RRDs::graph ('-', '-a', 'PNG', @timesel, @$args);
+ if (my $err = RRDs::error ())
+ {
+ print STDERR "RRDs::graph failed: $err\n";
+ exit (1);
+ }
+}
+
+exit (0);
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+#!/usr/bin/perl
+
+# Copyright (C) 2008 Florian octo Forster <octo at verplant.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; only version 2 of the License is applicable.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+use strict;
+use warnings;
+use lib ('../lib');
+use utf8;
+
+use FindBin ('$RealBin');
+use CGI (':cgi');
+use CGI::Carp ('fatalsToBrowser');
+use HTML::Entities ('encode_entities');
+
+use Data::Dumper;
+
+use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type));
+use Collectd::Graph::Common (qw(get_files_from_directory get_all_hosts
+ get_timespan_selection get_selected_files get_host_selection
+ get_plugin_selection));
+use Collectd::Graph::Type ();
+
+our $Debug = param ('debug') ? 1 : 0;
+
+our $TimeSpans =
+{
+ Hour => 3600,
+ Day => 86400,
+ Week => 7 * 86400,
+ Month => 31 * 86400,
+ Year => 366 * 86400
+};
+
+my $action = param ('action') || 'list_hosts';
+our %Actions =
+(
+ list_hosts => \&action_list_hosts,
+ show_selection => \&action_show_selection
+);
+
+if (!exists ($Actions{$action}))
+{
+ print STDERR "No such action: $action\n";
+ exit 1;
+}
+
+tl_read_config ("$RealBin/../etc/collection3.conf");
+
+$Actions{$action}->();
+exit (0);
+
+sub can_handle_xhtml
+{
+ my %types = ();
+
+ if (!defined $ENV{'HTTP_ACCEPT'})
+ {
+ return;
+ }
+
+ for (split (',', $ENV{'HTTP_ACCEPT'}))
+ {
+ my $type = lc ($_);
+ my $q = 1.0;
+
+ if ($type =~ m#^([^;]+);q=([0-9\.]+)$#)
+ {
+ $type = $1;
+ $q = 0.0 + $2;
+ }
+ $types{$type} = $q;
+ }
+
+ if (!defined ($types{'application/xhtml+xml'}))
+ {
+ return;
+ }
+ elsif (!defined ($types{'text/html'}))
+ {
+ return (1);
+ }
+ elsif ($types{'application/xhtml+xml'} < $types{'text/html'})
+ {
+ return;
+ }
+ else
+ {
+ return (1);
+ }
+} # can_handle_xhtml
+
+{my $html_started;
+sub start_html
+{
+ return if ($html_started);
+
+ if (can_handle_xhtml ())
+ {
+ print <<HTML;
+Content-Type: application/xhtml+xml; charset=UTF-8
+
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd"
+ xml:lang="en">
+HTML
+ }
+ else
+ {
+ print <<HTML;
+Content-Type: text/html; charset=UTF-8
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+HTML
+ }
+ print <<HTML;
+ <head>
+ <title>collection.cgi, Version 3</title>
+ <link rel="icon" href="../share/shortcut-icon.png" type="image/png" />
+ <link rel="stylesheet" href="../share/style.css" type="text/css" />
+ </head>
+ <body>
+HTML
+ $html_started = 1;
+}}
+
+sub end_html
+{
+ print <<HTML;
+ </body>
+</html>
+HTML
+}
+
+sub show_selector
+{
+ my $timespan_selection = get_timespan_selection ();
+ my $host_selection = get_host_selection ();
+ my $plugin_selection = get_plugin_selection ();
+
+ print <<HTML;
+ <form action="${\script_name ()}" method="get">
+ <fieldset>
+ <legend>Data selection</legend>
+ <select name="hostname" multiple="multiple" size="15">
+HTML
+ for (sort (keys %$host_selection))
+ {
+ my $host = encode_entities ($_);
+ my $selected = $host_selection->{$_}
+ ? ' selected="selected"'
+ : '';
+ print qq# <option value="$host"$selected>$host</option>\n#;
+ }
+ print <<HTML;
+ </select>
+ <select name="plugin" multiple="multiple" size="15">
+HTML
+ for (sort (keys %$plugin_selection))
+ {
+ my $plugin = encode_entities ($_);
+ my $selected = $plugin_selection->{$_}
+ ? ' selected="selected"'
+ : '';
+ print qq# <option value="$plugin"$selected>$plugin</option>\n#;
+ }
+ print <<HTML;
+ </select>
+ <select name="timespan">
+HTML
+ for (sort { $TimeSpans->{$a} <=> $TimeSpans->{$b} } (keys (%$TimeSpans)))
+ {
+ my $name = encode_entities ($_);
+ my $value = $TimeSpans->{$_};
+ my $selected = ($value == $timespan_selection)
+ ? ' selected="selected"'
+ : '';
+ print qq# <option value="$value"$selected>$name</option>\n#;
+ }
+ print <<HTML;
+ </select>
+ <input type="hidden" name="action" value="show_selection" />
+ <input type="submit" name="ok_button" value="OK" />
+ </fieldset>
+ </form>
+HTML
+} # show_selector
+
+sub action_list_hosts
+{
+ start_html ();
+ show_selector ();
+
+ my @hosts = get_all_hosts ();
+ print " <ul>\n";
+ for (sort @hosts)
+ {
+ my $url = encode_entities (script_name () . "?action=show_selection;hostname=$_");
+ my $name = encode_entities ($_);
+ print qq# <li><a href="$url">$name</a></li>\n#;
+ }
+ print " </ul>\n";
+
+ end_html ();
+} # action_list_hosts
+
+=head1 MODULE LOADING
+
+This script makes use of the various B<Collectd::Graph::Type::*> modules. If a
+file like C<foo.rrd> is encountered it tries to load the
+B<Collectd::Graph::Type::Foo> module and, if that fails, falls back to the
+B<Collectd::Graph::Type> base class.
+
+If you want to create a specialized graph for a certain type, you have to
+create a new module which inherits from the B<Collectd::Graph::Type> base
+class. A description of provided (and used) methods can be found in the inline
+documentation of the B<Collectd::Graph::Type> module.
+
+There are other, more specialized, "abstract" classes that possibly better fit
+your need. Unfortunately they are not yet documented.
+
+=over 4
+
+=item B<Collectd::Graph::Type::GenericStacked>
+
+Specialized class that groups files by their plugin instance and stacks them on
+top of each other. Example types that inherit from this class are
+B<Collectd::Graph::Type::Cpu> and B<Collectd::Graph::Type::Memory>.
+
+=item B<Collectd::Graph::Type::GenericIO>
+
+Specialized class for input/output graphs. This class can only handle files
+with exactly two data sources, input and output. Example types that inherit
+from this class are B<Collectd::Graph::Type::DiskOctets> and
+B<Collectd::Graph::Type::IfOctets>.
+
+=back
+
+=cut
+
+sub action_show_selection
+{
+ start_html ();
+ show_selector ();
+
+ my $ident = {};
+
+ my $all_files;
+ my $types = {};
+
+ $all_files = get_selected_files ();
+
+ if ($Debug)
+ {
+ print "<pre>", Data::Dumper->Dump ([$all_files], ['all_files']), "</pre>\n";
+ }
+
+ for (@$all_files)
+ {
+ my $file = $_;
+ my $type = ucfirst (lc ($file->{'type'}));
+
+ $type =~ s/[^A-Za-z_]//g;
+ $type =~ s/_([A-Za-z])/\U$1\E/g;
+
+ if (!defined ($types->{$type}))
+ {
+ $types->{$type} = tl_load_type ($file->{'type'});
+ if (!$types->{$type})
+ {
+ cluck ("tl_load_type (" . $file->{'type'} . ") failed");
+ next;
+ }
+ }
+
+ $types->{$type}->addFiles ($file);
+ }
+#print STDOUT Data::Dumper->Dump ([$types], ['types']);
+
+ print qq# <table>\n#;
+ for (sort (keys %$types))
+ {
+ my $type = $_;
+ my $graphs_num = $types->{$type}->getGraphsNum ();
+
+ my $timespan = get_timespan_selection ();
+
+ for (my $i = 0; $i < $graphs_num; $i++)
+ {
+ my $args = $types->{$type}->getGraphArgs ($i);
+ my $url = encode_entities ("graph.cgi?$args;begin=-$timespan");
+
+ print " <tr>\n";
+ print " <td rowspan=\"$graphs_num\">$type</td>\n" if ($i == 0);
+
+ print qq# <td><img src="$url" /></td>\n#;
+ print " </tr>\n";
+ }
+ }
+
+ print " </table>\n";
+ end_html ();
+}
+
+=head1 SEE ALSO
+
+L<Collectd::Graph::Type>
+
+=head1 AUTHOR AND LICENSE
+
+Copyright (c) 2008 by Florian Forster
+E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
+General Public License, VersionE<nbsp>2 (GPLv2).
+
+=cut
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+#!/usr/bin/perl
+
+# Copyright (C) 2008 Florian octo Forster <octo at verplant.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; only version 2 of the License is applicable.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+use strict;
+use warnings;
+use lib ('../lib');
+use utf8;
+
+use FindBin ('$RealBin');
+use CGI (':cgi');
+use CGI::Carp ('fatalsToBrowser');
+use URI::Escape ('uri_escape');
+
+use Data::Dumper;
+
+use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type));
+use Collectd::Graph::Common (qw(get_all_hosts get_files_for_host type_to_module_name));
+use Collectd::Graph::Type ();
+
+our $Debug = param ('debug') ? 1 : 0;
+
+tl_read_config ("$RealBin/../etc/collection3.conf");
+
+if ($Debug)
+{
+ print "Content-Type: text/plain; charset=utf-8\n\n";
+}
+else
+{
+ print "Content-Type: application/json; charset=utf-8\n\n";
+}
+
+print "{\n";
+
+my @hosts = get_all_hosts ();
+for (my $i = 0; $i < @hosts; $i++)
+{
+ my $host = $hosts[$i];
+ my $files = get_files_for_host ($host);
+ my %graphs = ();
+ my @graphs = ();
+
+ # Group files by graphs
+ for (@$files)
+ {
+ my $file = $_;
+ my $type = $file->{'type'};
+
+ # Create a new graph object if this is the first of this type.
+ if (!defined ($graphs{$type}))
+ {
+ $graphs{$type} = tl_load_type ($file->{'type'});
+ if (!$graphs{$type})
+ {
+ cluck ("tl_load_type (" . $file->{'type'} . ") failed");
+ next;
+ }
+ }
+
+ $graphs{$type}->addFiles ($file);
+ } # for (@$files)
+
+ print qq( "$host":\n {\n);
+
+ @graphs = keys %graphs;
+ for (my $j = 0; $j < @graphs; $j++)
+ {
+ my $type = $graphs[$j];
+ my $graphs_num = $graphs{$type}->getGraphsNum ();
+ my @args = ();
+
+ for (my $k = 0; $k < $graphs_num; $k++)
+ {
+ my $args = $graphs{$type}->getGraphArgs ($k);
+ my $url = 'http://' . $ENV{'SERVER_NAME'} . "/cgi-bin/graph.cgi?" . $args;
+ push (@args, $url);
+ }
+
+ print qq( "$type": [ )
+ . join (', ', map { qq("$_") } (@args));
+
+ if ($j == (@graphs - 1))
+ {
+ print qq( ]\n);
+ }
+ else
+ {
+ print qq( ],\n);
+ }
+ } # for (keys %graphs)
+
+ if ($i == (@hosts - 1))
+ {
+ print qq( }\n);
+ }
+ else
+ {
+ print qq( },\n\n);
+ }
+} # for (my $i = 0; $i < @hosts; $i++)
+
+print "}\n";
+
+exit (0);
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+Deny from all
--- /dev/null
+<Type cpu>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "CPU {plugin_instance} usage"
+ RRDVerticalLabel "Jiffies"
+ RRDFormat "%5.2lf"
+ DSName idle Idle
+ DSName nice Nice
+ DSName user User
+ DSName wait Wait-IO
+ DSName system System
+ DSName softirq SoftIRQ
+ DSName interrupt IRQ
+ DSName steal Steal
+ Order idle nice user wait system softirq interrupt steal
+ Color idle e8e8e8
+ Color nice 00e000
+ Color user 0000ff
+ Color wait ffb000
+ Color system ff0000
+ Color softirq ff00ff
+ Color interrupt a000a0
+ Color steal 000000
+</Type>
+<Type df>
+ Module Df
+ DataSources free used
+</Type>
+<Type disk_octets>
+ Module GenericIO
+ DataSources read write
+ DSName "read Read "
+ DSName write Written
+ RRDTitle "Disk Traffic ({plugin_instance})"
+ RRDVerticalLabel "Bytes per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf%s"
+</Type>
+<Type disk_ops>
+ Module GenericIO
+ DataSources read write
+ DSName "read Read "
+ DSName write Written
+ RRDTitle "Disk Operations ({plugin_instance})"
+ RRDVerticalLabel "Operations per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf"
+</Type>
+<Type disk_merged>
+ Module GenericIO
+ DataSources read write
+ DSName "read Read "
+ DSName write Written
+ RRDTitle "Disk Merged Operations ({plugin_instance})"
+ RRDVerticalLabel "Merged operations/s"
+# RRDOptions ...
+ RRDFormat "%5.1lf"
+</Type>
+<Type disk_time>
+ Module GenericIO
+ DataSources read write
+ DSName "read Read "
+ DSName write Written
+ RRDTitle "Disk time per operation ({plugin_instance})"
+ RRDVerticalLabel "Avg. Time/Op"
+# RRDOptions ...
+ RRDFormat "%5.1lf%ss"
+ Scale 0.001
+</Type>
+<Type entropy>
+ DataSources entropy
+ DSName entropy Entropy bits
+ RRDTitle "Available entropy on {hostname}"
+ RRDVerticalLabel "Bits"
+ RRDFormat "%4.0lf"
+</Type>
+<Type fanspeed>
+ DataSources value
+ DSName value RPM
+ RRDTitle "Fanspeed ({type_instance})"
+ RRDVerticalLabel "RPM"
+ RRDFormat "%6.1lf"
+ Color value 00b000
+</Type>
+<Type if_errors>
+ Module GenericIO
+ DataSources rx tx
+ DSName rx RX
+ DSName tx TX
+ RRDTitle "Interface Errors ({type_instance})"
+ RRDVerticalLabel "Errors per second"
+# RRDOptions ...
+ RRDFormat "%.3lf"
+</Type>
+<Type if_rx_errors>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "Interface receive errors ({plugin_instance})"
+ RRDVerticalLabel "Erorrs/s"
+ RRDFormat "%.1lf"
+ Color length f00000
+ Color over 00e0ff
+ Color crc 00e000
+ Color frame ffb000
+ Color fifo f000c0
+ Color missed 0000f0
+</Type>
+<Type if_tx_errors>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "Interface transmit errors ({plugin_instance})"
+ RRDVerticalLabel "Erorrs/s"
+ RRDFormat "%.1lf"
+ Color aborted f00000
+ Color carrier 00e0ff
+ Color fifo 00e000
+ Color heartbeat ffb000
+ Color window f000c0
+</Type>
+<Type if_octets>
+ Module GenericIO
+ DataSources rx tx
+ DSName rx RX
+ DSName tx TX
+ RRDTitle "Interface Traffic ({type_instance})"
+ RRDVerticalLabel "Bits per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf%s"
+ Scale 8
+</Type>
+<Type if_packets>
+ Module GenericIO
+ DataSources rx tx
+ DSName rx RX
+ DSName tx TX
+ RRDTitle "Interface Packets ({type_instance})"
+ RRDVerticalLabel "Packets per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf%s"
+</Type>
+<Type ipt_bytes>
+ DataSources value
+ DSName value Bytes/s
+ RRDTitle "Traffic ({type_instance})"
+ RRDVerticalLabel "Bytes per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf%s"
+</Type>
+<Type ipt_packets>
+ DataSources value
+ DSName value Packets/s
+ RRDTitle "Packets ({type_instance})"
+ RRDVerticalLabel "Packets per second"
+# RRDOptions ...
+ RRDFormat "%5.1lf"
+</Type>
+<Type irq>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "Interrupts on {hostname}"
+ RRDVerticalLabel "IRQs/s"
+ RRDFormat "%5.1lf"
+</Type>
+<Type load>
+ Module Load
+</Type>
+<Type memory>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "Physical memory utilization on {hostname}"
+ RRDVerticalLabel "Bytes"
+ RRDFormat "%5.1lf%s"
+ RRDOptions -b 1024 -l 0
+ DSName "free Free "
+ DSName "cached Cached "
+ DSName "buffered Buffered"
+ DSName "used Used "
+ #Order used buffered cached free
+ Order free cached buffered used
+ Color free 00e000
+ Color cached 0000ff
+ Color buffered ffb000
+ Color used ff0000
+</Type>
+<Type ping>
+ DataSources ping
+ DSName "ping Latency"
+ RRDTitle "Network latency ({type_instance})"
+ RRDVerticalLabel "Milliseconds"
+ RRDFormat "%5.2lfms"
+</Type>
+<Type ps_state>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "Processes on {hostname}"
+ RRDVerticalLabel "Processes"
+ RRDFormat "%5.1lf%s"
+ DSName running Running
+ DSName sleeping Sleeping
+ DSName paging Paging
+ DSName zombies Zombies
+ DSName blocked Blocked
+ DSName stopped Stopped
+ Order paging blocked zombies stopped running sleeping
+ Color running 00e000
+ Color sleeping 0000ff
+ Color paging ffb000
+ Color zombies ff0000
+ Color blocked ff00ff
+ Color stopped a000a0
+</Type>
+<Type tcp_connections>
+ Module GenericStacked
+ DataSources value
+ RRDTitle "TCP connections ({plugin_instance})"
+ RRDVerticalLabel "Connections"
+ RRDFormat "%5.1lf"
+ Order LISTEN CLOSING LAST_ACK CLOSE_WAIT CLOSE TIME_WAIT FIN_WAIT2 FIN_WAIT1 SYN_RECV SYN_SENT ESTABLISHED CLOSED
+ Color ESTABLISHED 00e000
+ Color SYN_SENT 00e0ff
+ Color SYN_RECV 00e0a0
+ Color FIN_WAIT1 f000f0
+ Color FIN_WAIT2 f000a0
+ Color TIME_WAIT ffb000
+ Color CLOSE 0000f0
+ Color CLOSE_WAIT 0000a0
+ Color LAST_ACK 000080
+ Color LISTEN ff0000
+ Color CLOSING 000000
+ Color CLOSED 0000f0
+</Type>
+<Type temperature>
+ DataSources value
+ DSName value Temp
+ RRDTitle "Temperature ({type_instance})"
+ RRDVerticalLabel "°Celsius"
+ RRDFormat "%4.1lf°C"
+</Type>
+<Type users>
+ DataSources users
+ DSName users Users
+ RRDTitle "Users ({type_instance}) on {hostname}"
+ RRDVerticalLabel "Users"
+ RRDFormat "%.1lf"
+ Color users 0000f0
+</Type>
+<Type voltage>
+ DataSources value
+ DSName value Volts
+ RRDTitle "Voltage ({type_instance})"
+ RRDVerticalLabel "Volts"
+ RRDFormat "%4.1lfV"
+ Color value f00000
+</Type>
+# vim: set sw=2 sts=2 et syntax=apache :
--- /dev/null
+Deny from all
--- /dev/null
+package Collectd::Graph::Common;
+
+use strict;
+use warnings;
+
+use vars (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue));
+
+use Carp (qw(confess cluck));
+use CGI (':cgi');
+use Exporter;
+
+$ColorCanvas = 'FFFFFF';
+$ColorFullBlue = '0000FF';
+$ColorHalfBlue = 'B7B7F7';
+
+@Collectd::Graph::Common::ISA = ('Exporter');
+@Collectd::Graph::Common::EXPORT_OK = (qw(
+ $ColorCanvas
+ $ColorFullBlue
+ $ColorHalfBlue
+
+ sanitize_hostname
+ sanitize_plugin sanitize_plugin_instance
+ sanitize_type sanitize_type_instance
+ group_files_by_plugin_instance
+ get_files_from_directory
+ filename_to_ident
+ ident_to_filename
+ ident_to_string
+ get_all_hosts
+ get_files_for_host
+ get_files_by_ident
+ get_selected_files
+ get_timespan_selection
+ get_host_selection
+ get_plugin_selection
+ get_faded_color
+ sort_idents_by_type_instance
+ type_to_module_name
+ epoch_to_rfc1123
+));
+
+our $DataDir = '/var/lib/collectd/rrd';
+
+return (1);
+
+sub _sanitize_generic_allow_minus
+{
+ my $str = "" . shift;
+
+ # remove all slashes
+ $str =~ s#/##g;
+
+ # remove all dots and dashes at the beginning and at the end.
+ $str =~ s#^[\.-]+##;
+ $str =~ s#[\.-]+$##;
+
+ return ($str);
+}
+
+sub _sanitize_generic_no_minus
+{
+ # Do everything the allow-minus variant does..
+ my $str = _sanitize_generic_allow_minus (@_);
+
+ # .. and remove the dashes, too
+ $str =~ s#/##g;
+
+ return ($str);
+} # _sanitize_generic_no_minus
+
+sub sanitize_hostname
+{
+ return (_sanitize_generic_allow_minus (@_));
+}
+
+sub sanitize_plugin
+{
+ return (_sanitize_generic_no_minus (@_));
+}
+
+sub sanitize_plugin_instance
+{
+ return (_sanitize_generic_allow_minus (@_));
+}
+
+sub sanitize_type
+{
+ return (_sanitize_generic_no_minus (@_));
+}
+
+sub sanitize_type_instance
+{
+ return (_sanitize_generic_allow_minus (@_));
+}
+
+sub group_files_by_plugin_instance
+{
+ my @files = @_;
+ my $data = {};
+
+ for (my $i = 0; $i < @files; $i++)
+ {
+ my $file = $files[$i];
+ my $key = $file->{'plugin_instance'} || '';
+
+ $data->{$key} ||= [];
+ push (@{$data->{$key}}, $file);
+ }
+
+ return ($data);
+}
+
+sub filename_to_ident
+{
+ my $file = shift;
+ my $ret;
+
+ if ($file =~ m#([^/]+)/([^/\-]+)(?:-([^/]+))?/([^/\-]+)(?:-([^/]+))?\.rrd$#)
+ {
+ $ret = {hostname => $1, plugin => $2, type => $4};
+ if (defined ($3))
+ {
+ $ret->{'plugin_instance'} = $3;
+ }
+ if (defined ($5))
+ {
+ $ret->{'type_instance'} = $5;
+ }
+ if ($`)
+ {
+ $ret->{'_prefix'} = $`;
+ }
+ }
+ else
+ {
+ return;
+ }
+
+ return ($ret);
+} # filename_to_ident
+
+sub ident_to_filename
+{
+ my $ident = shift;
+
+ my $ret = '';
+
+ if (defined ($ident->{'_prefix'}))
+ {
+ $ret .= $ident->{'_prefix'};
+ }
+ else
+ {
+ $ret .= "$DataDir/";
+ }
+
+ if (!$ident->{'hostname'})
+ {
+ cluck ("hostname is undefined")
+ }
+ if (!$ident->{'plugin'})
+ {
+ cluck ("plugin is undefined")
+ }
+ if (!$ident->{'type'})
+ {
+ cluck ("type is undefined")
+ }
+
+ $ret .= $ident->{'hostname'} . '/' . $ident->{'plugin'};
+ if (defined ($ident->{'plugin_instance'}))
+ {
+ $ret .= '-' . $ident->{'plugin_instance'};
+ }
+
+ $ret .= '/' . $ident->{'type'};
+ if (defined ($ident->{'type_instance'}))
+ {
+ $ret .= '-' . $ident->{'type_instance'};
+ }
+ $ret .= '.rrd';
+
+ return ($ret);
+} # ident_to_filename
+
+sub ident_to_string
+{
+ my $ident = shift;
+
+ my $ret = '';
+
+ $ret .= $ident->{'hostname'} . '/' . $ident->{'plugin'};
+ if (defined ($ident->{'plugin_instance'}))
+ {
+ $ret .= '-' . $ident->{'plugin_instance'};
+ }
+
+ $ret .= '/' . $ident->{'type'};
+ if (defined ($ident->{'type_instance'}))
+ {
+ $ret .= '-' . $ident->{'type_instance'};
+ }
+
+ return ($ret);
+} # ident_to_string
+
+sub get_files_from_directory
+{
+ my $dir = shift;
+ my $recursive = @_ ? shift : 0;
+ my $dh;
+ my @directories = ();
+ my $ret = [];
+
+ opendir ($dh, $dir) or die ("opendir ($dir): $!");
+ while (my $entry = readdir ($dh))
+ {
+ next if ($entry =~ m/^\./);
+
+ $entry = "$dir/$entry";
+
+ if (-d $entry)
+ {
+ push (@directories, $entry);
+ }
+ elsif (-f $entry)
+ {
+ my $ident = filename_to_ident ($entry);
+ if ($ident)
+ {
+ push (@$ret, $ident);
+ }
+ }
+ }
+ closedir ($dh);
+
+ if ($recursive > 0)
+ {
+ for (@directories)
+ {
+ my $temp = get_files_from_directory ($_, $recursive - 1);
+ if ($temp && @$temp)
+ {
+ push (@$ret, @$temp);
+ }
+ }
+ }
+
+ return ($ret);
+} # get_files_from_directory
+
+sub get_all_hosts
+{
+ my $dh;
+ my @ret = ();
+
+ opendir ($dh, "$DataDir") or confess ("opendir ($DataDir): $!");
+ while (my $entry = readdir ($dh))
+ {
+ next if ($entry =~ m/^\./);
+ next if (!-d "$DataDir/$entry");
+ push (@ret, sanitize_hostname ($entry));
+ }
+ closedir ($dh);
+
+ if (wantarray ())
+ {
+ return (@ret);
+ }
+ elsif (@ret)
+ {
+ return (\@ret);
+ }
+ else
+ {
+ return;
+ }
+} # get_all_hosts
+
+sub get_all_plugins
+{
+ my @hosts = @_;
+ my $ret = {};
+ my $dh;
+
+ if (!@hosts)
+ {
+ @hosts = get_all_hosts ();
+ }
+
+ for (@hosts)
+ {
+ my $host = $_;
+ opendir ($dh, "$DataDir/$host") or next;
+ while (my $entry = readdir ($dh))
+ {
+ my $plugin;
+ my $plugin_instance = '';
+
+ next if ($entry =~ m/^\./);
+ next if (!-d "$DataDir/$host/$entry");
+
+ if ($entry =~ m#^([^-]+)-(.+)$#)
+ {
+ $plugin = $1;
+ $plugin_instance = $2;
+ }
+ elsif ($entry =~ m#^([^-]+)$#)
+ {
+ $plugin = $1;
+ $plugin_instance = '';
+ }
+ else
+ {
+ next;
+ }
+
+ $ret->{$plugin} ||= {};
+ $ret->{$plugin}{$plugin_instance} = 1;
+ } # while (readdir)
+ closedir ($dh);
+ } # for (@hosts)
+
+ if (wantarray ())
+ {
+ return (sort (keys %$ret));
+ }
+ else
+ {
+ return ($ret);
+ }
+} # get_all_plugins
+
+sub get_files_for_host
+{
+ my $host = sanitize_hostname (shift);
+ return (get_files_from_directory ("$DataDir/$host", 2));
+} # get_files_for_host
+
+sub _filter_ident
+{
+ my $filter = shift;
+ my $ident = shift;
+
+ for (qw(hostname plugin plugin_instance type type_instance))
+ {
+ my $part = $_;
+ my $tmp;
+
+ if (!defined ($filter->{$part}))
+ {
+ next;
+ }
+ if (!defined ($ident->{$part}))
+ {
+ return (1);
+ }
+
+ if (ref $filter->{$part})
+ {
+ if (!grep { $ident->{$part} eq $_ } (@{$filter->{$part}}))
+ {
+ return (1);
+ }
+ }
+ else
+ {
+ if ($ident->{$part} ne $filter->{$part})
+ {
+ return (1);
+ }
+ }
+ }
+
+ return (0);
+} # _filter_ident
+
+sub get_files_by_ident
+{
+ my $ident = shift;
+ my $all_files;
+ my @ret = ();
+
+ #if ($ident->{'hostname'})
+ #{
+ #$all_files = get_files_for_host ($ident->{'hostname'});
+ #}
+ #else
+ #{
+ $all_files = get_files_from_directory ($DataDir, 3);
+ #}
+
+ @ret = grep { _filter_ident ($ident, $_) == 0 } (@$all_files);
+
+ return (\@ret);
+} # get_files_by_ident
+
+sub get_selected_files
+{
+ my $ident = {};
+
+ for (qw(hostname plugin plugin_instance type type_instance))
+ {
+ my $part = $_;
+ my @temp = param ($part);
+ if (!@temp)
+ {
+ next;
+ }
+ elsif (($part eq 'plugin') || ($part eq 'type'))
+ {
+ $ident->{$part} = [map { _sanitize_generic_no_minus ($_) } (@temp)];
+ }
+ else
+ {
+ $ident->{$part} = [map { _sanitize_generic_allow_minus ($_) } (@temp)];
+ }
+ }
+
+ return (get_files_by_ident ($ident));
+} # get_selected_files
+
+sub get_timespan_selection
+{
+ my $ret = 86400;
+ if (param ('timespan'))
+ {
+ my $temp = int (param ('timespan'));
+ if ($temp && ($temp > 0))
+ {
+ $ret = $temp;
+ }
+ }
+
+ return ($ret);
+} # get_timespan_selection
+
+sub get_host_selection
+{
+ my %ret = ();
+
+ for (get_all_hosts ())
+ {
+ $ret{$_} = 0;
+ }
+
+ for (param ('hostname'))
+ {
+ my $host = _sanitize_generic_allow_minus ($_);
+ if (defined ($ret{$host}))
+ {
+ $ret{$host} = 1;
+ }
+ }
+
+ if (wantarray ())
+ {
+ return (grep { $ret{$_} > 0 } (sort (keys %ret)));
+ }
+ else
+ {
+ return (\%ret);
+ }
+} # get_host_selection
+
+sub get_plugin_selection
+{
+ my %ret = ();
+ my @hosts = get_host_selection ();
+
+ for (get_all_plugins (@hosts))
+ {
+ $ret{$_} = 0;
+ }
+
+ for (param ('plugin'))
+ {
+ if (defined ($ret{$_}))
+ {
+ $ret{$_} = 1;
+ }
+ }
+
+ if (wantarray ())
+ {
+ return (grep { $ret{$_} > 0 } (sort (keys %ret)));
+ }
+ else
+ {
+ return (\%ret);
+ }
+} # get_plugin_selection
+
+sub _string_to_color
+{
+ my $color = shift;
+ if ($color =~ m/([0-9A-Fa-f][0-9A-Fa-f])([0-9A-Fa-f][0-9A-Fa-f])([0-9A-Fa-f][0-9A-Fa-f])/)
+ {
+ return ([hex ($1) / 255.0, hex ($2) / 255.0, hex ($3) / 255.0]);
+ }
+ return;
+} # _string_to_color
+
+sub _color_to_string
+{
+ confess ("Wrong number of arguments") if (@_ != 1);
+ return (sprintf ('%02hx%02hx%02hx', map { int (255.0 * $_) } @{$_[0]}));
+} # _color_to_string
+
+sub get_faded_color
+{
+ my $fg = shift;
+ my $bg;
+ my %opts = @_;
+ my $ret = [undef, undef, undef];
+
+ $opts{'background'} ||= [1.0, 1.0, 1.0];
+ $opts{'alpha'} ||= 0.25;
+
+ if (!ref ($fg))
+ {
+ $fg = _string_to_color ($fg)
+ or confess ("Cannot parse foreground color $fg");
+ }
+
+ if (!ref ($opts{'background'}))
+ {
+ $opts{'background'} = _string_to_color ($opts{'background'})
+ or confess ("Cannot parse background color " . $opts{'background'});
+ }
+ $bg = $opts{'background'};
+
+ for (my $i = 0; $i < 3; $i++)
+ {
+ $ret->[$i] = ($opts{'alpha'} * $fg->[$i])
+ + ((1.0 - $opts{'alpha'}) * $bg->[$i]);
+ }
+
+ return (_color_to_string ($ret));
+} # get_faded_color
+
+sub sort_idents_by_type_instance
+{
+ my $idents = shift;
+ my $array_sort = shift;
+
+ my %elements = map { $_->{'type_instance'} => $_ } (@$idents);
+ splice (@$idents, 0);
+
+ for (@$array_sort)
+ {
+ next if (!exists ($elements{$_}));
+ push (@$idents, $elements{$_});
+ delete ($elements{$_});
+ }
+ push (@$idents, map { $elements{$_} } (sort (keys %elements)));
+} # sort_idents_by_type_instance
+
+sub type_to_module_name
+{
+ my $type = shift;
+ my $ret;
+
+ $ret = ucfirst (lc ($type));
+
+ $ret =~ s/[^A-Za-z_]//g;
+ $ret =~ s/_([A-Za-z])/\U$1\E/g;
+
+ return ("Collectd::Graph::Type::$ret");
+} # type_to_module_name
+
+sub epoch_to_rfc1123
+{
+ my @days = (qw(Sun Mon Tue Wed Thu Fri Sat));
+ my @months = (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec));
+
+ my $epoch = @_ ? shift : time ();
+ my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
+ my $string = sprintf ('%s, %02d %s %4d %02d:%02d:%02d GMT', $days[$wday], $mday,
+ $months[$mon], 1900 + $year, $hour ,$min, $sec);
+ return ($string);
+}
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::Type;
+
+=head1 NAME
+
+Collectd::Graph::Type - Base class for the collectd graphing infrastructure
+
+=cut
+
+# Copyright (C) 2008 Florian octo Forster <octo at verplant.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; only version 2 of the License is applicable.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+use strict;
+use warnings;
+
+use Carp (qw(confess cluck));
+use RRDs ();
+use URI::Escape (qw(uri_escape));
+
+use Collectd::Graph::Common (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue
+ ident_to_filename
+ ident_to_string
+ get_faded_color));
+
+return (1);
+
+=head1 DESCRIPTION
+
+This module serves as base class for more specialized classes realizing
+specific "types".
+
+=head1 MEMBER VARIABLES
+
+As typical in Perl, a Collectd::Graph::Type object is a blessed hash reference.
+Member variables are entries in that hash. Inheriting classes are free to add
+additional entries. To set the member variable B<foo> to B<42>, do:
+
+ $obj->{'foo'} = 42;
+
+The following members control the behavior of Collectd::Graph::Type.
+
+=over 4
+
+=item B<files> (array reference)
+
+List of RRD files. Each file is passed as "ident", i.E<nbsp>e. broken up into
+"hostname", "plugin", "type" and optionally "plugin_instance" and
+"type_instance". Use the B<addFiles> method rather than setting this directly.
+
+=item B<data_sources> (array reference)
+
+List of data sources in the RRD files. If this is not given, the default
+implementation of B<getDataSources> will use B<RRDs::info> to find out which
+data sources are contained in the files.
+
+=item B<ds_names> (array reference)
+
+Names of the data sources as printed in the graph. Should be in the same order
+as the data sources are returned by B<getDataSources>.
+
+=item B<rrd_title> (string)
+
+Title of the RRD graph. The title can contain "{hostname}", "{plugin}" and so
+on which are replaced with their actual value. See the B<getTitle> method
+below.
+
+=item B<rrd_opts> (array reference)
+
+List of options directly passed to B<RRDs::graph>.
+
+=item B<rrd_format> (string)
+
+Format to use with B<GPRINT>. Defaults to C<%5.1lf>.
+
+=item B<rrd_colors> (hash reference)
+
+Mapping of data source names to colors, used when graphing the different data
+sources. Colors are given in the typical hexadecimal RGB form, but without
+leading "#", e.E<nbsp>g.:
+
+ $obj->{'rrd_colors'} = {foo => 'ff0000', bar => '00ff00'};
+
+=back
+
+=head1 METHODS
+
+The following methods are used by the graphing front end and may be overwritten
+to customize their behavior.
+
+=over 4
+
+=cut
+
+sub _get_ds_from_file
+{
+ my $file = shift;
+ my $info = RRDs::info ($file);
+ my %ds = ();
+ my @ds = ();
+
+ if (!$info || (ref ($info) ne 'HASH'))
+ {
+ return;
+ }
+
+ for (keys %$info)
+ {
+ if (m/^ds\[([^\]]+)\]/)
+ {
+ $ds{$1} = 1;
+ }
+ }
+
+ @ds = (keys %ds);
+ if (wantarray ())
+ {
+ return (@ds);
+ }
+ elsif (@ds)
+ {
+ return (\@ds);
+ }
+ else
+ {
+ return;
+ }
+} # _get_ds_from_file
+
+sub new
+{
+ my $pkg = shift;
+ my $obj = bless ({files => []}, $pkg);
+
+ if (@_)
+ {
+ $obj->addFiles (@_);
+ }
+
+ return ($obj);
+}
+
+=item B<addFiles> ({ I<ident> }, [...])
+
+Adds the given idents (which are hash references) to the B<files> member
+variable, see above.
+
+=cut
+
+sub addFiles
+{
+ my $obj = shift;
+ push (@{$obj->{'files'}}, @_);
+}
+
+=item B<getGraphsNum> ()
+
+Returns the number of graphs that can be generated from the added files. By
+default this number equals the number of files.
+
+=cut
+
+sub getGraphsNum
+{
+ my $obj = shift;
+ return (scalar @{$obj->{'files'}});
+}
+
+=item B<getDataSources> ()
+
+Returns the names of the data sources. If the B<data_sources> member variable
+is unset B<RRDs::info> is used to read that information from the first file.
+Set the B<data_sources> member variable instead of overloading this method!
+
+=cut
+
+sub getDataSources
+{
+ my $obj = shift;
+
+ if (!defined $obj->{'data_sources'})
+ {
+ my $ident;
+ my $filename;
+
+ if (!@{$obj->{'files'}})
+ {
+ return;
+ }
+
+ $ident = $obj->{'files'}[0];
+ $filename = ident_to_filename ($ident);
+
+ $obj->{'data_sources'} = _get_ds_from_file ($filename);
+ if (!$obj->{'data_sources'})
+ {
+ cluck ("_get_ds_from_file ($filename) failed.");
+ }
+ }
+
+ if (!defined $obj->{'data_sources'})
+ {
+ return;
+ }
+ elsif (wantarray ())
+ {
+ return (@{$obj->{'data_sources'}})
+ }
+ else
+ {
+ $obj->{'data_sources'};
+ }
+} # getDataSources
+
+
+=item B<getTitle> (I<$index>)
+
+Returns the title of the I<$index>th B<graph> (not necessarily file!). If the
+B<rrd_title> member variable is unset, a generic title is generated from the
+ident. Otherwise the substrings "{hostname}", "{plugin}", "{plugin_instance}",
+"{type}", and "{type_instance}" are replaced by their respective values.
+
+=cut
+
+sub getTitle
+{
+ my $obj = shift;
+ my $ident = shift;
+ my $title = $obj->{'rrd_title'};
+
+ if (!$title)
+ {
+ return (ident_to_string ($ident));
+ }
+
+ my $hostname = $ident->{'hostname'};
+ my $plugin = $ident->{'plugin'};
+ my $plugin_instance = $ident->{'plugin_instance'};
+ my $type = $ident->{'type'};
+ my $type_instance = $ident->{'type_instance'};
+
+ if (!defined $plugin_instance)
+ {
+ $plugin_instance = 'no instance';
+ }
+
+ if (!defined $type_instance)
+ {
+ $type_instance = 'no instance';
+ }
+
+ $title =~ s#{hostname}#$hostname#g;
+ $title =~ s#{plugin}#$plugin#g;
+ $title =~ s#{plugin_instance}#$plugin_instance#g;
+ $title =~ s#{type}#$type#g;
+ $title =~ s#{type_instance}#$type_instance#g;
+
+ return ($title);
+}
+
+=item B<getRRDArgs> (I<$index>)
+
+Return the arguments needed to generate the graph from the RRD file(s). If the
+file has only one data source, this default implementation will generate that
+typical min, average, max graph you probably know from temperatures and such.
+If the RRD files have multiple data sources, the average of each data source is
+printes as simple line.
+
+=cut
+
+sub getRRDArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $ident = $obj->{'files'}[$index];
+ if (!$ident)
+ {
+ cluck ("Invalid index: $index");
+ return;
+ }
+ my $filename = ident_to_filename ($ident);
+
+ my $rrd_opts = $obj->{'rrd_opts'} || [];
+ my $rrd_title = $obj->getTitle ($ident);
+ my $format = $obj->{'rrd_format'} || '%5.1lf';
+
+ my $rrd_colors = $obj->{'rrd_colors'};
+ my @ret = ('-t', $rrd_title, @$rrd_opts);
+
+ if (defined $obj->{'rrd_vertical'})
+ {
+ push (@ret, '-v', $obj->{'rrd_vertical'});
+ }
+
+ my $ds_names = $obj->{'ds_names'};
+ if (!$ds_names)
+ {
+ $ds_names = {};
+ }
+
+ my $ds = $obj->getDataSources ();
+ if (!$ds)
+ {
+ confess ("obj->getDataSources failed.");
+ }
+
+ if (!$rrd_colors)
+ {
+ my @tmp = ('0000ff', 'ff0000', '00ff00', 'ff00ff', '00ffff', 'ffff00');
+
+ for (my $i = 0; $i < @$ds; $i++)
+ {
+ $rrd_colors->{$ds->[$i]} = $tmp[$i % @tmp];
+ }
+ }
+
+ for (my $i = 0; $i < @$ds; $i++)
+ {
+ my $f = $filename;
+ my $ds_name = $ds->[$i];
+
+ # We need to escape colons for RRDTool..
+ $f =~ s#:#\\:#g;
+ $ds_name =~ s#:#\\:#g;
+
+ push (@ret,
+ "DEF:min${i}=${f}:${ds_name}:MIN",
+ "DEF:avg${i}=${f}:${ds_name}:AVERAGE",
+ "DEF:max${i}=${f}:${ds_name}:MAX");
+ }
+
+ if (@$ds == 1)
+ {
+ my $ds_name = $ds->[0];
+ my $color_fg = $rrd_colors->{$ds_name} || '000000';
+ my $color_bg = get_faded_color ($color_fg);
+
+ if ($ds_names->{$ds_name})
+ {
+ $ds_name = $ds_names->{$ds_name};
+ }
+ $ds_name =~ s#:#\\:#g;
+
+ push (@ret,
+ "AREA:max0#${color_bg}",
+ "AREA:min0#${ColorCanvas}",
+ "LINE1:avg0#${color_fg}:${ds_name}",
+ "GPRINT:min0:MIN:${format} Min,",
+ "GPRINT:avg0:AVERAGE:${format} Avg,",
+ "GPRINT:max0:MAX:${format} Max,",
+ "GPRINT:avg0:LAST:${format} Last\\l");
+ }
+ else
+ {
+ for (my $i = 0; $i < @$ds; $i++)
+ {
+ my $ds_name = $ds->[$i];
+ my $color = $rrd_colors->{$ds_name} || '000000';
+
+ if ($ds_names->{$ds_name})
+ {
+ $ds_name = $ds_names->{$ds_name};
+ }
+
+ push (@ret,
+ "LINE1:avg${i}#${color}:${ds_name}",
+ "GPRINT:min${i}:MIN:${format} Min,",
+ "GPRINT:avg${i}:AVERAGE:${format} Avg,",
+ "GPRINT:max${i}:MAX:${format} Max,",
+ "GPRINT:avg${i}:LAST:${format} Last\\l");
+ }
+ }
+
+ return (\@ret);
+} # getRRDArgs
+
+=item B<getGraphArgs> (I<$index>)
+
+Returns the parameters that should be passed to the CGI script to generate the
+I<$index>th graph. The returned string is already URI-encoded and will possibly
+set the "hostname", "plugin", "plugin_instance", "type", and "type_instance"
+parameters.
+
+The default implementation simply uses the ident of the I<$index>th file to
+fill this.
+
+=cut
+
+sub getGraphArgs
+{
+ my $obj = shift;
+ my $index = shift;
+ my $ident = $obj->{'files'}[$index];
+
+ my @args = ();
+ for (qw(hostname plugin plugin_instance type type_instance))
+ {
+ if (defined ($ident->{$_}))
+ {
+ push (@args, uri_escape ($_) . '=' . uri_escape ($ident->{$_}));
+ }
+ }
+
+ return (join (';', @args));
+}
+
+=item B<getLastModified> ([I<$index>])
+
+If I<$index> is not given, the modification time of all files is scanned and the most recent modification is returned. If I<$index> is given, only the files belonging to the I<$index>th graph will be considered.
+
+=cut
+
+sub getLastModified
+{
+ my $obj = shift;
+ my $index = @_ ? shift : -1;
+
+ my $mtime = 0;
+
+ if ($index == -1)
+ {
+ for (@{$obj->{'files'}})
+ {
+ my $ident = $_;
+ my $filename = ident_to_filename ($ident);
+ my @statbuf = stat ($filename);
+
+ if (!@statbuf)
+ {
+ next;
+ }
+
+ if ($mtime < $statbuf[9])
+ {
+ $mtime = $statbuf[9];
+ }
+ }
+ }
+ else
+ {
+ my $ident = $obj->{'files'}[$index];
+ my $filename = ident_to_filename ($ident);
+ my @statbuf = stat ($filename);
+
+ $mtime = $statbuf[9];
+ }
+
+ if (!$mtime)
+ {
+ return;
+ }
+ return ($mtime);
+} # getLastModified
+
+=back
+
+=head1 SEE ALSO
+
+L<Collectd::Graph::Type::GenericStacked>
+
+=head1 AUTHOR AND LICENSE
+
+Copyright (c) 2008 by Florian Forster
+E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
+General Public License, VersionE<nbsp>2 (GPLv2).
+
+=cut
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::Type::Df;
+
+use strict;
+use warnings;
+use base ('Collectd::Graph::Type');
+
+use Collectd::Graph::Common (qw(ident_to_filename get_faded_color));
+
+return (1);
+
+sub getDataSources
+{
+ return ([qw(free used)]);
+} # getDataSources
+
+sub new
+{
+ my $pkg = shift;
+ my $obj = Collectd::Graph::Type->new (@_);
+ $obj->{'data_sources'} = [qw(free used)];
+ $obj->{'rrd_opts'} = ['-v', 'Bytes'];
+ $obj->{'rrd_title'} = 'Disk space ({type_instance})';
+ $obj->{'rrd_format'} = '%5.1lf%sB';
+ $obj->{'colors'} = [qw(00b000 ff0000)];
+
+ return (bless ($obj, $pkg));
+} # new
+
+sub getRRDArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $ident = $obj->{'files'}[$index];
+ if (!$ident)
+ {
+ cluck ("Invalid index: $index");
+ return;
+ }
+ my $filename = ident_to_filename ($ident);
+ $filename =~ s#:#\\:#g;
+
+ my $faded_green = get_faded_color ('00ff00');
+ my $faded_red = get_faded_color ('ff0000');
+
+ return (['-t', 'Free space (' . $ident->{'type_instance'} . ')', '-v', 'Bytes', '-l', '0',
+ "DEF:free_min=${filename}:free:MIN",
+ "DEF:free_avg=${filename}:free:AVERAGE",
+ "DEF:free_max=${filename}:free:MAX",
+ "DEF:used_min=${filename}:used:MIN",
+ "DEF:used_avg=${filename}:used:AVERAGE",
+ "DEF:used_max=${filename}:used:MAX",
+ "CDEF:both_avg=free_avg,used_avg,+",
+ "AREA:both_avg#${faded_green}",
+ "AREA:used_avg#${faded_red}",
+ 'LINE1:both_avg#00ff00:Free',
+ 'GPRINT:free_min:MIN:%5.1lf%sB Min,',
+ 'GPRINT:free_avg:AVERAGE:%5.1lf%sB Avg,',
+ 'GPRINT:free_max:MAX:%5.1lf%sB Max,',
+ 'GPRINT:free_avg:LAST:%5.1lf%sB Last\l',
+ 'LINE1:used_avg#ff0000:Used',
+ 'GPRINT:used_min:MIN:%5.1lf%sB Min,',
+ 'GPRINT:used_avg:AVERAGE:%5.1lf%sB Avg,',
+ 'GPRINT:used_max:MAX:%5.1lf%sB Max,',
+ 'GPRINT:used_avg:LAST:%5.1lf%sB Last\l']);
+} # getRRDArgs
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::Type::GenericIO;
+
+use strict;
+use warnings;
+use base ('Collectd::Graph::Type');
+
+use Carp ('confess');
+
+use Collectd::Graph::Common (qw($ColorCanvas ident_to_filename get_faded_color));
+
+return (1);
+
+sub getRRDArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $ident = $obj->{'files'}[$index] || confess ("Unknown index $index");
+ my $filename = ident_to_filename ($ident);
+
+ my $rrd_opts = $obj->{'rrd_opts'} || [];
+ my $rrd_title = $obj->getTitle ($ident);
+ my $format = $obj->{'rrd_format'} || '%5.1lf%s';
+
+ my $ds_list = $obj->getDataSources ();
+ my $ds_names = $obj->{'ds_names'};
+ if (!$ds_names)
+ {
+ $ds_names = {};
+ }
+
+ my $colors = $obj->{'rrd_colors'} || {};
+ my @ret = ('-t', $rrd_title, @$rrd_opts);
+
+ if (defined $obj->{'rrd_vertical'})
+ {
+ push (@ret, '-v', $obj->{'rrd_vertical'});
+ }
+
+ if (@$ds_list != 2)
+ {
+ my $num = 0 + @$ds_list;
+ confess ("Expected two data sources, but there is/are $num");
+ }
+
+ my $rx_ds = $ds_list->[0];
+ my $tx_ds = $ds_list->[1];
+
+ my $rx_ds_name = $ds_names->{$rx_ds} || $rx_ds;
+ my $tx_ds_name = $ds_names->{$tx_ds} || $tx_ds;
+
+ my $rx_color_fg = $colors->{$rx_ds} || '0000ff';
+ my $tx_color_fg = $colors->{$tx_ds} || '00b000';
+
+ my $rx_color_bg = get_faded_color ($rx_color_fg);
+ my $tx_color_bg = get_faded_color ($tx_color_fg);
+ my $overlap_color = get_faded_color ($rx_color_bg, background => $tx_color_bg);
+
+ $filename =~ s#:#\\:#g;
+ $rx_ds =~ s#:#\\:#g;
+ $tx_ds =~ s#:#\\:#g;
+ $rx_ds_name =~ s#:#\\:#g;
+ $tx_ds_name =~ s#:#\\:#g;
+
+ if ($obj->{'scale'})
+ {
+ my $factor = $obj->{'scale'};
+
+ push (@ret,
+ "DEF:min_rx_raw=${filename}:${rx_ds}:MIN",
+ "DEF:avg_rx_raw=${filename}:${rx_ds}:AVERAGE",
+ "DEF:max_rx_raw=${filename}:${rx_ds}:MAX",
+ "DEF:min_tx_raw=${filename}:${tx_ds}:MIN",
+ "DEF:avg_tx_raw=${filename}:${tx_ds}:AVERAGE",
+ "DEF:max_tx_raw=${filename}:${tx_ds}:MAX",
+ "CDEF:min_rx=min_rx_raw,${factor},*",
+ "CDEF:avg_rx=avg_rx_raw,${factor},*",
+ "CDEF:max_rx=max_rx_raw,${factor},*",
+ "CDEF:min_tx=min_tx_raw,${factor},*",
+ "CDEF:avg_tx=avg_tx_raw,${factor},*",
+ "CDEF:max_tx=max_tx_raw,${factor},*");
+ }
+ else # (!$obj->{'scale'})
+ {
+ push (@ret,
+ "DEF:min_rx=${filename}:${rx_ds}:MIN",
+ "DEF:avg_rx=${filename}:${rx_ds}:AVERAGE",
+ "DEF:max_rx=${filename}:${rx_ds}:MAX",
+ "DEF:min_tx=${filename}:${tx_ds}:MIN",
+ "DEF:avg_tx=${filename}:${tx_ds}:AVERAGE",
+ "DEF:max_tx=${filename}:${tx_ds}:MAX");
+ }
+
+ push (@ret,
+ "CDEF:overlap=avg_rx,avg_tx,LT,avg_rx,avg_tx,IF",
+ "AREA:avg_rx#${rx_color_bg}",
+ "AREA:avg_tx#${tx_color_bg}",
+ "AREA:overlap#${overlap_color}",
+ "LINE1:avg_rx#${rx_color_fg}:${rx_ds_name}",
+ "GPRINT:min_rx:MIN:${format} Min,",
+ "GPRINT:avg_rx:AVERAGE:${format} Avg,",
+ "GPRINT:max_rx:MAX:${format} Max,",
+ "GPRINT:avg_rx:LAST:${format} Last\\l",
+ "LINE1:avg_tx#${tx_color_fg}:${tx_ds_name}",
+ "GPRINT:min_tx:MIN:${format} Min,",
+ "GPRINT:avg_tx:AVERAGE:${format} Avg,",
+ "GPRINT:max_tx:MAX:${format} Max,",
+ "GPRINT:avg_tx:LAST:${format} Last\\l");
+
+ return (\@ret);
+} # getRRDArgs
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::Type::GenericStacked;
+
+use strict;
+use warnings;
+use base ('Collectd::Graph::Type');
+
+use Collectd::Graph::Common (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue
+ group_files_by_plugin_instance ident_to_filename sanitize_type_instance
+ get_faded_color sort_idents_by_type_instance));
+
+return (1);
+
+sub getGraphsNum
+{
+ my $obj = shift;
+ my $group = group_files_by_plugin_instance (@{$obj->{'files'}});
+
+ return (scalar (keys %$group));
+}
+
+sub getRRDArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $group = group_files_by_plugin_instance (@{$obj->{'files'}});
+ my @group = sort (keys %$group);
+
+ my $rrd_opts = $obj->{'rrd_opts'} || [];
+ my $format = $obj->{'rrd_format'} || '%5.1lf';
+
+ my $idents = $group->{$group[$index]};
+ my $ds_name_len = 0;
+
+ my $rrd_title = $obj->getTitle ($idents->[0]);
+
+ my $colors = $obj->{'rrd_colors'} || {};
+ my @ret = ('-t', $rrd_title, @$rrd_opts);
+
+ if (defined $obj->{'rrd_vertical'})
+ {
+ push (@ret, '-v', $obj->{'rrd_vertical'});
+ }
+
+ if ($obj->{'custom_order'})
+ {
+ sort_idents_by_type_instance ($idents, $obj->{'custom_order'});
+ }
+
+ $obj->{'ds_names'} ||= {};
+ my @names = map { $obj->{'ds_names'}{$_->{'type_instance'}} || $_->{'type_instance'} } (@$idents);
+
+ for (my $i = 0; $i < @$idents; $i++)
+ {
+ my $ident = $idents->[$i];
+ my $filename = ident_to_filename ($ident);
+
+ if ($ds_name_len < length ($names[$i]))
+ {
+ $ds_name_len = length ($names[$i]);
+ }
+
+ # Escape colons _after_ the length has been checked.
+ $names[$i] =~ s/:/\\:/g;
+
+ push (@ret,
+ "DEF:min${i}=${filename}:value:MIN",
+ "DEF:avg${i}=${filename}:value:AVERAGE",
+ "DEF:max${i}=${filename}:value:MAX");
+ }
+
+ for (my $i = @$idents - 1; $i >= 0; $i--)
+ {
+ if ($i == (@$idents - 1))
+ {
+ push (@ret,
+ "CDEF:cdef${i}=avg${i}");
+ }
+ else
+ {
+ my $j = $i + 1;
+ push (@ret,
+ "CDEF:cdef${i}=cdef${j},avg${i},+");
+ }
+ }
+
+ for (my $i = 0; $i < @$idents; $i++)
+ {
+ my $type_instance = $idents->[$i]{'type_instance'};
+ my $color = '000000';
+ if (exists $colors->{$type_instance})
+ {
+ $color = $colors->{$type_instance};
+ }
+
+ $color = get_faded_color ($color);
+
+ push (@ret,
+ "AREA:cdef${i}#${color}");
+ }
+
+ for (my $i = 0; $i < @$idents; $i++)
+ {
+ my $type_instance = $idents->[$i]{'type_instance'};
+ my $ds_name = sprintf ("%-*s", $ds_name_len, $names[$i]);
+ my $color = '000000';
+ if (exists $colors->{$type_instance})
+ {
+ $color = $colors->{$type_instance};
+ }
+ push (@ret,
+ "LINE1:cdef${i}#${color}:${ds_name}",
+ "GPRINT:min${i}:MIN:${format} Min,",
+ "GPRINT:avg${i}:AVERAGE:${format} Avg,",
+ "GPRINT:max${i}:MAX:${format} Max,",
+ "GPRINT:avg${i}:LAST:${format} Last\\l");
+ }
+
+ return (\@ret);
+}
+
+sub getGraphArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $group = group_files_by_plugin_instance (@{$obj->{'files'}});
+ my @group = sort (keys %$group);
+
+ my $idents = $group->{$group[$index]};
+
+ my @args = ();
+ for (qw(hostname plugin plugin_instance type))
+ {
+ if (defined ($idents->[0]{$_}))
+ {
+ push (@args, $_ . '=' . $idents->[0]{$_});
+ }
+ }
+
+ return (join (';', @args));
+} # getGraphArgs
+
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::Type::Load;
+
+use strict;
+use warnings;
+use base ('Collectd::Graph::Type');
+
+use Collectd::Graph::Common (qw($ColorCanvas ident_to_filename get_faded_color));
+
+return (1);
+
+sub new
+{
+ my $pkg = shift;
+ my $obj = Collectd::Graph::Type->new (@_);
+ $obj->{'data_sources'} = [qw(shortterm midterm longterm)];
+ $obj->{'rrd_opts'} = ['-v', 'System load'];
+ $obj->{'rrd_title'} = 'System load';
+ $obj->{'rrd_format'} = '%.2lf';
+ $obj->{'colors'} = [qw(00ff00 0000ff ff0000)];
+
+ print STDERR "Hi, this is Collectd::Graph::Type::Load::new\n";
+
+ return (bless ($obj, $pkg));
+} # new
+
+sub getRRDArgs
+{
+ my $obj = shift;
+ my $index = shift;
+
+ my $ident = $obj->{'files'}[$index];
+ if (!$ident)
+ {
+ cluck ("Invalid index: $index");
+ return;
+ }
+ my $filename = ident_to_filename ($ident);
+ $filename =~ s#:#\\:#g;
+
+ my $faded_green = get_faded_color ('00ff00');
+
+ return (['-t', 'System load', '-v', 'System load',
+ "DEF:s_min=${filename}:shortterm:MIN",
+ "DEF:s_avg=${filename}:shortterm:AVERAGE",
+ "DEF:s_max=${filename}:shortterm:MAX",
+ "DEF:m_min=${filename}:midterm:MIN",
+ "DEF:m_avg=${filename}:midterm:AVERAGE",
+ "DEF:m_max=${filename}:midterm:MAX",
+ "DEF:l_min=${filename}:longterm:MIN",
+ "DEF:l_avg=${filename}:longterm:AVERAGE",
+ "DEF:l_max=${filename}:longterm:MAX",
+ "AREA:s_max#${faded_green}",
+ "AREA:s_min#${ColorCanvas}",
+ "LINE1:s_avg#00ff00: 1 min",
+ "GPRINT:s_min:MIN:%.2lf Min,",
+ "GPRINT:s_avg:AVERAGE:%.2lf Avg,",
+ "GPRINT:s_max:MAX:%.2lf Max,",
+ "GPRINT:s_avg:LAST:%.2lf Last\\l",
+ "LINE1:m_avg#0000ff: 5 min",
+ "GPRINT:m_min:MIN:%.2lf Min,",
+ "GPRINT:m_avg:AVERAGE:%.2lf Avg,",
+ "GPRINT:m_max:MAX:%.2lf Max,",
+ "GPRINT:m_avg:LAST:%.2lf Last\\l",
+ "LINE1:l_avg#ff0000:15 min",
+ "GPRINT:l_min:MIN:%.2lf Min,",
+ "GPRINT:l_avg:AVERAGE:%.2lf Avg,",
+ "GPRINT:l_max:MAX:%.2lf Max,",
+ "GPRINT:l_avg:LAST:%.2lf Last\\l"]);
+} # sub getRRDArgs
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 :
--- /dev/null
+package Collectd::Graph::TypeLoader;
+
+=head1 NAME
+
+Collectd::Graph::TypeLoader - Load a module according to the "type"
+
+=cut
+
+# Copyright (C) 2008 Florian octo Forster <octo at verplant.org>
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; only version 2 of the License is applicable.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+use strict;
+use warnings;
+
+use Carp (qw(cluck confess));
+use Exporter ();
+use Config::General ('ParseConfig');
+use Collectd::Graph::Type ();
+
+@Collectd::Graph::TypeLoader::ISA = ('Exporter');
+@Collectd::Graph::TypeLoader::EXPORT_OK = ('tl_read_config', 'tl_load_type');
+
+our $Configuration = undef;
+
+our @ArrayMembers = (qw(data_sources rrd_opts custom_order));
+our @ScalarMembers = (qw(rrd_title rrd_format rrd_vertical scale));
+our @DSMappedMembers = (qw(ds_names rrd_colors));
+
+our %MemberToConfigMap =
+(
+ data_sources => 'datasources',
+ ds_names => 'dsname',
+ rrd_title => 'rrdtitle',
+ rrd_opts => 'rrdoptions',
+ rrd_format => 'rrdformat',
+ rrd_vertical => 'rrdverticallabel',
+ rrd_colors => 'color',
+ scale => 'scale', # GenericIO only
+ custom_order => 'order' # GenericStacked only
+);
+
+return (1);
+
+=head1 EXPORTED FUNCTIONS
+
+=over 4
+
+=item B<tl_read_config> (I<$file>)
+
+Reads the configuration from the file located at I<$file>.
+
+=cut
+
+sub tl_read_config
+{
+ my $file = shift;
+ my %conf;
+
+ if ($Configuration)
+ {
+ return (1);
+ }
+
+ %conf = ParseConfig (-ConfigFile => $file,
+ -LowerCaseNames => 1,
+ -UseApacheInclude => 1,
+ -IncludeDirectories => 1,
+ ($Config::General::VERSION >= 2.38) ? (-IncludeAgain => 0) : (),
+ -MergeDuplicateBlocks => 1,
+ -CComments => 0);
+ if (!%conf)
+ {
+ return;
+ }
+
+ $Configuration = \%conf;
+ return (1);
+} # tl_read_config
+
+sub _create_object
+{
+ my $module = shift;
+ my $obj;
+
+ local $SIG{__WARN__} = sub {};
+ local $SIG{__DIE__} = sub {};
+
+ eval <<PERL;
+ require $module;
+ \$obj = ${module}->new ();
+PERL
+ if (!$obj)
+ {
+ return;
+ }
+
+ return ($obj);
+} # _create_object
+
+sub _load_module_from_config
+{
+ my $conf = shift;
+
+ my $module = $conf->{'module'};
+ my $obj;
+
+ if ($module && !($module =~ m/::/))
+ {
+ $module = "Collectd::Graph::Type::$module";
+ }
+
+ if ($module)
+ {
+ print STDERR "\$module = $module;\n";
+ $obj = _create_object ($module);
+ if (!$obj)
+ {
+ cluck ("Creating an $module object failed");
+ return;
+ }
+ }
+ else
+ {
+ $obj = Collectd::Graph::Type->new ();
+ if (!$obj)
+ {
+ cluck ("Creating an Collectd::Graph::Type object failed");
+ return;
+ }
+ }
+
+ for (@ScalarMembers) # {{{
+ {
+ my $member = $_;
+ my $key = $MemberToConfigMap{$member};
+ my $val;
+
+ if (!defined $conf->{$key})
+ {
+ next;
+ }
+ $val = $conf->{$key};
+
+ if (ref ($val) ne '')
+ {
+ cluck ("Invalid value type for $key: " . ref ($val));
+ next;
+ }
+
+ $obj->{$member} = $val;
+ } # }}}
+
+ for (@ArrayMembers) # {{{
+ {
+ my $member = $_;
+ my $key = $MemberToConfigMap{$member};
+ my $val;
+
+ if (!defined $conf->{$key})
+ {
+ next;
+ }
+ $val = $conf->{$key};
+
+ if (ref ($val) eq 'ARRAY')
+ {
+ $obj->{$member} = $val;
+ }
+ elsif (ref ($val) eq '')
+ {
+ $obj->{$member} = [split (' ', $val)];
+ }
+ else
+ {
+ cluck ("Invalid value type for $key: " . ref ($val));
+ }
+ } # }}}
+
+ for (@DSMappedMembers) # {{{
+ {
+ my $member = $_;
+ my $key = $MemberToConfigMap{$member};
+ my @val_list;
+
+ if (!defined $conf->{$key})
+ {
+ next;
+ }
+
+ if (ref ($conf->{$key}) eq 'ARRAY')
+ {
+ @val_list = @{$conf->{$key}};
+ }
+ elsif (ref ($conf->{$key}) eq '')
+ {
+ @val_list = ($conf->{$key});
+ }
+ else
+ {
+ cluck ("Invalid value type for $key: " . ref ($conf->{$key}));
+ next;
+ }
+
+ for (@val_list)
+ {
+ my $line = $_;
+ my $ds;
+ my $val;
+
+ if (!defined ($line) || (ref ($line) ne ''))
+ {
+ next;
+ }
+
+ ($ds, $val) = split (' ', $line, 2);
+ if (!$ds || !$val)
+ {
+ next;
+ }
+
+ $obj->{$member} ||= {};
+ $obj->{$member}{$ds} = $val;
+
+ print STDERR "\$obj->{$member}{$ds} = $val;\n";
+ } # for (@val_list)
+ } # }}} for (@DSMappedMembers)
+
+ return ($obj);
+} # _load_module_from_config
+
+sub _load_module_generic
+{
+ my $type = shift;
+ my $module = ucfirst (lc ($type));
+ my $obj;
+
+ $module =~ s/[^A-Za-z_]//g;
+ $module =~ s/_([A-Za-z])/\U$1\E/g;
+
+ $obj = _create_object ($module);
+ if (!$obj)
+ {
+ $obj = Collectd::Graph::Type->new ();
+ if (!$obj)
+ {
+ cluck ("Creating an Collectd::Graph::Type object failed");
+ return;
+ }
+ }
+
+ return ($obj);
+} # _load_module_generic
+
+=item B<tl_load_type> (I<$type>)
+
+Does whatever is necessary to get an object with which to graph RRD files of
+type I<$type>.
+
+=cut
+
+sub tl_load_type
+{
+ my $type = shift;
+
+ if (defined $Configuration->{'type'}{$type})
+ {
+ return (_load_module_from_config ($Configuration->{'type'}{$type}));
+ }
+ else
+ {
+ return (_load_module_generic ($type));
+ }
+} # tl_load_type
+
+=back
+
+=head1 SEE ALSO
+
+L<Collectd::Graph::Type::GenericStacked>
+
+=head1 AUTHOR AND LICENSE
+
+Copyright (c) 2008 by Florian Forster
+E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
+General Public License, VersionE<nbsp>2 (GPLv2).
+
+=cut
+
+# vim: set shiftwidth=2 softtabstop=2 tabstop=8 et fdm=marker :
--- /dev/null
+Options -ExecCGI
+SetHandler none
--- /dev/null
+table
+{
+ border-collapse: collapse;
+}
+td, th
+{
+ border: 1px solid gray;
+}
+/* vim: set sw=2 sts=2 et : */