#!/usr/bin/perl
# -*- perl -*-
#
# Copyright (C) 2002-2006 Jimmy Olsen, Audun Ytterdal
#
# 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-html.in 1474 2008-02-18 01:00:00Z matthias $
#
$|=1;

use strict;
use HTML::Template;
use Getopt::Long;
use Munin;
use POSIX qw(strftime);

my @times = ( "day", "week", "month", "year" );

my $DEBUG=0;
my $VERSION="1.2.6";
my $conffile = "/etc/munin/munin.conf";
my $force_root = 0;
my $do_usage = 0;
my $do_version = 0;
my $stdout = 0;
my $log = new IO::Handle;

# Get options
$do_usage=1  unless
GetOptions ( "host=s"       => \(),
             "force-root!"  => \$force_root,
	     "service=s"    => \(),
	     "config=s"     => \$conffile,
	     "debug!"       => \$DEBUG,
	     "stdout!"      => \$stdout,
	     "help"         => \$do_usage, 
	     "version!"     => \$do_version );

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

Options:
    --[no]force-root    Force running, even as root. [--noforce-root]
    --help		View this message.
    --debug		View debug messages.
    --version		View version information.
    --service <service>	Compatability. No effect.
    --host <host>	Compatability. No effect.
    --config <file>	Use <file> as configuration file. 
			[/etc/munin/munin.conf]

";
    exit 0;
}

