contrib/collection3: Add an basic, extensible, modular graphing front-end.
authorFlorian Forster <octo@huhu.verplant.org>
Tue, 8 Jul 2008 11:08:13 +0000 (13:08 +0200)
committerFlorian Forster <octo@huhu.verplant.org>
Tue, 8 Jul 2008 11:08:13 +0000 (13:08 +0200)
17 files changed:
contrib/collection3/README [new file with mode: 0644]
contrib/collection3/bin/graph.cgi [new file with mode: 0755]
contrib/collection3/bin/index.cgi [new file with mode: 0755]
contrib/collection3/bin/json.cgi [new file with mode: 0755]
contrib/collection3/etc/.htaccess [new file with mode: 0644]
contrib/collection3/etc/collection3.conf [new file with mode: 0644]
contrib/collection3/lib/.htaccess [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Common.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Type.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Type/Df.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/Type/Load.pm [new file with mode: 0644]
contrib/collection3/lib/Collectd/Graph/TypeLoader.pm [new file with mode: 0644]
contrib/collection3/share/.htaccess [new file with mode: 0644]
contrib/collection3/share/shortcut-icon.png [new file with mode: 0644]
contrib/collection3/share/style.css [new file with mode: 0644]

diff --git a/contrib/collection3/README b/contrib/collection3/README
new file mode 100644 (file)
index 0000000..01d01bb
--- /dev/null
@@ -0,0 +1,42 @@
+ 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).
+
diff --git a/contrib/collection3/bin/graph.cgi b/contrib/collection3/bin/graph.cgi
new file mode 100755 (executable)
index 0000000..c84199d
--- /dev/null
@@ -0,0 +1,148 @@
+#!/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 :
diff --git a/contrib/collection3/bin/index.cgi b/contrib/collection3/bin/index.cgi
new file mode 100755 (executable)
index 0000000..b0001d2
--- /dev/null
@@ -0,0 +1,335 @@
+#!/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 :
diff --git a/contrib/collection3/bin/json.cgi b/contrib/collection3/bin/json.cgi
new file mode 100755 (executable)
index 0000000..0dceb62
--- /dev/null
@@ -0,0 +1,120 @@
+#!/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 :
diff --git a/contrib/collection3/etc/.htaccess b/contrib/collection3/etc/.htaccess
new file mode 100644 (file)
index 0000000..3a42882
--- /dev/null
@@ -0,0 +1 @@
+Deny from all
diff --git a/contrib/collection3/etc/collection3.conf b/contrib/collection3/etc/collection3.conf
new file mode 100644 (file)
index 0000000..583eeac
--- /dev/null
@@ -0,0 +1,255 @@
+<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 :
diff --git a/contrib/collection3/lib/.htaccess b/contrib/collection3/lib/.htaccess
new file mode 100644 (file)
index 0000000..3a42882
--- /dev/null
@@ -0,0 +1 @@
+Deny from all
diff --git a/contrib/collection3/lib/Collectd/Graph/Common.pm b/contrib/collection3/lib/Collectd/Graph/Common.pm
new file mode 100644 (file)
index 0000000..ec171da
--- /dev/null
@@ -0,0 +1,585 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/Type.pm b/contrib/collection3/lib/Collectd/Graph/Type.pm
new file mode 100644 (file)
index 0000000..60097e5
--- /dev/null
@@ -0,0 +1,480 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/Type/Df.pm b/contrib/collection3/lib/Collectd/Graph/Type/Df.pm
new file mode 100644 (file)
index 0000000..b4eb8b1
--- /dev/null
@@ -0,0 +1,68 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm b/contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm
new file mode 100644 (file)
index 0000000..58b7566
--- /dev/null
@@ -0,0 +1,113 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm b/contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm
new file mode 100644 (file)
index 0000000..273d89e
--- /dev/null
@@ -0,0 +1,145 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/Type/Load.pm b/contrib/collection3/lib/Collectd/Graph/Type/Load.pm
new file mode 100644 (file)
index 0000000..f13665e
--- /dev/null
@@ -0,0 +1,71 @@
+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 :
diff --git a/contrib/collection3/lib/Collectd/Graph/TypeLoader.pm b/contrib/collection3/lib/Collectd/Graph/TypeLoader.pm
new file mode 100644 (file)
index 0000000..6223eaa
--- /dev/null
@@ -0,0 +1,301 @@
+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 :
diff --git a/contrib/collection3/share/.htaccess b/contrib/collection3/share/.htaccess
new file mode 100644 (file)
index 0000000..e139ace
--- /dev/null
@@ -0,0 +1,2 @@
+Options -ExecCGI
+SetHandler none
diff --git a/contrib/collection3/share/shortcut-icon.png b/contrib/collection3/share/shortcut-icon.png
new file mode 100644 (file)
index 0000000..6af57e5
Binary files /dev/null and b/contrib/collection3/share/shortcut-icon.png differ
diff --git a/contrib/collection3/share/style.css b/contrib/collection3/share/style.css
new file mode 100644 (file)
index 0000000..a6648aa
--- /dev/null
@@ -0,0 +1,9 @@
+table
+{
+  border-collapse: collapse;
+}
+td, th
+{
+  border: 1px solid gray;
+}
+/* vim: set sw=2 sts=2 et : */