#!/usr/bin/perl -w
# -*- cperl -*-
#
# Copyright (C) 2002-2006 Jimmy Olsen, Audun Ytterdal, Nicolai Langfeldt
#
# 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; version 2 dated June,
# 1991.
#
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#
# $Id: munin-graph.in 1588 2008-04-08 18:06:16Z matthias $

$|=1;

use strict;
use IO::Socket;
use RRDs;
use Munin;
use POSIX qw(strftime);
use Digest::MD5;
use Getopt::Long;
use Time::HiRes;

my $graph_time= Time::HiRes::time;
my $DEBUG = 0;
my $VERSION = "1.2.6";

# Limit graphing to certain hosts and/or services
my @limit_hosts = ();
my @limit_services = ();

# RRDtool 1.2 requires \\: in comments
my $RRDkludge = $RRDs::VERSION < 1.2 ? '' : '\\';

# And RRDtool 1.2 and later has draws lines with crayons so we hack
# the LINE* options a bit.
my $LINEkluge=0;
if ($RRDs::VERSION >= 1.2) {
    $LINEkluge=1;
}

# Force drawing of "graph no".
my $force_graphing = 0;
my $force_lazy = 1;
my $force_root = 0;
my $do_usage = 0;
my $do_version = 0;
my $cron = 0;
my $list_images = 0;
my $skip_locking = 0;
my $skip_stats = 0;
my $stdout = 0;
my $conffile = "/etc/munin/munin.conf";
my %draw = ("day" => 1, "week" => 1, "month" => 1, "year" => 1, "sumyear" => 1, "sumweek" => 1);

my $log = new IO::Handle;

# Get options
$do_usage=1  unless 
GetOptions ( "force!"       => \$force_graphing,
	     "lazy!"        => \$force_lazy,
	     "force-root!"  => \$force_root,
	     "host=s"       => \@limit_hosts,
	     "service=s"    => \@limit_services,
	     "config=s"     => \$conffile,
	     "stdout!"      => \$stdout,
	     "day!"         => \$draw{'day'},
	     "week!"        => \$draw{'week'},
	     "month!"       => \$draw{'month'},
	     "year!"        => \$draw{'year'},
	     "sumweek!"     => \$draw{'sumweek'},
	     "sumyear!"     => \$draw{'sumyear'},
	     "list-images!" => \$list_images,
	     "skip-locking!"=> \$skip_locking,
	     "skip-stats!"  => \$skip_stats,
	     "debug!"       => \$DEBUG,
	     "version!"     => \$do_version,
	     "cron!"        => \$cron,
	     "help"         => \$do_usage );

if ($do_usage)
{
    print "Usage: $0 [options]

Options:
    --[no]force		Force drawing of graphs that are not usually
			drawn due to options in the config file. [--noforce]
    --[no]force-root	Force running, even as root. [--noforce-root]
    --[no]lazy		Only redraw graphs when needed. [--lazy]
    --help		View this message.
    --version		View version information.
    --debug		View debug messages.
    --[no]cron		Behave as expected when run from cron. (Used internally 
			in Munin.)
    --service <service>	Limit graphed services to <service>. Multiple --service
			options may be supplied.
    --host <host>	Limit graphed hosts to <host>. Multiple --host options
    			may be supplied.
    --config <file>	Use <file> as configuration file. [/etc/munin/munin.conf]
    --[no]list-images	List the filenames of the images created. 
    			[--nolist-images]
    --[no]day		Create day-graphs.   [--day]
    --[no]week		Create week-graphs.  [--week]
    --[no]month		Create month-graphs. [--month]
    --[no]year		Create year-graphs.  [--year]
    --[no]sumweek	Create summarised week-graphs.  [--summweek]
    --[no]sumyear	Create summarised year-graphs.  [--sumyear]

";
    exit 0;
}

if ($do_version)
{
    print "munin-graph version $VERSION.\n";
    print "Written by Audun Ytterdal, Jimmy Olsen, Tore Anderson / Linpro AS\n";
    print "\n";
    print "Copyright (C) 2002-2004\n";
    print "This is free software released under the GNU Public License. There is NO\n";
    print "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n";
    exit 0;
}

if ($> == 0 and !$force_root)
{
    print "You are running this program as root, which is neither smart nor necessary. 
If you really want to run it as root, use the --force-root option. Else, run
it as the user \"_munin\". Aborting.\n\n";
    exit (1);
}

my $config= &munin_config ($conffile);

if (&munin_get ($config, "graph_strategy", "cron") ne "cron" and $cron)
{ # We're run from cron, but munin.conf says we use dynamic graph generation
    exit 0;
}

munin_runlock("$config->{rundir}/munin-graph.lock") unless $skip_locking;

unless ($skip_stats)
{
	open (STATS,">$config->{dbdir}/munin-graph.stats.tmp") or logger("Unable to open $config->{dbdir}/munin-graph.stats.tmp");
}

logger("Starting munin-graph");

my @COLOUR = ("#22ff22", "#0022ff", "#ff0000", "#00aaaa", "#ff00ff",
	      "#ffa500", "#cc0000", "#0000cc", "#0080C0", "#8080C0", "#FF0080",
	      "#800080", "#688e23", "#408080", "#808000", "#000000", "#00FF00",
	      "#0080FF", "#FF8000", "#800000", "#FB31FB");


my $range_colour = "#22ff22";
my $single_colour = "#00aa00";

my %times = (
	     "day"   => "-30h",
	     "week"  => "-8d",
	     "month" => "-33d",
	     "year"  => "-400d");

my %resolutions = (
             "day"   => "300",
             "week"  => "1500",
             "month" => "7200",
             "year"  => "86400");