if ($do_version)
{
    print <<"EOT";
munin-html version $VERSION.
Written by Knut Haugen, Audun Ytterdal, Jimmy Olsen, Tore Anderson / Linpro AS

Copyright (C) 2002-2006

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 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;
my $limits;
$config = &munin_config ($conffile, $config);
$limits = &munin_readconfig ($config->{dbdir}."/limits", 1, 1);
if (!defined $config->{'cgiurl_graph'})
{
    if (defined $config->{'cgiurl'})
    {
	$config->{'cgiurl_graph'} = $config->{'cgiurl'} . "/munin-cgi-graph";
    }
    else
    {
	$config->{'cgiurl_graph'} = "/cgi-bin/munin-cgi-graph";
    }
}

logger("Starting munin-html, checking lock");

munin_runlock("$config->{rundir}/munin-html.lock");
my $template = HTML::Template->new(filename => "$config->{tmpldir}/munin-overview.tmpl",
				   die_on_bad_params => 0,
				   loop_context_vars => 1);

my $domaintemplate = HTML::Template->new(filename => "$config->{tmpldir}/munin-domainview.tmpl",
				   die_on_bad_params => 0,
				   loop_context_vars => 1);

my $nodetemplate = HTML::Template->new(filename => "$config->{tmpldir}/munin-nodeview.tmpl",
				       die_on_bad_params => 0,
				       loop_context_vars => 1);

my $servicetemplate = HTML::Template->new(filename => "$config->{tmpldir}/munin-serviceview.tmpl",
				       die_on_bad_params => 0,
				       loop_context_vars => 1);

my %comparisontemplates = (  day => HTML::Template->new(filename => "$config->{tmpldir}/munin-comparison-day.tmpl", die_on_bad_params => 0, loop_context_vars => 1),
	                    week => HTML::Template->new(filename => "$config->{tmpldir}/munin-comparison-week.tmpl", die_on_bad_params => 0, loop_context_vars => 1),
	                   month => HTML::Template->new(filename => "$config->{tmpldir}/munin-comparison-month.tmpl", die_on_bad_params => 0, loop_context_vars => 1),
	                    year => HTML::Template->new(filename => "$config->{tmpldir}/munin-comparison-year.tmpl", die_on_bad_params => 0, loop_context_vars => 1)
			  );

my @domains;

my @domainorder;
if ($config->{domain_order}) {
    @domainorder = split /\s+/, $config->{domain_order};
}
foreach my $d (sort (keys %{$config->{domain}})) {
    unless (grep (/^$d$/, @domainorder)) {
	push @domainorder, $d;
    }
}

#Make sure the logo and the stylesheet file is in the html dir
my @files = ("style.css", "logo.png", "definitions.html");
foreach my $file( (@files) ) {
    if ((! -e "$config->{htmldir}/$file") or
	 (-e "$config->{tmpldir}/$file") and 
	 ((stat ("$config->{tmpldir}/$file"))[9] > (stat("$config->{htmldir}/$file"))[9])) {
        unless (system("cp", "$config->{tmpldir}/$file", "$config->{htmldir}/")){
            logger("copied $file into htmldir");
        } else {
            logger("could not copy $file into htmldir");
        }
    }
}

#make domain list
my @domainlist = map { { DOMAIN => $_ } } @domainorder;
my $timestamp = strftime("%Y-%m-%d T %T", localtime);
for my $domain (@domainorder) {
    logger("processing domain: $domain");
    my %domain;
    $domain{domain}=$domain;
    my @nodes;
    my %comparisons;
    my @nodeorder = ();
    if ($config->{domain}->{$domain}->{node_order}) {
	@nodeorder = split /\s+/, $config->{domain}->{$domain}->{node_order};
    } 
    foreach my $n (sort (keys %{$config->{domain}->{$domain}->{node}}))
    {
	unless (grep (/^$n$/, @nodeorder))
	{
	    push @nodeorder, $n;
	}
    }
    for my $node (@nodeorder) {
        logger("processing node: $node");
	my %node;
	$node{node}=$node;
	$node{url}="$domain/$node.html";
	my @services;
	my @categories;
	my %categories;
	my %tmp_cats;
	my @serviceorder;
	if ($config->{domain}->{$domain}->{node}->{$node}->{service_order}) {
	    @serviceorder = split /\s+/, $config->{domain}->{$domain}->{node}->{$node}->{service_order};
	} else {
	    @serviceorder = sort (keys %{$config->{domain}->{$domain}->{node}->{$node}->{client}});
	}
	
	for my $service (@serviceorder) {
	    logger("processing service: $service");
	    next unless defined( $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service} )
	                    &&  $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service} ne "";
	    next unless (munin_get_bool ($config, "graph", 1, $domain, $node, $service));
	    my @service;
	    my %service;
	    my $fieldnum = 0;
	    my @graph_info;
	    my @field_info;
	    $service{service}=$service;
	    $service{label}=$config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{graph_title};
		
	    my $method = &munin_get ($config, "graph_strategy", "cron");
	    if ($method eq "cgi")
	    {
		$service{imgday}=$config->{'cgiurl_graph'}."/$domain/$node/$service-day.png";
		$service{imgweek}=$config->{'cgiurl_graph'}."/$domain/$node/$service-week.png";
		$service{imgmonth}=$config->{'cgiurl_graph'}."/$domain/$node/$service-month.png";
		$service{imgyear}=$config->{'cgiurl_graph'}."/$domain/$node/$service-year.png";

		if (&munin_get_bool_val ($config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{"graph_sums"}, 0))
		{
		    $service{imgweeksum} = $config->{'cgiurl_graph'}."/$domain/$node/$service-week-sum.png";
		    $service{imgyearsum} = $config->{'cgiurl_graph'}."/$domain/$node/$service-year-sum.png";
		}
	
		if (my ($w, $h) = &calculate_png_size ($config, $domain, $node, $service))
		{
		    for my $scale (@times)
		    {
			$service{"img".$scale."width"} = $w;
			$service{"img".$scale."height"} = $h;
		    }
		    if (&munin_get_bool_val ($config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{"graph_sums"}, 0))
		    {
			for my $scale (["week", "year"])
			{
			    $service{"img".$scale."sumwidth"} = $w;
			    $service{"img".$scale."sumheight"} = $h;
			}
		    }
		}
	    }
	    else
	    {
		    $service{imgday}="$node-$service-day.png";
		    $service{imgweek}="$node-$service-week.png";
		    $service{imgmonth}="$node-$service-month.png";
		    $service{imgyear}="$node-$service-year.png";

		    for my $scale (@times)
		    {
			    if (my ($w, $h) = &get_png_size (&munin_get_picture_filename ($config, $domain, $node, $service, $scale)))
			    {
				    $service{"img".$scale."width"} = $w;
				    $service{"img".$scale."height"} = $h;
			    }
		    }

		    if (&munin_get_bool_val ($config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{"graph_sums"}, 0))
		    {
			$service{imgweeksum} = "$node-$service-week-sum.png";
			$service{imgyearsum} = "$node-$service-year-sum.png";
			for my $scale (["week", "year"])
			{
				if (my ($w, $h) = &get_png_size (&munin_get_picture_filename ($config, $domain, $node, $service, $scale, 1)))
				{
					$service{"img".$scale."sumwidth"} = $w;
					$service{"img".$scale."sumheight"} = $h;
				}
			}
		    }
	    }
	    $service{url}="$node-$service.html";
	    $service{domain}="$domain";
	    $service{node}=$node;
	    $service{category}= lc $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{graph_category} || "other";

        # Do "help" section
		if (defined $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{graph_info})
		{
			my %graph_info;
			$graph_info{info} = $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{graph_info};
			push @{$service{graphinfo}}, \%graph_info;
		}
		$service{fieldlist} .= "<tr><th align='left' valign='top'>Field</th><th align='left' valign='top'>Type</th><th align='left' valign='top'>Warn</th><th align='left' valign='top'>Crit</th><th></tr>";
#		foreach my $field (keys %{$config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}})
		foreach my $f (@{&munin_get_field_order ($config->{domain}->{$domain}->{node}->{$node}, $config, $domain, $node, $service)})
		{
			$f =~ s/=(.*)$//;
			my $path = $1;
			next unless (&munin_draw_field ($config->{domain}->{$domain}->{node}->{$node}, $service, $f));
			print "DEBUG: single_value: Checking field \"$f\" ($path).\n" if $DEBUG;

			if (defined $path)
			{
			    munin_get_rrd_filename ($config->{domain}->{$domain}->{node}->{$node}, $config, $domain, $node, $service, $f, $path);
			}

			my %field_info;
			$fieldnum++;

			$field_info{'hr'}      = 1 unless ($fieldnum % 3);
			$field_info{'field'}   = $f;
			$field_info{'label'}   = $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{$f.".label"} || $f;
			$field_info{'type'}    = lc ($config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{$f.".type"} || "GAUGE");
			$field_info{'warn'}    = $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{$f.".warning"} || "None";
			$field_info{'crit'}    = $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{$f.".critical"} || "None";
			$field_info{'info'}    = $config->{domain}->{$domain}->{node}->{$node}->{client}->{$service}->{$f.".info"} || "";

			my $state = &munin_field_status ($config, $limits, $domain, $node, $service, $f, 1);
			if (defined $state)
			{
				$field_info{'state_warning'}  = 1 if $state eq "warning";
				$field_info{'state_critical'} = 1 if $state eq "critical";
				$field_info{'state_unknown'}  = 1 if $state eq "unknown";
			}
			push @{$service{fieldinfo}}, \%field_info;
		}

		{
			my $state = &munin_service_status ($config, $limits, $domain, $node, $service, 1);
			if (defined $state)
			{
				$service{'state_warning'}  = 1 if $state eq "warning";
				$service{'state_critical'} = 1 if $state eq "critical";
				$service{'state_unknown'}  = 1 if $state eq "unknown";
			}
		}

	    push @services, \%service;
	    push @service, \%service;
	    push @{$tmp_cats{$service{'category'}}}, \%service;
	    $servicetemplate->param(SERVICES => \@service,
				    SERVICE => $service,
				    NODE => $node,
				    DOMAIN => $domain, 
				    DOMAINS => \@domainlist, 
				    TIMESTAMP => $timestamp);
	    open (FILE, ">$config->{htmldir}/$domain/$node-$service.html") or 
	die "Cannot open $config->{htmldir}/$domain/$node-$service.html: $!";
	    print FILE $servicetemplate->output;
	    close FILE;
	}
	foreach my $key (keys %tmp_cats)
	{
	    next if $key eq "other";
	    my %tmp;
	    my $state = &munin_category_status ($config, $limits, $domain, $node, $key, 1);
	    print "DEBUG: Pushing category \"$node\" -> \"$key\"...\n" if $DEBUG;
	    if (defined $state)
	    {
		    $tmp{'state_warning'}  = 1 if $state eq "warning";
		    $tmp{'state_critical'} = 1 if $state eq "critical";
		    $tmp{'state_unknown'}  = 1 if $state eq "unknown";
	    }
	    $tmp{name} = ucfirst $key;
	    $tmp{services} = \@{$tmp_cats{$key}};
	    $tmp{node} = $node;
	    $tmp{domain} = $domain;
	    $categories{ucfirst $key} = \%tmp;
	    $comparisons{$key}{$node} = \%tmp;
	}
	if (defined $tmp_cats{'other'})
	{
	    my $key = 'other';
	    my %tmp;
	    my $state = &munin_category_status ($config, $limits, $domain, $node, $key, 1);
	    print "DEBUG: Pushing category \"$node\" -> \"$key\"...\n" if $DEBUG;
	    if (defined $state)
	    {
		    $tmp{'state_warning'}  = 1 if $state eq "warning";
		    $tmp{'state_critical'} = 1 if $state eq "critical";
		    $tmp{'state_unknown'}  = 1 if $state eq "unknown";
	    }
	    $tmp{name} = ucfirst $key;
	    $tmp{services} = \@{$tmp_cats{$key}};
	    $tmp{node} = $node;
	    $tmp{domain} = $domain;
	    $categories{ucfirst $key} = \%tmp;
	    $comparisons{'other'}{$node} = \%tmp;
	}
	# Handle category_order
	@categories = ();
	if ($config->{domain}->{$domain}->{node}->{$node}->{category_order}) {
	    foreach my $cat (split /\s+/, $config->{domain}->{$domain}->{node}->{$node}->{category_order})
	    {
		push @categories, $categories{ucfirst $cat};
	    }
	    foreach my $cat (sort keys %categories)
	    {
		push @categories, $categories{$cat}
			unless (grep { $_->{name} eq $cat } @categories);
	    }
	} else {
	    @categories = map { $categories{$_} } sort keys %categories;
	}
	$nodetemplate->param(SERVICES => \@services,
			     NODE => $node,
			     DOMAIN => $domain,
			     DOMAINS => \@domainlist, 
			     TIMESTAMP => $timestamp,
			     CATEGORIES => \@categories);
	open (FILE, ">$config->{htmldir}/$domain/$node.html") or
	die "Cannot open $config->{htmldir}/$domain/$node.html: $!";
 	print FILE $nodetemplate->output;
	close FILE;
	$node{services} = \@services;
	$node{categories} = \@categories;
	$node{domain} = $domain;
	push @nodes,\%node;

    }
    $domaintemplate->param(NODES => \@nodes,
                           DOMAIN => $domain, 
                           DOMAINS => \@domainlist, 
     			   COMPARE => (&munin_get_bool ($config, "compare", 1, $domain) and @nodeorder > 1),
                           TIMESTAMP => $timestamp);
    open (FILE, ">$config->{htmldir}/$domain/index.html") or
	die "Cannot open $config->{htmldir}/$domain/index.html: $!";
    print FILE $domaintemplate->output;
    close FILE;
    
    $domain{nodes} = \@nodes;
    $domain{domain} = $domain;
    $domain{compare} = (&munin_get_bool ($config, "compare", 1, $domain) and @nodeorder > 1);

    my @cats = ();
    foreach my $key (sort keys %comparisons)
    {
	my %cat;
	my %servlist;
	my $nodewidth = 0;

	foreach my $node (sort keys %{$comparisons{$key}})
	{
	    foreach my $serv (@{$comparisons{$key}{$node}->{services}})
	    {
		$servlist{$serv->{service}}{$node} = $serv;
		$nodewidth = $serv->{imgdaywidth} if (defined $serv->{imgdaywidth} and $serv->{imgdaywidth} > $nodewidth);
	    }
	}
	foreach my $sname (sort keys %servlist)
	{
	    my %s;
	    foreach my $node (@nodeorder)
	    {
		if (defined $servlist{$sname}{$node})
		{
		    $servlist{$sname}{$node}->{width} = $nodewidth;
		    push (@{$s{nodes}}, $servlist{$sname}{$node});
		}
		else
		{
		    my %ts;
		    $ts{label} = "Not present";
		    $ts{service} = "$sname";
		    $ts{title} = "Not present";
		    $ts{node} = $node;
		    $ts{width} = $nodewidth;
		    push (@{$s{nodes}}, \%ts);
		}
	    }
	    push @{$cat{services}}, \%s;
	}
	$cat{name} = ucfirst $key;
	$cat{numnodes} = @nodeorder;
	$cat{numnodes} = @nodeorder;
	push @cats, \%cat;
    }

  if (&munin_get_bool ($config, "compare", 1, $domain) and @nodeorder > 1) {
    foreach my $t (@times) {
      $comparisontemplates{$t}->param(DOMAIN => $domain,
				      DOMAINS => \@domainlist,
				      TIMESTAMP => $timestamp,
				      CATEGORIES => \@cats);
      open (FILE, ">$config->{htmldir}/$domain/comparison-$t.html") or
	die "Cannot open $config->{htmldir}/$domain/comparison-$t.html: $!";
      print FILE $comparisontemplates{$t}->output;
      close FILE;
    }
  }
  push @domains,\%domain;
}

