#!/usr/bin/perl -w
# -*- cperl -*-
#
# Copyright (C) 2002-2008 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 1525 2008-03-03 21:34:42Z jo $

$|=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.3.4';

# 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 <<"EOT";
munin-graph version $VERSION.
Written by Audun Ytterdal, Jimmy Olsen, Tore Anderson / Linpro AS

Copyright (C) 2002-2005

This is free software released under the GNU General Public License. There
is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. For details, please refer to the file COPYING that is included
with this software or refer to
  http://www.fsf.org/licensing/licenses/gpl.txt
EOT
    exit 0;
}

if ($> == 0 and !$force_root)
{
    print "You are running this program as root, which is dumb and will
cause many problems later.  Please run it as user 'munin'.
If you really want to run it as root, use the --force-root option.
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 %PALETTE; # Hash of available palettes
my @COLOUR;  # Array of actuall colours to use

{
    no warnings;
    $PALETTE{'old'} = [ # This is the old munin palette.  It lacks contrast.
       qw(#22ff22 #0022ff #ff0000 #00aaaa #ff00ff
	  #ffa500 #cc0000 #0000cc #0080C0 #8080C0 #FF0080
	  #800080 #688e23 #408080 #808000 #000000 #00FF00
	  #0080FF #FF8000 #800000 #FB31FB
	 )];
	
    $PALETTE{'default'} = [ # New default palette.Better contrast,more colours
       #Greens Blues   Oranges Dk yel  Dk blu  Purple  lime    Reds    Gray
     qw(#00CC00 #0066B3 #FF8000 #FFCC00 #330099 #990099 #CCFF00 #FF0000 #808080
        #008F00 #00487D #B35A00 #B38F00         #6B006B #8FB300 #B30000 #BEBEBE
        #80FF80 #80C9FF #FFC080 #FFE680 #AA80FF #EE00CC #FF8080
        #666600 #FFBFFF #00FFCC #CC6699 #999900
       )]; # Line variations: Pure, earthy, dark pastel, misc colours
}


my $palette = &munin_get ($config, "palette", "default");

if (defined($PALETTE{$palette})) {
    @COLOUR=@{$PALETTE{$palette}};
} else {
    die "Unknown palette named by 'palette' keyword: $palette\n";
}

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]
	);

my $watermark = "Munin $VERSION";

# Make array of what is probably needed to graph
my $work_array = [];
if (@limit_hosts) { # Limit what to update if needed
    foreach my $nodename (@limit_hosts) {
        push @$work_array, map { @{munin_find_field ($_->{$nodename}, "graph_title")} } @{munin_find_field($config, $nodename)};
    }
} else { # ...else just search for all adresses to update
    push @$work_array, @{munin_find_field($config, "graph_title")};
}


for my $service (@$work_array) {
    process_service ($service);
}

$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 get_title {
    my $service = shift;
    my $scale   = shift;

    return (munin_get ($service, "graph_title", $service) . " - by $scale");
}

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

    my $args    = munin_get ($service, "graph_args");
    if (defined $args) {
	push @$result, split /\s/,$args;
	return $result;
    } else {
	return undef;
    }
}

sub get_vlabel
{
    my $service = shift;
    my $scale   = munin_get ($service, "graph_period", "second");
    my $res     = munin_get ($service, "graph_vlabel", munin_get ($service, "graph_vtitle"));

    if (defined $res) {
	$res =~ s/\$\{graph_period\}/$scale/g;
    }
    return $res;
}

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

    if (!defined ($ret = munin_get_bool ($service, "graph_scale"))) {
	$ret = !munin_get_bool ($service, "graph_noscale", 0);
    }

    return $ret;
}

sub get_header {
    my $service = shift;
    my $scale   = shift;
    my $sum     = shift;
    my $result  = [];
    my $tmp_field;

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

    # Title
    push @$result, ("--title", get_title ($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 ($service))) {
	push (@$result, @{$tmp_field});
    }
    if (defined ($tmp_field = get_vlabel ($service))) {
	push @$result, ("--vertical-label", $tmp_field);
    }

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

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

    return $result;
}

