#!/usr/bin/perl # #----------------------------------------------- # tSmoke.pl # Dan McGinn-Combs, Sep 2003 # tSmoke.v 0.4 2004/03 McGinn-Combs #----------------------------------------------- # # Modified for Smokeping official distribution since 20050526 # Original README follows # # tSmoke.v04.README # - added downtime report (--downtime) # - a few tweaks to the calculations to ensure it's consistent # # tSmoke.v03.README # - Initial Release # - The script, started through cron, will cull through a config file and # determine which hosts are down at a point in time (Morning report) and # send out an smtp message to a mobile phone (for example). # # - It will also cull through the same config file and, using an included html # file (small change to General section of the config file), send an html # message which shows the availability over the past day, week, month # and quarter. # # - It can also show detail data depending on the setting of # command line option "detail". # # tSmoke.v02.README # - Local testing version #----------------------------------------------- # # 1) This program is run via CRON or the command line # 2) It extracts RRD information from a smokeping config file # 3) It pulls data from RRD files to determine if anything is offline, that is returning 0 PINGs # 4) tSmoke reports status via an SMTP alert # 5) tSmoke also generates an SMTP mail showing historical view of availability # # Many thanks to the following people for their help and guidance: # Jim Horwath of Agere Systems Inc. for his examples and pointers to Spreadsheet::WriteExcel # Frank Harper the author of SLAMon, a tool for tracking Service Level Agreements # Tobias Oetiker, or course, the author of Smokeping, RRDTool and MRTG # use strict; # We need to use # -- Smokeping libraries # -- RRDTool # -- Getopt::Long # # Point the lib variables to your implementation use lib qw(lib); use lib "/usr/local/rrdtool-1.0.39/lib/perl"; use Smokeping 2.003003; use Net::SMTP; use Getopt::Long; use Pod::Usage; use RRDs; # Point to your Smokeping config file my $cfgfile = "etc/config.dist"; # global variables my $cfg; #this is designed to work on IPv4 only my $havegetaddrinfo = 0; # we want opts everywhere my %opt; #Hashes for the data my (%Daily,%Weekly,%Monthly,%Quarterly); # the entries my (%DailyC,%WeeklyC,%MonthlyC,%QuarterlyC); # a count of the entries ###################### ### Moving Average ### ###################### # Just a reminder of how to do a moving average if you ever want to # PREV,UN,,UN,1,,IF,PREV,IF,,UN,1,,IF,-,,*,A,UN,1,A,IF,+ # Change Log: # DMC - Added Quarterly Status # DMC - Added HTML mail reporting and consolidated functions # DMC = Added an external HTML mail template, tMail my $RCS_VERSION = '$id: tSmoke.v 0.4 2004/03 McGinn-Combs'; sub test_mail($) { my $cfg = shift; my $mail = <<"EOF"; Subject: tSmoke test To: $cfg->{Alerts}{to} This is a test mail with 'tSmoke --testmail'. EOF print "Sending a test mail to $cfg->{Alerts}{to} from $cfg->{Alerts}{from}..."; Smokeping::sendmail($cfg->{Alerts}{from}, $cfg->{Alerts}{to}, $mail); print "done.\n"; }; sub morning_update($) { # Send out a morning summary of devices that are down my $cfg = shift; my $Body = ""; my $TmpBody = ""; my $To = ""; if ( $opt{to} ) { $To = $opt{to}; } else { $To = $cfg->{Alerts}{to}; } # Get a list of the existing RRD Files my @rrds = split ( /\n/,list_rrds($cfg->{Targets},"","") ); my $Count = $#rrds + 1; my $Down = 0; foreach my $target (@rrds) { my $Loss = 0; my ($start,$step,$names,$data) = RRDs::fetch "$target","AVERAGE","--start","-300"; my $ERR=RRDs::error; die "ERROR while reading $_: $ERR\n" if $ERR; foreach my $line (@$data) { $Loss += $$line[3]; } $Down += 1 if $Loss == 0; $target =~ s/^([a-zA-Z0-9]*\/)*//; $target =~ s/.rrd//; $TmpBody .= "$target\n" if $Loss == 0; } $Body = <{Alerts}{from},$To,$Body); } sub weekly_update($) { # Send out a formatted HTML Table of the # Previous Day, Week, Month and Quarter Availability # Get a list of the existing RRD Files my @rrds = split ( /\n/,list_rrds($cfg->{Targets},"","") ); my $To = ""; if ( $opt{to} ) { $To = $opt{to}; } else { $To = $cfg->{Alerts}{to}; } my $Body =''; # Calculations Based on the following: # RRDs::graph "fake.png", # '--start','-86400', # '-end','-300', # "DEF:loss=${rrd}:loss:AVERAGE", # "CDEF:avail=loss,0,100,IF", or more precisely "CDEF:avail=loss,2,GE,0,100,IF" # and adding in the check for unknown for systems just coming on line # "CDEF:avail=loss,UN,0,loss,IF,$pings,GE,0,100,IF" # Arbitrarily a loss of 10% of Pings means the system was down my $pings = $cfg->{Database}{pings} * .1; foreach my $target (@rrds) { # Get an average Availability for each RRD file my $ERR; my ($DAverage,$Dxsize,$Dysize) = RRDs::graph "fake.png", "--start","-86400", "--end","-600", "--step","1008", "DEF:loss=$target:loss:AVERAGE", "CDEF:avail=loss,UN,0,loss,IF,$pings,GE,0,100,IF", "PRINT:avail:AVERAGE:%.2lf"; $ERR=RRDs::error; die "ERROR while reading $_: $ERR\n" if $ERR; my ($WAverage,$Wxsize,$Wysize) = RRDs::graph "fake.png", "--start","-604800", "--end","-600", "--step","4320", "DEF:loss=$target:loss:AVERAGE", "CDEF:avail=loss,UN,0,loss,IF,$pings,GE,0,100,IF", "PRINT:avail:AVERAGE:%.2lf"; $ERR=RRDs::error; die "ERROR while reading $_: $ERR\n" if $ERR; my ($MAverage,$Mxsize,$Mysize) = RRDs::graph "fake.png", "--start","-2592000", "--end","-600", "--step","4320", "DEF:loss=$target:loss:AVERAGE", "CDEF:avail=loss,UN,0,loss,IF,$pings,GE,0,100,IF", "PRINT:avail:AVERAGE:%.2lf"; $ERR=RRDs::error; die "ERROR while reading $_: $ERR\n" if $ERR; my ($QAverage,$Qxsize,$Qysize) = RRDs::graph "fake.png", "--start","-7776000", "--end","-600", "--step","4320", "DEF:loss=$target:loss:AVERAGE", "CDEF:avail=loss,UN,0,loss,IF,$pings,GE,0,100,IF", "PRINT:avail:AVERAGE:%.2lf"; $ERR=RRDs::error; die "ERROR while reading $_: $ERR\n" if $ERR; $target =~ s/$cfg->{General}{datadir}\///; $target =~ s/.rrd//; my @Path; push @Path,split/\//,$target; update_stats ( \@Path, @$DAverage[0], @$WAverage[0], @$MAverage[0], @$QAverage[0]); } # Prepare the e-mail message $Body = <{General}{tmail} or die "ERROR: can't read $cfg->{General}{tmail}\n"; while (){ my $Summary = Summary_Sheet(); s/<##SUMMARY##>/$Summary/ig; my $Daily = DetailSheet(86400); s/<##DAYDETAIL##>/$Daily/ig; my $Weekly = DetailSheet(604800); s/<##WEEKDETAIL##>/$Weekly/ig; my $Monthly = DetailSheet(2592000); s/<##MONTHDETAIL##>/$Monthly/ig; my $Quarterly = DetailSheet(7776000); s/<##QUARTERDETAIL##>/$Quarterly/ig; $Body .= $_; } close tSMOKE; Smokeping::sendmail($cfg->{Alerts}{from}, $To, $Body); } sub update_stats($$$$$); sub update_stats($$$$$) { # Update the uptime percentages in the Hash Arrays my $Path = shift; my $DAverage = shift; my $WAverage = shift; my $MAverage = shift; my $QAverage = shift; #Enter everything once as it exists #Trim off the rightmost component (hostname) and reenter the code #If there is only one component, this is the final level #This is an average of averages my $Ticket = join ( ".",@$Path); $Daily { $Ticket } += $DAverage; $Weekly { $Ticket } += $WAverage; $Monthly { $Ticket } += $MAverage; $Quarterly {$Ticket } += $QAverage; $DailyC { $Ticket }++; $WeeklyC { $Ticket }++; $MonthlyC { $Ticket }++; $QuarterlyC { $Ticket }++; my $Length = @$Path; @$Path = @$Path [ 0 .. $Length - 2 ]; update_stats(\@$Path,$DAverage,$WAverage,$MAverage,$QAverage) if $Length > 1; } sub Summary_Sheet() { my $Body = ''; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; foreach (sort { $a cmp $b } keys %Monthly) { next if ( $_ =~ /\./ ); # this is a major section heading $Body .= "\n"; $Body .= ""; $Body .= "" if $Quarterly{$_}/$QuarterlyC{$_} >= 99 ; $Body .= "" if $Quarterly{$_}/$QuarterlyC{$_} > 95 and $Quarterly{$_}/$QuarterlyC{$_} < 99 ; $Body .= "" if $Quarterly{$_}/$QuarterlyC{$_} > 90 and $Quarterly{$_}/$QuarterlyC{$_} < 95 ; $Body .= "" if $Quarterly{$_}/$QuarterlyC{$_} < 90 ; $Body .= "" if $Monthly{$_}/$MonthlyC{$_} >= 99 ; $Body .= "" if $Monthly{$_}/$MonthlyC{$_} > 95 and $Monthly{$_}/$MonthlyC{$_} < 99 ; $Body .= "" if $Monthly{$_}/$MonthlyC{$_} > 90 and $Monthly{$_}/$MonthlyC{$_} < 95 ; $Body .= "" if $Monthly{$_}/$MonthlyC{$_} < 90 ; $Body .= "" if $Weekly{$_}/$WeeklyC{$_} >= 99; $Body .= "" if $Weekly{$_}/$WeeklyC{$_} > 95 and $Weekly{$_}/$WeeklyC{$_} < 99 ; $Body .= "" if $Weekly{$_}/$WeeklyC{$_} > 90 and $Weekly{$_}/$WeeklyC{$_} < 95 ; $Body .= "" if $Weekly{$_}/$WeeklyC{$_} < 90 ; $Body .= "" if $Daily{$_}/$DailyC{$_} >= 99; $Body .= "" if $Daily{$_}/$DailyC{$_} > 95 and $Daily{$_}/$DailyC{$_} < 99 ; $Body .= "" if $Daily{$_}/$DailyC{$_} > 90 and $Daily{$_}/$DailyC{$_} < 95 ; $Body .= "" if $Daily{$_}/$DailyC{$_} < 90 ; $Body .= "\n"; } $Body .= "
IT Network Systems Availability Summary
Compiled: ". scalar(localtime) . "
Service Past Quarter Past Month Past Week Past Day
$_" . sprintf('%.2f',$Quarterly{$_}/$QuarterlyC{$_}) . "%" . sprintf('%.2f',$Quarterly{$_}/$QuarterlyC{$_}) . "%" . sprintf('%.2f',$Quarterly{$_}/$QuarterlyC{$_}) . "%" . sprintf('%.2f',$Quarterly{$_}/$QuarterlyC{$_}) . "%" . sprintf('%.2f',$Monthly{$_}/$MonthlyC{$_}) . "%" . sprintf('%.2f',$Monthly{$_}/$MonthlyC{$_}) . "%" . sprintf('%.2f',$Monthly{$_}/$MonthlyC{$_}) . "%" . sprintf('%.2f',$Monthly{$_}/$MonthlyC{$_}) . "%" . sprintf('%.2f',$Weekly{$_}/$WeeklyC{$_}) . "%" . sprintf('%.2f',$Weekly{$_}/$WeeklyC{$_}) . "%" . sprintf('%.2f',$Weekly{$_}/$WeeklyC{$_}) . "%" . sprintf('%.2f',$Weekly{$_}/$WeeklyC{$_}) . "%" . sprintf('%.2f',$Daily{$_}/$DailyC{$_}) . "%" . sprintf('%.2f',$Daily{$_}/$DailyC{$_}) . "%" . sprintf('%.2f',$Daily{$_}/$DailyC{$_}) . "%" . sprintf('%.2f',$Daily{$_}/$DailyC{$_}) . "%
"; $Body .= "