my %sumtimes = ( # time => [ label, seconds-in-period ]
	    "week"   => ["hour", 12],
	    "year"   => ["day", 288]
	);

for my $key ( keys %{$config->{domain}}) {
    my $domain_time= Time::HiRes::time;
    mkdir "$config->{htmldir}/$key",0777;
    logger("Processing domain: $key");
    &process_domain($key);
    $domain_time = sprintf ("%.2f",(Time::HiRes::time - $domain_time));
    logger("Processed domain: $key ($domain_time sec)");
    print STATS "GD|$key|$domain_time\n" unless $skip_stats;
}


$graph_time = sprintf ("%.2f",(Time::HiRes::time - $graph_time));
logger("Munin-graph finished ($graph_time sec)");
print STATS "GT|total|$graph_time\n" unless $skip_stats;
rename ("$config->{dbdir}/munin-graph.stats.tmp", "$config->{dbdir}/munin-graph.stats");
close STATS unless $skip_stats;
close $log;

munin_removelock("$config->{rundir}/munin-graph.lock") unless $skip_locking;

# ### The End

sub process_domain {
    my ($domain) = @_;
    for my $key ( keys %{$config->{domain}->{$domain}->{node}}) {
	my $node_time= Time::HiRes::time;

	process_node($domain,$key ,$config->{domain}->{$domain}->{node}->{$key} );	
	$node_time = sprintf ("%.2f",(Time::HiRes::time - $node_time));
	logger ("Processed node: $key ($node_time sec)");
	print STATS "GN|$domain|$key|$node_time\n" unless $skip_stats;
	
    }
}

sub get_title {
    my $node    = shift;
    my $service = shift;
    my $scale   = shift;

    return ($node->{client}->{$service}->{'graph_title'}?
	      $node->{client}->{$service}->{'graph_title'}:$service) .
	      " - by $scale";
}

sub get_custom_graph_args
{
    my $node    = shift;
    my $service = shift;
    my $result  = [];

    if ($node->{client}->{$service}->{graph_args}) {
	push @$result, split /\s/,$node->{client}->{$service}->{graph_args};
	return $result;
    }
    else
    {
	return undef;
    }
}

sub get_vlabel
{
    my $node    = shift;
    my $service = shift;
    my $scale   = shift;

    if ($node->{client}->{$service}->{graph_vlabel}) {
	(my $res = $node->{client}->{$service}->{graph_vlabel}) =~ s/\$\{graph_period\}/$scale/g;
	return $res;
    }
    elsif ($node->{client}->{$service}->{graph_vtitle})
    {
	return $node->{client}->{$service}->{graph_vtitle};
    }
    return undef;
}

sub should_scale
{
    my $node    = shift;
    my $service = shift;

    if (defined $node->{client}->{$service}->{graph_scale})
    {
	return &munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1);
    }
    elsif (defined $node->{client}->{$service}->{graph_noscale})
    {
	return ! &munin_get_bool_val ($node->{client}->{$service}->{graph_noscale}, 0);
    }

    return 1;
}

sub get_header {
    my $node    = shift;
    my $config  = shift;
    my $domain  = shift;
    my $host    = shift;
    my $service = shift;
    my $scale   = shift;
    my $sum     = shift;
    my $result  = [];
    my $tmp_field;

    # Picture filename
    push @$result, &munin_get_picture_filename ($config, $domain, $host, $service, $scale, $sum||undef);

    # Title
    push @$result, ("--title", &get_title ($node, $service, $scale));

    # When to start the graph
    push @$result, "--start",$times{$scale};

    # Custom graph args, vlabel and graph title
    if (defined ($tmp_field = &get_custom_graph_args ($node, $service))) {
	push (@$result, @{$tmp_field});
    }
    if (defined ($tmp_field = &get_vlabel ($node, $service, munin_get ($config, "graph_period", "second", $domain, $host, $service)))) {
	push @$result, ("--vertical-label", $tmp_field);
    }

    push @$result,"--height", &munin_get ($config, "graph_height", "175", $domain, $host, $service);
    push @$result,"--width", &munin_get ($config, "graph_width", "400", $domain, $host, $service);
    push @$result,"--imgformat", "PNG";
    push @$result,"--lazy" if ($force_lazy);

    push (@$result, "--units-exponent", "0") 
	if (! &should_scale ($node, $service));

    return $result;
}

sub get_sum_command
{
    my $node    = shift;
    my $service = shift;
    my $field   = shift;

    if (defined $node->{client}->{$service}->{$field.".special_sum"})
    {
	return $node->{client}->{$service}->{$field.".special_sum"};
    }
    elsif (defined $node->{client}->{$service}->{$field.".sum"})
    {
	return $node->{client}->{$service}->{$field.".sum"};
    }

    return undef;
}

sub get_stack_command
{
    my $node    = shift;
    my $service = shift;
    my $field   = shift;

    if (defined $node->{client}->{$service}->{$field.".special_stack"})
    {
	return $node->{client}->{$service}->{$field.".special_stack"};
    }
    elsif (defined $node->{client}->{$service}->{$field.".stack"})
    {
	return $node->{client}->{$service}->{$field.".stack"};
    }

    return undef;
}