sub get_sum_command
{
    my $field   = shift;

    if (defined $field->{"special_sum"}) { # Deprecated
	return $field->{"special_sum"};
    }

    return munin_get ($field, "sum");
}

sub get_stack_command
{
    my $field   = shift;

    if (defined $field->{"special_stack"}) { # Deprecated
	return $field->{"special_stack"}; 
    } 
    
    return munin_get ($field, "stack");
}

sub expand_specials
{
    my $service = shift;
    my $preproc = shift;
    my $order   = shift;
    my $single  = shift;
    my $result  = [];

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

	if ($field =~ /^-(.+)$/) { # Invisible field
	    $field = $1;
	    munin_set_var_loc ($service, [$field, "graph"], "no");
	}

	$fieldnum++;
	if ($field =~ /^([^=]+)=(.+)$/) { # Aliased in graph_order
	    my $fname = $1;
	    my $spath = $2;
	    my $src   = munin_get_node_partialpath ($service, $spath);
	    my $sname = munin_get_node_name ($src);

	    next unless defined $src;
	    logger ("Debug: Copying settings from $sname to $fname.") if $DEBUG;

	    foreach my $foption ("draw", "type", "rrdfile", "fieldname", "info") {
		if (!defined $service->{$fname}->{$foption}) {
		    if (defined $src->{$foption}) {
			munin_set_var_loc ($service, [$fname, $foption], $src->{$foption});
		    }
		}
	    }

	    # cdef is special...
	    if (!defined $service->{$fname}->{"cdef"}) {
		if (defined $src->{"cdef"}) {
		    (my $tmpcdef = $src->{"cdef"}) =~ s/([,=])$sname([,=]|$)/$1$fname$2/g;
		    munin_set_var_loc ($service, [$fname, "cdef"], $tmpcdef);
		}
	    }
	    
	    if (!defined $service->{$fname}->{"label"}) {
		munin_set_var_loc ($service, [$fname, "label"], $fname);
	    }
	    munin_set_var_loc ($service, [$fname, "filename"], munin_get_rrd_filename ($src));

	} elsif (defined ($tmp_field = get_stack_command ($service->{$field}))) {
	    logger ("DEBUG: expand_specials ($tmp_field): Doing stack...") if $DEBUG;
	    my @spc_stack = ();
	    foreach my $pre (split (/\s+/, $tmp_field)) {
		(my $name = $pre) =~ s/=.+//;
		if (!@spc_stack) {
		    munin_set_var_loc ($service, [$name, "draw"], munin_get ($service->{$field}, "draw", "LINE2"));
		    munin_set_var_loc ($service, [$field, "process"], "no");
		} else {
		    munin_set_var_loc ($service, [$name, "draw"], "STACK");
		}
		push (@spc_stack, $name);
		push (@$preproc, $pre);
		push @$result, "$name.label";
		push @$result, "$name.draw";
		push @$result, "$name.cdef";

		munin_set_var_loc ($service, [$name, "label"], $name);
		munin_set_var_loc ($service, [$name, "cdef"], "$name,UN,0,$name,IF");
		if (munin_get ($service->{$field}, "cdef") and !munin_get_bool ($service->{$name}, "onlynullcdef", 0)) {
		    logger ("NotOnlynullcdef ($field)...") if $DEBUG;
		    $service->{$name}->{"cdef"} .= "," . $service->{$field}->{"cdef"};
		    $service->{$name}->{"cdef"} =~ s/\b$field\b/$name/g;
		} else {
		    logger ("Onlynullcdef ($field)...") if $DEBUG;
		    munin_set_var_loc ($service, [$name, "onlynullcdef"], 1);
		    push @$result, "$name.onlynullcdef";
		}
	    }
	} elsif (defined ($tmp_field = get_sum_command ($service->{$field}))) {
	    my @spc_stack = ();
	    my $last_name = "";
	    logger ("DEBUG: expand_specials ($tmp_field): Doing sum...") if $DEBUG;

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

		munin_set_var_loc ($service, [$name, "cdef"], "$name,UN,0,$name,IF");
		munin_set_var_loc ($service, [$name, "graph"], "no");
		munin_set_var_loc ($service, [$name, "label"], $name);
		push @$result, "$name.cdef";
		push @$result, "$name.graph";
		push @$result, "$name.label";

		push (@spc_stack, $name);
		push (@$preproc, "$name=$pre");
	    }
	    $service->{$last_name}->{"cdef"} .= "," . join (',+,', @spc_stack[0 .. @spc_stack-2]) . ',+';

	    if (my $tc = munin_get ($service->{$field}, "cdef", 0)) { # Oh bugger...
		print "Oh bugger...($field)...\n" if $DEBUG;
		$tc =~ s/\b$field\b/$service->{$last_name}->{"cdef"}/;
		$service->{$last_name}->{"cdef"} = $tc;
	    }
	    munin_set_var_loc ($service, [$field, "process"], "no");
	    munin_set_var_loc ($service, [$last_name, "draw"], munin_get ($service->{$field}, "draw"));
	    munin_set_var_loc ($service, [$last_name, "label"], munin_get ($service->{$field}, "label"));
	    munin_set_var_loc ($service, [$last_name, "graph"], munin_get ($service->{$field}, "graph", "yes"));

	    if (my $tmp = munin_get($service->{$field}, "negative")) {
		munin_set_var_loc ($service, [$last_name, "negative"], $tmp);
	    }

	    munin_set_var_loc ($service, [$field, "realname"], $last_name);

	} elsif (my $nf = munin_get ($service->{$field}, "negative", 0)) {
	    if (!munin_get_bool ($service->{$nf}, "graph", 1) or munin_get_bool ($service->{$nf}, "skipdraw", 0)) {
		munin_set_var_loc ($service, [$nf, "graph"], "no");
	    }
	}
    }
    return $result;
}

