#!/usr/bin/perl -w
######################################################################
### Observe interface counters in real time.
######################################################################
### Copyright (c) 1995-2000, Simon Leinen.
###
### This program is free software; you can redistribute it under the
### "Artistic License" included in this distribution (file "Artistic").
######################################################################
### Author:       Simon Leinen  <simon@switch.ch>
### Date Created: 21-Feb-1999
###
### Real-time full-screen display of the octet and (Cisco-specific)
### CRC error counters on interfaces of an SNMP-capable node
###
### Description: 
###
### Call this script with "-h" to learn about command usage.
###
### The script will poll the RFC 1213 ifTable at specified intervals
### (default is every five seconds).
###
### For each interface except for those that are down, a line is
### written to the terminal which lists the interfaces name (ifDescr),
### well as the input and output transfer rates, as computed from the
### deltas of the respective octet counts since the last sample.
###
### "Alarms"
###
### When an interface is found to have had CRC errors in the last
### sampling interval, or only output, but no input traffic, it is
### shown in inverse video.  In addition, when a link changes state
### (from normal to inverse or vice versa), a bell character is sent
### to the terminal.
###
### Miscellaneous
###
### Note that on the very first display, the actual SNMP counter
### values are displayed.  THOSE ABSOLUTE COUNTER VALUES HAVE NO
### DEFINED SEMANTICS WHATSOEVER.  However, in some versions of
### Cisco's software, the values seem to correspond to the total
### number of counted items since system boot (modulo 2^32).  This can
### be useful for certain kinds of slowly advancing counters (such as
### CRC errors, hopefully).
###
### The topmost screen line shows the name of the managed node, as
### well as a few hard-to-explain items I found useful while debugging
### the script.
###
### Please send any patches and suggestions for improvement to the
### author (see e-mail address above).  Hope you find this useful!
###
### Original Purpose:
###
### This script should serve as an example of how to "correctly"
### traverse the rows of a table.  This functionality is implemented in
### the map_table() subroutine.  The example script displays a few
### columns of the RFC 1213 interface table and Cisco's locIfTable.  The
### tables share the same index, so they can be handled by a single
### invocation of map_table().
###
require 5.003;

use strict;

use BER;
use SNMP_Session "0.96";	# requires map_table_4() and ipv4only
use POSIX;			# for exact time
use Curses;
use Math::BigInt;
use Math::BigFloat;

### Forward declarations
sub out_interface ($$$$$$@);
sub pretty_ps ($$);
sub usage ($ );

my $version = '1';

my $desired_interval = 5.0;

my $switch_engine_p = 0;

my $all_p = 0;

my $port = 161;

my $max_repetitions = 0;

my $suppress_output = 0;

my $suppress_curses = 0;

my $debug = 0;

my $show_out_discards = 0;

my $cisco_p = 0;

## Whether to use 64-bit counters.  Can be requested with `-l' option.
my $counter64_p = 0;

## Whether to select IPv4-only in open().  Can be set using `-4' option.
my $ipv4_only_p = 0;

my $host;

my $community;

my $use_getbulk_p = 1;

