#!/usr/bin/perl
#
# This program is essentially a perl implementation of existing functionality 
# in the Nagios code which generates a report of the top X number of service
# notifications for a particular time period. 
#
#Copyright (C) 2005  Michael Place
#
#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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# Mike Place, XMission Internet <mp@xmission.com> http://www.xmission.com
#
# BUGS: Depending on your point of view, this script may display an extra day's
# 	worth of data. Try running with $debug enabled to make sure that you're
# 	getting the time period that you really want.
#
# 	It is currently possible to run with -n and without -v. This will produce
# 	no output. 
#
# 	This script evaluates SERVICE NOTIFICATIONs and *not* SERVICE ALERTs, therefore
# 	you will see different output than what Nagios might display in its own reports
# 	which are compiled against SERVICE ALERTs. If you don't understand the difference
# 	and you email me to complain, I will delete your email without sending you either
# 	an ALERT or a NOTIFICATION.
#
# 	Grammar formatting might be incorrect for emails which output results < 2. For example,
# 	you might get something that says 'Nagios downtime report for the past 1 days'. 
#
# 	Doesn't complain if less than the number of log files requested don't exist. It 
# 	just processes what is there. Additionally, if files ending in .log are present in the
# 	directory specified by -f (or /var/log/nagios by default), this script will process those
# 	and then it will give you meaningless results that you will find most troublesome.
#
# 	Error handling is horrible, that's all I can really say.
#
# 	Almost certainly others.
# 	
#############################################################################

use strict;
#global var declerations
our($opt_h,$opt_v,$opt_l,$opt_n,$opt_f,$opt_d); #for getopt
our($debug);
our($email_from, $email_to);

# init useful modules
use Getopt::Std;
use Time::localtime;
use File::Find ();
use Array::Uniq;
use Mail::Mailer;

# SCRIPT CONFIGURATION VARIABLES
## YOU PROBABLY WANT TO CHANGE AT LEAST SOME OF THESE
$debug = '0';
($email_from, $email_to) = ('nagios@yourdomain.com', 'you@yourdomain.com'); 
$opt_l = '10'; 			#default to 10 lines of output, unless overriden by -l
$opt_f = '/var/log/nagios'; 	#default to /var/log/nagios, unless overriden by -f

########################
# MAIN PROGRAM EXECUTION
########################

# get options
# -d: # of days
# -v  display report to stdout
# -l  output # of lines
# -n  no email
# -f  nagios log file directory
# -h  help

getopts('hvnfl:d:');
if(defined($opt_h)) {
	&usage();
	exit;
}

unless(defined($opt_d or $opt_h)) {
	&usage();
	exit; 
}

my(@global_found_files) = &getfiles($opt_d);
my(@final_sorted_list) = &compile(@global_found_files);
&present(@final_sorted_list);

########################
# SUBROUTINES
########################

# sub: getfiles ()
# subroutine to figure out the number of logs to process and what the 
# names of those logs are. this routine should accept a time period
# (in days) from GetOpts and it should return an array containing
# a full list of filenames to process.