sub single_value
{
    my $service = shift;

    my $graphable = munin_get ($service, "graphable", 0);;
    if (!$graphable) {
	foreach my $field (@{munin_get_field_order ($service)}) {
	    logger ("DEBUG: single_value: Checking field \"$field\".");
	    $graphable++ if munin_draw_field ($service->{$field});
	}
	munin_set_var_loc ($service, ["graphable"], $graphable);
    }
    logger ("Debug: service ". join (' :: ', @{munin_get_node_loc ($service)}) ." has $graphable elements.");
    return ($graphable == 1);
}

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 $field   = shift;
    return munin_get_bool ($field, "process", 1);
}

sub process_service {
    my ($service) = @_;

    # Make my graphs
    my $sname = munin_get_node_name ($service);
    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
    return if (skip_service ($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($service)};

    # Array to keep 'preprocess'ed fields.
    my @rrd_preprocess = ();
    logger ("Debug: Expanding specials for $sname: \"" . join("\",\"", @field_order) . "\".") if $DEBUG;

    @added = @{&expand_specials ($service, \@rrd_preprocess, \@field_order, \$force_single_value)};

    @field_order = (@rrd_preprocess, @field_order);
    logger ("DEBUG: Checking field lengths for $sname: \"" . join("\",\"", @rrd_preprocess) . "\".") if $DEBUG;

    # Get max label length
    $max_field_len = munin_get_max_label_length ($service, \@field_order);

    # Global headers makes the value tables easier to read no matter how
    # wide the labels are.
    my $global_headers = 1;

    # Default format for printing under graph.
    my $avgformat;
    my $rrdformat=$avgformat="%6.2lf";

    if (munin_get ($service, "graph_args", "") =~ /--base\s+1024/) {
	# If the base unit is 1024 then 1012.56 is a valid
	# number to show.  That's 7 positions, not 6.
	$rrdformat=$avgformat="%7.2lf";
    }

    # Plugin specified complete printf format
    $rrdformat = munin_get ($service, "graph_printf", $rrdformat);

    my $rrdscale = '';
    if (munin_get_bool ($service, "graph_scale", 1)) {
	$rrdscale = '%s';
    }

    # Array to keep negative data until we're finished with positive.
    my @rrd_negatives = ();

    my $filename = "unknown";
    my %total_pos;
    my %total_neg;
    my $autostacking=0;

    logger ("DEBUG: Treating fields \"" . join ("\",\"", @field_order) . "\".") if $DEBUG;
    for my $fname (@field_order) {
	my $path     = undef;
	my $field    = undef;

	if ($fname =~ s/=(.+)//) {
	    $path = $1;
	}
	$field = munin_get_node ($service, [$fname]);

	next if (!defined $field or !$field or !process_field ($field));
	logger ("DEBUG: Processing field \"$fname\" [".munin_get_node_name($field)."].") if $DEBUG;

	my $fielddraw = munin_get ($field, "draw", "LINE2");

	if ($field_count == 0 and $fielddraw eq 'STACK') {
	    # Illegal -- first field is a STACK
	    logger ("ERROR: First field (\"$fname\") of graph " . join (' :: ', munin_get_node_loc ($service)) .
		    " is STACK. STACK can only be drawn after a LINEx or AREA.");
	    $fielddraw = "LINE2";
	}

	if ($fielddraw eq 'AREASTACK') {
	    if ($autostacking==0) {
		$fielddraw='AREA';
		$autostacking=1;
	    } else {
		$fielddraw='STACK';
	    }
	}

	if ($fielddraw =~ /LINESTACK(\d+(?:.\d+)?)/ ) {
	    if ($autostacking==0) {
		$fielddraw="LINE$1";
		$autostacking=1;
	    } else {
		$fielddraw='STACK';
	    }
	}

	# Getting name of rrd file
	$filename = munin_get_rrd_filename ($field, $path);

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

	# It does not look like $fieldname.rrdfield is possible to set
	my $rrdfield = munin_get ($field, "rrdfield", "42");

	my $single_value = $force_single_value || single_value ($service);

	my $has_negative = munin_get ($field, "negative");

	# Trim the fieldname to make room for other field names.
	$rrdname = &get_field_name ($fname);
	if ($rrdname ne $fname) {
	    # A change was made
	    munin_set ($field, "cdef_name", $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 (munin_get_bool ($field, "onlynullcdef", 0)) { 
	    push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : ""));
	}

	if (munin_get ($field, "type", "GAUGE") ne "GAUGE" and graph_by_minute ($service)) {
		push (@rrd, expand_cdef($service, \$rrdname, "$fname,60,*"));
	}

	if (my $tmpcdef = munin_get ($field, "cdef")) {
	    push (@rrd,expand_cdef($service, \$rrdname, $tmpcdef));
	    push (@rrd, "CDEF:c$rrdname=g$rrdname");
	    logger ("DEBUG: Field name after cdef set to $rrdname") if $DEBUG;
	} elsif (!munin_get_bool ($field, "onlynullcdef", 0)) {
	    push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : ""));
	}

	next if !munin_draw_field ($field);
	logger ("DEBUG: Drawing field \"$fname\".") 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,-") if !munin_get ($field, "negative");
	    push (@rrd, "AREA:i$rrdname#ffffff");
	    push (@rrd, "STACK:min_max_diff$range_colour");
	    push (@rrd, "LINE2:re_zero#000000") if !munin_get ($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 $colour;

	if (my $tmpcol = munin_get ($field, "colour")) {
	    $colour = "#" . $tmpcol;
	} elsif ($single_value) {
	    $colour = $single_colour;
	} else {
	    $colour = $COLOUR[$field_count%@COLOUR];
	}

	$field_count++;

	my $tmplabel = munin_get ($field, "label", $fname);

	push (@rrd, $fielddraw . ":g$rrdname" . $colour . ":" .
	    escape ($tmplabel) . (" " x ($max_field_len + 1 - length $tmplabel)));

	# Check for negative fields (typically network (or disk) traffic)
	if ($has_negative) {
	    my $negfieldname = orig_to_cdef ($service, munin_get ($field, "negative"));
	    my $negfield     = $service->{$negfieldname};
	    if (my $tmpneg = munin_get ($negfield, "realname")) {
		$negfieldname = $tmpneg;
		$negfield     = $service->{$negfieldname};
	    }
	    
	    if (!@rrd_negatives) {
		# zero-line, to redraw zero afterwards.
		push (@rrd_negatives, "CDEF:re_zero=g$negfieldname,UN,0,0,IF");
	    }

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

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

	    push (@rrd_negatives, $fielddraw . ":ng$negfieldname" . $colour );

	    # Draw HRULEs
	    my $linedef = munin_get ($negfield, "line");
	    if ($linedef) {
		my ($number, $ldcolour, $label) = split (/:/, $linedef, 3);
		push (@rrd_negatives, "HRULE:".$number.
		    ($ldcolour ? "#$ldcolour" : $colour));

	    } elsif (my $tmpwarn = munin_get ($negfield, "warn")) {
		push (@rrd_negatives, "HRULE:".$negfieldname.
		    (defined $single_value and $single_value) ? "#ff0000" : $colour);
	    }

	    push (@rrd, "GPRINT:c$negfieldname:LAST:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:c$rrdname:LAST:$rrdformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:i$negfieldname:MIN:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:i$rrdname:MIN:$rrdformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:g$negfieldname:AVERAGE:$avgformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:g$rrdname:AVERAGE:$avgformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:a$negfieldname:MAX:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:a$rrdname:MAX:$rrdformat" . $rrdscale . "\\j");
	    push (@{$total_pos{'min'}}, "i$rrdname");
	    push (@{$total_pos{'avg'}}, "g$rrdname");
	    push (@{$total_pos{'max'}}, "a$rrdname");
	    push (@{$total_neg{'min'}}, "i$negfieldname");
	    push (@{$total_neg{'avg'}}, "g$negfieldname");
	    push (@{$total_neg{'max'}}, "a$negfieldname");
	} else {
	    push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:c$rrdname:LAST:$rrdformat" . $rrdscale . "");
	    push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:i$rrdname:MIN:$rrdformat" . $rrdscale . "");
	    push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:g$rrdname:AVERAGE:$avgformat" . $rrdscale . "");
	    push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers;
	    push (@rrd, "GPRINT:a$rrdname:MAX:$rrdformat" . $rrdscale . "\\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 ($field, "line");
	if ($linedef) {
	    my ($number, $ldcolour, $label) = split (/:/, $linedef, 3);
	    $label =~ s/:/\\:/g if defined $label;
	    push (@rrd, "HRULE:".$number.
		($ldcolour ? "#$ldcolour" :
		  ((defined $single_value and $single_value) ?
		   "#ff0000" : $colour)).
		  ((defined $label and length ($label)) ? ":$label" : ""),
		  "COMMENT: \\j"
		);
	} elsif (my $tmpwarn = munin_get ($field, "warn")) {
	    push (@rrd,"HRULE:".$tmpwarn.($single_value ? "#ff0000" : $colour));
	}
    }

    my $graphtotal = munin_get ($service, "graph_total");
    if (@rrd_negatives) {
	push (@rrd, @rrd_negatives);
	push (@rrd, "LINE2:re_zero#000000"); # Redraw zero.
	if (defined $graphtotal 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:$graphtotal" . (" " x ($max_field_len - length ($graphtotal) + 1)));
	    push (@rrd, "GPRINT:gnegtotal:LAST:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:gpostotal:LAST:$rrdformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:inegtotal:MIN:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:ipostotal:MIN:$rrdformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:gnegtotal:AVERAGE:$avgformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:gpostotal:AVERAGE:$avgformat" . $rrdscale . "");
	    push (@rrd, "GPRINT:anegtotal:MAX:$rrdformat" . $rrdscale . "/\\g");
	    push (@rrd, "GPRINT:apostotal:MAX:$rrdformat" . $rrdscale . "\\j");
	}
    } elsif (defined $graphtotal 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:$graphtotal" . (" " x ($max_field_len - length ($graphtotal) + 1)));
	push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers;
	push (@rrd, "GPRINT:gpostotal:LAST:$rrdformat" . $rrdscale . "");
	push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers;
	push (@rrd, "GPRINT:ipostotal:MIN:$rrdformat" . $rrdscale . "");
	push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers;
	push (@rrd, "GPRINT:gpostotal:AVERAGE:$avgformat" . $rrdscale ."");
	push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers;
	push (@rrd, "GPRINT:apostotal:MAX:$rrdformat" . $rrdscale . "\\j");
    }

    for my $time (keys %times) {
	next unless ($draw{$time});
	my $picfilename = munin_get_picture_filename ($service, $time);
	(my $picdirname = $picfilename) =~ s/\/[^\/]+$//;

	my @complete = ();
	if ($RRDkludge) {
	    push (@complete,
		  '--font' ,'LEGEND:7:/usr/share/munin/VeraMono.ttf',
		  '--font' ,'UNIT:7:/usr/share/munin/VeraMono.ttf',
		  '--font' ,'AXIS:7:/usr/share/munin/VeraMono.ttf');
	}
	push(@complete,'-W', $watermark) if $RRDs::VERSION >= 1.2;

	# Do the header (title, vtitle, size, etc...)
	push @complete, @{get_header ($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;

	# Make sure directory exists
	munin_mkdir_p ($picdirname, 0777);

	RRDs::graph (@complete);
	if (my $ERROR = RRDs::error) {
	    logger ("Unable to graph ". munin_get_picture_filename ($service, $time) . ": $ERROR");
	} elsif ($list_images) {
	    # Command-line option to list images created
	    print munin_get_picture_filename ($service, $time),"\n";
	}
    }

    if (munin_get_bool ($service, "graph_sums", 0)) {
	foreach my $time (keys %sumtimes) {
	    my $picfilename = munin_get_picture_filename ($service, $time, 1);
	    (my $picdirname = $picfilename) =~ s/\/[^\/]+$//;
	    next unless ($draw{"sum".$time});
	    my @rrd_sum;
	    push @rrd_sum, @{get_header ($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 = munin_get ($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 (munin_get ($service->{$fname}, "type", "GAUGE") 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 ($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 ($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 = munin_get ($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;

	    # Make sure directory exists
	    munin_mkdir_p ($picdirname, 0777);

	    RRDs::graph (@rrd_sum);

	    if (my $ERROR = RRDs::error) {
		logger ("Unable to graph ". munin_get_picture_filename ($service, $time) . ": $ERROR");
	    } elsif ($list_images) {
		# Command-line option to list images created
		print munin_get_picture_filename ($service, $time, 1),"\n";
	    }
	}
    }

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

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

sub graph_by_minute {
    my $service = shift;

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

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

    return undef unless ref ($service) eq "HASH";

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

sub skip_service {
    my $service = shift;
    my $sname   = munin_get_node_name ($service);

    # Skip if we've limited services with cli options
    return 1 if (@limit_services and !grep /^$sname$/, @limit_services);

    # Always graph if --force is present
    return 0 if $force_graphing;

    # See if we should skip it because of conf-options
    return 1 if (munin_get ($service, "graph", "yes") eq "on-demand" or
	    !munin_get_bool ($service, "graph", 1));

    # 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 (@{munin_find_field ($service, "label")}) {
	my $fieldname = munin_get_node_name ($field);
	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;
	}
    }

    munin_set_var_loc ($service, [$$cfield_ref, "cdef_name"], $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: $!";
	}
    } else {
	close (STDERR);
	*STDERR = \$log;
    }
}

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/lib/munin/*
	/var/log/munin/munin-graph
	/var/run/munin/*

=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
