#!/usr/bin/perl
# -*- cperl -*-

=encoding utf8

=head1 NAME

munin-graph - generate graphs from RRD files

=begin comment

Copyright (C) 2004-2010 Jimmy Olsen, Steve Schnepp

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, see <http://www.gnu.org/licenses/>.

=end comment

=cut

use strict;
use warnings;
use IO::Handle;

use Date::Parse;
use POSIX qw(strftime);
use Time::HiRes qw(gettimeofday tv_interval);
use IO::File;

use Munin::Master::GraphOld;
use Munin::Master::Utils;

use Munin::Master::Logger;
use Log::Log4perl qw( :easy );

my $GRAPHER = "$Munin::Common::Defaults::MUNIN_LIBDIR/munin-graph";
my $conffile = "$Munin::Common::Defaults::MUNIN_CONFDIR/munin.conf";

my %period  = ( "day"   => 300,
		"week"  => 1800,
		"month" => 7200,
		"year"  => 86400,
		"week-sum" => 1800,
		"year-sum" => 86400
	    );

my $logfile;
my $scale = "day";

my @params = @ARGV;

push @params, "--no-fork"; # We do *not* want to fork. Perf -> FastCGI
push @params, "--log-file", $logfile;

my $config = graph_startup(\@params);

logger_open($config->{'logdir'});
logger_debug() if $config->{debug} or defined($ENV{CGI_DEBUG});

if (! graph_check_cron() ) {
	# Should not be launched from cron.
	INFO "[INFO] graphing is cgi, do nothing";	
	exit 0;
}

INFO "Starting munin-graph"; 
my $graph_time = Time::HiRes::time;

# BEGIN FAST-CGI LOOP:
my $nb_request = 0;
my $nb_request_max = 0;
my $graph_fh = new IO::File($config->{dbdir} . "/graphs");
while (my $path = <$graph_fh>) {
    my $pinpoint = undef;
    DEBUG "Request path is $path";

    # The full URL looks like this:
    # Case 1:
    # http://localhost:8080/munin-cgi/munin-cgi-graph/client/\
    #    Backend/dafnes.client.example.com/diskstats_iops-week.png
    # $path should be
    #   client/Backend/dafnes.client.example.com/diskstats_iops-week.png
    #
    # Interesting bits about that url: Nested groups!
    #
    # Case 2:
    # http://localhost:8080/munin-cgi/munin-cgi-graph/client/\
    #    Backend/dafnes.client.example.com/diskstats_iops/sda-week.png
    # path should be
    #    client/Backend/dafnes.client.example.com/diskstats_iops/\
    #    sda-week.png
    #
    # Interesting bit that url: Nested groups at the start and multigraph
    # nesting bits at the end.
    #
    # Case 3:
    # http://localhost:8080/munin-cgi/munin-cgi-graph/client/\
    #   dafnes.client.example.com/if_err_bond0-day.png
    # path:
    #   client/dafnes.client.example.com/if_err_bond0-day.png
    #
    # Simplest (old munin 1.2): No nesting at any end, fixed number of /es
    #
    # Despite the slippery structure of the $path this expression works with
    # the rest of the code.  To make a more scientific try we would need to
    # split on / and traverse the $config to determine what kind of part
    # (domain, nested domain, host, service/plugin, or nested service)
    # we're looking at.
    #
    # Scale will in any case work out since - is only used before the
    # day/week/month/year/pinpoint part, and the next part is always .png.
    #

    my ($dom, $host, $serv, $scale) =
      $path =~ m#^/(.*)/([^/]+)/(\w+)-([\w=,]+)\.png#; ## avoid bug in vim

    DEBUG "asked for ($dom, $host, $serv, $scale)";

    if ($scale =~ /pinpoint=(\d+),(\d+)/) {
	    $pinpoint = [ $1, $2, ];
    }

    if (! &verify_parameters ($dom, $host, $serv, $scale)) {
	ERROR "Invalid parameters!";
	next;
    }

    my $filename = get_picture_filename ($config, $dom, $host, $serv, $scale, $ENV{QUERY_STRING});

    my $time = time;

    my $no_cache = 0;
    # Compute the cache values
    my $graph_ttl = $period{$scale} || 1;
    my $graph_last_expires = $time - ($time % $graph_ttl);

    if ($pinpoint || ! -f $filename || (stat(_))[9] < $graph_last_expires) {
        # Should generate it
	my $scale_options;
	if ($pinpoint) {
	    $scale_options = "--pinpoint=" . $pinpoint->[0] . "," . $pinpoint->[1];
	} else {
	    $scale_options = "--$scale";
	}
	next unless
	  draw_graph_or_complain($dom, $host, $serv, $scale_options, $filename);
    }

    # Now send it: headers
    DEBUG "X-Munin-Request: $nb_request/$nb_request_max";

} continue {
	$nb_request++;
	if ($nb_request_max && $nb_request > $nb_request_max) {
		# Cycle
		last;
	}
}
# END FAST-CGI LOOP - Time to die.  Nicely.

