#!/usr/local/bin/perl
#
# Check a switch's stp
#
# $Header: /opt/home/doke/work/nagios/RCS/check_stp,v 1.9 2013/06/03 23:47:45 doke Exp $



use strict;
use warnings;
no warnings 'redefine';
use Getopt::Long;
use Net::SNMP;


use vars qw( $host $community $warn_age $timeout $verbose $help $mib2
    $enterprises $cisco $juniper $prefered_maxmsgsize $maxrepetitions
    $retries @crits @warns @unknowns @oks @ignores $rc $sep %snmp_sessions 
    $startup_time @perf );

$community = 'public';
$warn_age = 0;   # warn if stp changed in past n seconds 
$timeout = 2;
$retries = 3;
$prefered_maxmsgsize = 1472;   # default
$startup_time = 300;  # let switch stabilize for this many seconds before testing stp 

$maxrepetitions = 5;

$host = '';
$verbose = 0;
$help = 0;

$mib2 = '1.3.6.1.2.1';
$enterprises = '1.3.6.1.4.1';
$cisco = "$enterprises.9";
$juniper = "$enterprises.2636";

sub usage {
    my( $rc ) = @_;
    print "Usage: $0 [options] -H <host> [-C <community>]
    -H s     hostname
    -C s     snmp community [$community]
    -w n     warn if spanning tree changed within n seconds
    -v       verbose
    -h       help
";
    exit $rc;
    }

Getopt::Long::Configure ("bundling");
GetOptions(
    'H=s' => \$host,
    'C=s' => \$community,
    'w=i' => \$warn_age,
    'v+' => \$verbose,
    'h' => \$help,
    );
&usage( 0 ) if ( $help );

&usage( 1 ) if ( ! $host );
&usage( 1 ) if ( ! $community );

&check_stp();

$rc = 0;
$sep = '';
if ( $#crits >= 0 ) {
    $rc = 2;
    print "CRITICAL ", join( ", ", @crits );
    $sep = '; ';
    }
if ( $#warns >= 0 ) {
    $rc = 1 if ( $rc == 0 );
    print $sep, "Warning ", join( ", ", @warns );
    $sep = '; ';
    }
if ( $#unknowns >= 0 ) {
    $rc = 3 if ( $rc == 0 );
    print $sep, "Unknown ", join( ", ", @unknowns );
    $sep = '; ';
    }
if ( $rc == 0 || $verbose ) {
    print $sep, "Ok ", join( ", ", @oks );
    $sep = '; ';
    }
if ( $#ignores >= 0 ) {
    print $sep, "Ignoring ", join( ", ", @ignores );
    }
if ( $#perf >= 0 ) {
    print ' | ', join( " ", @perf );
    }
print "\n";
exit $rc;


##################