$template->param(DOMAINS => \@domains, 
                 TIMESTAMP => $timestamp);
open (FILE, ">$config->{htmldir}/index.html") or die "Cannot open $config->{htmldir}/index.html: $!";
print FILE $template->output;
close FILE;

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

sub logger_open {
    my $dirname = shift;

    if (!$log->opened)
    {
	  unless (open ($log, ">>$dirname/munin-html.log"))
	  {
		  print STDERR "Warning: Could not open log file \"$dirname/munin-html.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-html.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-html.log\" for writing: $!";
			  print STDERR "$now - $comment\n";
		  }
	  }
	  else
	  {
		  print STDERR "$now - $comment\n";
	  }
    }
}

sub calculate_png_size
{
    	my $config = shift;
	my $domain = shift;
	my $node   = shift;
	my $serv   = shift;

        # base size of graph rectangle + space outside graph rectangle
	my $height = munin_get ($config, "graph_height", 100, $domain, $node, $serv) + 156;
	my $width  = munin_get ($config, "graph_width" , 400, $domain, $node, $serv) + 93;

	# In addition, the height increases by 15 pixels for each label underneath
	foreach my $field (keys %{$config->{domain}->{$domain}->{node}->{$node}->{client}->{$serv}})
	{
	    if ($field =~ /^([^\.]+)\.label/)
	    {
		if (munin_draw_field ($config->{domain}->{$domain}->{node}->{$node}, $serv, $1))
		{
		    $height += 15;
		    my $tmpline = munin_get ($config, "line", undef, $domain, $node, $serv, $1);
		    if ($tmpline)
		    {
			my @tmparr = ($tmpline =~ /:/g);
			if (scalar (@tmparr) > 2)
			{ # We've got line definitions with labels...
			    $height += 15;
			}
		    }
		}
	    }
	}
	# ...and +15 if there's a graph total
	$height += 15 if (munin_get ($config, "graph_total", undef, $domain, $node, $serv));
	# ...and +15 if there's min/max-headers above the labels
	$height += 15 if (munin_graph_column_headers ($config, $domain, $node, $serv));

	return ($width, $height);
}

