#!/usr/bin/perl
#
#    Big Sister network monitor
#    Copyright (C) 2005  Simon Moser
#
#    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; either version 2 of the License, or
#    (at your option) any later version.
#
#    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., 675 Mass Ave, Cambridge, MA 02139, USA.
#

#=============================================================================

#=============================================================================
#
$BigSister::common::Usage	  = "[-c config] [-d date] [-m mode]";
#
#=============================================================================

@BigSister::common::options = ( "c=s", "d=s", "m=s" );

use strict;
use FileHandle;
use File::Basename;
use File::Copy;
use Cwd;

use lib "$ENV{BIGSISTER_CHROOT}/usr/local/bigsister/bin"; use lib "$ENV{BIGSISTER_CHROOT}/usr/local/bigsister/uxmon"; #inslib
use BigSister::common;
proginit();

use cfgfile;
use Reader::Reader;
use ReportCommon;


# files & directories
my $wd = cwd();
my $configdir = $BigSister::common::fs{'adm'}."/reporting";
my $graphicsdir = $configdir."/graphics";
my $bindir = $BigSister::common::fs{'bin'};
my $reportdir = $BigSister::common::fs{'var'}."/reportdb";
my $cachedir = $reportdir."/graphics";
my $webdir = $BigSister::common::fs{'www'}."/reports";
my $masterdoc = $configdir."/sla_report_template.tex";
my $reportsource = $reportdir."/slareport.tex";
my $cfgfile = $configdir."/slareport";
my $downloadfile = undef;


my @tmpfiles = ();
my $syspath = $ENV{"PATH"};


# the command line argument overrides the default configuration file
$cfgfile = defined( $BigSister::common::opt_c ) ? $BigSister::common::opt_c : $cfgfile;
BigSister::common::log( "notice", "use config file $cfgfile");

# the output file name is derived from the config file name
my ($today, $thismonth, $thisyear) = (localtime)[3,4,5]; my $isodate = ($thisyear+1900)."-".sprintf("%02d", $thismonth+1)."-".sprintf("%02d", $today);
$downloadfile = basename($cfgfile)."-$isodate";


# open config file
my $config = new cfgfile( "$cfgfile" );
unless ( $config->rewind()) {
	BigSister::common::log("err", "failed to read config file $cfgfile");
	exit(1);
} 

# parse configuration file
my $interval = "monthly";
my $mailhost = "localhost";
my $mailsender = "bigsis\@localhost";
my @recipients = ();
my $reporturl = undef;
my $format = undef;
while ( $_ = $config->nextline() ) {
	chomp(); 
	next if ( /^$/ );
	if ( /^\s*interval\s*=\s*(daily|monthly|yearly)\s*$/ ) {
		$interval = $1;
	} elsif ( /^\s*masterdocument\s*=\s*(.*?)\s*$/) {
		$masterdoc = $configdir."/".$1;
	} elsif ( /^\s*mailhost\s*=\s*(.*?)\s*$/ ) {
		$mailhost = $1;
	} elsif ( /^\s*report_recipients\s*=\s*(.*?)\s*$/ ) {
		@recipients = split /,/, $1;
	} elsif ( /^\s*report_sender\s*=\s*(.*?)\s*$/ ) {
		$mailsender = $1;
	} elsif ( /^\s*baseurl\s*=\s*(.*?)\s*$/ ) {
		$reporturl = $1."/reports";
	} elsif ( /^\s*format\s*=\s*(.*?)\s*$/ ) {
		$format = $1;
	} else { 
		BigSister::common::log("warning", "unused statement: $_");
	}
}
$config->close();

# the command line argument overrides the default mode
if ( defined $BigSister::common::opt_m ) {
	if ( defined $BigSister::common::opt_m =~ /daily|monthly|yearly/ ) {
		$interval = $BigSister::common::opt_m;
	} else {
		BigSister::common::log("warning", "unrecognized mode: $BigSister::common::opt_m");
	}
}

# calculate start date
# possible arguments for option -d: 	
#	-xday
#	-xweek
#
my ($start_time, $end_time) = ReportCommon::start_date( $BigSister::common::opt_d );
my ($day, $month, $year) = (localtime($start_time))[3,4,5];
$year += 1900;
$month = sprintf("%02d", $month + 1);
$day = sprintf("%02d", $day);

# define the search pattern for the statistic files
my $pattern;
if ( $interval eq "yearly") {
	$pattern = "day-".$year;
} elsif ( $interval eq "monthly") {
	$pattern = "day-".$year."-".$month;
} elsif ( $interval eq "daily") {
	$pattern = "day-".$year."-".$month."-".$day;
}