$graph_time = sprintf("%.2f", (Time::HiRes::time - $graph_time));
INFO "Munin-graph finished ($graph_time sec)";

exit 0;

sub get_w3c_date_from_epoch {
    my($epoch) = @_;
    return strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($epoch));
}


sub send_graph_data {
    # Serve the graph contents.
    my($filename) = @_;
    my $buffer;

    if (! open (GRAPH_PNG_FILE, '<', $filename) ) {
	ERROR "[FATAL] Could not open image file \"$filename\" for reading: $!\n";
	die "[FATAL] Could not open image file \"$filename\" for reading: $!\n";
    }

    # No buffering wanted when sending the file
    local $| = 1;

    while (sysread(GRAPH_PNG_FILE,$buffer,40960)) {
	print $buffer;
    }

    close (GRAPH_PNG_FILE);
}


sub get_picture_filename {
    my $config  = shift;
    my $domain  = shift;
    my $name    = shift;
    my $service = shift;
    my $scale   = shift;
    my $params  = shift;

    my $cgi_tmp_dir = $config->{htmldir};

    $params = $params ? "?$params" : "";
    $params =~ tr/\//_/; # / are forbidden in a filename
    $params = $1 if $params =~ m/(.*)/; # XXX - Q&D untaint
    return "$cgi_tmp_dir/$domain/$name/$service-$scale.png" . $params;
}


sub verify_parameters {
    my $dom   = shift;
    my $host  = shift;
    my $serv  = shift;
    my $scale = shift;

    if (!$dom) {
	WARN '[WARNING] Request for graph without specifying domain. Bailing out.';
	return 0;
    }
    if (!$host) {
	WARN '[WARNING] Request for graph without specifying host. Bailing out.';
	return 0;
    }
    if (!$serv) {
	WARN '[WARNING] Request for graph without specifying service. Bailing out.';
	return 0;
    }

    if (!$scale) {
	WARN '[WARNING] Request for graph without specifying scale. Bailing out.';
	return 0;
    } else {
	if (!defined $period{$scale} && $scale !~ /pinpoint=\d+,\d+/) {
	    WARN '[WARNING] Weird pinpoint setting "'.$scale.'". Bailing out.';
	    return 0;
	}
    }
    return 1;
}


sub file_newer_than {
    my $filename = shift;
    my $time     = shift;

    if (-f $filename) {
	my @stats = stat (_);
	# $stats[9] holds the "last update" time and this needs
	# to be in the last update period
	my $last_update = $stats[9];
	if ($last_update > $time) {
	    return $last_update;
	} else {
	    return 0;
	}
    }

    # No file found
    return 0;
}


sub draw_graph {
    my $dom  = shift;
    my $host  = shift;
    my $serv  = shift;
    my $scale = shift;

    my $filename = shift;

    # remove old file if present
    if (-f $filename and !unlink($filename)) {
	ERROR "[FATAL] Could not remove \"$filename\": $!";
	die("[FATAL] cannot remove old graph file \"$filename\": $!");
    }

    $serv =~ s{[^\w_\/"'\[\]\(\)+=-]}{_}g; $serv =~ /^(.+)$/; $serv = $1; #"
    # . needs to be legal in host names
    $host =~ s{[^\w_\/"'\[\]\(\)\.+=-]}{_}g; $host =~ /^(.+)$/; $host = $1; #"

    # FIXME: Make "root" implied!
    my @params = ( '--host', $host,
		   '--only-fqn', "root/$dom/$host/$serv",
		   $scale,
		   '--output-file', $filename );

    graph_main(\@params);

    return $filename;
}


sub draw_graph_or_complain {
    my $t0 = [ gettimeofday ];

    # Actuall work done here.
    my $ret = draw_graph(@_);

    my $graph_duration = tv_interval($t0);

    if (! -f $ret) {
	my ($dom, $host, $serv, $scale, $filename ) = @_;
	WARN "[WARNING] Could not draw graph \"$filename\": $ret";
        return 0;
    } else {
	return $ret;
    }
}


sub rfctime_newer_than {
    # See if the file has been modified since "the last time".
    # Format of since_string If-Modified-Since: Wed, 23 Jun 2004 16:11:06 GMT
    my $since_string = shift;
    my $created      = shift;
    my $ifmodsec = str2time($since_string);

    return 1 if ($ifmodsec < $created);
    return 0;
}

# vim: syntax=perl ts=8