while (defined $ARGV[0]) {
    if ($ARGV[0] =~ /^-v/) {
	if ($ARGV[0] eq '-v') {
	    shift @ARGV;
	    usage (1) unless defined $ARGV[0];
	} else {
	    $ARGV[0] = substr($ARGV[0], 2);
	}
	if ($ARGV[0] eq '1') {
	    $version = '1';
	} elsif ($ARGV[0] eq '2c' or $ARGV[0] eq '2') {
	    $version = '2c';
	} else {
	    usage (1);
	}
    } elsif ($ARGV[0] =~ /^-m/) {
	if ($ARGV[0] eq '-m') {
	    shift @ARGV;
	    usage (1) unless defined $ARGV[0];
	} else {
	    $ARGV[0] = substr($ARGV[0], 2);
	}
	if ($ARGV[0] =~ /^[0-9]+$/) {
	    $max_repetitions = $ARGV[0];
	} else {
	    usage (1);
	}
    } elsif ($ARGV[0] =~ /^-p/) {
	if ($ARGV[0] eq '-p') {
	    shift @ARGV;
	    usage (1) unless defined $ARGV[0];
	} else {
	    $ARGV[0] = substr($ARGV[0], 2);
	}
	if ($ARGV[0] =~ /^[0-9]+$/) {
	    $port = $ARGV[0];
	} else {
	    usage (1);
	}
    } elsif ($ARGV[0] =~ /^-t/) {
	if ($ARGV[0] eq '-t') {
	    shift @ARGV;
	    usage (1) unless defined $ARGV[0];
	} else {
	    $ARGV[0] = substr($ARGV[0], 2);
	}
	if ($ARGV[0] =~ /^[0-9]+(\.[0-9]+)?$/) {
	    $desired_interval = $ARGV[0];
	} else {
	    usage (1);
	}
    } elsif ($ARGV[0] eq '-B') {
	$use_getbulk_p = 0;
    } elsif ($ARGV[0] eq '-s') {
	$switch_engine_p = 1;
    } elsif ($ARGV[0] eq '-a') {
	$all_p = 1;
    } elsif ($ARGV[0] eq '-c') {
	$cisco_p = 1;
    } elsif ($ARGV[0] eq '-l') {
	$counter64_p = 1;
    } elsif ($ARGV[0] eq '-n') {
	$suppress_output = 1;
	$suppress_curses = 1;
    } elsif ($ARGV[0] eq '-C') {
	$suppress_output = 0;
	$suppress_curses = 1;
    } elsif ($ARGV[0] eq '-d') {
	$suppress_output = 0;
	$suppress_curses = 1;
	$debug = 1;
    } elsif ($ARGV[0] eq '-D') {
	$show_out_discards = 1;
    } elsif ($ARGV[0] eq '-4') {
	$ipv4_only_p = 1;
    } elsif ($ARGV[0] eq '-h') {
	usage (0);
	exit 0;
    } elsif ($ARGV[0] =~ /^-/) {
	usage (1);
    } else {
	if (!defined $host) {
	    $host = $ARGV[0];
	} elsif (!defined $community) {
	    $community = $ARGV[0];
	} else {
	    usage (1);
	}
    }
    shift @ARGV;
}
defined $host or usage (1);
defined $community or $community = 'public';
usage (1) if $#ARGV >= $[;

my $ifDescr = [1,3,6,1,2,1,2,2,1,2];
my $ifAdminStatus = [1,3,6,1,2,1,2,2,1,7];
my $ifOperStatus = [1,3,6,1,2,1,2,2,1,8];
my $ifInOctets = [1,3,6,1,2,1,2,2,1,10];
my $ifOutOctets = [1,3,6,1,2,1,2,2,1,16];
my $ifInUcastPkts = [1,3,6,1,2,1,2,2,1,11];
my $ifOutUcastPkts = [1,3,6,1,2,1,2,2,1,17];
my $ifOutDiscards = [1,3,6,1,2,1,2,2,1,19];
my $ifAlias = [1,3,6,1,2,1,31,1,1,1,18];
## Counter64 variants
my $ifHCInOctets = [1,3,6,1,2,1,31,1,1,1,6];
my $ifHCOutOctets = [1,3,6,1,2,1,31,1,1,1,10];
## Cisco-specific variables enabled by `-c' option
my $locIfInCRC = [1,3,6,1,4,1,9,2,2,1,1,12];
my $locIfOutCRC = [1,3,6,1,4,1,9,2,2,1,1,12];

