#!/opt/vdops/bin/perl # This script produces a report listing ifName, ifDescr, dot3StatsFCSErrors, # and dot3StatsLateCollisions from the specified device # V Who When What # --------------------------------------------------------------------------- # 1.1.0 skendric 2011-06-09 Remove reliance on 5.12 features, add support # for reading a directory's worth of files # 1.0.2 skendric 2010-04-30 Misc syntax errors # 1.0.1 skendric 2009-12-21 Use feature say/switch # 1.0.0 skendric 2008-04-21 First Version # # # Author: Stuart Kendrick, sbk {put at sign here} skendric {put dot here} com # # Source: http://www.skendric.com/device # # This software is available under the GNU GENERAL PUBLIC LICENSE, see # http://www.fsf.org/licenses/gpl.html # # # This script takes the following approach: # - Accepts command-line arguments specifying a device or a list of # devices, along with SNMP community string # - Walks ifName, ifDescr, dot3StatsFCSErrors, and dot3StatsLateCollisions # - Produces a report listing these four parameters # # Requirements: # # Assumptions: # # Tested under: # -perl-5.8.8 # -Net-SNMP-5.2.0 # # Instructions: # - Type "show-if-errors --help" to see command-line parameters # # Caveats: # # Known Bugs: # # To do: # -Add support for SNMPv3 # Begin script # Load modules use v5.8.8; use strict; use warnings; use Carp qw(carp cluck croak confess); use Data::Dumper; use English qw( -no_match_vars ); use Getopt::Std; use List::MoreUtils qw(first_index uniq); use Net::hostent; use Net::IPAddress; use Net::Ping::External qw(ping); use Net::SNMP qw( :asn1 :debug :snmp); use Readonly; use Regexp::Common; use Socket; # Declare global variables # The important stuff my %errors; # Sum of FCSErrors and LateCollisions on all # ports, keyed by target my @hurting; # List of devices with interfaces reporting # errors my %if_with_errors; # Hash of array references, listing the # ifIndex numbers of interfaces reporting errors, # keyed by target my @target; # List of host names or IP addresses pointing # to Ethernet switches # MIB variables we will walk my %dot3StatsFCSErrors; # The following four hashes are keyed by host my %dot3StatsLateCollisions; # These four hashes are keyed by host name or my %ifDescr; # IP address and contain references to hashes my %ifName; # of the relevant values, keyed by ifIndex # SNMP variables my $pause; # Number of seconds to pause before hammering # the device with the next snmpwalk my $snmp_max_msg_size; # Maximum size of SNMP PDU in bytes my $snmp_max_rep; # Maximum number of iterations over the repeating # variables: an snmpbulkwalk parameter my $snmp_non_rep; # Number of supplied variables that should not be # iterated over: an snmpbulkwalk parameter my $snmp_port; # Port on which target SNMP daemons are listening my $snmp_read; # SNMP read community string my $snmp_retries; # Number of times to retry an SNMP operation # before giving up my $snmp_timeout; # Seconds to wait before declaring an SNMP # operation lost and retrying my $snmp_translate; # Translate SNMP GET return string into human # readable format my $snmp_version; # SNMP version (1 or 2c) # Details my $debug; # Debug level my %option; # Command-line argument hash my $program_name; # Name of this program my $report_mode; # Determines which reports to print my $usage; # One line description of command line switches my $version; # Version of this program # Define global variables $debug = 0; # 10 = Logging # 9 = Database SELECT operations # 8 = Per IP/MAC/Port processing # 7 = Database INSERT/UPDATE/DELETE # 6 = Dump SNMP var # 5 = Dump snmp_packets # 4 = Grody: print big var # 3 = Verbose: print mid var # 2 = Simple: print small var # 1 = Basic: subroutine trace # 0 = Disable debugging $program_name = 'show-if-errors'; $report_mode = 'b'; $usage = 'Usage: show-if-errors [-d {integer}] [-c {read string] [-k {seconds}] [-m d|i|b] [-v {1|2}] [-f {filename} | target1 target2 target3 ...]'; $version = '1.1.0'; # Constants Readonly my $BANG => q{!}; Readonly my $CR => q{\n}; Readonly my $EMPTY_STR => q{}; Readonly my $SPACE => q{ }; # Module behavior $OUTPUT_AUTOFLUSH = 1; $Getopt::Std::STANDARD_HELP_VERSION = 1; # SNMP Stuff $pause = 0; $snmp_max_msg_size = 65535; $snmp_max_rep = 1000; $snmp_non_rep = 0; $snmp_port = 161; $snmp_retries = 3; $snmp_read = 'public'; $snmp_timeout = 1; $snmp_translate = 0; $snmp_version = 2; # Grab arguments getopts('c:d:f:k:m:p:v:', \%option); $debug = $option{d} if defined $option{d}; @target = @ARGV; ### Begin Main Program ############################################### { process_args(); # Grab arguments and check them validate_target(); # Check target list gather_data(); # Walk the MIB variables look_for_pain(); # Find the devices reporting the most errors print_report(); # Print report } ##### End Main Program ############################################### ######################################################################## # Read file, add contents to @target ######################################################################## sub consume_file { my $file = shift; # File to read my @hosts; # Debug trace trace_location('begin') if $debug; # Open file open my $fh, '<', $file or die "Cannot open $file: $!"; # Read lines from file while (my $line = <$fh>) { my $host; next LINE if $line =~ /^#/; next LINE if $line =~ /^\s+$/; ($host) = ($line =~ /^(\S+)/); chomp $host; push @hosts, $host if defined $host; } # Clean-up close $fh or warn "Cannot close $file: $!"; # Debug trace trace_location('end') if $debug; return @hosts; } ######################################################################## # Do the work ######################################################################## sub gather_data { my $nb = 0; # 'Need blank line': helps pretty-print output my @prune; # list of targets which quit responding # Debug trace trace_location('begin') if $debug; # Notify operator print "Gathering interface stats...\n"; # Walk through targets, walking MIB variables TARGET: for my $target (@target) { my (%arg, $result); print " Processing $target\n" if $debug; # Walk ifDescr print " Walking ifDescr\n" if $debug; my %if_descr; %arg = ( host => $target, oid => '.1.3.6.1.2.1.2.2.1.2' ); $result = snmpWalk(\%arg); unless (defined $result) { print "$target not responding, skipping\n"; push @prune, $target; $nb = 0; next TARGET; } for my $varbind (@$result) { my ($iid, $val); $iid = $varbind->{iid}; $val = $varbind->{val}; $if_descr{$iid} = $val; } $ifDescr{$target} = \%if_descr; sleep $pause; # Walk ifName print " Walking ifName\n" if $debug; my %if_name; %arg = ( host => $target, oid => '.1.3.6.1.2.1.31.1.1.1.1' ); $result = snmpWalk(\%arg); unless (defined $result) { print "$target not responding, skipping\n"; push @prune, $target; $nb = 0; next TARGET; } for my $varbind (@$result) { my ($iid, $val); $iid = $varbind->{iid}; $val = $varbind->{val}; $if_name{$iid} = $val } $ifName{$target} = \%if_name; sleep $pause; # Walk dot3StatsFCSErrors print " Walking dot3StatsFCSErrors\n" if $debug; my %if_fcs; %arg = ( host => $target, oid => '.1.3.6.1.2.1.10.7.2.1.3' ); $result = snmpWalk(\%arg); unless (defined $result) { print "$target not responding, skipping\n"; push @prune, $target; $nb = 0; next TARGET; } for my $varbind (@$result) { my ($iid, $val); $iid = $varbind->{iid}; $val = $varbind->{val}; $if_fcs{$iid} = $val } $dot3StatsFCSErrors{$target} = \%if_fcs; sleep $pause; # Walk dot3StatsLateCollisions print " Walking dot3StatsLateCollisions\n" if $debug; my %if_col; %arg = ( host => $target, oid => '.1.3.6.1.2.1.10.7.2.1.8' ); $result = snmpWalk(\%arg); unless (defined $result) { print "$target not responding, skipping\n"; push @prune, $target; $nb = 0; next TARGET; } for my $varbind (@$result) { my ($iid, $val); $iid = $varbind->{iid}; $val = $varbind->{val}; $if_col{$iid} = $val } $dot3StatsLateCollisions{$target} = \%if_col; sleep $pause; # Entertain operator print $BANG unless $debug; $nb = 1; } # Make things look pretty print "\n\n"; # Remove targets which quit responding prune_target(@prune); die 'No surviving targets' unless @target > 0; # Debug info if ($debug == 8) { for my $target (@target) { print "For $target, dumping stats\n"; print "Dumping ifDescr\n"; print "Dumper($ifDescr{$target})\n"; print "Dumping ifName\n"; print "Dumper($ifName{$target})\n"; print "Dumping dot3StatsFCSErrors\n"; print "Dumper($dot3StatsFCSErrors{$target})\n"; print "Dumping dot3StatsLateCollisions\n"; print "Dumper($dot3StatsLateCollisions{$target})\n"; } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Poke through the data, identifying the devices reporting the most # errors ######################################################################## sub look_for_pain { # Debug trace trace_location('begin') if $debug; # Notify operator print "Looking for pain...\n"; # Walk through targets, summing the errors seen on each port for my $target (@target) { my (@if_with_fcs, @if_with_col, @if_error); print " Processing $target\n" if $debug; # Examine dot3StatsFCSError for my $iid (keys %{$dot3StatsFCSErrors{$target}}) { my $if_fcs = $dot3StatsFCSErrors{$target}->{$iid}; if ($if_fcs > 0) { $errors{$target} += $if_fcs; push @if_with_fcs, $iid; } } # Examine dot3StatsLateCollisions for my $iid (keys %{$dot3StatsLateCollisions{$target}}) { my $if_col = $dot3StatsLateCollisions{$target}->{$iid}; if ($if_col > 0) { $errors{$target} += $if_col; push @if_with_col, $iid; } } # Record interfaces reporting errors push @if_error, @if_with_fcs; push @if_error, @if_with_col; @if_error = uniq @if_error; $if_with_errors{$target} = \@if_error; # Debug info if ($debug > 1) { print " Errored indices: ", join $SPACE, @if_error, "\n"; } # Entertain operator print $BANG unless $debug; } # Make things look pretty print "\n\n"; # Identify the devices with interfaces reporting errors for my $target (reverse sort {$a cmp $b} keys %errors) { push @hurting, $target if $errors{$target} > 0; } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Print the detailed report ######################################################################## sub print_interface { # Debug trace trace_location('begin') if $debug; # Display the errored interfaces on the top hurters print "The following interfaces are reporting FCS errors and/or late collisions:\n"; printf <{$iid}; $if_name = $ifName{$target}->{$iid}; $if_fcs = $dot3StatsFCSErrors{$target}->{$iid}; $if_col = $dot3StatsLateCollisions{$target}->{$iid}; printf "%-20s %-21s %-12s %-1.2e %-1.2e\n", $target, $if_descr, $if_name, $if_fcs, $if_col; } } # If we haven't found any errors, say so print "No interfaces are reporting errors\n" if @hurting == 0; print "\n\n"; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Print the report ######################################################################## sub print_report { # Debug trace trace_location('begin') if $debug; # Pick the reports to print if ($report_mode eq 'd') { print_device(); } elsif ($report_mode eq 'i') { print_interface(); } elsif ($report_mode eq 'b') { print_device(); print_interface(); } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Print the device report ######################################################################## sub print_device { # Debug trace trace_location('begin') if $debug; # Display the top hurters print "The following devices are reporting interface errors:\n"; printf <matches($pause); # Set SNMP variables $snmp_port = $option{p} if defined $option{p}; die '-p must be a integer' unless $RE{num}{int}->matches($snmp_port); $snmp_read = $option{c} if defined $option{c}; $snmp_version = $option{v} if defined $option{v}; $snmp_version = 2 if $snmp_version eq '2c'; die '-v must be either 1 or 2' unless $snmp_version =~ /^1$|^2$/; # Look for target list unless (defined $option{f} or @ARGV > 0) { die 'Must specify a target list via -f or command-line list'; } # If given a command-line list of targets, grab them @target = @ARGV; # If given a file name... if (defined $option{f}) { print "Reading targets from $option{f}\n" if $debug; # If this is actually a directory name, read all the text files # contained therein if (-d $option{f}) { my ($dir, $file); print "Reading targets from directory $option{f}\n" if $debug; opendir $dir, $option{f} or die "Cannot open $option{f}: $!"; FILE: while (defined ($file = readdir($dir)) ) { next FILE unless -T "$option{f}/$file"; push @target, consume_file("$option{f}/$file"); } closedir $dir or warn "Cannot close $option{f}: $!"; } # If this is the name of a valid text file, consume it elsif (-T $option{f}) { print "Reading targets from file $option{f}\n" if $debug; @target = consume_file($option{f}); } # Otherwise, whine else { die "Unable to read hosts in $option{f}"; } } # End 'If given a file name...' # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Given a list, remove those elements from @target ######################################################################## sub prune_target { my @remove = @_; # Debug trace trace_location('begin') if $debug; # Check to see if we have work to do if (@remove > 0) { # Eliminate duplicates @remove = uniq @remove; # Remove entries which failed checks REMOVE: for my $remove (@remove) { my $index = first_index { $_ eq $remove } @target; next REMOVE unless defined $index; print "Removing $remove\n" if ($debug > 1 and $debug < 5); splice @target, $index, 1; } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Given a hash which contains an OID, return the result of a GET # %hash = ( # host => {string or ip address}, # oid => {object value or oid}, # msg_size => {integer bytes}, # read => {string}, # retries => {integer}, # port => {port}, # timeout => {seconds or milliseconds}, # translate => {0 or 1}, # version => {integer}, # ); ######################################################################## sub snmpGet { my $arg_ref = shift; # Input hash ref my $bitmask; my $error; # Holds error string from SNMP operation my $host; # Host to query my $oid; # Variable to GET my $msg_size; # Maximum size of PDU my $port; # UDP port for SNMP queries my $read; # SNMP community string my $result; # Net::SNMP object holding answer my $retries; # Number of times to retry the query my $session; # Module object my $timeout; # Number of seconds to wait before retrying my $translate; # Translate output into human-readable format my $val; # Holds result of GET my $version; # SNMP version: 1, 2, or 3 # Debug trace trace_location('begin') if $debug > 3; # Sanity check confess 'No parameters!' unless defined $arg_ref; confess 'Parameter not a hash ref' unless ref $arg_ref eq 'HASH'; confess 'Must specify host' unless defined $arg_ref->{host}; confess 'Must specify oid' unless defined $arg_ref->{oid}; # Unpack args $host = $arg_ref->{host}; $oid = $arg_ref->{oid}; $msg_size = defined $arg_ref->{msg_size} ? $arg_ref->{msg_size} : $snmp_max_msg_size ; $port = defined $arg_ref->{port} ? $arg_ref->{port} : $snmp_port ; $read = defined $arg_ref->{read} ? $arg_ref->{read} : $snmp_read ; $retries = defined $arg_ref->{retries} ? $arg_ref->{retries} : $snmp_retries ; $timeout = defined $arg_ref->{timeout} ? $arg_ref->{timeout} : $snmp_timeout ; $translate = defined $arg_ref->{translate} ? $arg_ref->{translate} : $snmp_translate ; $version = defined $arg_ref->{version} ? $arg_ref->{version} : $snmp_version ; # Set debug if ($debug == 5) { $bitmask = DEBUG_ALL } else { $bitmask = DEBUG_NONE } # Create new session ($session, $error) = Net::SNMP->session( -hostname => $host, -port => $port, -version => $version, -timeout => $timeout, -retries => $retries, -community => $read, -translate => $translate, -maxmsgsize => $msg_size, -debug => $bitmask ); unless (defined $session) { print "Cannot build Net::SNMP object for $host: $error\n"; return; } # Get value $result = $session->get_request( -varbindlist => [$oid] ); $error = $session->error; # Print debugging information if ($debug > 3 and $debug < 6) { print "For $host:\n"; print " oid = $oid\n" if defined $oid; print " val = $val\n" if defined $val; } # Handle errors unless (defined $result) { undef $val; print "Unable to retrieve $oid from $host using version $version and string $read: $error\n" if $debug > 3; } # Unpack $result $val = $result->{$oid}; # Clean-up $session->close; # Debug trace trace_location('end') if $debug > 3; return $val; } ######################################################################## # Given a hash which includes starting oid or object value, walk the # tree starting at that oid, returning a reference to an array of varbind # hashes which themselves contain the results # %hash = ( # host => {string or ip address}, # oid => {object value or oid}, # max_rep => {integer}, # msg_size => {integer bytes}, # read => {string}, # retries => {integer}, # non_rep => {integer}, # port => {port}, # timeout => {seconds or milliseconds}, # translate => {0 or 1}, # version => {integer} # ); ######################################################################## sub snmpWalk { my @answer; # An array of varbinds my $arg_ref = shift; # Input hash ref my $bitmask; # Net::SNMP debug my $error; # Holds error string from SNMP operation my $host; # Host to query my $max_rep; # Maximum number of iterations over $starting_oid my $msg_size; # Maximum size of PDU my $non_rep; # Number of non-repetitions my $port; # UDP port for SNMP queries my $read; # SNMP community string my $result; # Holds result of walk my $retries; # Number of times to retry the query my $s; # Module object my $starting_oid; # Place from which we start to walk my $timeout; # Number of seconds to wait before retrying my $translate; # Translate output into human-readable format my $version; # SNMP version: 1, 2, or 3 # Debug trace trace_location('begin') if $debug > 2; # Sanity check confess 'No parameters!' unless defined $arg_ref; confess 'Parameter not a hash ref' unless ref $arg_ref eq 'HASH'; confess 'Must specify host' unless defined $arg_ref->{host}; confess 'Must specify oid' unless defined $arg_ref->{oid}; # Unpack args $host = $arg_ref->{host}; $starting_oid = $arg_ref->{oid}; $max_rep = defined $arg_ref->{max_rep} ? $arg_ref->{max_rep} : $snmp_max_rep ; $msg_size = defined $arg_ref->{msg_size} ? $arg_ref->{msg_size} : $snmp_max_msg_size ; $non_rep = defined $arg_ref->{non_rep} ? $arg_ref->{non_rep} : $snmp_non_rep ; $port = defined $arg_ref->{port} ? $arg_ref->{port} : $snmp_port ; $read = defined $arg_ref->{read} ? $arg_ref->{read} : $snmp_read ; $retries = defined $arg_ref->{retries} ? $arg_ref->{retries} : $snmp_retries ; $timeout = defined $arg_ref->{timeout} ? $arg_ref->{timeout} : $snmp_timeout ; $translate = defined $arg_ref->{translate} ? $arg_ref->{translate} : $snmp_translate ; $version = defined $arg_ref->{version} ? $arg_ref->{version} : $snmp_version ; # Debug info print " snmpWalk on $host for $starting_oid using $read\n" if $debug > 3; # Set debug level if ($debug == 5) { $bitmask = DEBUG_ALL } else { $bitmask = DEBUG_NONE } # Create new session ($s, $error) = Net::SNMP->session( -hostname => $host, -port => $port, -version => $version, -timeout => $timeout, -retries => $retries, -community => $read, -translate => $translate, -maxmsgsize => $msg_size, -debug => $bitmask, ); unless (defined $s) { print "Cannot build Net::SNMP object for $host: $error\n"; return; } # Perform the walk using get_next_request if the agent speaks SNMP v1 if ($version == 1) { my $nextoid = $starting_oid; WALK: while (defined $s->get_next_request( -varbindlist => [$nextoid] )) { my ($iid, $oid, $type, $val, %varbind); # Grab the result $oid = ($s->var_bind_names())[0]; # Quit if we've left the tree last WALK unless oid_base_match($starting_oid, $oid); last WALK if $s->var_bind_list()->{$oid} eq 'endOfMibView'; # Build the varbind ($iid) = ($oid) =~ /$starting_oid\.(.*)/; $val = $s->var_bind_list()->{$oid}; $type = snmp_type_ntop($s->var_bind_types()->{$oid}); %varbind = ( oid => $oid, iid => $iid, val => $val, type => $type ); push @answer, \%varbind; $nextoid = $oid; } # End WALK } # End 'version 1' # Perform the walk using get_bulk_request if the agent speaks SNMP v2c/3 else { my $nextoid = $starting_oid; BULKWALK: while (defined $s->get_bulk_request( -varbindlist => [$nextoid], -maxrepetitions => $max_rep )) { # Walk the list of OIDs we just generated, building varbinds my @oids = oid_lex_sort(keys %{$s->var_bind_list()}); for my $oid (@oids) { my ($iid, $type, $val, %varbind); # Quit if we've left the tree last BULKWALK unless oid_base_match($starting_oid, $oid); last BULKWALK if $s->var_bind_list()->{$oid} eq 'endOfMibView'; # Build the varbind ($iid) = ($oid) =~ /$starting_oid\.(.*)/; $val = $s->var_bind_list()->{$oid}; $type = snmp_type_ntop($s->var_bind_types()->{$oid}); %varbind = ( oid => $oid, iid => $iid, val => $val, type => $type ); push @answer, \%varbind; } # End 'Walk the list of OIDs' $nextoid = pop @oids; } # End BULKWALK } # End 'use get_bulk_request' # Handle errors $error = $s->error; if (defined $error and $error ne $EMPTY_STR) { print "Unable to walk $starting_oid on $host: $error\n" if $debug > 3; $s->close; return; } # Clean-up $s->close; # Debug trace trace_location('end') if $debug > 2; return \@answer; } ######################################################################## # Show the programmer where we are ######################################################################## sub trace_location { my $location = shift; my ($subroutine) = (caller (1))[3]; if ($location eq 'begin') { print "Entering $subroutine\n"; } elsif ($location eq 'end') { print "Leaving $subroutine\n"; } return 1; } ######################################################################## # Validate targets ######################################################################## sub validate_target { my $nb = 0; # 'Need blank line': helps pretty-print output my @prune; # list of targets which failed a check # Debug trace trace_location('begin') if $debug; # Notify operator print "Validating target list...\n"; # Walk through targets, checking for validity TARGET: for my $target (@target) { my (%arg, $sys_descr); print " Processing $target\n" if $debug; # If target looks like a hostname, verify that we can resolve it if ($target =~ /[a-zA-Z]/) { my ($h, $ip); $h = gethost($target); $ip = inet_ntoa($h->addr) if defined $h; unless (defined $ip) { print "\n" if $nb; print "Cannot resolve $target, ignoring\n"; push @prune, $target; $nb = 0; next TARGET; } } # If target looks like an IP address, check its format else { unless (validaddr($target)) { print "\n" if $nb; print "$target is not a valid IP address, ignoring\n"; push @prune, $target; $nb = 0; next TARGET; } } # Verify that the target returns pings unless ( ping (host => $target, count => 3, timeout => 1) ) { print "\n" if $nb; print "Cannot ping $target, ignoring\n"; push @prune, $target; $nb = 0; next TARGET; } # Verify that we can speak SNMP to the target by getting sysDescr.0 %arg = ( host => $target, oid => '.1.3.6.1.2.1.1.1.0'); $sys_descr = snmpGet(\%arg); unless (defined $sys_descr and $sys_descr ne $EMPTY_STR) { print "\n" if $nb; print "Cannot speak SNMP to $target, ignoring\n"; push @prune, $target; $nb = 0; next TARGET; } # Entertain operator print $BANG unless $debug; $nb = 1; } # Make things look pretty print "\n\n"; # Remove targets which failed a check prune_target(@prune); die 'No valid targets' unless @target > 0; # Debug info if ($debug) { print "Surviving hosts:\n"; for my $target (@target) { print " $target\n"; } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Output help ######################################################################## sub HELP_MESSAGE { print <