#!/usr/bin/perl 

#(C) Fredrik Widell (fredrik@widell.net) 2003
#use as you wish, modify as you wish
#this script will probably destroy your whole
#network in a second, dont blame me. send me
#a email if you want to. This script needs
#syslogs from cisco-routers and send emails
#when a hit occurs, quite useful to get a 
#picture of the behaviour of lines/routers.
#just uncomment any match belov to lower
#the amount of spam.
#
# Version 1.1
#

use strict;

#needed to get logs in 5min interval (for me atleast)
use Date::Manip;


#######################################################################
# change values below according to your network
#######################################################################

#preferably you run this script via crontab like this
#0,5,10,15,20,25,30,35,40,45,50,55 * * * * /path/to/script/logparser.pl > /dev/null 2>&1

#my netname, f.ex SUNET
my $netname="SOMETEXT";

#my cisco logfile
my $logfile="/var/log/cisco.log";

#this is my domain, which I strip from the logs
my $domain='\.sunet\.se';

#where to send the mail
my $mailadress="your\@mailadress.se";

#the from-adress
my $mymailadress="my-sender\@mailadress.se";

#wheres my mailer,
#my $mta='/usr/bin/mail';
my $mta='/usr/sbin/sendmail';

#This is the logs I am interested in, names to the left
#can be anything, names to the right must match a syslogentry
#to trigger a mail.
my %match=(
'isis',                        ': ISIS:',
'traceback',                   'Traceback=',
'cpuhog',                      'SYS-3-CPUHOG:',
'bgpchange',                   'BGP-5-ADJCHANGE:',
'bgpnotification',             'BGP-3-NOTIFICATION:',
'bgp-max-prefixes',            'BGP-4-MAXPFX:',
'srp-wrap',                    'SRP-4-WRAP_STATE_CHANGE:',
'srp-alarm',                   'SRP-4-ALARM:',
'lineproto-alarm',             'LINEPROTO-5-UPDOWN:',
'sonet-alarm',                 'SONET-4-ALARM:',
'link-alarm',                  'LINK-3-UPDOWN:',
'ipc-failure',                 'FIB-3-FIBDISABLE:',
'reset-slot',                  'GRP-4-RSTSLOT:',
'linecard-crash',              'LCINFO-3-CRASH:',
'DRAM-error',                  'MEM_ECC-3-SBE:',
'configure',                   'SYS-5-CONFIG_I:',
'tftp-configure',              'SYS-5-CONFIG:',
'restart',                     'SYS-5-RESTART:',
'mds-lc-fail',                 'MDS-2-LC_FAILED_IPC_ACK:',
'mem-parity-error',            'RX192-3-REGVAL:',
'grp-alarm',                   'GRP-3-',
'fabric-interface-asic',       'FIA-3-',
'performance-route-processor', 'PRP-3-',
'dram-error',                  'LC-3-',
'dram-error',                  'LC-4-',
'memory-parity-error',         '-3-REGVAL',
'severity0',                   '-0-\\w+:\\s',
'severity1',                   '-1-\\w+:\\s',
'severity2',                   '-2-\\w+:\\s'
);

#or, if you use junipers, you could have this f.ex,
#
#actually, the below is not working, need to rewrite
#the code first not to be ciscospecific in the logparsing :(
#
#my %match=(
#'link down',            'SNMP_TRAP_LINK_DOWN',
#'bgp notification',     'NOTIFICATION',
#'board reset',          'CHASSISD_GBUS_RESET_EVENT',
#'isis adjacency',       'RPD_ISIS_ADJDOWN',
#'sonet alarm',          'SONET alarm',
#'routing engine',       'UI_MASTERSHIP_EVENT',
#'sfp',                  'SFP',
#'user',                 'UI_',
#'ifx',                  'IFX',
#'bgp',                  'BGP_'
#);



#######################################################################

#creating an inverse hash of the above %match to
#use when creating a hash with matches. ju.
my $type;
my %invmatch;
foreach $type (keys(%match)) {
	$invmatch{$match{$type}}=$type;
}

#getting todays date/time suitable for log-format, "Feb 27 23:03:43"
my $rightnow=&UnixDate("today","%b %e ");
my $date=&DateCalc("today","- 5minutes");
$date =~ s/^........//;
$rightnow .= $date;
my $timestamp=$date;


#fixing regexp to match a 5-min interval (eg 5 minutes earlier than right now)
$rightnow =~ /(....$)/;
my $trash=$1;
$trash =~ s/...$//;
if ($trash < 5 ) {
	$trash='[5-9]';
} else {
	$trash='[0-4]';
}
$rightnow =~ s/....$/$trash/;


#opening the logfile.
open LOGFILE, "<  $logfile"  or die "can't open datafile: $!";


#creating a regexp to match all the alarms at once
my $matches=join('|',values(%match));

#prevline is used to get more data from tracebacks, 
#the log before a traceback is probably related to the traceback
my $prevline='';
my $logs;
my $hit=0;
my %maildata;
my $notseen;