my $cseL3SwitchedTotalPkts = [1,3,6,1,4,1,9,9,97,1,4,1,1,1];
my $cseL3SwitchedTotalOctets = [1,3,6,1,4,1,9,9,97,1,4,1,1,2];
my $cseL3CandidateFlowHits = [1,3,6,1,4,1,9,9,97,1,4,1,1,3];
my $cseL3EstablishedFlowHits = [1,3,6,1,4,1,9,9,97,1,4,1,1,4];
my $cseL3ActiveFlows = [1,3,6,1,4,1,9,9,97,1,4,1,1,5];
my $cseL3FlowLearnFailures = [1,3,6,1,4,1,9,9,97,1,4,1,1,6];
my $cseL3IntFlowInvalids = [1,3,6,1,4,1,9,9,97,1,4,1,1,7];
my $cseL3ExtFlowInvalids = [1,3,6,1,4,1,9,9,97,1,4,1,1,8];

my $clock_ticks = POSIX::sysconf( &POSIX::_SC_CLK_TCK );

my $win = new Curses
    unless $suppress_curses;

my %old;
my $sleep_interval = $desired_interval + 0.0;
my $interval;
my $linecount;

sub rate_32 ($$$@) {
    my ($old, $new, $interval, $multiplier) = @_;
    $multiplier = 1 unless defined $multiplier;
    my $diff = $new-$old;
    if ($diff < 0) {
	$diff += (2**32);
    }
    return $diff / $interval * $multiplier;
}

sub rate_64 ($$$@) {
    my ($old, $new, $interval, $multiplier) = @_;
    $multiplier = 1 unless defined $multiplier;
    return 0 if $old == $new;
    my $diff = Math::BigInt->new ($new-$old);
    if ($diff < 0) {
	$diff = $diff->add (2**64);
    }
    warn "rate_64 ($old, $new, $interval, $multiplier)\n"
	if $debug;
    warn "  diff: $diff\n"
	if $debug;
    ## hrm.  Why is this so complicated?
    ## I want a real programming language (such as Lisp).
    my $result = new Math::BigFloat ($diff->bnorm ());
    warn "  result: $result\n"
	if $debug;
    $result /= $interval;
    warn "  result: $result\n"
	if $debug;
    $result *= $multiplier;
    warn "  result: $result\n"
	if $debug;
    return $result;
}

sub rate ($$$$@) {
    my ($old, $new, $interval, $counter64_p, $multiplier) = @_;
    $multiplier = 1 unless defined $multiplier;
    return $counter64_p
	? rate_64 ($old, $new, $interval, $multiplier)
	: rate_32 ($old, $new, $interval, $multiplier);
}

sub rate_or_0 ($$$@) {
    my ($old, $new, $interval, $counter64_p, $multiplier) = @_;
    $counter64_p = 0 unless defined $counter64_p;
    $multiplier = 1 unless defined $multiplier;
    return defined $new
	? rate ($old, $new, $interval, $counter64_p, $multiplier)
	: 0;
}