sub get_png_size
{
	my $filename = shift;
	my $width = undef;
	my $height = undef;

	if (open (PNG, $filename))
	{
		my $incoming;
		binmode (PNG);
		if (read (PNG, $incoming, 4))
		{
			if ($incoming =~ /PNG$/)
			{
				if (read (PNG, $incoming, 12))
				{
					if (read (PNG, $incoming, 4))
					{
						$width = unpack ("N", $incoming);
						read (PNG, $incoming, 4);
						$height = unpack ("N", $incoming);
					}
				}
			}
		}
		close (PNG);
	}

	return ($width, $height);
}

logger("munin-html finished");
close $log;

=head1 NAME

munin-html - A program to draw html-pages on an Munin installation

=head1 SYNOPSIS

munin-html [options]

=head1 OPTIONS

=over 5

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

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

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

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

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

Limit hosts to those of E<lt>host<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<< --help >>

View help message.

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

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

=back

=head1 DESCRIPTION

Munin-html 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.

If munin.conf sets "graph_strategy cgi" then munin-html generates URLs
referencing the graph CGI instead of referencing pre-generated
graphs (made by munin-graph).

Munin-html creates the html pages.

=head1 FILES

	/etc/munin/munin.conf
	/var/db/munin/datafile
	/var/log/munin/munin-html
	/var/www/htdocs/munin/*
	/var/run/munin/*

=head1 VERSION

This is munin-html version 1.2.6

=head1 AUTHORS

Knut Haugen, Audun Ytterdal and Jimmy Olsen.

=head1 BUGS

munin-html 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 © 2002-2004 Knut Haugen, Audun Ytterdal, and Jimmy Olsen / Linpro AS.

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