sub getfiles($days) {
	if($debug) {
		print("opt_d is $opt_d\n");
	}
	## start figuring out dates and shit
	# first get today's date in funky nagios format
	my($tm) = localtime;
	my ($month, $day, $year) = ($tm->mon, $tm->mday, $tm->year + 1900); # don't forget to add 1900 to the year
	if($debug) {
		print("localtime is reporting: $month-$day-$year\n");
	}
	# just use File::Find to get mtime for files. This is *much* simpler and easier than
	# calculating dates.
	use vars qw/*name *dir *prune/;
	*name   = *File::Find::name;
	*dir    = *File::Find::dir;
	*prune  = *File::Find::prune;

	sub wanted;
	File::Find::find({wanted => \&wanted}, $opt_f);
	# depending on how you look at it, this is returning one too many days
	
	my(@found_files); # this placement is right, but i don't know why

	if($debug) {
		use Data::Dumper;
		print Dumper(\@found_files);
	}
	
	
		sub wanted {
			my ($dev,$ino,$mode,$nlink,$uid,$gid);
			(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
			(int(-M _) < $opt_d) &&
			/^.*\.log\z/s &&
			push(@found_files, $name);	
		}
	
	return(@found_files); #return it	

}

# sub: compile()
# subroutine to open up the files listed by getfiles() and create a 
# data structure that consists of '<machinename>;<service>'.
# This is culled from all lines matching SERVICE NOTIFICATION &&
# CRITICAL|WARNING. They are then sorted and counted and the
# sub returns an array containing elements ala '<machinename>;<service>'
#
## do we want to be able to select between SERVICE NOTIFICATION and SERVICE WARNING?
## a cmd switch wouldn't be hard to add

sub compile (@files) {
	
	#declare vars
	my(@machine_service); #tempting to use a hash, but doing so causes repeat keys to be walloped
	my(@sorted_machine_service);
	my(%count); #used as we iterate through @sorted_machine_services to count up the number of occurances
	my(@sorted_array); # this is what we return to the main loop
	
	if($debug) {
		use Data::Dumper;
		print Dumper(\@global_found_files);
	}

	#open each file in turn
	my($log_file);
	foreach $log_file (@global_found_files) {
		open(LOG_HANDLE, "< $log_file") or die $!;
		while(<LOG_HANDLE>) {
			my (@line_fields) = split('\;', $_);
			#for the most part, we now have arrays that looks like this:
			#$VAR1 = [
			#          '[1135667844] SERVICE NOTIFICATION: nagbot',
			#          'mailcenter',
			#          'Queue load',
			#          'CRITICAL',
			#          'notify-by-nagbot',
			#          'Max.  = 2912
			#
			#we only want to work with lines which are SERVICE NOTIFICATION && CRITICAL|WARNING
			if(@line_fields[0] =~ /SERVICE NOTIFICATION/ && @line_fields[3] =~ /CRITICAL/) {
			#	$machine_service{@line_fields[1]} = @line_fields[2];
				my($cat) = (@line_fields[1] . ':' . @line_fields[2]);	
				push(@machine_service, $cat);
			}
				

		}
	}
	#whew! we made it through the list of logfiles.
	#now we have a complete list in @machine_service
	#that looks like this:
	# 'xwi:Library Day-Riverside Branch',
	# 'mailcenter:Queue load',
	#
	# now it's time to sort these
	@sorted_machine_service = sort(@machine_service);	

	#all that's left to do now is to itirate through the array and
	#count occurances of each unique element
	#
	#so, what we'll do is step through the array and for each unique
	#occurence of a service pair, we'll create a hash key like
	#$count{$SERVICE} = <# of occurences>
	#this hash is what should be returned to the main program to
	#be processed by &present
	
	my($occ);
	my($counter) = '0';
	foreach $occ (@sorted_machine_service) {
		#have we seen this before?
		if(exists($count{$occ})) {
			#if so, add one to the count
			my($tmp_cur) = $count{$occ};
			$tmp_cur++;
			$count{$occ} = $tmp_cur;
		} else {
		#if not, create a hash key
			$count{$occ}='1';
		}
	}
	#that properly counts, (though really, it would be neater to do $count{$occ} = ++$tmp_cur
	
	foreach (sort value_sort keys %count) {
		push(@sorted_array, "$_ has generated $count{$_} notifications\n");
	}

	sub value_sort {
		return $count{$a} <=> $count{$b};
	}

	if($debug) {
		use Data::Dumper;
		print Dumper(\@sorted_machine_service);
		print Dumper(\%count);
	}

	#return this out to main loop
	return(@sorted_array);
}

# sub: present()
# subroutine to take the array from compile() and format it. It then
# sends it using Net::Mailer

sub present(@final_sorted_list) {
	my(@rev_sorted_list) = reverse(@final_sorted_list);
	
	# if -l, limit the number of elements in the array
	# use $counter, which is already init'd
	my $counter = '0';
	my @final_rev_sorted_list;
	while($counter < $opt_l) {
		push(@final_rev_sorted_list, shift(@rev_sorted_list));
		$counter++;
	
	}


	unless ($opt_n) {
		my($mailer)= Mail::Mailer->new("sendmail");
		$mailer->open({ From    => $email_from,
		                To      => $email_to, 
		                Subject => "Nagios downtime report for the past $opt_d days",
		              })
		or die "Can't open: $!\n";
		print $mailer @final_rev_sorted_list;
		$mailer->close();
	}

	if($opt_v) {
		my $line;
		foreach $line (@final_rev_sorted_list) {
			print $line;
		}
	}

		
}

# sub usage()
# subroutine to display usage information if Getopt::Std is passed -h
# or passed no vars at all.

sub usage() {
	print("\nadmin_report.pl 0.1 - Released December 27th, 2005
Copyright (c) 2005 Michael Place <mp\@xmission.com>\n
Usage: admin_report.pl [-v] [-d <days worth of logfiles to report>]\n
Options:\n
-d # of days to process\n-v Display report\n-l Limit number of lines to display\n-n No email\n-f Nagios log directory\n-h This help\n");
}