sub out_interface ($$$$$$@) {
    my ($index, $descr, $admin, $oper, $in, $out);
    my ($crc, $comment);
    my ($drops);
    my ($clock) = POSIX::times();
    my $alarm = 0;

    ($index, $descr, $admin, $oper, $in, $out, $comment, @_) = @_;
    ($crc, @_) = @_ if $cisco_p;
    ($drops, @_) = @_ if $show_out_discards;

    grep (defined $_ && ($_=pretty_print $_),
	  ($descr, $admin, $oper, $in, $out, $crc, $comment, $drops));
    $win->clrtoeol ()
	unless $suppress_curses;
    return unless $all_p || defined $oper && $oper == 1; # up
    return unless defined $in && defined $out;
    ## Suppress interfaces called "unrouted VLAN..."
    return if $descr =~ /^unrouted VLAN/;
    if (!defined $old{$index}) {
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT ("%5d  %-24s %10s %10s",
			   $index,
			   defined $descr ? $descr : '',
			   defined $in ? $in : '-',
			   defined $out ? $out : '-');
	} else {
	    $win->addstr ($linecount, 0,
			  sprintf ("%5d  %-24s %10s %10s",
				   $index,
				   defined $descr ? $descr : '',
				   defined $in ? $in : '-',
				   defined $out ? $out : '-'));
	}
	if ($show_out_discards) {
	    if ($suppress_output) {
		# do nothing
	    } elsif ($suppress_curses) {
		printf STDOUT (" %8s", defined $drops ? $drops : '-');
	    } else {
		$win->addstr (sprintf (" %8s",
				       defined $drops ? $drops : '-'));
	    }
	}
	if ($cisco_p) {
	    if ($suppress_output) {
		# do nothing
	    } elsif ($suppress_curses) {
		printf STDOUT (" %10s", defined $crc ? $crc : '-');
	    } else {
		$win->addstr (sprintf (" %10s",
				       defined $crc ? $crc : '-'));
	    }
	}
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT (" %s", defined $comment ? $comment : '');
	} else {
	    $win->addstr (sprintf (" %s", defined $comment ? $comment : ''));
	}
	print "\n" if !$suppress_output and $suppress_curses;
    } else {
	my $old = $old{$index};

	$interval = ($clock-$old->{'clock'}) * 1.0 / $clock_ticks;
	my $d_in = rate_or_0 ($old->{'in'}, $in, $interval, $counter64_p, 8);
	my $d_out = rate_or_0 ($old->{'out'}, $out, $interval, $counter64_p, 8);
	my $d_drops = rate_or_0 ($old->{'drops'}, $drops, $interval, 0);
	my $d_crc = rate_or_0 ($old->{'crc'}, $crc, $interval, 0);
	$alarm = ($d_crc != 0)
	    || 0 && ($d_out > 0 && $d_in == 0);
	print STDERR "\007" if $alarm && !$old->{'alarm'};
	print STDERR "\007" if !$alarm && $old->{'alarm'};
	$win->standout() if $alarm && !$suppress_curses;
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT ("%5d  %-24s %s %s",
			   $index,
			   defined $descr ? $descr : '',
			   pretty_ps ($in, $d_in),
			   pretty_ps ($out, $d_out));
	} else {
	    $win->addstr ($linecount, 0,
			  sprintf ("%5d  %-24s %s %s",
				   $index,
				   defined $descr ? $descr : '',
				   pretty_ps ($in, $d_in),
				   pretty_ps ($out, $d_out)));
	}
	if ($show_out_discards) {
	    if ($suppress_output) {
		# do nothing
	    } elsif ($suppress_curses) {
		printf STDOUT (" %8.1f %s", defined $drops ? $d_drops : 0);
	    } else {
		$win->addstr (sprintf (" %8.1f %s",
				       defined $drops ? $d_drops : 0));
	    }
	}
	if ($cisco_p) {
	    if ($suppress_output) {
		# do nothing
	    } elsif ($suppress_curses) {
		printf STDOUT (" %10.1f", defined $crc ? $d_crc : 0);
	    } else {
		$win->addstr (sprintf (" %10.1f",
				       defined $crc ? $d_crc : 0));
	    }
	}
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT (" %s", defined $comment ? $comment : '');
	} else {
	    $win->addstr (sprintf (" %s",
				   defined $comment ? $comment : ''));
	}
	$win->standend() if $alarm && !$suppress_output;
	print "\n" if !$suppress_output and $suppress_curses;
    }
    $old{$index} = {'in' => $in,
		    'out' => $out,
		    'crc' => $crc,
		    'drops' => $drops,
		    'clock' => $clock,
		    'alarm' => $alarm};
    ++$linecount;
    $win->refresh ()
	unless $suppress_output;
}

