#!/usr/bin/perl -w
# snmpiio.pl
# pod at tail
$|++;
use strict;
use Net::SNMP;
use Tie::IxHash;
use constant SECS_PER_MIN => 60;
use constant SECS_PER_HR => 3600;
use constant SECS_PER_DAY => 86400;
use constant SECS_PER_WEEK => 604800;
######################################################################
+####
# Edit defaults, options, and binaries to suit your environment:
######################################################################
+####
my $target = shift or Usage();
my $ifIndex = shift or Usage();
my $iters = (shift or 4);
my $delay = (shift or 30);
my $outdir = (shift or '.');
my %opt =(
community => 'public',
tmpdir => '/tmp',
timeout => 15,
port => 161,
p2p => '-compression 9',
);
# Chart::Graph uses system(gnuplot). I borrowed some ideas from it.
# Maybe can use GD (no system calls) when Debian 3.0 releases.
my %bin = (
gnp => '/usr/bin/gnuplot',
p2p => '/usr/bin/pnmtopng',
prt => '/usr/local/bin/pdq',
);
######################################################################
+####
# Muck over the input:
######################################################################
+####
# These are standard OIDs, so should work 'most anywhere.
tie my %oid, "Tie::IxHash";
%oid = (
ifName => ".1.3.6.1.2.1.31.1.1.1.1.$ifIndex",
ifSpeed => ".1.3.6.1.2.1.2.2.1.5.$ifIndex",
ifInOctets => ".1.3.6.1.2.1.2.2.1.10.$ifIndex",
ifOutOctets => ".1.3.6.1.2.1.2.2.1.16.$ifIndex",
);
# date-stamped filenames for uniquity.
my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
my %stamp;
$stamp{start} = DateTime();
$stamp{ymd} = sprintf("%04d%02d%02d",$year+1900,$mon+1,$mday);
tie my %file, "Tie::IxHash";
%file = (
gnpOpt => "$opt{tmpdir}/$target-$ifIndex-$stamp{ymd}.gnpOpt",
tmp => "$outdir/$target-$ifIndex-$stamp{ymd}.tmp",
csv => "$outdir/$target-$ifIndex-$stamp{ymd}.csv",
xls => "$outdir/$target-$ifIndex-$stamp{ymd}.xls",
tab => "$outdir/$target-$ifIndex-$stamp{ymd}.tab",
ps => "$outdir/$target-$ifIndex-$stamp{ymd}.ps",
pnm => "$outdir/$target-$ifIndex-$stamp{ymd}.pnm",
png => "$outdir/$target-$ifIndex-$stamp{ymd}.png",
);
# Sanity-checks, not untainting:
Usage() unless (
$target =~ (/\w+/) and
$ifIndex =~ (/\d+/) and
$iters =~ (/\d+/) and
$delay =~ (/\d+/) and
$outdir =~ (/.*/)
);
unless($iters >= 3) {
print "\n\nSorry, minimum iterations allowed is 3.\n\n";
exit 1;
}
unless($delay >= 10) {
print "\n\nSorry, minimum delay allowed is 10.\n\n";
exit 1;
}
unless(-d $outdir) {
print "\n\nSorry, $outdir isn't an existing directory.\n\n";
exit 1;
}
unless(-w $outdir) {
print "\n\nSorry, you don't have write perms for $outdir.\n\n";
exit 1;
}
# Provide estimate of total runtime:
my %run;
$run{secs} = $iters * $delay;
$run{est} = $run{secs};
$run{unit} = 'seconds';
if($run{secs} > SECS_PER_MIN) {
$run{est} = $run{secs}/SECS_PER_MIN;
$run{est} = sprintf("%.0f",$run{est});
if($run{est} == 1.0) {
$run{est} = 1;
$run{unit} = 'minute';
} else { $run{unit} = 'minutes'; }
}
if($run{secs} > SECS_PER_HR) {
$run{est} = $run{secs}/SECS_PER_HR;
$run{est} = sprintf("%.1f",$run{est});
if($run{est} == 1.0) {
$run{est} = 1;
$run{unit} = 'hour';
} else { $run{unit} = 'hours'; }
}
if($run{secs} > SECS_PER_DAY) {
$run{est} = $run{secs}/SECS_PER_DAY;
$run{est} = sprintf("%.1f",$run{est});
if($run{est} == 1.0) {
$run{est} = 1;
$run{unit} = 'day';
} else { $run{unit} = 'days'; }
}
if($run{secs} > SECS_PER_WEEK) {
$run{est} = $run{secs}/SECS_PER_WEEK;
$run{est} = sprintf("%.1f",$run{est});
if($run{est} == 1.0) {
$run{est} = 1;
$run{unit} = 'week';
} else { $run{unit} = 'weeks'; }
}
# Files writable only by this user for security purposes:
# Delete pre-existing out file because subsequent writes append:
print "\nStart run of $0\n",
" $stamp{start}\n",
" $target ifIndex $ifIndex\n",
" $iters runs at ${delay} second intervals\n",
" $run{est} $run{unit} estimated runtime\n",
;
umask oct 133;
print " Clean up old file(s)...\n";
for(keys %file){ Unlink($file{$_}, 'verbose'); }
# Leading '#' so gnuplot will ignore non-data header:
print " Print csv header...";
open (CSV, ">$file{csv}") or die "Error opening $file{csv}: $!";
print (CSV "# EpochSec,\%rcvUtil,\%xmitUtil,\n") or die
"Error printing to $file{csv}: $!";
close CSV or die "Error closing $file{csv}: $!";
print " done!\n";
######################################################################
+####
# Get down to business:
######################################################################
+####
print " Query target device... ";
my $i = 1;
for(1..$iters) {
open (TMP, ">>$file{tmp}") or die "Error opening $file{tmp}: $!";
# Prepend epoch seconds to each csv line:
$stamp{epoch} = time;
print(TMP "$stamp{epoch},") or die "Error printing to $file{tmp}: $
+!";
# Here's where da *real* stuf happens:
my $session = Net::SNMP->session(
-hostname => $target,
-community => $opt{community},
-port => $opt{port},
-timeout => $opt{timeout},
);
unless (defined($session)) {
print TMP ("SNMP SESSION ERROR,")
or die "Error printing to $file{tmp}: $!";
}
for my $value(keys %oid) {
if (defined(my $response = $session->get_request($oid{$value})))
+ {
print(TMP $response->{$oid{$value}})
or die "Error printing to $file{tmp}: $!";
}
else { print (TMP '0') or die "Error printing to $file{tmp}: $!"
+; }
print (TMP ',') or die "Error printing to $file{tmp}: $!";
}
$session->close();
# Activity indicator:
# One line per iteration:
# End of iteration:
# Delay between iterations, but not after last iteration:
## print '.';
print (TMP "\n") or die "Error printing to $file{tmp}: $!";
close TMP or die "Error closing $file{tmp}: $!";
sleep $delay unless($i==$iters);
$i++;
}
print " done!\n";
# $ifName is global so available later, outside of this block.
my $ifName;
print " Calculate receive and transmit %utilization...";
{
# Contained block to limit 'print csv with $,':
# Initialize prior array to 0 to avoid annoying 'not numeric' error
+:
local $, = ',';
my @prior = (0) x 5;
open (TMP, "<$file{tmp}") or die "Error opening $file{tmp}: $!";
open (CSV, ">>$file{csv}") or die "Error opening $file{csv}: $!";
while(<TMP>) {
chomp;
my @data = split ',';
# Subtract epochSecs for deltaTime:
# Protect against 'divide by zero' error:
my $time = $data[0];
my $priortime = $prior[0];
my $dtime = $time - $priortime;
next unless $dtime;
$ifName = $data[1];
my $ifSpeed = $data[2];
my $inBytes = $data[3];
my $outBytes = $data[4];
my $priorInBytes = $prior[3];
my $priorOutBytes = $prior[4];
# Handle counter-roll-over-to-zero in approximate fashion,
# and prevent 'divide by zero' error if no OID response:
$priorInBytes = 1 if(
($inBytes < $priorInBytes) or ($priorInBytes == 0)
);
$priorOutBytes = 1 if(
($outBytes < $priorOutBytes) or ($priorOutBytes == 0)
);
$ifSpeed = 1 if($ifSpeed == 0);
my $rBytes = $inBytes - $priorInBytes;
my $xBytes = $outBytes - $priorOutBytes;
my $rutil = CalcUtil($rBytes,$dtime,$ifSpeed);
my $xutil = CalcUtil($xBytes,$dtime,$ifSpeed);
# No valid data (delta) on first iteration, so skip it.
if($prior[2] > 0) {
print CSV "$time,$rutil,$xutil,\n"
or die "Error printing to $file{csv}: $!";
}
# Set current values for 'prior' on next iteration:
@prior = @data;
}
close (TMP) or die "Error closing $file{tmp}: $!";
close (CSV) or die "Error closing $file{csv}: $!";
}
($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
$stamp{done} = DateTime();
print " done!\n";
######################################################################
+####
# Output formatting:
######################################################################
+####
if(-x $bin{gnp} && -B _) {
# Tab-delimited for feeding to gnuplot:
print " Create tab-delimited outfile from csv...";
open (CSV, "<$file{csv}") or die "Error opening $file{csv}: $!";
open (TAB, ">>$file{tab}") or die "Error opening $file{tab}: $!";
while(<CSV>) {
s/,/ /g;
print TAB or die "Error printing to $file{tab}: $!";
}
close (CSV) or die "Error closing $file{csv}: $!";
close (TAB) or die "Error closing $file{tab}: $!";
print " done!\n";
# Create option file for gnuplot:
# "plot \"$file{tab}\" using 1:2, \"$file{tab}\" using 1:3, \"$fi
+le{tab}\" using 1:4",
print " Create gnuplot option file...";
my @gnpOpt = (
'set data style lines',
"set title \"$target port $ifName ifIndex $ifIndex - $iters ru
+ns at $delay second intervals\"",
"set key title \"Receive, Transmit\"",
"set xlabel \"$stamp{start} through $stamp{done}\"",
'set ylabel "%bandwidth"',
'set grid',
'set noxtics',
'set terminal postscript',
"set output \"$file{ps}\"",
"plot \"$file{tab}\" using 1:2, \"$file{tab}\" using 1:3",
'set terminal pbm',
"set output \"$file{pnm}\"",
'replot',
'set terminal postscript',
"set output \"|$bin{prt}\"",
'replot',
);
open (GNPOPT, ">$file{gnpOpt}") or die "Error opening $file{gnpOpt}
+: $!";
for(@gnpOpt){
print (GNPOPT "$_\n") or die "Error printing to $file{gnuOpt}: $
+!";
}
close GNPOPT or die "Error closing $file{gnpOpt}: $!";
print " done!\n";
# Create chartfiles from tab-delim data with gnuplot:
if ((-r $file{gnpOpt} && -T _) and (-x $bin{gnp})) {
print " Create PNM+PS graphs from tab-delimited...";
system("$bin{gnp} $file{gnpOpt}")
and die "Error running $bin{gnp}: $?";
print " done!\n";
}
# Create PNG from PNM with pnmtopng:
if ((-r $file{pnm} && -B _) and (-x $bin{p2p})) {
print " Create PNG graph from PNM...";
system("$bin{p2p} $opt{p2p} $file{pnm} > $file{png}")
and die " Error running $bin{p2p}: $?";
print " done!\n";
}
}
# Create xls from csv *if* Spreadsheet::WriteExcel module installed.
my $have_SWE;
BEGIN {
$have_SWE = 0;
eval { require Spreadsheet::WriteExcel };
unless ($@) {
Spreadsheet::WriteExcel->import();
$have_SWE = 1;
}
}
if ($have_SWE == 1) {
print " Create xls outfile from csv...";
open (CSVFILE, $file{csv}) or die "Error opening $file{csv}: $!";
my $workbook = Spreadsheet::WriteExcel -> new($file{xls});
my $worksheet = $workbook -> addworksheet();
my $row = 0;
while (<CSVFILE>) {
chomp;
my @field = split(',', $_);
my $column = 0;
foreach my $token (@field) {
$worksheet -> write($row, $column, $token);
$column++;
}
$row++;
}
my $format1 = $workbook -> addformat();
$format1 -> set_bold();
$format1 -> set_align('center');
$format1 -> set_bg_color('tan');
$worksheet -> set_row(0, undef, $format1);
$workbook -> close() or die "Error closing $workbook: $!";
print " done!\n";
}
######################################################################
+####
# Conditional cleanup of temp files:
######################################################################
+####
print " Clean up temp file(s)...\n";
if(-e $file{csv} && -B_){
Unlink($file{gnpOpt}, 'verbose');
Unlink($file{tmp}, 'verbose');
}
if(-e $file{xls} && -B_){
Unlink($file{csv}, 'verbose');
}
if(-e $file{ps} && -T_){
Unlink($file{xls}, 'verbose');
Unlink($file{tab}, 'verbose');
Unlink($file{pnm}, 'verbose');
}
if(-e $file{png} && -B_){
Unlink($file{ps}, 'verbose');
}
# Bring it on home:
print "Run complete of $0.\n",
" $stamp{done}\n";
for(keys %file){
print " $file{$_}\n" if(-e $file{$_});
}
print "\n\n";
exit 0;
######################################################################
+#####
# Subroutines:
######################################################################
+#####
# for Start/Done times
sub DateTime {
my $DateTime = sprintf(
"%04d-%02d-%02d %02d:%02d:%02d",
$year+1900,$mon+1,$mday,$hour,$min,$sec
);
}
######################################################################
+#####
# Octets to %bandwidth calculations:
sub CalcUtil {
my $Bytes = $_[0];
my $secs = $_[1];
my $speed = $_[2];
my $MBytes = $Bytes / 1048576;
my $Mbits = $MBytes * 8;
my $Mbps = $Mbits / $secs;
# * 100 convert decimal to percent
# * 1000000 move decimal point to match $speed
# * 1.048576 bits per Megabit
my $util = ($Mbps * 100 * 1000000) / ($speed * 1.048576);
# Avoid reported util > %100 after 1 or more no response:
$util /= 2 while $util > 100;
# Round output, but not @data(next iteration's @prior):
my $utilRound = sprintf("%.3f",$util);
}
######################################################################
+#####
# Delete outfiles from prior run (if same name as this run):
sub Unlink {
my $file = $_[0];
my $echo = $_[1];
if (-e $file && -w _) {
print " unlink $file" if ($echo eq 'verbose');
unlink $file or die "Error unlinking $file: $!";
print " -- *poof*\n" if ($echo eq 'verbose');
}
}
######################################################################
+#####
sub Usage {
print "
Ooot!
You forgot to enter necessary information, or entered bad information!
Usage:
snmpiio.pl target iterations delay ifIndex outdir
target: an IPaddress, DNS name, or FQDN.
ifIndex: SNMP parameter specifying port or interface.
iterations: how many queries you wish to run. (minimum 3, default
+ 4)
delay: seconds to wait between iterations. (minimum 10, default
+30)
outdir: destination dir for outfile. No trailing '/'. (default '
+.')
Example:
snmpiio.pl routerC 7 600 30 /datadir
This script $0
Net::Snmp $Net::SNMP::VERSION
Tie::IxHash $Tie::IxHash::VERSION
Spreadsheet::WriteExcel $Spreadsheet::WriteExcel::VERSION
Perl $]
Local OS $^O
";
exit 1;
}
######################################################################
+#####
=head1 NAME
snmpiio.pl
=head1 SYNOPSIS
Query SNMP-enabled devices for interface (in|out)put octets.
Output files in csv, tab-delimited, pnm, and png formats.
Intended for spotchecks or periodic monitoring of individual interfac
+es.
Tools like MRTG, MCSview, SNMPc, CWSI do well for large number of por
+ts.
snmpiio.pl target iterations delay ifIndex outdir
target: an IPaddress, DNS name, or FQDN.
ifIndex: SNMP parameter specifying port or interface.
iterations: how many queries you wish to run. (minimum 3, defaul
+t 4)
delay: seconds to wait between iterations. (minimum 10, default
+ 30)
outdir: destination dir for outfile. No trailing '/'. (default
+'.')
Example:
snmpiio.pl routerC 7 600 30 MyString /datadir
Assorted conversions:
2880 iterations 30 sec delay = 24 hours
8640 iterations 30 sec delay = 3 days
20160 iterations 30 sec delay = 7 days
40320 iterations 30 sec delay = 14 days
100Mbps = 12.5MBps
1GB = 8,589,934,592 bits (1,073,741,824 * 8)
1MB = 8,388,608 bits (1,048,576 * 8)
1KB = 8,192 bits (1024 * 8)
1Gb = 1,073,741,824 bits (1024-e3)
1Mb = 1,048,576 bits (1024-e2)
1Kb = 1,024 bits (1024-e1)
ifInOctets+ifOutOctets = Bytes
((Bytes*8)/1024)/seconds = Kbps
((Bytes*8)/1048576)/seconds = Mbps
=head1 TESTED
Perl 5.00503 Debian 2.2r3
Net::SNMP 3.6
Tie::IxHash 1.21
Spreadsheet::WriteExcel 0.31
Gnuplot 3.71
pnmtopng 2.37.4-1
Excel 2000 9.0.3821 SR-1
=head1 UPDATED
2001-10-29 11:00 CDT
Post to PerlMonks.
Fix time est errs - include $run{est} = $run{secs} in each if loop.
Remove default value (2) for ifIndex, update Usage()+pod.
Include ifIndex in gnuplot title.
2001-10-26 16:40 CDT
Hashimafy passel o'scalars so fewer global vars.
Add "or die" where missing on couple print(FILE) lines.
Easy-to-read estimate of run duration.
Suggested by Zaxo:
(cleaner syntax, but not quite the output wanted)
print int($run-secs / SECS_PER_WEEK), ' week(s), ';
print int($run-secs / SECS_PER_DAY), ' day(s), ';
print int($run-secs / SECS_PER_HR), ' hour(s), ';
print int($run-secs / SECS_PER_MIN), ' minute(s), ';
Minimum iterations of 3, to provide 2 data-points.
Minimum delay of 10 seconds, to prevent target CPU saturation.
Use named scalar instead of $data[0], $prior[0].
Prevent %util > 100 (if no response 1 or more times).
Calc + display HDX utilization (rcv + xmit).
Retest Excel file.
%files for all file handling to eliminate redundent data of arrays.
Eliminate duplicate (start|done)stamp snippets with DateTime().
Print Perl+module+OS versions at Usage().
Replace @unlinkStart with keys of %file.
Add $ifName so can print module/port in outfile header.
STDOUT hot so messages appear promptly.
Conditional Unlink of tempfiles at wrap-up.
Round %util to 3 decimal points.
Subamify duplicate bw calculations.
Gnuplot key title.
Additional filechecks in PNM, PNG creation.
Process receive/transmit data separately.
Print PostScript outfile with pdq.
Automate output graph with gnuplot+pnmtopng.
2001-10-18 16:45 CDT
Separate $outdir -d and -w filechecks and messages.
Use hash instead of array for OIDs: OID key=name, value=numeric OID
+.
Prep for future gnuplot outfile:
Add human-readable $startstamp and $donestamp.
Change outfile to tab-delimited (was csv).
Debug unwanted column 7 in outfile - epochsecs of endtime.
Indenting to PerlStyle recommendation.
Fix minor tyops.
Rename to "snmpiio.pl" from "netsnmpiio.pl".
Unlink .tmp at end of run.
Confirm counter-roll-over-to-zero Does The Right Thing.
No xls outfile if Spreadsheet::WriteExcel module not installed.
Fix broken %util calculation.
Correct minor tyops.
Display MBytes transferred.
Gracefully handle non-responsive (host|OID):
(MBytes|Mbps|%Util) = -0.000
Snmp query syntax cleaner.
Don't print snmp query or session errors.
Fix incorrect Bps calculations.
Create xls outfile from csv.
Correct error in Usage and pod.
2001-08-13 10:35 CDT
Gracefully handle counter-roll-over-to-zero;
~4,000,000,000 on CatOS 5.
Cisco CatOS 5
Report 'blade #/port #' instead of '10/100 utp ethernet (cat 3/5)
+'.
Test with ActivePerl on Win2k.
Fix sundry tyops.
Post to PerlMonks networking code (requesting critique).
Net::SNMP instead of UCD-SNMP system call:
Initialize 'prior' array at '0' to
eliminate annoying 'not numeric' message.
Fix bad conversion math in pod.
2001-08-11 22:50 CDT
Add example temp+csv output to pod.
Round Bps to 0 decimal places.
Round %util to 4 decimal places (100ths of %).
Utilization percentage calculations, corrected for MegaBytes.
Bypass bogus first Bps output since no prior epochSec.
Bps calculations from raw SNMP data.
Query ifSpeed for %util calculations.
Defaults for all input values except target.
Sanity-check $outdir: letters, numbers, dots, dashes, underscores.
Filetest for write in outdir and outdir *is* directory.
Simplify iteration localtime syntax.
Cleaner syntax for no sleep after last iteration.
Debug errant commas in csv.
Unsubify snmp query localtime+time since only called once.
@raw array instead of initial temp file.
Filename of target-ifIndex-yyyymmdd.csv.
Add hostname,oid,ifIndex to csv output.
Use PcTime return values (print the values in main).
Add $outdir.
Sanity-check input for (wordchars|numbers) as appropriate.
More informative Usage().
Input from commandline instead of hardcoded.
Efforts to improve data structure.
No sleep after last iteration.
Leading-zero padded time/date in outfiles.
yyyymmdd filenames w/sprintf.
2001-08-08 16:00 CDT
Initial working code.
=head1 TODOS
Query + separate report:
ifInDiscards, ifInErrors, icmpOutErrors, tcpInErrors, udpInErrors
Query switch for host MAC+IP for outfile(s) header (SNMP::BridgeQuery
+).
then
Query DNS for host name for outfile(s) header (Net::DNS).
Retest with Win2k + ActivePerl.
Retest with Win2k + Cygwin + Perl5.6.
Pod:Usage to automagically synchronize pod with Usage().
available for < 5.6 ?
Read params from external config file.
=head1 AUTHOR
ybiC
=head1 CREDITS
Thanks to:
masem, merlyn, tachyon, HamNRye, tilly, lemming, wog, crazyinsomniac.
Oh yeah, and to some guy named vroom. ;^)
=cut
|