#matching the logs
while (defined($logs = <LOGFILE>)){

	#removing unnessecary data in the logs, clogs the report
	#eg, just the domainname and some magic numbers
	$logs =~ s/$domain.*%|$domain: \d{2,7}: -/\;/;

	#pretty printing of the logs, pad blanks after routername
	my ($ett, $tva) = split(';', $logs);
	$logs = sprintf "%-30s %s",$ett,$tva;

	#matchmaking
	if ($logs =~ /(^$rightnow.*)($matches)(.*)/gs) {
		
		#this is the hit from my $matches
		my $match=$2;

		#used do recognize a spamming router which
		#sends the same logs every 5 minutes, can
		#be annoying.
		my $last=$3;

		#get the routername
		my $nearhost=$1;
		$nearhost =~ /(^${rightnow}:.. )(.*)/;
		my $host=$2;
		$host =~ s/ .*//;
		#print "host=$host\n";

		#take a copy of the logs from the router
		#to determine wheather we should apply
		#some dampening so we do not get spammed
		#of a misbehaving router.
		if ( -e "/tmp/$host.$invmatch{$match}.tmp" ) {
			#print "/tmp/$host.$invmatch{$match}.tmp exists\n";
			open ROUTERTMP, "+<  /tmp/$host.$invmatch{$match}.tmp"; 

			#copy the old log (without a timestamp) to $trash
			$trash=join("", <ROUTERTMP>);

			#print "trash=$trash\n";
			#print "match . last=${match}${last}\n";

			#check if the old log is the same as the new logentry
			if ($trash eq $match . $last) {

				#backup of new log with timestamp, this timestamp
				#will determine when the router is allowed to notify
				#us again.
				open ROUTER, ">> /tmp/$host.$invmatch{$match}";
					print ROUTER $logs;
				close ROUTER;

				#if the log is same as before, delete it from $logs
				#so we do not send a mail again
				#first check how old the first log is:
				open ROUTER, "< /tmp/$host.$invmatch{$match}";
					$trash=<ROUTER>;
				close ROUTER;
				$trash =~ /(\d{2}:\d{2}:\d{2})/;
				$trash = $1; 
				my $delta=&DateCalc($trash,$timestamp);
				$delta =~ /(\+.:.:.:.:)(\d{1,2})(:.*)/;
				$delta = $2;
				print "first seen logentry for $invmatch{$match} for $host is $trash\n";
				print "timestamp=$timestamp, difference is $delta\n";
				if ($delta >= 1) {
					my $count;
					open ROUTER, "< /tmp/$host.$invmatch{$match}";
					$count++ while <ROUTER>;
					close ROUTER;
					$logs .= "### $host reported the exact same logs $count times between $trash and $timestamp###\n\n";
					print "deleting /tmp/$host.$invmatch{$match}.tmp", "/tmp/$host.$invmatch{$match}\n";
					unlink ("/tmp/$host.$invmatch{$match}.tmp", "/tmp/$host.$invmatch{$match}");
				} else {
					print "already seen this $invmatch{$match} from $host\ndeleting logs\n";
					#$logs .= "### I will surpress further logs like the above from $host for 1 hour ###\n\n";
					$logs = "";
					$notseen = 0;
				}

			} else {
				print "this $invmatch{$match} is new from $host\n";
				seek(ROUTERTMP, 0, 0);
				print ROUTERTMP $match . $last;
				$notseen = 1;
			}
			close ROUTERTMP;
				
		} else {
			open ROUTERTMP, "> /tmp/$host.$invmatch{$match}.tmp";
				#print $logs;
				print "saving unseen log $invmatch{$match} from $host\n";
				print ROUTERTMP $match . $last;
				$notseen = 1;
			close ROUTERTMP;
		}


		#magic handling of tracebacks, add previous line
		if ($match eq 'Traceback=' && $notseen) {
			$maildata{$invmatch{$match}}.=$prevline;
		}

		#magic handling of bgp, view the neigbors hostname also if exist
		if ($match eq 'BGP-5-ADJCHANGE:') {

			#get the ip-adress
			$logs =~ /([1-9]\d{2,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
			my $ip=$1;

			#get the peers hostname
			my @octets = split ('\.', $ip) ;
                	my $ip_number = pack ("CCCC", @octets[0..3]);
                	(my $name, my $aliases, my $type, my $len, my $addr) = gethostbyaddr ($ip_number, 2) ;

			#if exist, modify logs
                	if ($name) { 
				$logs =~ s/[1-9]\d{2,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/$ip=$name/;
			} 
		}
		if ($match eq 'BGP-3-NOTIFICATION:') {

			#get the ip-adress
			$logs =~ /([1-9]\d{2,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
			my $ip=$1;

			#get the peers hostname
			my @octets = split ('\.', $ip) ;
                	my $ip_number = pack ("CCCC", @octets[0..3]);
                	(my $name, my $aliases, my $type, my $len, my $addr) = gethostbyaddr ($ip_number, 2) ;

			#if exist, modify logs
                	if ($name) { 
				$logs =~ s/[1-9]\d{2,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/$ip=$name/;
			} 
		}


		#if match, add to maildata
		if ($logs) {
			$maildata{$invmatch{$match}}.=$logs;
			$hit=1
		}
	}
	$prevline=$logs;
}

close LOGFILE;

#if hit, send a mail
my $subject;
if ($hit) {
	print "I will send a mail \n";

	#create metadatasubject with the matched logs
	$subject=join(',',sort(keys(%maildata)));

	#using local mail, the uncommented below uses 'mail' and the live
	#one below uses sendmail, to create specific headers according to
	#the logs to be used by ex procmail
	#open TOMAIL, "| $mta -s \"$subject $netname $rightnow \" $mailadress";
	open TOMAIL, "| $mta -t";
		print TOMAIL "From: $mymailadress\n";
		print TOMAIL "To: $mailadress\n";
		foreach $trash (sort(keys(%maildata))) {
			print TOMAIL "X-filter: $trash\n";
		}
		print TOMAIL "Subject: $subject $netname $rightnow\n\n";

		#looping through maildata to make a report
		foreach $trash (sort(keys(%maildata))) {
			print TOMAIL "\n";
			print TOMAIL "----------------------------------\n";
			print TOMAIL "New entries in $trash\n";
			print TOMAIL "----------------------------------\n";
			print TOMAIL $maildata{$trash};
		}
	close TOMAIL
}



#done.