sub check_stp {
    my( $result, @oids, $oid, $val, $sysDescr_oid, $sysUpTime_oid,
	$sysName_oid, $sysDescr, $sysUpTime, $sysName, $vendor, $model,
	$version, $nsts, $vlan, %vlans, $tstc_oid, $tc_oid, $tstc, $tc,
	$session, $now, $msg );

    # get the sysDescr, sysName, and sysUpTime
    # we'll want the sysDescr later to determine the vendor and model
    # which controls which version of the mib to use
    $sysDescr_oid = "$mib2.1.1.0";
    $sysUpTime_oid = "$mib2.1.3.0";
    $sysName_oid = "$mib2.1.5.0";
    @oids = ( $sysDescr_oid, $sysUpTime_oid, $sysName_oid );
    $result = snmp_get( $host, $community, "sysDescr, sysUptime and sysName", \@oids );
    if ( ! defined( $result ) ) {
	$session = get_session( $host, $community );
	$verbose && print "snmp error ", $session->error(), "\n";
	push @unknowns, "couldn't get sysDescr " . $session->error();
	return;
	}
    $sysDescr = $result->{ $sysDescr_oid };
    $sysUpTime = $result->{ $sysUpTime_oid };
    $sysName = $result->{ $sysName_oid };

    if ( $verbose ) {
	print "sysDescr = $sysDescr\n";
	print "sysUpTime = $sysUpTime\n";
	print "sysName = $sysName\n";
	}

    $sysName =~ s/^([\w\d-]+)\..*/$1/;  # strip off the domain name for brevity
    push @oks, $sysName;

    if ( $sysUpTime < $startup_time * 100 ) { 
	push @oks, "System has not been up long enough for spanning tree to stabilize";
	return;
	}

    $nsts = 0;
    if ( $sysDescr =~ m/Cisco .* Version (\d\S+), /is ) {
	$vendor = 'Cisco';
	$version = $1;
	push @oks, "Cisco IOS $version";

	$result = snmp_walk( $host, $community, 'stpxPVSTVlanEnable',
	    "$cisco.9.82.1.2.2.1.2" );
	if ( $result ) {
	    foreach $oid ( keys %$result ) {
		$val = $result->{ $oid };
		# 1 is enabled
		if ( $val == 1 && $oid =~ m/2\.(\d+)$/ ) {
		    $vlan = $1;
		    $vlans{ $vlan } = 1;
		    }
		$nsts++;
		}
	    }
	else { 
	    # enterprises.cisco.ciscoMgmt.ciscoVtpMIB.vtpMIBObjects.vlanInfo.vtpVlanTable.vtpVlanEntry.vtpVlanState
	    # value = 1 for operational
	    $result = snmp_walk( $host, $community, 'vtpVlanState', 
		"$cisco.9.46.1.3.1.1.2" ) || return;
	    foreach $oid ( keys %$result ) {
		$val = $result->{ $oid };
		next if ( $val != 1 );  # 1 = operational
		if ( $val == 1 && $oid =~ m/2\.1\.(\d+)$/ ) {
		    $vlan = $1;
		    $vlans{ $vlan } = 1;
		    }
		$nsts++;
		}
	    }

	if ( $nsts ) { 

	    $tstc_oid = "$mib2.17.2.3.0";   # dot1dStpTimeSinceTopologyChange
	    $tc_oid = "$mib2.17.2.4.0";   # dot1dStpTopChanges
	    foreach $vlan ( sort keys %vlans ) { 
		next if ( $vlan >= 1000 && $vlan <= 1005 );
		@oids = ( $tstc_oid, $tc_oid );
		$result = snmp_get( $host, "$community\@$vlan", 
		    "dot1dStpTopologyChange info for vlan $vlan", \@oids );
		$now = time;
		if ( $result ) { 
		    $tc = $result->{ $tc_oid };
		    if ( defined $tc && $tc > 0 ) { 
			$tstc = $result->{ $tstc_oid };
			$msg = "TimeSinceTopologyChange for vlan $vlan is " 
			    . ticks_to_str( $tstc ) 
			    . " at " . scalar( localtime( $now - $tstc / 100 ) )
			    . " ($tc changes)"; 
			if ( $tstc < $warn_age * 100 
				&& $tstc < ( $sysUpTime - $startup_time * 100 ) ) {
			    push @warns, $msg;
			    }
			else { 
			    push @oks, $msg;
			    }
			push @perf, sprintf( "tstc_vlan%d=%ds tc_vlan%d=%d", 
			    $vlan, $tstc / 100, $vlan, $tc );
			}
		    }
		}
	    }
	}
    else {
	my( $jnxBoxDescr, $mimst, %top_changes, $name, %mimst_region_names );

	# sysDescr is unreliable on juniper because you can change it
	# sysDescr might look like: Juniper Networks, Inc. ex3300-48p internet router, kernel JUNOS 11.4R2.14 #0: 2012-03-17 16:32:02 UTC
	# or not
	# so go for the juniper specific model number oid
	$jnxBoxDescr = snmp_get_one( $host, $community, 'jnxBoxDescr',
	    "$juniper.3.1.2.0" );

	if ( $jnxBoxDescr ) {
	    # ok, it's a juniper something or other

	    $vendor = 'Juniper';
	    $model = $jnxBoxDescr;
	    $model =~ s/.*Juniper (\S+) (?:Ethernet Switch|.*Router).*/$1/i;
	    if ( $model ) {
		push @oks, "Juniper $model";
		}

	    $result = snmp_walk( $host, $community, 
		'jnxMIMstMstiRegionName',
		"$juniper.3.46.1.1.3.1.27" );
	    if ( $result ) {
		foreach $oid ( keys %$result ) {
		    if ( $oid =~ m/27\.(\d+)$/ ) {
			$mimst = $1;
			$name = $result->{ $oid };
			$name =~ s/\000//g;
			$mimst_region_names{ $mimst } = $name;
			}
		    }
		}

	    $result = snmp_walk( $host, $community, 
		'jnxMIMstCistTopChanges',
		"$juniper.3.46.1.1.3.1.35" );
	    if ( $result ) {
		foreach $oid ( keys %$result ) {
		    if ( $oid =~ m/35\.(\d+)$/ ) {
			$mimst = $1;
			$tc = $result->{ $oid };
			$top_changes{ $mimst } = $tc;
			$nsts++;
			}
		    }

		$result = snmp_walk( $host, $community, 
		    'jnxMIMstCistTimeSinceTopologyChange',
		    "$juniper.3.46.1.1.3.1.34" );
		$now = time;
		if ( $result ) {
		    foreach $oid ( keys %$result ) {
			if ( $oid =~ m/34\.(\d+)$/ ) {
			    $mimst = $1;
			    $tc = $top_changes{ $mimst };
			    next if ( $tc < 1 );  
			    $tstc = $result->{ $oid } * 100;
			    $verbose && print "mimst $mimst, tstc $tstc\n";
			    $msg = "TimeSinceTopologyChange for mimst $mimst "
				. ( ( $mimst_region_names{ $mimst } ) 
				    ? "'" . $mimst_region_names{ $mimst } . "' " 
				    : '' )
				. ticks_to_str( $tstc ) 
				. ' at ' . scalar( localtime( $now - $tstc / 100 ) )
				. " ($tc changes)"; 
			    if ( $tstc < $warn_age * 100 
				    && $tstc < ( $sysUpTime - $startup_time * 100 ) ) {
				push @warns, $msg;
				}
			    else { 
				push @oks, $msg;
				}
			    push @perf, sprintf( "region_name_mimst%d=%s tstc_mimst%d=%ds tc_mimst%d=%d", 
				$mimst, 
				( ( $mimst_region_names{ $mimst } ) 
				    ? "'" . $mimst_region_names{ $mimst } . "' " 
				    : '' ),
				$mimst, $tstc / 100, $mimst, $tc );
			    }
			}
		    }
		}
	    }
	elsif ( $verbose )  {
	    push @unknowns, "unknown vendor\n";
	    return;
	    }

	}

    if ( ! $nsts ) {    
	push @unknowns, "no spanning trees found\n";
	}

    push @oks, "up " . &ticks_to_str( $sysUpTime );
    }





