#!/usr/bin/env perl
#
# $Id: pollcisco.pl,v 1.30 2009/11/01 19:01:02 ksb Exp $
#
# pollcisco will poll any Cisco device that supports SNMP.  We use
# snmp2c for everything we can, and fallback to v1 for 2900s and
# unknown devices.  We also use 64 bit counters where we can.
#
# -- csg, petef, ksb
#

use lib '/usr/local/lib/sac/perl'.join('.', unpack('c*', $^V)),
	'/usr/local/lib/sac';

use strict;
use Socket;
use Sys::Hostname;
use IO::Socket;
use Carp qw(carp);

use Net::SNMP (qw(oid_lex_sort oid_context_match));
use Getopt::Std;

use vars qw($VERSION @FINDMIB @PORTMIB @PORTMIB64 $s);
$VERSION='$Id: pollcisco.pl,v 1.30 2009/11/01 19:01:02 ksb Exp $';

my($progname) = $0;
$progname ||= 'pollcisco';
$progname =~ m,.*/(.+)$, and $progname = $1;

sub snmpwalk1 {
	my($start) = shift;
	my($next, @ret, $oid, @oids, $tp);

	$next = $start;
	@ret = ();
	while (defined($s->get_next_request($next))) {
		$next = (keys(%{$s->var_bind_list}))[0];
		last if (!oid_context_match($start, $next));
		$tp = $next;
		$tp =~ s/.*\.//;
		push(@ret, $tp);
	}

	return(@ret);
}

sub snmpwalk2 {
	my($start) = shift;
	my($next, @ret, $oid, @oids, $tp);

	$next = $start;
	@ret = ();
top:	while (defined($s->get_bulk_request(-maxrepetitions => 1024, -varbindlist => [$next]))) {
		@oids = oid_lex_sort(keys(%{$s->var_bind_list}));
		foreach $oid (@oids) {
			$tp = $oid;
			$tp =~ s/.*\.//;
			$next = $oid;
			last top if (!oid_context_match($start, $oid));
			last top if ($s->var_bind_list->{$oid} eq 'endOfMibView');
			push(@ret, $tp);
		}
	}

	return(@ret);
}

# We search these MIB variable to find valid ports, their names
# and their status.

# LIST PORT MIB
@FINDMIB= (
	undef,
	'1.3.6.1.2.1.2.2.1.7',			# ifAdminStatus
	'1.3.6.1.2.1.2.2.1.8',			# ifOperStatus
);

# We search these MIB variables for the ports we find above, and
# gather/summarize the statistics for rrdup.

@PORTMIB= (
	'1.3.6.1.2.1.2.2.1.10',			# ifInOctets
	'1.3.6.1.2.1.2.2.1.11',			# ifInUcastPkts
	'1.3.6.1.2.1.2.2.1.12',			# ifInNUcastPkts + broadcast
	'1.3.6.1.2.1.2.2.1.16',			# ifOutOctets
	'1.3.6.1.2.1.2.2.1.17',			# ifOutUcastPkts
	'1.3.6.1.2.1.2.2.1.18',			# ifOutNUcastPkts + broadcast
	'1.3.6.1.2.1.2.2.1.14',			# ifInErrors
	'1.3.6.1.2.1.2.2.1.20'			# ifOutErrors
);

@PORTMIB64= (
# N.B. to be compatible with the originale we'd actually have to add
#  ifHCOutBroadcastPkts (13) to 12 and
#  ifHCInBroadcastPkts (9) tp 8 to include broadcast frames
	'1.3.6.1.2.1.31.1.1.1.6',		# ifHCInOctets
	'1.3.6.1.2.1.31.1.1.1.7',		# ifHCInUcastPkts
	'1.3.6.1.2.1.31.1.1.1.8',		# ifHCInMulticastPkts - broad
	'1.3.6.1.2.1.31.1.1.1.10',		# ifHCOutOctets
	'1.3.6.1.2.1.31.1.1.1.11',		# ifHCOutUcastPkts
	'1.3.6.1.2.1.31.1.1.1.12',		# ifHCOutMulticastPkts - broad
	'1.3.6.1.2.1.2.2.1.14',			# ifInErrors
	'1.3.6.1.2.1.2.2.1.20'			# ifOutErrors
);