BigSister::common::log("notice", "performance data storage is $reportdir");
opendir( DIR, $reportdir ) || die "cannot read $reportdir: $!";
my @candidates = sort grep( /$pattern.*\.statistic\..*/, readdir( DIR ) );
closedir( DIR );

# find the relevant files in the reportdir
my @stat = ();
foreach $_ ( @candidates ) {
	if ( /day-(.*)\.statistic\.(.*)\.(.*)/ ) {
		push(@stat, 
			{
				"file" => $reportdir."/".$_,
				"date" => $1,
				"class" => $2,
				"frame" => $3
			}
		);
	} else {
		BigSister::common::log("warning", "failed to extract information from filename: $_" );
	}
}

# create the argument for the report reader
my $args = {
	"file" => $cfgfile,
	"stat" => \@stat
};

# read statistic files
my $slareporter = Reader::Reader::create("SLAReport", $args);
$slareporter -> read();

# compute sla-violations
my %slareport = %{$slareporter -> calc_status()};

# copy the master document to the reportdir, include-statements 
# are replaced by the corresponding sections. If a section is not implemented, 
# then this will be logged as a warning.
my $master = new FileHandle();
my $reportbase = new FileHandle();
$master->open( "< $masterdoc" ) or BigSister::common::log("error", "failed to open $masterdoc for reading: $!" );
$reportbase->open( "> $reportsource" ) or BigSister::common::log("error", "failed to open $reportsource for writing: $!" );
if ( $master && $reportbase ) {
	push @tmpfiles, $reportsource;
	while( <$master> ) {
		if ( $_ =~ /\\include{(.*?)}/ ) {
			my $sectioncontent = createchapter($1);
			
			if ( $sectioncontent ) {
				print $reportbase $sectioncontent;
			} elsif ( -f "$configdir/$1" ) {
				print $reportbase $_;
			} else {
				BigSister::common::log("warning", "section $1 not implemented, this section has been removed from the report");
			}
		} else {
			print $reportbase $_;
		}
	}
	$master->close() or BigSister::common::log("warning", "failed to close $masterdoc: $!" );
	$reportbase->close() or BigSister::common::log("warning", "failed to close $reportsource: $!" );
} else {
	BigSister::common::log("err", "could not generate SLA report" );
	exit(1);
}

# create or update cache
BigSister::common::log("notice", "caching graphics in $cachedir");
unless ( -d "$cachedir" ) {
	mkdir( "$cachedir" ) or BigSister::common::log("warning", "failed to create $cachedir: $!" );
}
opendir( DIR, $graphicsdir ) || die "cannot read $graphicsdir: $!";
	while ( defined (my $filename = readdir(DIR) ) ) {
		my $source = "$graphicsdir/$filename";
		if ( -f "$source" ) {
			my $cachedfile = "$cachedir/$filename";
			if ( -f "$cachedfile" ) {
				if ( ( stat("$cachedfile") )[9] < ( stat("$filename") )[9] ) {
					BigSister::common::log("info", "updating cache for  $source: $!" );
					copy("$source", "$cachedfile") or BigSister::common::log("warning", "failed to copy $source: $!" );
				}
			} else {
				copy("$source", "$cachedfile") or BigSister::common::log("warning", "failed to copy $source: $!" );
			}
		}
	}
closedir( DIR );


# convert the tex-document to the target format
my ($reportdoc, $extension) = report_transform($reportsource, $format);

# move the generated document to the download area
unless ( -d "$webdir" ) {
	mkdir( "$webdir" ) or BigSister::common::log("warning", "failed to create $webdir: $!" );
}