sub out_switching_engine ($$$$$$@) {
    my ($index,
	$pkts, $octets,
	$candidate_flow_hits,
	$established_flow_hits,
	$active_flows,
	$flow_learn_failures,
	$int_flow_invalids,
	$ext_flow_invalids) = @_;
    my ($clock) = POSIX::times();
    my $alarm = 0;

    grep (defined $_ && ($_=pretty_print $_),
	  ($pkts, $octets,
	   $candidate_flow_hits,
	   $established_flow_hits,
	   $active_flows,
	   $flow_learn_failures,
	   $int_flow_invalids,
	   $ext_flow_invalids));
    warn "RETRIEVED: pkts: $pkts\noctets: $octets\n"
	if $debug;
    $win->clrtoeol ()
	unless $suppress_curses;
    return unless defined $pkts and defined $octets;
    ## Suppress interfaces called "unrouted VLAN..."
    if (!defined $old{$index}) {
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT ("%5d %10s %10s\n",
			   $index,
			   defined $pkts ? $pkts : '-',
			   defined $octets ? $octets : '-');
	} else {
	    $win->addstr ($linecount, 0,
			  sprintf ("%5d %10s %10s",
				   $index,
				   defined $pkts ? $pkts : '-',
				   defined $octets ? $octets : '-'));
	}
    } else {
	my $old = $old{$index};

	$interval = ($clock-$old->{'clock'}) * 1.0 / $clock_ticks;
	my $d_pkts = rate_or_0 ($old->{'pkts'}, $pkts, $interval, 0);
	my $d_octets = rate_or_0 ($old->{'octets'}, $octets, $interval, 1, 8);
	warn "RATE: pkts: $d_pkts\nbits: $d_octets\n"
	    if $debug;
	$alarm = 0;
	print STDERR "\007" if $alarm && !$old->{'alarm'};
	print STDERR "\007" if !$alarm && $old->{'alarm'};
	$win->standout() if $alarm && !$suppress_curses;
	if ($suppress_output) {
	    # do nothing
	} elsif ($suppress_curses) {
	    printf STDOUT ("%2d  %s %s",
			   $index,
			   pretty_ps ($pkts, $d_pkts),
			   pretty_ps ($octets, $d_octets));
	} else {
	    $win->addstr ($linecount, 0,
			  sprintf ("%2d  %s %s",
				   $index,
				   pretty_ps ($pkts, $d_pkts),
				   pretty_ps ($octets, $d_octets)));
	}
	$win->standend() if $alarm && !$suppress_curses;
	print "\n" if !$suppress_output and $suppress_curses;
    }
    $old{$index} = {'pkts' => $pkts,
		    'octets' => $octets,
		    'clock' => $clock,
		    'alarm' => $alarm};
    ++$linecount;
    $win->refresh ()
	unless $suppress_curses;
}

sub pretty_ps ($$) {
    my ($count, $bps) = @_;
    if (! defined $count) {
	return '      -   ';
    } elsif ($bps > 1000000) {
	return sprintf ("%8.4f M", $bps/1000000);
    } elsif ($bps > 1000) {
	return sprintf ("%9.1fk", $bps/1000);
    } else {
	return sprintf ("%10.0f", $bps);
    }
}

$win->erase ()
    unless $suppress_curses;
my $session =
    ($version eq '1' ? SNMPv1_Session->open ($host, $community, $port, undef, undef, undef, undef, $ipv4_only_p)
     : $version eq '2c' ? SNMPv2c_Session->open ($host, $community, $port, undef, undef, undef, undef, $ipv4_only_p)
     : die "Unknown SNMP version $version")
  || die "Opening SNMP_Session";
$session->debug (1) if $debug;
$use_getbulk_p = 0 if $version eq '1';
$session->{'use_getbulk'} = 0 unless $use_getbulk_p;

### max_repetitions:
###
### We try to be smart about the value of $max_repetitions.  Starting
### with the session default, we use the number of rows in the table
### (returned from map_table_4) to compute the next value.  It should
### be one more than the number of rows in the table, because
### map_table needs an extra set of bindings to detect the end of the
### table.
###
$max_repetitions = $session->default_max_repetitions
    unless $max_repetitions;