sub expand_specials
{
    my $node    = shift;
    my $config  = shift;
    my $domain  = shift;
    my $host    = shift;
    my $service = shift;
    my $preproc = shift;
    my $order   = shift;
    my $single  = shift;
    my $result  = [];

    my $fieldnum = 0;
    for my $field (@$order) { # Search for 'specials'...

	if ($field =~ /^-(.+)$/)
	{
	    $field = $1;
	    unless (defined $node->{client}->{$service}->{$field.".graph"} or
		    defined $node->{client}->{$service}->{$field.".skipdraw"})
	    {
		$node->{client}->{$service}->{$field.".graph"} = "no";
	    }
	}

	$fieldnum++;
	my $tmp_field;
	if (defined ($tmp_field = &get_stack_command ($node, $service, $field)))
	{
	    print "DEBUG: Doing special_stack...\n" if $DEBUG;
	    my @spc_stack = ();
	    foreach my $pre (split (/\s+/, $tmp_field))
	    {
		(my $name = $pre) =~ s/=.+//;
		if (!@spc_stack)
		{
		    $node->{client}->{$service}->{$name.".draw"} = $node->{client}->{$service}->{$field.".draw"};
		    $node->{client}->{$service}->{$field.".process"} = "no";
		}
		else
		{
		    $node->{client}->{$service}->{$name.".draw"} = "STACK";
		}
		push (@spc_stack, $name);
		push (@$preproc, $pre);
		push @$result, "$name.label";
		push @$result, "$name.draw";
		push @$result, "$name.cdef";

		$node->{client}->{$service}->{$name.".label"} = $name;
		$node->{client}->{$service}->{$name.".cdef"} = "$name,UN,0,$name,IF";
		if (exists $node->{client}->{$service}->{$field.".cdef"} and !exists $node->{client}->{$service}->{$name.".onlynullcdef"})
		{
		    print "NotOnlynullcdef ($field)...\n" if $DEBUG;
		    $node->{client}->{$service}->{$name.".cdef"} .= "," .
			$node->{client}->{$service}->{$field.".cdef"};
		    $node->{client}->{$service}->{$name.".cdef"} =~ s/\b$field\b/$name/g;
		}
		else
		{
		    print "Onlynullcdef ($field)...\n" if $DEBUG;
		    $node->{client}->{$service}->{$name.".onlynullcdef"} = 1;
		    push @$result, "$name.onlynullcdef";
		}
	    }
	}
	elsif (defined ($tmp_field = &get_sum_command ($node, $service, $field)))
	{
	    my @spc_stack = ();
	    my $last_name = "";
	    print "DEBUG: Doing special_sum...\n" if $DEBUG;

		if (@$order == 1 or 
			@$order == 2 && $node->{client}->{$service}->{$field.".negative"}) 
		{
			$single = 1;
		}
		
	    foreach my $pre (split (/\s+/, $tmp_field))
	    {
		(my $path = $pre) =~ s/.+=//;
		my $name = "z".$fieldnum."_".scalar (@spc_stack);
		$last_name = $name;

		$node->{client}->{$service}->{$name.".cdef"}  = "$name,UN,0,$name,IF";
		$node->{client}->{$service}->{$name.".graph"} = "no";
		$node->{client}->{$service}->{$name.".label"} = $name;
		push @$result, "$name.cdef";
		push @$result, "$name.graph";
		push @$result, "$name.label";

		push (@spc_stack, $name);
		push (@$preproc, "$name=$pre");
	    }
	    $node->{client}->{$service}->{$last_name.".cdef"} .=
		"," . join (',+,', @spc_stack[0 .. @spc_stack-2]) . ',+';
	    if (exists $node->{client}->{$service}->{$field.".cdef"} and 
		    length $node->{client}->{$service}->{$field.".cdef"})
	    { # Oh bugger...
		my $tc = $node->{client}->{$service}->{$field.".cdef"};
		print "Oh bugger...($field)...\n" if $DEBUG;
		$tc =~ s/\b$field\b/$node->{client}->{$service}->{$last_name.".cdef"}/;
		$node->{client}->{$service}->{$last_name.".cdef"} = $tc;
	    }
	    $node->{client}->{$service}->{$field.".process"} = "no";
	    $node->{client}->{$service}->{$last_name.".draw"} = $node->{client}->{$service}->{$field.".draw"};
	    $node->{client}->{$service}->{$last_name.".label"} = $node->{client}->{$service}->{$field.".label"};
	    if (defined $node->{client}->{$service}->{$field.".graph"})
	    {
		$node->{client}->{$service}->{$last_name.".graph"} = $node->{client}->{$service}->{$field.".graph"};
	    }
	    else
	    {
		$node->{client}->{$service}->{$last_name.".graph"} = "yes";
	    }
	    if (defined $node->{client}->{$service}->{$field.".negative"})
	    {
		$node->{client}->{$service}->{$last_name.".negative"} = $node->{client}->{$service}->{$field.".negative"};;
	    }
	    $node->{client}->{$service}->{$field.".realname"} = $last_name;
	    print "Setting node->{client}->{$service}->{$field} -> realname = $last_name...\n" if $DEBUG;
	}
	elsif (defined $node->{client}->{$service}->{$field.".negative"})
	{
	    my $nf = $node->{client}->{$service}->{$field.".negative"};
	    unless (defined $node->{client}->{$service}->{$nf.".graph"} or
		    defined $node->{client}->{$service}->{$nf.".skipdraw"})
	    {
		$node->{client}->{$service}->{$nf.".graph"} = "no";
	    }
	}
    }
    return $result;
}