if ( "$reportdoc" && -d "$webdir" ) {		
	my $report = new FileHandle();
	my $download = new FileHandle();	
	if ( $report->open(" < $reportdoc ") ) {
		if ( $download->open("> $webdir/$downloadfile.$extension") ) {
			BigSister::common::log("notice", "write report to file $webdir/$downloadfile" );
			my $blksize = (stat $report)[11] || 16384; # preferred blocksize
			my $buf = undef;
			while (my $len = sysread($report, $buf, $blksize) ) {
				if ( ! defined $len ) {
					next if $! =~ /^Interrupted/;
					BigSister::common::log("err", "system read error: $!" );
					exit(1);
				}
				my $offset = 0;
				my $written = undef;
				while ( $len ) {
					unless( defined( $written = syswrite($download, $buf, $len, $offset) ) ) {
						BigSister::common::log("err", "system write error: $!" );
					}
					$len -= $written;
					$offset += $written;
				}
			}
			$download->close() or BigSister::common::log("warning", "failed to close $webdir/$downloadfile: $!" );
			
			# send a notification
			my $mailtext = "From: $mailsender\n";
			$mailtext .= "Subject: Bigsister SLA Report\n";
			$mailtext .= "\n";
			$mailtext .= "This is the BigSister Network Monitoring program. You are registered\n";
			$mailtext .= "as a recipient of $interval SLA report.\n";
			$mailtext .= "The latest SLA report can be downloaded here:\n";
			$mailtext .= "$reporturl/$downloadfile.$extension";
			
			$ENV{"PATH"} .= Platform::mergepath( $bindir );
			
			my $smtpmail = path_find("smtpmail");
			unless ( $smtpmail ) {
				BigSister::common::log("err", "smtpmail not available, unable to send notification" );
				exit(1);
			}
			
			unless ( defined $ENV{"BS_EV_mailhost"} ) {
				$ENV{"BS_EV_mailhost"} = $mailhost;
			}	
			BigSister::common::log("info", "use mailhost ".$ENV{"BS_EV_mailhost"} );
			
			foreach my $recipient ( @recipients ) {
				BigSister::common::log("notice", "sending notification to $recipient" );
				$mailtext = "To: $recipient".$mailtext;
				my $notify = new FileHandle();
				if ( $notify->open( "| $smtpmail $recipient" ) ) {
					print $notify $mailtext;
					$notify->close();
				} else {
					 BigSister::common::log("warning", "failed to send notification to $recipient: $!" );
				}
			}
			
		} else {
			BigSister::common::log("err", "failed to open $webdir/$downloadfile for writing: $!" );
		}
		$report->close() or BigSister::common::log("err", "failed to close $reportdoc: $!" );
	} else {
		BigSister::common::log("err", "failed to open $reportdoc for reading: $!" );
	}
}

# cleanup, restore working directory and system path
END {
	chdir $wd;
	$ENV{"PATH"} = $syspath;
	foreach my $file ( @tmpfiles ) {
		unlink( $file ) or BigSister::common::log("warning", "failed to unlink $file: $!" );
	}
}


sub report_transform {
	my ( $report, $format ) = @_;
	BigSister::common::log("info", "enter report_transform" );
	my %transformdata = ();
	my $outfile = undef;
	
	if ( "pdf" eq $format ) {
		%transformdata = %{ transform_pdf($report) };
	} else {
		BigSister::common::log("warning", "format $format not yet implemented" );
	}

	if ( scalar( @{$transformdata{"cmd"}} ) > 0 ) {
		chdir $reportdir or BigSister::common::log("err", "failed to change to directory $reportdir: $!" );
		foreach my $cmd ( @{$transformdata{"cmd"}} ) {
			my $conf = new FileHandle();
			BigSister::common::log("notice", "running $cmd" );
			$conf->open( "$cmd |" ) or BigSister::common::log("error", "failed to run $cmd: $!" );
			my $procout = "process output for $cmd\n";
			while ( <$conf> ) {
				$procout .= $_;
			}
			$conf->close();
			BigSister::common::log("info", "$procout" );
		}
		chdir $wd or BigSister::common::log("err", "failed to change to directory $wd: $!" );
		
		# check if the file was generated
		if ( $outfile = $transformdata{"output"} ) {
			unless ( -f $outfile ) {
				BigSister::common::log("err", "transformed report $outfile not found" );
				return undef;
			} 
		}
		
		# delete these files later
		push @tmpfiles, @{$transformdata{"tmpfiles"}};
		
	} else {
		BigSister::common::log("err", "unable to transform $report to format $format: no suitable converter found" );
		return undef;
	}
	BigSister::common::log("info", "leave report_transform" );
	return ($outfile, $format);
}

sub transform_pdf {
	my ( $report ) = @_;
	BigSister::common::log("info", "enter transform_pdf" );
	my %tdata = ();
	my ($base, $dir, $ext) = fileparse($report, ".tex");
	
	# try to locate pdflatex 
	my $pdflatex = path_find( "pdflatex" );
	if ( $pdflatex ) {
		$tdata{"cmd"} = ["$pdflatex $report", "$pdflatex $report"];
		$tdata{"tmpfiles"} = [$dir.$base.".aux", $dir.$base.".log", $dir.$base.".toc", $dir.$base.".pdf"];
		$tdata{"output"} = $dir.$base.".pdf";
		BigSister::common::log("info", "leave transform_pdf" );
		return \%tdata;
	}
	
	# try to locate latex and dvipdf
	my $latex = path_find( "latex" );
	if ( $latex ) {
		my $dvipdf = path_find( "dvipdf" );
		if ( $dvipdf ) {
			$tdata{"cmd"} = ["$latex $report", "$latex $report", "$dvipdf $report"];
			$tdata{"tmpfiles"} = [$dir.$base.".aux", $dir.$base.".dvi", $dir.$base.".log", $dir.$base.".toc", $dir.$base.".pdf"];
			$tdata{"output"} = $dir.$base.".pdf";
			BigSister::common::log("info", "leave transform_pdf" );
			return \%tdata;
		}
	}
	
	# sorry, nothing suitable found
	BigSister::common::log("warning", "failed to generate pdf document, no suitable program is installed on this system" );
	return undef;
}