while (1) {
    unless ($suppress_output) {
	if ($suppress_curses) {
	    printf STDOUT ("interval: %4.1fs %d reps\n",
			   $interval || $desired_interval,
			   $max_repetitions);
	} else {
	    $win->addstr (0, 0, sprintf ("%-20s interval %4.1fs %d reps",
					 $host,
					 $interval || $desired_interval,
					 $max_repetitions));
	    $win->standout();
	    $win->addstr (1, 0,
			  sprintf (("%5s  %-24s %10s %10s"),
				   "index", "name",
				   "bits/s", "bits/s"));
	    if ($show_out_discards) {
		$win->addstr (sprintf ((" %8s"),
				       "drops/s"));
	    }
	    if ($cisco_p) {
		$win->addstr (sprintf ((" %10s"), "pkts/s"));
	    }
	    $win->addstr (sprintf ((" %s"), "description"));
	    $win->addstr (2, 0,
			  sprintf (("%2s  %-24s %10s %10s"),
				   "", "",
				   "in", "out"));
	    if ($show_out_discards) {
		$win->addstr (sprintf ((" %8s"),
				       ""));
	    }
	    if ($cisco_p) {
		$win->addstr (2, 0,
			      sprintf ((" %10s %s"),
				       "CRC",
				       ""));
	    }
	    $win->clrtoeol ();
	    $win->standend();
	}
    }
    $linecount = 3;
    my @oids;

    if ($switch_engine_p) {
	@oids = (
		 $cseL3SwitchedTotalPkts,
		 $cseL3SwitchedTotalOctets,
		 $cseL3CandidateFlowHits,
		 $cseL3EstablishedFlowHits,
		 $cseL3ActiveFlows,
		 $cseL3FlowLearnFailures,
		 $cseL3IntFlowInvalids,
		 $cseL3ExtFlowInvalids
		 );
    } else {
	@oids = ($ifDescr,$ifAdminStatus,$ifOperStatus);
	if ($counter64_p) {
	    @oids = (@oids,$ifHCInOctets,$ifHCOutOctets);
	} else {
	    @oids = (@oids,$ifInOctets,$ifOutOctets);
	}
	@oids = (@oids,$ifAlias);
	if ($cisco_p) {
	    push @oids, $locIfInCRC;
	}
	if ($show_out_discards) {
	    push @oids, $ifOutDiscards;
	}
    }
    my $calls =
	$switch_engine_p
	? $session->map_table_4
	(\@oids, \&out_switching_engine, $max_repetitions)
	: $session->map_table_4
	(\@oids, \&out_interface, $max_repetitions);
    $win->clrtobot (), $win->refresh ()
	unless $suppress_curses;
    $max_repetitions = $calls + 1
	if $calls > 0;
    $sleep_interval -= ($interval - $desired_interval)
	if defined $interval;
    select (undef, undef, undef, $sleep_interval);
}
1;

sub usage ($) {
    warn <<EOM;
Usage: $0 [-t secs] [-v (1|2c)] [-c] [-l] [-m max] [-4] [-p port] host [community]
       $0 -h

  -h           print this usage message and exit.

  -c           also use Cisco-specific variables (locIfInCrc)

  -l           use 64-bit counters (requires SNMPv2 or higher)

  -t secs      specifies the sampling interval.  Defaults to 5 seconds.

  -v version   can be used to select the SNMP version.  The default
   	       is SNMPv1, which is what most devices support.  If your box
   	       supports SNMPv2c, you should enable this by passing "-v 2c"
   	       to the script.  SNMPv2c is much more efficient for walking
   	       tables, which is what this tool does.

  -B           do not use get-bulk

  -m max       specifies the maxRepetitions value to use in getBulk requests
               (only relevant for SNMPv2c).

  -4           use only IPv4 addresses, even if host also has an IPv6
               address.  Use this for devices that are IPv6-capable
               but whose SNMP agent doesn\'t listen to IPv6 requests.

  -m port      can be used to specify a non-standard UDP port of the SNMP
               agent (the default is UDP port 161).

  host         hostname or IP address of a router

  community    SNMP community string to use.  Defaults to "public".
EOM
    exit (1) if $_[0];
}