sub single_value
{
    my $node    = shift;
    my $config  = shift;
    my $domain  = shift;
    my $host    = shift;
    my $service = shift;
    my $field   = shift;
    my $order   = shift;

    return 1 if @$order == 1;
    return 1 if (@$order == 2 and $node->{client}->{$service}->{$field.".negative"});

    my $graphable = 0;
    if (!defined $node->{client}->{$service}->{"graphable"})
    {
#	foreach my $field (keys %{$node->{client}->{$service}})
	foreach my $field (&munin_get_field_order ($node, $config, $domain, $host, $service))
	{
	    print "DEBUG: single_value: Checking field \"$field\".\n" if $DEBUG;
	    if ($field =~ /^([^\.]+)\.label/ or $field =~ /=/)
	    {
		$graphable++ if &munin_draw_field ($node, $service, $1);
	    }
	}
	$node->{client}->{$service}->{"graphable"} = $graphable;
    }
    return 1 if ($node->{client}->{$service}->{"graphable"} == 1);
    
    return 0;
}

sub get_field_name
{
    my $name = shift;
    
    $name = substr (Digest::MD5::md5_hex ($name), -15)
	if (length $name > 15);

    return $name;
}

sub process_field {
    my $node    = shift;
    my $service = shift;
    my $field   = shift;
    return (&munin_get_bool_val ($node->{client}->{$service}->{$field.".process"}, 1));
}