sub path_find {
	my ( $prog ) = @_;
	BigSister::common::log("info", "enter path_find" );
	my $pathdiv = Platform::path_divider();

	foreach my $path ( split /$pathdiv/, $ENV{PATH} ) {
		if ( -x "$path/$prog" ) {
			BigSister::common::log("info", "leave path_find" );
			return "$path/$prog";
		}
	}
	return undef;
}


sub createchapter {
	my ($type) = @_;
	my $chaptermethod = "createchapter_".$type;
	BigSister::common::log("notice", "calling method $chaptermethod" );
	return eval("$chaptermethod");
}

#
# This method will create the follwing table: Criteria | Timeframe | SLA Status
#
sub createchapter_managementsummary {
	BigSister::common::log("info", "enter method createchapter_managementsummary" );
	my $mgmtsum = "\\newpage\n";
	$mgmtsum .= "\\chapter{"
		.escapetext( $slareporter->format_field("summary") )
		."}\n";
	
	foreach my $service ( keys %slareport ) {
		BigSister::common::log("notice", "processing service $service" );
		my $subsect_content = "\\section{"
			.escapetext( $slareporter->format_field("$service") )
			."}\n";
		$subsect_content .= "\\begin{description}\n";
		$subsect_content .= "\\item "
			.escapetext( $slareporter->format_field($slareport{$service}{"class"}) )
			."\n";
		$subsect_content .= "\\end{description}\n";
		$subsect_content .= "\\begin{tabular}[t]{|l|c|c|}\n";
		$subsect_content .= "\\hline\n";
		$subsect_content .= "\\bf{".escapetext( $slareporter->format_field("summary_crit") )."}"
			." & "
			."\\bf{".escapetext( $slareporter->format_field("summary_frame") )."}"
			." & "
			."\\bf{".escapetext( $slareporter->format_field("summary_status") )."}"
			." \\\\ \n";
		$subsect_content .= "\\hline\n";
		foreach my $criteria ( keys %{$slareport{$service}{"criteria"}} ) {
			foreach my $frame ( keys %{$slareport{$service}{"criteria"}{$criteria}} ) {
				
				my $slastatus = $slareporter->format_field("sla_na");
				if ( defined $slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"nok"} ) {
					$slastatus = $slareporter->format_field("sla_nok")." ("
						.scalar( @{$slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"nok"}} )
						."x )";
				} elsif ( defined $slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"ok"} ) {
					$slastatus = $slareporter->format_field("sla_ok");
				}
				
				$subsect_content .= escapetext( $slareporter->format_field("$criteria") )
					." ".escapetext( $slareport{$service}{"criteria"}{$criteria}{$frame}{"operator"} )
					." ".escapetext( $slareport{$service}{"criteria"}{$criteria}{$frame}{"threshold_unit"} );
				$subsect_content .= " & "
					.escapetext( $slareporter->format_field("$frame") )
					." & "
					.escapetext( $slastatus )
					." \\\\ \n";
				$subsect_content .= "\\hline\n";
			}
		}
		$subsect_content .= "\\end{tabular}\n\n";
		BigSister::common::log("info", "section content: \n$subsect_content" );
		$mgmtsum .= $subsect_content;
	}
	BigSister::common::log("info", "leave method createchapter_managementsummary" );
	return $mgmtsum;
}