###########################################




sub snmp_get {
    my( $host, $community, $name, $oids ) = @_;
    my( $session, $result, $oid, $val );

    $verbose && print "snmp_get( $host, $community, $name, $oids )\n";

    $session = get_session( $host, $community );
    return undef unless( $session );

    $result = $session->get_request( -varbindlist => $oids );
    if ( ! defined( $result ) ) {
        #warn "snmpget error: ", $session->error(), "\n";
        $verbose && print "snmpget error: ", $session->error(), "\n";
        return undef;
        }

    foreach $oid ( @$oids ) {
	if ( ! exists $result->{ $oid } ) {
	    #warn "snmpget error: requested oid not in response\n";
	    $verbose && print "    $oid: no response\n";
	    $result->{ $oid } = undef;
	    }
	elsif ( $result->{ $oid } eq 'noSuchInstance' )  {
	    $verbose && print "    $oid: noSuchInstance\n";
	    $result->{ $oid } = undef;
	    }
	elsif ( $result->{ $oid } eq 'noSuchObject' )  {
	    $verbose && print "    $oid: noSuchObject\n";
	    $result->{ $oid } = undef;
	    }
	elsif ( $verbose ) {
	    $val = $result->{ $oid };
	    $val =~ tr/\040-\176//cd;
	    print "    $oid: $val\n";
	    }
	}

    return $result;
    }