\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "
Legend:
if uptime > 99% then GREEN
if uptime > 95% but < 99% then BLUE
if uptime > 90% but < 95% then YELLOW
if uptime < 90% then RED
\n"; return $Body; } sub NumDots($) { # Count the number of dots in a string # There's probably a better way to do this my $DNA = shift; my $a = 0; while($DNA =~ /\./ig){$a++} return $a } sub DetailSheet($) { # Populate the table with details depending on the value of %opts{detail} my $Seconds = shift; my $Body = ''; return '' unless $opt{detail}; # Monthly/Weekly/Daily $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; my %CornBeef; my %CornBeefC; CASE: { %CornBeef = %Daily, %CornBeefC = %DailyC, print "Doing Daily\n", last CASE if $Seconds == 86400; %CornBeef = %Weekly, %CornBeefC = %WeeklyC, print "Doing Weekly\n", last CASE if $Seconds == 604800; %CornBeef = %Monthly, %CornBeefC = %MonthlyC, print "Doing Monthly\n", last CASE if $Seconds == 2592000; %CornBeef = %Quarterly, %CornBeefC = %QuarterlyC, print "Doing Quarterly\n", last CASE if $Seconds == 7776000; } # end of CASE block foreach (sort { $a cmp $b } keys %CornBeef ) { next if NumDots ($_) > $opt{detail}; if ( $_ =~ /\./ ) { #this is a sub section $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; } else { # this is a non-sub section $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= "\n"; $Body .= ""; } } $Body .= "
IT Network Systems Availability Previous " . $Seconds/86400 . " Day(s)
Compiled: ". scalar(localtime) . "
Service Seconds Percent
$_" . sprintf('%.0f',(100 - $CornBeef{$_} / $CornBeefC{$_}) * ($Seconds/100)) . "" . sprintf('%.2f',$CornBeef{$_} / $CornBeefC{$_}) . "%
" . $_ . "" . sprintf('%.0f',(100 - $CornBeef{$_} / $CornBeefC{$_}) * ($Seconds/100)) . "" . sprintf('%.2f',$CornBeef{$_} / $CornBeefC{$_}) . "%
\n"; return $Body; } sub list_rrds($$$); sub list_rrds($$$) { # List the RRD's used by this configuration my $tree = shift; my $path = shift; my $print = shift; my $prline; foreach my $rrds (keys %{$tree}) { if (ref $tree->{$rrds} eq 'HASH'){ $prline .= list_rrds( $tree->{$rrds}, $path."/$rrds", $print ); } if ($rrds eq 'host' and $tree->{$rrds} !~ m|/| ) { $prline .= "$cfg->{General}{datadir}$path".".rrd\n"; } } return $prline; } sub load_cfg ($) { my $cfgfile = shift; # my $parser = get_parser; my $parser = Smokeping::get_parser; $cfg = Smokeping::get_config $parser, $cfgfile; } ########################################################################### # The Main Program ########################################################################### sub main($); main($cfgfile); sub main ($) { umask 022; my $cfgfile = shift; my $sendto; GetOptions(\%opt, 'quiet','version','testmail','listrrds','to=s','detail=n','morning','weekly','help','man') or pod2usage(2); if($opt{version}) { print "$RCS_VERSION\n"; exit(0) }; if($opt{help}) { pod2usage(-verbose => 1); exit 0 }; if($opt{man}) { pod2usage(-verbose => 2); exit 0 }; load_cfg $cfgfile; print "tSmoke for network managed by $cfg->{General}{owner}\nat $cfg->{General}{contact}\n(c) 2003 Dan McGinn-Combs\n" unless $opt{quiet}; if($opt{testmail}) { test_mail($cfg) }; if($opt{listrrds}) { print "List of Round Robin Databases used by this implementation\n"; my @rrds = split ( /\n/,list_rrds($cfg->{Targets},"","") ); foreach (@rrds) { print "RRD: $_\n"; }; } if($opt{morning}) { morning_update($cfg) }; if($opt{weekly}) { weekly_update($cfg) }; exit 0; } =head1 NAME tSmoke - Commandline tool for sending SmokePing information =head1 SYNOPSIS B [ B<--testmail> | B<--morning> | B<--weekly> | B<--version> | B<--help> | B<--man>] Options: --man Show the manpage --help Help :-) --version Show SmokePing Version --testmail Send a test message --listrrds List the RRDs used by this Smokeping --morning Send a morning synopsis --weekly Send a weekly status report --to E-mail address to send message (i.e. '--to=xyz@company.com.invalid' --detail How much detail to send in weekly report (i.e. '--detail=1') --quiet Do not print welcome =head1 DESCRIPTION The B tool is a commandline tool which interfaces with the SmokePing system. Its main function is to send a message indicating the current status of the systems being monitored by Smokeping or an HTML mail file containing the status over the past day, past week and past month including an overview. Typical crontab used to invoke this are # Quick morning alert to see what's down 0 6 * * * /usr/local/smokeping/bin/tSmoke.pl --q --to=mobilephone@att.net.invalid --morning # Weekly report on the percent availability of network systems with no detail 0 8 * * * /usr/local/smokeping/bin/tSmoke.pl --q --to=mailbox@company.com.invalid --weekly --detail=0 =head1 SETUP When installing tSmoke, some variables must be adjusted to fit your local system. We need to use the following B: =over =item Smokeping =item RRDTool Perl bindings =item Getopt::Long =back Set up your libraries: use lib "/usr/local/smokeping/lib"; use lib "/usr/local/rrdtool-1.0.39/lib/perl"; Point to your Smokeping B file my $cfgfile = "/usr/local/smokeping/etc/config"; Modify the Smokeping config file to include a path for tmail in the General section: tmail = /usr/local/smokeping/etc/tmail =head1 COPYRIGHT Copyright (c) 2003 by Dan McGinn-Combs. All right reserved. =head1 LICENSE 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. =head1 AUTHOR Dan McGinn-Combs Ed.mcginn-combs@mindspring.comE Modified for Smokeping official distribution by Niko Tyni Entyni@iki.fiE =cut