#
# This method will create the following table: Date | Measurand [unit] | Status
#
sub createchapter_sladetail {
	BigSister::common::log("info", "enter method createchapter_sladetail" );
	my $sladet = "\\newpage\n";
	$sladet .= "\\chapter{"
		.$slareporter->format_field("detail")
		."}\n";
	foreach my $service ( keys %slareport ) {
		BigSister::common::log("notice", "processing service $service" );
		my $subsect_content = "\\section{"
			.escapetext( $slareporter->format_field("$service") )
			."}\n";
			
		$subsect_content .= "\\begin{description}\n";
		$subsect_content .= "\\item "
			.escapetext( $slareporter->format_field($slareport{$service}{"class"}) )
			."\n";
		$subsect_content .= "\\end{description}\n";
		
		foreach my $criteria ( keys %{$slareport{$service}{"criteria"}} ) {
			$subsect_content .= "\\newpage";
			$subsect_content .= "\\subsection{"
				.escapetext( $slareporter->format_field("$criteria") )
				."} \n";
			my $firstparagraph = 1;
			foreach my $frame ( keys %{$slareport{$service}{"criteria"}{$criteria}} ) {
			  if ( ! $firstparagraph ) {
				  # insert sla-graphic here
				  $subsect_content .= "\\newpage";
				}
				$firstparagraph = 0;
				$subsect_content .= "\\begin{description}\n";
				$subsect_content .= "\\item Timeframe: ".escapetext( $slareporter->format_field("$frame") )."\n";
				$subsect_content .= "\\item Threshold definition: "
					.escapetext( $slareporter->format_field("$criteria") )
					." ".escapetext( $slareport{$service}{"criteria"}{$criteria}{$frame}{"operator"} )
					." ".escapetext( $slareport{$service}{"criteria"}{$criteria}{$frame}{"threshold_unit"} );
				$subsect_content .= "\n";
				$subsect_content .= "\\end{description}\n";
				
				
				if ( scalar( keys %{$slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"bydate"}} ) > 0 ) {
					my $currmonth = undef;
					foreach my $date ( sort keys %{$slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"bydate"}} ) {
						$date =~ /^\d{4}-(\d{2})-\d{2}/;
						if ( not defined $currmonth ) {
							$currmonth = $1;
							
							# print a new table header
							$subsect_content .= "\n";
							$subsect_content .= "\\begin{tabular}[t]{|l|c|c|}\n";
							$subsect_content .= "\\hline\n";
							$subsect_content .= "\\bf{".escapetext( $slareporter->format_field("detail_date") )."}"
								." & "
								."\\bf{".escapetext( $slareporter->format_field("detail_measurand") )."}"
								." & "
								."\\bf{".escapetext( $slareporter->format_field("detail_status") )."}"
								." \\\\ \n";
							$subsect_content .= "\\hline\n";
						} 
						
						if ( defined $currmonth && $currmonth ne $1 ){
							$currmonth = $1;
							
							# print a table (page break)
							$subsect_content .= "\\end{tabular}\n\n";
							$subsect_content .= "\\hspace{10pt}\n";
							
							# print a new table header
							$subsect_content .= "\n";
							$subsect_content .= "\\begin{tabular}[t]{|l|c|c|}\n";
							$subsect_content .= "\\hline\n";
							$subsect_content .= "\\bf{".escapetext( $slareporter->format_field("detail_date") )."}"
								." & "
								."\\bf{".escapetext( $slareporter->format_field("detail_measurand") )."}"
								." & "
								."\\bf{".escapetext( $slareporter->format_field("detail_status") )."}"
								." \\\\ \n";
							$subsect_content .= "\\hline\n";
						}						

						my $measurand = $slareport{$service}{"criteria"}{$criteria}{$frame}{"statistics"}{$date};
						if ( defined $slareport{$service}{"criteria"}{$criteria}{$frame}{"unit"} ) {
							my $unit = $slareport{$service}{"criteria"}{$criteria}{$frame}{"unit"};
							$measurand = $slareporter->convert_unit(
								{ 	"to" => "$unit",
									"value" => "$measurand" }
							);
						}
						my $status = $slareporter->format_field($slareport{$service}{"criteria"}{$criteria}{$frame}{"status"}{"bydate"}{$date});
						
						$subsect_content .= escapetext( $date )
							." & "
							.escapetext( $measurand )
							." & "
							.escapetext( $status )
							." \\\\ \n";
						$subsect_content .= "\\hline\n";
					}				
					$subsect_content .= "\\end{tabular}\n\n";
				} else {
					$subsect_content .= escapetext( $slareporter->format_field("no_data") );
				}					
			}	
		}
		BigSister::common::log("info", "section content: \n$subsect_content" );
		$sladet .= $subsect_content;
	}
	BigSister::common::log("info", "leav method createchapter_sladetail" );
	return $sladet;
}

sub escapetext {
	my ($text) = @_;
	if ( $text ) {
		$text =~ s/\|/\\textbar/g;
		$text =~ s/\\/\\textbackslash/g;
		$text =~ s/</\\textless/g;
		$text =~ s/>/\\textgreater/g;
		$text =~ s//\\"a/g;
		$text =~ s//\\"o/g;
		$text =~ s//\\"u/g;
		$text =~ s/(&|%|\$|_|#|{|})/\\$1/g;
	}
	return $text;
}