MAIN: {
	my(%opts);				# getopt
	my($hostname);				# hostname we're walking
	my(@mib);				# list of mib vars to fetch
	my($dres);				# results from SNMP Query
	my($e);					# snmp session errors
	my($snmpwalk);				# function ptr to snmpwalk[12]
	my($samplehost,$pegport);		# host to send updates to
	my($speedmib);				# MIB to poll for speeds

	getopts('H:c:N:hp:VDx', \%opts);

	$hostname = shift(@ARGV);
	if ($hostname eq '') {
		$opts{'h'} = 1;
	}

	$samplehost = $opts{'H'};
	$samplehost ||= 'peg.sac.fedex.com:31415';
	if ($samplehost =~ m/^([^:]+):([0-9]+)$/) {
		$pegport = $2;
		$samplehost = $1;
	} else {
		$pegport = 31415;
	}
	# set a default snmp read-community name
	$opts{'c'} ||= 'public';
	if ($opts{'V'}) {
		print "$progname: $VERSION\n",
			"$progname: read-comminity: $opts{'c'}\n";
		exit(0);
	}
	if ($opts{'h'}) {
		print "$progname: usage [-Dx] [-c read] [-H sample] [-N node] [-p delay] switch\n",
		    "$progname -h\n",
		    "$progname -V\n",
		    "c read    specify a SNMP read-community\n",
		    "D         dump port names only\n",
		    "h         show this help message\n",
		    "H sample  host receiving RRD samples (also allows :port)\n",
		    "N node    report the switch by this node name\n",
		    "p delay   resample the switch every delay seconds\n",
		    "V         show version information\n",
		    "x         trace PEG updates on stderr\n";
		exit(0);
	}


	($s, $e) = Net::SNMP->session(-hostname=>$hostname,-community=>$opts{'c'});
	die "NET::SNMP->session v1: $hostname: $e" unless $s;

	# See what kind of Cisco we're talking to, and if it can speak SNMP v2c
	my($version);
	@mib = ("1.3.6.1.2.1.1.1.0");
	$version = $s->get_request(@mib);
	die "$hostname: can't determine version\n" unless $version;
	my($swmodel) = $$version{$mib[0]};

	# depending on what version we are, set the MIB to find the ports
	# and figure out what version of snmp to use.
	$snmpwalk = \&snmpwalk2;
	$speedmib = '1.3.6.1.2.1.31.1.1.1.15';
	if ($swmodel =~ m/(WS-C65[0-9][0-9]|\(c6sup2_rp-)/o) {
		# 6500
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18';
		# Use 64-bit counters if we're on a 6500.
		@PORTMIB = @PORTMIB64;
	} elsif ($swmodel =~ m/^Content Switch.*v2c/o) {
		# CSS
		$FINDMIB[0] = '1.3.6.1.2.1.2.2.1.2';
		$speedmib ='1.3.6.1.2.1.2.2.1.5';
		$snmpwalk = \&snmpwalk1;	# fall back to snmp v1
	} elsif ($swmodel =~ m/WS-C5[05][0-9][0-9]/o) {
		# 5000/5500
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		# I think ZZZ @PORTMIB = @PORTMIB64;
	} elsif ($swmodel =~ m/WS-C1[4][0-9][0-9]/o) {
		# 1400 FDDI switch
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18'; # wrong
		$snmpwalk = \&snmpwalk1;	# fall back to snmp v1
	} elsif ($swmodel =~ m/\(C35[05][0-9].* version 12[.]/io) {
		# 3550 with version 12 IOS on it is 64 bit counters only
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18';
		@PORTMIB = @PORTMIB64;
	} elsif ($swmodel =~ m/\(C35[05][0-9]/o) {
		# 3500
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18';
	} elsif ($swmodel =~ m/\(C375[0-9]-/o) {
		# 3750
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18';
		@PORTMIB = @PORTMIB64;
	} elsif ($swmodel =~ m/Catalyst 4500 L3/o) {
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.18';
		@PORTMIB = @PORTMIB64;
	} elsif ($swmodel =~ m/Cisco NX-OS\(tm\) n5000/o) {
		$FINDMIB[0] = '1.3.6.1.2.1.31.1.1.1.1';
		$FINDMIB[3] = '1.3.6.1.2.1.31.1.1.1.14';
		@PORTMIB = @PORTMIB64;
	} else {
		# 2900, maybe everything else?
		$FINDMIB[0] = '1.3.6.1.2.1.2.2.1.2';
		$snmpwalk = \&snmpwalk1;	# fall back to snmp v1
	}

	if ($snmpwalk == \&snmpwalk2) {
		# Open an SNMP v2c connection now that we know it's supported.
		$s->close();
		($s, $e) = Net::SNMP->session(-hostname=>$hostname,-community=>$opts{'c'},-version=>'2c');
		die "NET::SNMP->session v2c: $hostname: $e" unless $s;
	}

	# Walk the interface table looking for valid ports, and push
	# each of them them into a list of ports to inspect later.

	my(@ports,@deadports);			# ports we find
	my($tp);				# target port iterator
	my($pname) = '';			# port name (mutated for rrd)
	my($rrdname);				# what PEG calls us
	my($p);					# switchport iterator
	my($descr);				# port description
	my($statusIndex);			# port status index
	my($alive);				# boolean of port's status
	my($last) = 100;			# last port speed
	$rrdname = $opts{'N'} || "$hostname";

	my($res);
	foreach $tp (&$snmpwalk($FINDMIB[0])) {
		# localize the mib for this interface
		@mib = ();
		foreach (@FINDMIB) {
			push(@mib, join('.', $_, $tp));
		}

		# if we didn't find the next port
		$res = $s->get_request(@mib);
		next unless (defined($res));
		$descr = (scalar(@FINDMIB)>3) ? $$res{$mib[3]} : '';

		# cleanup the port name to match our *.rrd naming scheme
		$pname = $$res{$mib[0]};
		$pname =~ s,^\s+,,o;
		$pname =~ s,(FastEthernet|Fa),FE,o;
		$pname =~ s,(GigabitEthernet|Gi),GE,o;
		$pname =~ s,(Ethernet),GE,o;
		$pname =~ s,\\,-,og;
		$pname =~ s,\/,-,og;
		$pname =~ s,\s+$,,o;

		$alive = (($$res{$mib[1]} == 1) && ($$res{$mib[2]} == 1));

		# skip any interfaces that don't get "real" traffic
		# XXX: next unless it's something we recognize
		next if ($pname =~ /(null|nu)/io);	# skip null pseudo-ifs
		next if ($pname =~ /(vlan|vl)/io);	# skip vlan pseudo-ifs
		next if ($pname =~ /^s[cl]0$/o);	# skip sl0/sc0 control
		next if ($pname =~ /^eo0/io);		# skip unknown bs
		next if ($pname =~ /-mgmt/io);		# skip CSS mgmt
		next if ($pname =~ /formac/io);		# skip CSM mgmt
		next if ($pname =~ /CPU/io);		# skip CSM mgmt

		# if we _didn't_ get a prefix, lookup the speed.
		if ($pname =~ m/^[0-9]/) {
			@mib = ("$speedmib.$tp");
			$dres = $s->get_request(@mib);
			# if we didn't get one keep the last value.
			$last = $$dres{$mib[0]} if (!defined($dres));
			if (1000 == $last) {
				$pname = "GE$pname";
			} else {
				$pname = "FE$pname";
			}
		}
		# Check ifAdminStatus and ifOperStatus, & note this port
		if ($alive) {
			push(@ports, { 'name'=>$pname, 'port'=>$tp, 'descr'=>$descr });
		} else {
			push(@deadports, { 'name'=>$pname, 'port'=>$tp, 'descr'=>$descr });
		}
	}

	if (scalar(@deadports) + scalar(@ports) == 0) {
		die "$hostname: $swmodel: unknown switch model (bad index MIB)\n";
	}

	# If we were just asked to dump ports, do that, and exit.
	# On a 3500, we can get the port description from
	# 1.3.6.1.2.1.31.1.1.1.18.  this might be nice in another program to
	# dump more information.

	# Open up a data socket to PEG
	my($ipout,$proto,$sockaddr,$update);
	$ipout = inet_aton($samplehost);
	$proto = getprotobyname('udp');
	$sockaddr = sockaddr_in($pegport, $ipout);
	socket(SOCKET, PF_INET, SOCK_DGRAM, $proto);
	# Now walk the per-port tables collecting stats, generating a
	# line for each port as we fetch it.
	while (1) {
		foreach $p (@ports) {
			@mib = ();

			# should be a map, I think -- ksb
			$pname = $$p{'port'};
			@mib = map { "$_.$pname" } @PORTMIB;

			$res = $s->get_request(@mib);
			if (!defined($res)) {
				carp '# ', $s->error() if $opts{'x'};
				next;
			}
			my($foo) = join(':', map { $$res{$_} } @mib);
			next if ($foo =~ m/noSuchInstance/oi);
		
			# map spaces in 'port-channel 14' -> 'port-channel_14'
			$$p{'name'} =~ s/[\s:]+/_/g;
			if ($opts{'D'}) {
				print "switch/$rrdname/".$$p{'name'}.".rrd\n";
				next;
			}
			$update = "switch/$rrdname/" . $$p{'name'} . '.rrd ' .
				'ibyte:iucast:inucast:obyte:oucast:onucast:ierr:oerr N:' . $foo;
			print STDERR $update, "\n" if $opts{'x'};
			send(SOCKET, "00 $update", 0, $sockaddr);
		}
		last if (!exists $opts{'p'} || $opts{'D'});
		sleep($opts{'p'});
	}

	$s->close();
	close(SOCKET);
	exit 0;
}