sub snmp_get_one {
    my( $host, $community, $name, $oid ) = @_;
    my( $session, @oids, $result, $oid2, $val );

    $verbose && print "snmp_get_one( $host, $community, $name, $oid )\n";

    $session = get_session( $host, $community );
    return undef unless( $session );

    @oids = ( $oid );
    $result = $session->get_request( -varbindlist => \@oids );
    if ( ! defined( $result ) ) {
        #warn "snmpget error: ", $session->error(), "\n";
        return undef;
        }

    if ( ! exists $result->{ $oid } ) {
        #warn "snmp_get_one error: requested oid not in response\n";
	$verbose && print "    $oid: no response\n";
        return undef;
        }
    elsif ( $result->{ $oid } eq 'noSuchInstance' )  {
	$verbose && print "    $oid: noSuchInstance\n";
        return undef;
        }
    elsif ( $result->{ $oid } eq 'noSuchObject' )  {
	$verbose && print "    $oid: noSuchObject\n";
        return undef;
        }

    if ( $verbose ) {
	$val = $result->{ $oid };
	$val =~ tr/\040-\176//cd;
	print "    $oid: $val\n";
	}
    return $result->{ $oid };
    }




sub snmp_walk {
    my( $host, $community, $name, $baseoid ) = @_;
    my( $session, $result );

    $verbose && print "walking $name\n";

    $session = get_session( $host, $community );
    return undef unless( $session );

    $result = $session->get_table( -baseoid => $baseoid,
	-maxrepetitions => $maxrepetitions );
    #print "session error ", $session->error(), "\n";
    if ( ! defined( $result )
            && $session->error() !~ m/Requested table is empty|Requested entries are empty or do not exist/ ) {
	$verbose && printf( "error walking $name table on %s: %s\n",
            $session->hostname, $session->error() );
        #push @unknowns, sprintf( "error walking $name table on %s: %s",
        #    $session->hostname, $session->error() );
        return undef;
        }

    if ( $verbose > 1 ) {
	my( $oid, $val );
	foreach $oid ( sort keys %$result ) {
	    $val = $result->{ $oid };
	    print "$oid = $val\n";
	    }
	}

    return $result;
    }







# walk a table
# With the optional columns parameter, just walk those selected columns out of it.
# Return a pointer to a 2d array.
sub snmp_table {
    my( $host, $community, $name, $baseoid, $columns ) = @_;
    my( $session, $result, $rows, $oid, $val, $col, $row, $data, @oids, $maxmsgsize );

    $verbose && print "snmp_table( $host, $community, $name, $baseoid, $columns )\n";

    $session = get_session( $host, $community );
    return undef unless( $session );

    if ( defined $columns ) {
	foreach $col ( @$columns ) {
	    push @oids, "$baseoid.1.$col";
	    }
	}
    else {
	push @oids, $baseoid;
	}

    $data = [];

    foreach $maxmsgsize ( $prefered_maxmsgsize, 1472, 1600, 1800, 2048, 4096, 9000 ) {
	$session->max_msg_size( $maxmsgsize );
	$verbose > 1 && print "snmp get_entries tring maxmsgsize $maxmsgsize\n";
	$result = $session->get_entries( -columns => \@oids,
	    -maxrepetitions => $maxrepetitions );
	if ( defined( $result ) ) {
	    last;
	    }
	elsif ( $session->error() =~ m/Requested table is empty|Requested entries are empty or do not exist/i ) {
	    # ok
	    return $data;
	    }
	elsif ( $session->error() =~ m/Message size exceeded buffer maxMsgSize/ ) {
	    # retry with different size
	    $verbose && print "snmp get_entries failed with maxmsgsize $maxmsgsize\n";
	    }
	else {
	    $verbose && print "snmp get_entries failed with error ", $session->error(), "\n";
	    last;
	    }
	# else try a bigger maxmessage size
	}
    if ( ! defined( $result ) ) {
	$verbose && printf "error walking $name table on %s: %s\n",
	    $session->hostname, $session->error();
	#push @unknowns, sprintf "error walking $name table on %s: %s",
	#    $session->hostname, $session->error();
	return undef;
	}

    foreach $oid ( sort keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	next if ( $val eq 'endOfMibView' );
	if ( $oid =~ m/.*\.(\d+)\.(\d+)$/ ) {
	    $col = $1; $row = $2;
	    $data->[$row][$col] = $val;
	    }
	}

    if ( scalar( @$data ) < 1 ) {
        $verbose && printf "no rows in $name table on %s: %s",
            $session->hostname, $session->error();
        #push @unknowns, sprintf "no rows in $name table on %s: %s",
        #    $session->hostname, $session->error();
	return undef;
        }

    return $data;
    }