sub process_node {
    my ($domain,$name,$node) = @_;

    # See if we should skip it because of command-line arguments
    return if (@limit_hosts and not grep (/^$name$/, @limit_hosts));

    # Make my graphs
    logger ("Processing $name") if $DEBUG;
    for my $service (keys %{$node->{client}}) {
	my $service_time= Time::HiRes::time;
	my $lastupdate = 0;
	my $now  = time;
	my $fnum = 0;
	my @rrd;
	my @added = ();

	# See if we should skip the service 
	next if (&skip_service ($node, $service));

	my $field_count = 0;
	my $max_field_len = 0;
	my @field_order = ();
	my $rrdname;
	my $force_single_value;

	@field_order =  @{&munin_get_field_order ($node, $config, $domain, $name, $service, \$force_single_value)};

	# Array to keep 'preprocess'ed fields.
	my @rrd_preprocess = ();
	print "DEBUG: Expanding specials \"", join "\",\"", @field_order, "\".\n" if $DEBUG;
	@added = @{&expand_specials ($node, $config, $domain, $name, $service, \@rrd_preprocess, \@field_order)};

	@field_order = (@rrd_preprocess, @field_order);
	print "DEBUG: Checking field lengths \"", join "\",\"", (@rrd_preprocess, @field_order), "\".\n" if $DEBUG;

	# Get max label length
	$max_field_len =
	  &munin_get_max_label_length ($node, $config, $domain, $name,
				       $service, \@field_order);
	# my $global_headers = ($max_field_len >= 16);
	# Global headers makes the value tables easier to read no matter how
	# wide the labels are.
	my $global_headers = 1;

	# Array to keep negative data until we're finished with positive.
	my @rrd_negatives = ();
	my $filename = "unknown";
	my %total_pos;
	my %total_neg;
	print "DEBUG: Treating fields \"", join "\",\"", @field_order, "\".\n" if $DEBUG;
	for my $field (@field_order) {
	    my $path  = undef;
	    if ($field =~ s/=(.+)//)
	    {
		$path = $1;
	    }

	    next unless &process_field ($node, $service, $field);
	    print "DEBUG: Processing field \"$field\".\n" if $DEBUG;

	    if ($field_count == 0 and munin_get ($config, "draw", "LINE2", $domain, $name, $service, $field) eq "STACK")
	    { # Illegal -- first field is a STACK
		logger ("ERROR: First field (\"$field\") of graph \"$domain\" :: \"$name\" :: \"$service\" is STACK. STACK can only be drawn after a LINEx or AREA.");
	    }
		
	    # Getting name of rrd file
	    $filename = &munin_get_rrd_filename ($node, $config, $domain, $name, $service, $field, $path);

	    my $update = RRDs::last ($filename);
	    $update = 0 if ! defined $update;
	    if ($update > $lastupdate)
	    {
		$lastupdate = $update;
	    }

	    my $rrdfield = ($node->{client}->{$service}->{$field.".rrdfield"} || "42");
	    my $single_value = $force_single_value || &single_value ($node, $config, $domain, $name, $service, $field, \@field_order);
	    my $has_negative = exists $node->{client}->{$service}->{$field.".negative"};

	    # Trim the fieldname to make room for other field names.
	    $rrdname = &get_field_name ($field);
	    if ($rrdname ne $field) # A change was made
	    {
		set_cdef_name ($node->{client}->{$service}, $field, $rrdname);
	    }

	    push (@rrd, "DEF:g$rrdname=" .
		  $filename . ":" . $rrdfield . ":AVERAGE");
	    push (@rrd, "DEF:i$rrdname=" .
		  $filename . ":" . $rrdfield . ":MIN");
	    push (@rrd, "DEF:a$rrdname=" .
		  $filename . ":" . $rrdfield . ":MAX");
	    if (exists $node->{client}->{$service}->{$field.".onlynullcdef"} and $node->{client}->{$service}->{$field.".onlynullcdef"})
	    {
		push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : ""));
	    }
	    if (($node->{client}->{$service}->{$field.".type"}||"GAUGE") ne "GAUGE" and graph_by_minute ($config, $domain, $name, $service))
	    {
		push (@rrd, &expand_cdef($node->{client}->{$service}, \$rrdname, "$field,60,*"));
	    }
	    if ($node->{client}->{$service}->{$field.".cdef"})
	    {
		push (@rrd, &expand_cdef($node->{client}->{$service}, \$rrdname, $node->{client}->{$service}->{$field.".cdef"}));
		push (@rrd, "CDEF:c$rrdname=g$rrdname");
		print "DEBUG: Field name after cdef set to $rrdname\n" if $DEBUG;
	    }
	    elsif (!(exists $node->{client}->{$service}->{$field.".onlynullcdef"} and $node->{client}->{$service}->{$field.".onlynullcdef"}))
	    {
		push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : ""));
	    }

	    next unless &munin_draw_field ($node, $service, $field);
	    print "DEBUG: Drawing field \"$field\".\n" if $DEBUG;

	    if ($single_value) # Only one field. Do min/max range.	
	    {
		push (@rrd, "CDEF:min_max_diff=a$rrdname,i$rrdname,-");
		push (@rrd, "CDEF:re_zero=min_max_diff,min_max_diff,-") 
		    unless ($node->{client}->{$service}->{$field.".negative"});
		push (@rrd, "AREA:i$rrdname#ffffff");
		push (@rrd, "STACK:min_max_diff$range_colour");
		push (@rrd, "LINE2:re_zero#000000")
		    unless ($node->{client}->{$service}->{$field.".negative"});
	    }

	    if ($has_negative and !@rrd_negatives) # Push "global" headers...
	    {
		push (@rrd, "COMMENT:" . (" " x $max_field_len));
		push (@rrd, "COMMENT:Cur (-/+)");
		push (@rrd, "COMMENT:Min (-/+)");
		push (@rrd, "COMMENT:Avg (-/+)");
		push (@rrd, "COMMENT:Max (-/+) \\j");
	    }
	    elsif ($global_headers == 1)
	    {
		push (@rrd, "COMMENT:" . (" " x $max_field_len));
		push (@rrd, "COMMENT: Cur$RRDkludge:");
		push (@rrd, "COMMENT:Min$RRDkludge:");
		push (@rrd, "COMMENT:Avg$RRDkludge:");
		push (@rrd, "COMMENT:Max$RRDkludge:  \\j");
		$global_headers++;
	    }

	    my $custom_colour = $node->{client}->{$service}->{$field.".colour"};
	    $custom_colour = "#" . $custom_colour if $custom_colour;

	    push (@rrd, ($node->{client}->{$service}->{$field.".draw"} || "LINE2") .
		  ":g$rrdname" . 
		  ($custom_colour || ($single_value ? $single_colour : $COLOUR[$field_count++%@COLOUR])) . ":" .
		  (escape ($node->{client}->{$service}->{"$field.label"}) || escape ($field))
		  . (" " x ($max_field_len + 1 -
			    length ($node->{client}->{$service}->{"$field.label"} || $field))));

	    # Check for negative fields (typically network traffic)
	    if ($has_negative)
	    {
		my $negfield = &orig_to_cdef ($node->{client}->{$service}, $node->{client}->{$service}->{$field.".negative"});
		print "DEBUG: negfield = $negfield\n" if $DEBUG;
		if (exists $node->{client}->{$service}->{$negfield.".realname"})
		{
		    $negfield = $node->{client}->{$service}->{$negfield.".realname"};
		}
		
		if (!@rrd_negatives) # zero-line, to redraw zero afterwards.
		{
		    push (@rrd_negatives, "CDEF:re_zero=g$negfield,UN,0,0,IF");
		}

		push (@rrd_negatives, "CDEF:ng$negfield=g$negfield,-1,*");

		if ($single_value) # Only one field. Do min/max range.	
		{
			push (@rrd, "CDEF:neg_min_max_diff=i$negfield,a$negfield,-");
			push (@rrd, "CDEF:ni$negfield=i$negfield,-1,*");
			push (@rrd, "AREA:ni$negfield#ffffff");
			push (@rrd, "STACK:neg_min_max_diff$range_colour");
		}

		push (@rrd_negatives, ($node->{client}->{$service}->{$negfield.".draw"} || "LINE2") .
		    ":ng$negfield" . 
		    ((defined $single_value and $single_value) ? $single_colour : $COLOUR[($field_count-1)%@COLOUR]));

		# Draw HRULEs
		my $linedef = munin_get ($config, "line", undef, $domain, $name, $service, $node->{client}->{$service}->{$field.".negative"});
		if ($linedef)
		{
		    my ($number, $colour, $label) = split (/:/, $linedef, 3);
		    push (@rrd_negatives, "HRULE:".$number.
			($colour ? "#$colour" :
			  ((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR]))
			);
		}
		elsif ($node->{client}->{$service}->{"$negfield.warn"})
		{
		    push (@rrd_negatives, "HRULE:".$node->{client}->{$service}->{$node->{client}->{$service}->{$field.".negative"}.".warn"}.
			((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR]));
		}

		push (@rrd, "GPRINT:c$negfield:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:c$rrdname:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:i$negfield:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:i$rrdname:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:g$negfield:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:g$rrdname:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:a$negfield:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:a$rrdname:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j");
		push (@{$total_pos{'min'}}, "i$rrdname");
		push (@{$total_pos{'avg'}}, "g$rrdname");
		push (@{$total_pos{'max'}}, "a$rrdname");
		push (@{$total_neg{'min'}}, "i$negfield");
		push (@{$total_neg{'avg'}}, "g$negfield");
		push (@{$total_neg{'max'}}, "a$negfield");
	    }
	    else
	    {
		push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers;
		push (@rrd, "GPRINT:c$rrdname:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, "yes")?"%s":"") . "");
		push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers;
		push (@rrd, "GPRINT:i$rrdname:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers;
		push (@rrd, "GPRINT:g$rrdname:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers;
		push (@rrd, "GPRINT:a$rrdname:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j");
		push (@{$total_pos{'min'}}, "i$rrdname");
		push (@{$total_pos{'avg'}}, "g$rrdname");
		push (@{$total_pos{'max'}}, "a$rrdname");
	    }


	    # Draw HRULEs
	    my $linedef = munin_get ($config, "line", undef, $domain, $name, $service, $field);
	    if ($linedef)
	    {
		my ($number, $colour, $label) = split (/:/, $linedef, 3);
		$label =~ s/:/\\:/g if defined $label;
		push (@rrd, "HRULE:".$number.
		    ($colour ? "#$colour" :
		      ((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR])) .
		      ((defined $label and length ($label)) ? ":$label" : ""), 
		      "COMMENT: \\j"
		    );
	    }
	    elsif ($node->{client}->{$service}->{"$field.warn"})
	    {
		push (@rrd, "HRULE:".$node->{client}->{$service}->{"$field.warn"}.($single_value ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR]));
	    }
	}

	if (@rrd_negatives)
	{
	    push (@rrd, @rrd_negatives);
	    push (@rrd, "LINE2:re_zero#000000"); # Redraw zero.
	    if (exists $node->{client}->{$service}->{graph_total} and 
		    exists $total_pos{'min'} and exists $total_neg{'min'} and 
		    @{$total_pos{'min'}} and @{$total_neg{'min'}})
	    {
		push (@rrd, "CDEF:ipostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'min'}}).(",+" x (@{$total_pos{'min'}}-1)));
		push (@rrd, "CDEF:gpostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'avg'}}).(",+" x (@{$total_pos{'avg'}}-1)));
		push (@rrd, "CDEF:apostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'max'}}).(",+" x (@{$total_pos{'max'}}-1)));
		push (@rrd, "CDEF:inegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'min'}}).(",+" x (@{$total_neg{'min'}}-1)));
		push (@rrd, "CDEF:gnegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'avg'}}).(",+" x (@{$total_neg{'avg'}}-1)));
		push (@rrd, "CDEF:anegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'max'}}).(",+" x (@{$total_neg{'max'}}-1)));
		push (@rrd, "CDEF:dpostotal=ipostotal,UN,ipostotal,UNKN,IF");
		push (@rrd, "LINE1:dpostotal#000000:" . $node->{client}->{$service}->{graph_total} . (" " x ($max_field_len - length ($node->{client}->{$service}->{graph_total}) + 1)));
		push (@rrd, "GPRINT:gnegtotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:gpostotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:inegtotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:ipostotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:gnegtotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:gpostotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
		push (@rrd, "GPRINT:anegtotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g");
		push (@rrd, "GPRINT:apostotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j");
	    }
	}
	elsif (exists $node->{client}->{$service}->{graph_total} and exists $total_pos{'min'} and @{$total_pos{'min'}})
	{
	    push (@rrd, "CDEF:ipostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'min'}}).(",+" x (@{$total_pos{'min'}}-1)));
	    push (@rrd, "CDEF:gpostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'avg'}}).(",+" x (@{$total_pos{'avg'}}-1)));
	    push (@rrd, "CDEF:apostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'max'}}).(",+" x (@{$total_pos{'max'}}-1)));
	    
	    push (@rrd, "CDEF:dpostotal=ipostotal,UN,ipostotal,UNKN,IF");
	    push (@rrd, "LINE1:dpostotal#000000:" . $node->{client}->{$service}->{graph_total} . (" " x ($max_field_len - length ($node->{client}->{$service}->{graph_total}) + 1)));
	    push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:gpostotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
	    push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:ipostotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
	    push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:gpostotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "");
	    push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:apostotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j");
	}

	for my $time (keys %times) {
	    next unless ($draw{$time});
	    my @complete = ();
	    if ($RRDkludge) {
		push (@complete,
 		      '--font' ,'LEGEND:7:/usr/local/libexec/munin/VeraMono.ttf',
 		      '--font' ,'UNIT:7:/usr/local/libexec/munin/VeraMono.ttf',

 		      '--font' ,'AXIS:7:/usr/local/libexec/munin/VeraMono.ttf');
	    }

	    logger ("Processing $name -> $time") if $DEBUG;

	    # Do the header (title, vtitle, size, etc...)
	    push @complete, @{&get_header ($node, $config, $domain, $name, $service, $time)};
	    if ($LINEkluge) {
		@rrd = map { s/LINE3:/LINE2.2:/; $_; } @rrd;
		@rrd = map { s/LINE2:/LINE1.6:/; $_; } @rrd;
		# LINE1 is thin enough.
	    }
	    push @complete, @rrd;

	    push (@complete, "COMMENT:Last update$RRDkludge: " . 
		  RRDescape(scalar localtime($lastupdate)) .  "\\r");

	    if (time - 300 < $lastupdate) {
		push @complete, "--end",
		  (int($lastupdate/$resolutions{$time}))*$resolutions{$time};
	    }
	    print "\n\nrrdtool \"graph\" \"",
	      join ("\"\n\t\"",@complete), "\"\n" if $DEBUG;
	    RRDs::graph (@complete);
	    if (my $ERROR = RRDs::error) {
		logger ("Unable to graph $filename: $ERROR");
	    } elsif ($list_images) {
		# Command-line option to list images created
		print &munin_get_picture_filename ($config, $domain, $name,
						   $service, $time),"\n";
	    }
	}

	if (&munin_get_bool_val ($node->{client}->{$service}->{"graph_sums"}, 0))
	{
	    foreach my $time (keys %sumtimes)
	    {
		next unless ($draw{"sum".$time});
		my @rrd_sum;
		push @rrd_sum, @{&get_header ($node, $config, $domain, $name, $service, $time, 1)};

		if (time - 300 < $lastupdate)
		{
			push @rrd_sum, "--end",(int($lastupdate/$resolutions{$time}))*$resolutions{$time};
		}
		push @rrd_sum, @rrd;
		push (@rrd_sum, "COMMENT:Last update$RRDkludge: " . RRDescape(scalar localtime($lastupdate)) .  "\\r");

		my $labelled = 0;
		my @defined = ();
		for (my $index = 0; $index <= $#rrd_sum; $index++)
		{
		    if ($rrd_sum[$index] =~ /^(--vertical-label|-v)$/)
		    {
			(my $label = $node->{client}->{$service}->{graph_vlabel}) =~ s/\$\{graph_period\}/$sumtimes{$time}[0]/g;
			splice (@rrd_sum, $index, 2, ("--vertical-label", $label));
			$index++;
			$labelled++;
		    }
		    elsif ($rrd_sum[$index] =~ /^(LINE[123]|STACK|AREA|GPRINT):([^#:]+)([#:].+)$/)
		    {
			my ($pre, $fname, $post) = ($1, $2, $3);
			next if $fname eq "re_zero";
			if ($post =~ /^:AVERAGE/)
			{
			    splice (@rrd_sum, $index, 1, $pre . ":x$fname" . $post);
			    $index++;
			    next;
			}
			next if grep /^x$fname$/, @defined;
			push @defined, "x$fname";
			my @replace;

			if (!defined ($node->{client}->{$service}->{$fname.".type"}) or $node->{client}->{$service}->{$fname.".type"} ne "GAUGE")
			{
			    if ($time eq "week")
			    { # Every plot is half an hour. Add two plots and multiply, to get per hour
				if (graph_by_minute ($config, $domain, $name, $service))
				{ # Already multiplied by 60
				    push @replace, "CDEF:x$fname=PREV($fname),UN,0,PREV($fname),IF,$fname,+,5,*,6,*";
				}
				else
				{
				    push @replace, "CDEF:x$fname=PREV($fname),UN,0,PREV($fname),IF,$fname,+,300,*,6,*";
				}
			    }
			    else
			    { # Every plot is one day exactly. Just multiply.
				if (graph_by_minute ($config, $domain, $name, $service))
				{ # Already multiplied by 60
				    push @replace, "CDEF:x$fname=$fname,5,*,288,*";
				}
				else
				{
				    push @replace, "CDEF:x$fname=$fname,300,*,288,*";
				}
			    }
			}
			push @replace, $pre . ":x$fname" . $post;
			splice (@rrd_sum, $index, 1, @replace);
			$index++;
		    }
		    elsif ($rrd_sum[$index] =~ /^(--lower-limit|--upper-limit|-l|-u)$/)
		    {
			$index++;
			$rrd_sum[$index] = $rrd_sum[$index] * 300 * $sumtimes{$time}->[1];
		    }
		}
		unless ($labelled)
		{
		    my $label = $node->{client}->{$service}->{"graph_vlabel_sum_$time"} || $sumtimes{$time}->[0];
		    unshift @rrd_sum, "--vertical-label", $label;
		}

		print "\n\nrrdtool \"graph\" \"", join ("\"\n\t\"",@rrd_sum), "\"\n" if $DEBUG;
		RRDs::graph (@rrd_sum);
		if (my $ERROR = RRDs::error) {
		    logger ("Unable to graph $filename: $ERROR");
		}
		elsif ($list_images) # Command-line option to list images created
		{
		    print &munin_get_picture_filename ($config, $domain, $name, $service, $time, 1), "\n";
		}
	    }
	}

	$service_time = sprintf ("%.2f",(Time::HiRes::time - $service_time));
	logger ("Graphed service : $service ($service_time sec * 4)");
	print STATS "GS|$domain|$name|$service|$service_time\n" unless $skip_stats;

	foreach (@added)
	{
	    delete $node->{client}->{$service}->{$_} if exists $node->{client}->{$service}->{$_};
	}
	@added = ();
    }
    
}

sub graph_by_minute
{
    my $config  = shift;
    my $domain  = shift;
    my $name    = shift;
    my $service = shift;

    return (munin_get ($config, "graph_period", "second", $domain, $name, $service) eq "minute");
}

sub orig_to_cdef
{
    my $service = shift;
    my $field   = shift;

    if (defined $service->{$field.".cdef_name"})
    {
	return &orig_to_cdef ($service, $service->{$field.".cdef_name"});
    }
    return $field;
}

sub set_cdef_name
{
    my $service = shift;
    my $field   = shift;
    my $new     = shift;

    $service->{$field.".cdef_name"} = $new;
    print "DEBUG: set_cdef_name from $field to $new.\n" if $DEBUG;
}

sub skip_service
{
    my $node = shift;
    my $service = shift;

    # Check to make sure that service exists
    return 1 unless (ref $node->{client}->{$service});

    # See if we should skip it because of conf-options
    return 1 if ($node->{client}->{$service}->{'graph'} and 
	($node->{client}->{$service}->{'graph'} eq "no" ||
	($node->{client}->{$service}->{'graph'} eq "on-demand") && !$force_graphing));

    # See if we should skip it because of command-line arguments
    return 1 if (@limit_services and not grep (/^$service$/, @limit_services));

    # Don't skip
    return 0;
}

sub expand_cdef
{
    my $service     = shift;
    my $cfield_ref  = shift;
    my $cdef        = shift;

    my $new_field = &get_field_name ("cdef$$cfield_ref");

    my ($max, $min, $avg) = ("CDEF:a$new_field=$cdef", "CDEF:i$new_field=$cdef", "CDEF:g$new_field=$cdef");

    foreach my $field (keys %$service)
    {
	next unless ($field =~ /^(.+)\.label$/);
	my $fieldname = $1;		
	my $rrdname = &orig_to_cdef ($service, $fieldname);
	if ($cdef =~ /\b$fieldname\b/)
	{
		$max =~ s/([,=])$fieldname([,=]|$)/$1a$rrdname$2/g;
		$min =~ s/([,=])$fieldname([,=]|$)/$1i$rrdname$2/g;
		$avg =~ s/([,=])$fieldname([,=]|$)/$1g$rrdname$2/g;
	}
    }

    &set_cdef_name ($service, $$cfield_ref, $new_field);
    $$cfield_ref = $new_field;

    return ($max, $min, $avg);
}

sub logger_open {
    my $dirname = shift;

    if (!$log->opened)
    {
	  unless (open ($log, ">>$dirname/munin-graph.log"))
	  {
		  print STDERR "Warning: Could not open log file \"$dirname/munin-graph.log\" for writing: $!";
	  }
    }
}

sub logger {
  my ($comment) = @_;
  my $now = strftime "%b %d %H:%M:%S", localtime;

  print "$now - $comment\n" if $stdout;
  if ($log->opened)
  {
	  print $log "$now - $comment\n";
  }
  else
  {
	  if (defined $config->{logdir})
	  {
		  if (open ($log, ">>$config->{logdir}/munin-graph.log"))
		  {
			  print $log "$now - $comment\n";
			  $log->flush;
			  close (STDERR);
			  open (STDERR, ">&", $log);
		  }
		  else
		  {
			  print STDERR "Warning: Could not open log file \"$config->{logdir}/munin-graph.log\" for writing: $!";
			  print STDERR "$now - $comment\n";
		  }
	  }
	  else
	  {
		  print STDERR "$now - $comment\n";
	  }
    }
}

sub parse_path
{
    my ($path, $domain, $node, $service, $field) = @_;
    my $filename = "unknown";

    if ($path =~ /^\s*([^:]*):([^:]*):([^:]*):([^:]*)\s*$/)
    {
	$filename = munin_get_filename ($config, $1, $2, $3, $4);
    }
    elsif ($path =~ /^\s*([^:]*):([^:]*):([^:]*)\s*$/)
    {
	$filename = munin_get_filename ($config, $domain, $1, $2, $3);
    }
    elsif ($path =~ /^\s*([^:]*):([^:]*)\s*$/)
    {
	$filename = munin_get_filename ($config, $domain, $node, $1, $2);
    }
    elsif ($path =~ /^\s*([^:]*)\s*$/)
    {
	$filename = munin_get_filename ($config, $domain, $node, $service, $1);
    }
    return $filename;
}

sub escape
{
    my $text = shift;
    return undef if not defined $text;
    $text =~ s/\\/\\\\/g;
    $text =~ s/:/\\:/g;
    return $text;
}

sub RRDescape
{
    my $text = shift;
    return $RRDs::VERSION < 1.2 ? $text : escape($text);
}

1;

=head1 NAME

munin-graph - A program to create graphs from data contained in rrd-files.

=head1 SYNOPSIS

munin-graph [--options]

=head1 OPTIONS

=over 5

=item B<< --[no]force >>

If set, force drawing of graphs that are not usually drawn due to options in the config file. [--noforce]

=item B<< --[no]lazy >>

If set, only redraw graphs when it would look different from the existing one. [--lazy]

=item B<< --help >>

View help.

=item B<< --[no]force-root >>

Force running as root (stupid and unnecessary). [--noforce-root]

=item B<< --[no]debug >>

If set, view debug messages. [--nodebug]

=item B<< --service <service> >>

Limit graphed services to E<lt>serviceE<gt>. Multiple --service options may be supplied. [unset]

=item B<< --host <host> >>

Limit graphed hosts to E<lt>hostE<gt>. Multiple --host options may be supplied. [unset]

=item B<< --config <file> >>

Use E<lt>fileE<gt> as configuration file. [/etc/munin/munin.conf]

=item B<< --[no]list-images >>

If set, list the filenames of the images created. [--nolist-images]

=item B<< --[no]day >>

If set, create day-based graphs. [--day]

=item B<< --[no]week >>

If set, create week-based graphs. [--week]

=item B<< --[no]month >>

If set, create month-based graphs. [--month]

=item B<< --[no]year >>

If set, create year-based graphs. [--year]

=back

=head1 DESCRIPTION

Munin-graph is a part of the package Munin, which is used in combination
with Munin's node.  Munin is a group of programs to gather data from
Munin's nodes, graph them, create html-pages, and optionally warn Nagios
about any off-limit values.

munin-graph does the graphing. It is usually only used from within
munin-cron.  If munin.conf sets "graph_strategy cgi" then munin-graph
does no work, instead munin-html generates references to the graphing
CGI.  Please see http://munin.projects.linpro.no/wiki/CgiHowto for
more information about CGI grpahing.

It checks the rrd-files for updated values, and redraws the graphs if
needed. To force redrawing of graphs (after setup-changes et alia), use
'--nolazy'.

=head1 FILES

	/etc/munin/munin.conf
	/var/db/munin/*
	/var/log/munin/munin-graph
	/var/run/munin/*

=head1 VERSION

This is munin-graph version 0.9.2-3

=head1 AUTHORS

Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt.

=head1 BUGS

munin-graph does, as of now, not check the syntax of the configuration file.

Please report other bugs in the bug tracker at L<http://munin.sf.net/>.

=head1 COPYRIGHT

Copyright (C) 2002-2006 Audun Ytterdal, Jimmy Olsen, Tore Anderson, and Nicolai Langfeldt

This is free software; see the source for copying conditions. There is
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.

This program is released under the GNU General Public License

=head1 SEE ALSO

For information on configuration options, please refer to the man page for
F<munin.conf>.

=cut

# vim: syntax=perl ts=8