# walk a table where the "row" index is actually multiple levels in the oid
# ie a 3 part index
sub snmp_table_multi_index {
    my( $host, $community, $name, $baseoid, $columns ) = @_;
    my( $session, $result, $rows, $oid, $val, $col, $row, $data, @oids, $maxmsgsize );

    $verbose && print "snmp_table_multi_index( $host, $community, $name, $baseoid, $columns )\n";

    $session = get_session( $host, $community );
    return undef unless( $session );

    if ( defined $columns ) {
	foreach $col ( @$columns ) {
	    push @oids, "$baseoid.1.$col";
	    }
	}
    else {
	push @oids, $baseoid;
	}

    $data = {};
    foreach $maxmsgsize ( $prefered_maxmsgsize, 1472, 1600, 1800, 2048, 4096, 9000 ) {
	$session->max_msg_size( $maxmsgsize );
	$verbose > 1 && print "snmp get_entries tring maxmsgsize $maxmsgsize\n";

	$result = $session->get_entries( -columns => \@oids,
	    -maxrepetitions => $maxrepetitions );
	if ( defined( $result ) ) {
	    last;
	    }
	elsif ( $session->error() =~ m/Requested table is empty|Requested entries are empty or do not exist/ ) {
	    # ok
	    return $data;
	    }
	elsif ( $session->error() !~ m/Message size exceeded buffer maxMsgSize/ ) {
	    last;
	    }
	}
    if ( ! defined( $result ) ) {
	$verbose && printf "error walking $name table on %s: %s\n",
	    $session->hostname, $session->error();
	#push @unknowns, sprintf "error walking $name table on %s: %s",
	#    $session->hostname, $session->error();
	return undef;
	}

    foreach $oid ( sort keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	next if ( $val eq 'endOfMibView' );
	if ( $oid =~ m/^$baseoid\.1\.(\d+)\.([\d\.]+)$/ )  {
	    $col = $1; $row = $2;
	    $data->{$row}[$col] = $val;
	    }
	}

    if ( scalar( keys %$data ) < 1 ) {
        $verbose && printf "no rows in $name table on %s: %s",
            $session->hostname, $session->error();
        #push @unknowns, sprintf "no rows in $name table on %s: %s",
        #    $session->hostname, $session->error();
	return undef;
        }

    return $data;
    }






sub get_session {
    my( $host, $community ) = @_;
    my( $session, $error );

    if ( $snmp_sessions{ "$host,$community" } ) { 
	return $snmp_sessions{ "$host,$community" };
	}

    # open the snmp session
    $verbose && print "opening snmp session to $host\n";
    ( $session, $error ) = Net::SNMP->session(
        -version => 'snmpv2c',
        -hostname => $host,
        -community => $community,
        -timeout => $timeout,
	-retries => $retries,
        #-maxmsgsize => 2000,
        #-debug => 0x02
        );
    if ( ! defined( $session ) ) {
	$verbose && print "snmp setup error: $error\n";
	push @unknowns, "snmp setup error: $error";
        return undef;
        }
    $session->translate( [ '-octetstring' => 0, '-timeticks' => 0 ] );
    #$session->translate( '-all' => 0 );
    #$session->translate( '-unsigned' => 1 );

    $verbose > 1 && print "snmp session default maxmsgsize = ",
	$session->max_msg_size(), "\n";

    $snmp_sessions{ "$host,$community" } = $session;
    return $session;
    }








sub ticks_to_str {
    my( $ticks ) = @_;
    my( @intervals, @letters, $interval, $str, $i, $n, $started );

    if ( $ticks < 100 ) { 
	return "0s";  
	}

    @intervals = (
	60480000,
	8640000,
	360000,
	6000,
	100,
	);

    @letters = (
	'w ',
	'd ',
	'h ',
	'm ',
	's',
	);

    $str = '';
    for ( $i = 0; $i < 5; $i++ ) {
	$interval = $intervals[ $i ];
	if ( $ticks >= $interval || $started ) {
	    $n = int( $ticks / $interval );
	    $ticks -= $n * $interval;
	    $str .= sprintf( "%u%s", $n, $letters[ $i ] );
	    $started = 1;   # show days in 3weeks 0days 3hours
	    }
	}
    return $str;
    }
















