#!/usr/bin/perl -w
#
# check_3com_link -  Check 3Com switch and stack crosslinks
# nagios: -epn
#
# Copyright (C) 2009 Jason Abraham <jtabraham@gmail.com>
#
# WCIT
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

use strict;
use warnings;
use Nagios::Plugin;
use File::Basename;
use Net::SNMP;

#Constants
use constant WCIT_Version       => "0.8";
use constant WCIT_WarnFlag      => 1;
use constant WCIT_CritFlag      => 2;
use constant WCIT_DefTCPTimeout => 5;
#Flag Vals
use constant WCIT_TestUnit        => 1;
use constant WCIT_TestFabric      => 2;
use constant WCIT_TestAggLink    => 4;
use constant WCIT_TestAggPort     => 8;
use constant WCIT_ValAggLinkAll  => 16;
use constant WCIT_ValAggPortAll   => 32;
# Huawei-3Com OIDs
use constant WCIT_OID_h3cFtmUnitID                => ".1.3.6.1.4.1.43.45.1.10.2.1.1.1.1.1.2"; #Table .UnitBy165+2
use constant WCIT_OID_h3cFtmFabricType            => ".1.3.6.1.4.1.43.45.1.10.2.1.1.1.5.0";
use constant WCIT_OID_hwAggLinkState              => ".1.3.6.1.4.1.43.45.1.5.25.25.1.1.1.5"; #Table .<linknum>
use constant WCIT_OID_hwAggLinkPortList           => ".1.3.6.1.4.1.43.45.1.5.25.25.1.1.1.4"; #Table .<linknum>
use constant WCIT_OID_hwAggPortListSelectedPorts  => ".1.3.6.1.4.1.43.45.1.5.25.25.1.1.1.6"; #Table .<linknum>

use constant WCIT_h3cAggLinkStateActive => 1;

#evil globals but Perl loves them
use vars qw($sess $np $status $msg $flags);
#really evil globals Save on funciton passing
use vars qw($perfLabel $perfUOM $perfWarn $perfCrit $perfMin $perfMax);

#INIT
$status = 0;
$msg = undef;
$flags = 0;

$np = Nagios::Plugin->new(
  usage => "Usage: %s -H <host> [ -C <community> ] 
  [ -n <num units> ] [ -s <unit\#> ] [ --uwarn ] [ -f <level> ] [ -F <level> ]
  [ -a <agglink list> ] [ --awarn ] [ -p <agglink list> ] [ --pwarn ]
  [ -T |--tcptimeout <secs> ]",
  version => WCIT_Version,
  blurb => "This plugin test various aspects of 3Com switch interconnectivity
  ranging from stacking/fabric to aggregated links on switches which support 
  the Huawei-3Com(H3C) a3com.jv-mib.huawei SNMP standard.", 
  extra => qq{
Fabric Topology Levels
----------------------
1 - Not Stacked / No Fabric
2 - Line
3 - Ring
4 - Mesh

It is important to remember that even if an aggrigated port fails to be 
active because it is offline or in standby, the link may still be active.
Therefore, it is recommended that you use the aggrigated link test in 
combination with the port test and use the --pwarn option.
}
);

# Define and document the valid command line options
# usage, help, version, timeout and verbose are defined by default.
$np->add_arg(
	spec => 'host|H=s',
	help => 
  qq{
 -H, --host=ADDRESS
   Host name or IP Address},
  required => 1
);
$np->add_arg(
	spec => 'community|C=s',
	help => 
  qq{-C, --community=STRING
   SNMP read community string (default: public)
   },
  required => 0,
  default => 'public'
);

$np->add_arg(
	spec => 'num|n=i',
	help => 
qq{-n, --num=INTEGER
   Total number of units that should be in the stack.  (default: not tested)},
  required => 0
);
$np->add_arg(
	spec => 'single|s=i',
	help => 
qq{-s, --single=INTEGER
   Only test for a specific unit in a stack.  Will override -n option. 
   (default: not tested)},
  required => 0
);
$np->add_arg(
  spec => 'uwarn|uwarnz+',
  help =>
  qq{--uwarn
     Issue warning on number of unit(s) test. (default: critical)
   },
  required => 0
); 

$np->add_arg(
	spec => 'fabwarn|f=i',
	help => 
qq{-f, --fabwarn=INTEGER
   Minimun farbric topology level allowed before a warning alarm is issued. 
   See below extra notes for topology levels (default: not tested)},
  required => 0
);
$np->add_arg(
	spec => 'fabcrit|F=i',
	help => 
qq{-F, --fabcrit=INTEGER
   Minimun farbric topology level allowed before a critical alarm is issued. 
   See below extra notes for topology levels (default: not tested)
   },
  required => 0
);

$np->add_arg(
	spec => 'aggitest|a=s',
	help => 
  qq{-a, --aggitest=1,2,5|all
   Check aggrigated links for active status.  Provide a comma seperated list of 
   links to check or specify "all" to check all available links. 
   (default: not tested)},
  required => 0,
);
$np->add_arg(
  spec => 'awarn|awarnz+',
  help =>
  qq{--awarn
     Issue warning on aggrigated link status test. (default: critical)
   },
  required => 0
);

$np->add_arg(
	spec => 'porttest|p=s',
	help => 
  qq{-p, --porttest=1,2,5|all
   Check an aggrigated link's ports status.  This test ensures that all ports 
   are active/selected. Note: a port in standby will result in a failed test.
   See extra notes below for recommendation on how to use this test. You must
   provide a comma seperated list of links to check or specify "all" to 
   check all available links. 
   (default: not tested)},
  required => 0,
);
$np->add_arg(
  spec => 'pwarn|pwarnz+',
  help =>
  qq{--awarn
     Issue warning on aggrigated link port test. (default: critical)
   },
  required => 0
);

$np->add_arg(
	spec => 'tcptimeout|T=i',
	help => 
  qq{-T, --tcptimeout=INTEGER
   Timeout value, in seconds, for SNMP responses (default: }.WCIT_DefTCPTimeout.qq{ secs)
   Not to be confused with general plugin timeout},
  required => 0,
  default => WCIT_DefTCPTimeout
);

##### Parse arguments end do error checking !!!MORE CHECKS NEEDED!!!
$np->getopts;

if( defined($np->opts->num) ) {
  $flags |= WCIT_TestUnit;
  $np->nagios_die("Stack number must greater than 0") if( $np->opts->num < 1 );
}
if( defined($np->opts->single) ) {
  $flags |= WCIT_TestUnit;
  $np->nagios_die("Unit number must greater than 0") if( $np->opts->single < 1 );
}

if( defined($np->opts->fabwarn) ) {
  $flags |= WCIT_TestFabric;
  $np->nagios_die("Invalid value for Fabric warn level (1-4)") if( $np->opts->fabwarn < 1 || $np->opts->fabwarn > 4 );
}
if( defined($np->opts->fabcrit) ) {
  $flags |= WCIT_TestFabric;
  $np->nagios_die("Invalid value for Fabric critical level (1-4)") if( $np->opts->fabcrit < 1 || $np->opts->fabcrit > 4 );
}

if( defined($np->opts->aggitest) ) {
  $flags |= WCIT_TestAggLink;
  if($np->opts->aggitest eq "all") { $flags |= WCIT_ValAggLinkAll; }
  elsif( $np->opts->aggitest !~ m/[0-9]+(,[0-9]+)*/ ){ $np->nagios_die("Invalid value for Aggrigated Link State(-a) test"); }
}
if( defined($np->opts->porttest) ) {
  $flags |= WCIT_TestAggPort;
  if($np->opts->porttest eq "all") { $flags |= WCIT_ValAggPortAll; }
  elsif( $np->opts->aggitest !~ m/[0-9]+(,[0-9]+)*/ ){ $np->nagios_die("Invalid value for Aggrigated Link Port(-p) test"); }
}

$np->nagios_die("No tests enabled/defined") if($flags == 0);

$np->nagios_die("TCP Timeout must greater than 0") if( $np->opts->tcptimeout < 1 );

##############################################################################
# Start actual work

#Create Session
my $err;
($sess,$err) = Net::SNMP->session(Hostname => $np->opts->host,
                                      Community => $np->opts->community,
                                      Version => 1,
                                      Timeout => $np->opts->tcptimeout,
                                      Translate => [
                                          -timeticks => 0x0
                                        ]
                                      );
$np->nagios_exit(CRITICAL,"$err") unless($sess);

# Start Plugin Timeout
alarm $np->opts->timeout;

#Start Tests
testUnit() if( $flags & WCIT_TestUnit );
testFab() if( $flags & WCIT_TestFabric );
testAggLink() if( $flags & WCIT_TestAggLink );
testAggPort() if( $flags & WCIT_TestAggPort );

alarm(0);

$np->nagios_exit(CRITICAL,$msg) if($status & WCIT_CritFlag);
$np->nagios_exit(WARNING,$msg) if($status & WCIT_WarnFlag);
$np->nagios_exit(OK,$msg);

#################################
# Start Test SUBS
sub testUnit {
  my ($resS, %resH, $key);
  $perfLabel = 'unit';
  $perfUOM = '';
  $msg = 'UNIT:';
  %resH = get_snmphash(WCIT_OID_h3cFtmUnitID,'h3cFtmUnitID');
  return do_wrapUp('CRIT(SNMP ERR)',WCIT_CritFlag) unless (%resH);
  if ( defined($np->opts->single) ) {
  #Test for single unit
    $perfWarn   = defined($np->opts->uwarn)?'1:':'';
    $perfCrit   = defined($np->opts->uwarn)?'':'1:';
    $perfMin    = 0;
    $perfMax    = 1;
    my $found = 0;
    foreach $key (keys %resH) {
      return do_wrapUpPerf('OK',0, 1) if( $np->opts->single == $resH{$key} ); 
    }
    return do_wrapUpPerf('WARN',WCIT_WarnFlag, 0) if( defined($np->opts->uwarn) );
    return do_wrapUpPerf('CRIT',WCIT_CritFlag, 0);
  }
  #Otherwise test for num of units
  $perfWarn   = defined($np->opts->uwarn)?$np->opts->num.':':'';
  $perfCrit   = defined($np->opts->uwarn)?'':$np->opts->num.':';
  $perfMin    = 0;
  $perfMax    = '';
  $resS = scalar keys(%resH);
  return do_wrapUpPerf('OK',0, $resS) if( $resS >= $np->opts->num );
  return do_wrapUpPerf('WARN',WCIT_WarnFlag,$resS) if( defined($np->opts->uwarn) );
  return do_wrapUpPerf('CRIT',WCIT_CritFlag,$resS);
}

sub testFab {
  my $fablevel;
  $perfLabel = 'fabric';
  $perfUOM = '';
  $perfWarn   = defined($np->opts->fabwarn)?$np->opts->fabwarn.':':'';
  $perfCrit   = defined($np->opts->fabcrit)?$np->opts->fabcrit.':':'';
  $perfMin    = 1;
  $perfMax    = 4;
  if( defined $msg ) { $msg .= '  FABRIC:'; }
  else { $msg = 'FABRIC:'; }
  $fablevel = get_snmpval(WCIT_OID_h3cFtmFabricType,'h3cFtmFabricType');
  return do_wrapUp('CRIT(SNMP ERR)',WCIT_CritFlag) unless (defined($fablevel));
  return do_wrapUpPerf('CRIT',WCIT_CritFlag,$fablevel) if(defined($np->opts->fabcrit) && $fablevel < $np->opts->fabcrit ); 
  return do_wrapUpPerf('WARN',WCIT_WarnFlag,$fablevel) if(defined($np->opts->fabwarn) && $fablevel < $np->opts->fabwarn );
  return do_wrapUpPerf('OK',0, $fablevel); 
}

sub testAggLink {
  my (%aggList, $downList, $key, $total, $totalBad, $percent);
  $perfLabel = 'agglink';
  $perfUOM = '%';
  $perfWarn   = defined($np->opts->awarn)?'100:':'';
  $perfCrit   = defined($np->opts->awarn)?'':'100:';
  $perfMin    = 0;
  $perfMax    = 100;
  if( defined $msg ) { $msg .= '  AGGLINK:'; }
  else { $msg = 'AGGLINK:'; }
  %aggList = get_snmphash(WCIT_OID_hwAggLinkState,'hwAggLinkState');
  return do_wrapUp('CRIT(SNMP ERR)',WCIT_CritFlag) unless (%aggList);
  $downList = undef;
  $totalBad = 0;
  unless( $flags & WCIT_ValAggLinkAll ) {
    my (@checkList,$alink);
    @checkList = split(/,/,$np->opts->aggitest);
    $total = scalar(@checkList);
    foreach $alink (@checkList) {
      if( !defined($aggList{$alink}) || $aggList{$alink} != WCIT_h3cAggLinkStateActive ) {
        $totalBad++;
        if( defined($downList) ) { $downList .= ",$alink"; }
        else { $downList = "$alink"; }
      }
    }
  } else {
    $total = scalar(keys %aggList);
    foreach $key (keys %aggList) {
      if( $aggList{$key} != WCIT_h3cAggLinkStateActive ) {
        $totalBad++;
        if( defined($downList) ) { $downList .= ",$key"; }
        else { $downList = "$key"; }
      }
    }
  }
  if($total > 0) { $percent = sprintf("%.0f",($total-$totalBad)/$total*100); }
  else { $percent = 0; };
  return do_wrapUpPerf('OK',0,$percent) unless( defined($downList) );
  return do_wrapUpPerf("WARN($downList)",WCIT_WarnFlag,$percent) if( defined($np->opts->awarn) );
  return do_wrapUpPerf("CRIT($downList)",WCIT_CritFlag,$percent); 
}

sub testAggPort {
  my (%aggListPorts, %aggListPortsSel, $downList, $key, $total, $totalBad, $percent);
  $perfLabel = 'aggport';
  $perfUOM = '%';
  $perfWarn   = defined($np->opts->pwarn)?'100:':'';
  $perfCrit   = defined($np->opts->pwarn)?'':'100:';
  $perfMin    = 0;
  $perfMax    = 100;
  if( defined $msg ) { $msg .= '  AGGPORT:'; }
  else { $msg = 'AGGPORT:'; }
  %aggListPorts = get_snmphash(WCIT_OID_hwAggLinkPortList,'hwAggLinkPortList');
  return do_wrapUp('CRIT(SNMP ERR)',WCIT_CritFlag) unless (%aggListPorts);
  %aggListPortsSel = get_snmphash(WCIT_OID_hwAggPortListSelectedPorts,'hwAggPortListSelectedPorts');
  return do_wrapUp('CRIT(SNMP ERR)',WCIT_CritFlag) unless (%aggListPortsSel);
  $downList = undef;
  $totalBad = 0;
  unless( $flags & WCIT_ValAggPortAll ) {
    my (@checkList,$alink);
    @checkList = split(/,/,$np->opts->porttest);
    $total = scalar(@checkList);
    foreach $alink (@checkList) {
      if( !defined($aggListPorts{$alink}) || $aggListPorts{$alink} ne $aggListPortsSel{$alink} ) {
        $totalBad++;
        if( defined($downList) ) { $downList .= ",$alink"; }
        else { $downList = "$alink"; }
      }
    }
  } else {
    $total = scalar(keys %aggListPorts);
    foreach $key (keys %aggListPorts) {
      if( $aggListPorts{$key} ne $aggListPortsSel{$key} ) {
        $totalBad++;
        if( defined($downList) ) { $downList .= ",$key"; }
        else { $downList = "$key"; }
      }
    }
  }
  if($total > 0) { $percent = sprintf("%.0f",($total-$totalBad)/$total*100); }
  else { $percent = 0; };
  return do_wrapUpPerf('OK',0,$percent) unless( defined($downList) );
  return do_wrapUpPerf("WARN($downList)",WCIT_WarnFlag,$percent) if( defined($np->opts->pwarn) );
  return do_wrapUpPerf("CRIT($downList)",WCIT_CritFlag,$percent); 
}

#################################
# Start Support SUBS

sub do_wrapUp {
  my ($txt,$stat) = @_;
  $msg .= $txt;
  $status |= $stat;
  return;
}

sub do_wrapUpPerf {
  my ($txt,$stat,$val) = @_;
  $msg .= $txt;
  $status |= $stat;
  $np->add_perfdata(
    label   => $perfLabel,
    value   => $val,
    uom     => $perfUOM,
    warning => $perfWarn,
    critical=> $perfCrit,
    min     => $perfMin,
    max     => $perfMax
  );
  return;
}

sub get_snmphash {
  my ($oid, $oidtext, $res, %orghash, %newhash, $oidLen, $orgKey, $newKey);
  ($oid, $oidtext) = @_;
  $res = $sess->get_table($oid);
  unless(defined $res) {
    print ">>SNMP Error:".$sess->error."  OID:$oidtext($oid)\n" if($np->opts->verbose);
    return ();
  }
  %orghash = %{$res};
  %newhash = ();
  $oidLen = length($oid)+1; #+1 for the . appeneded
  foreach $orgKey (keys %orghash)
  {
    $newKey = substr("$orgKey",$oidLen);
    $newhash{$newKey} = $orghash{$orgKey};
  }
  return %newhash;
}

sub get_snmpval {
  my ($oid, $oidtext) = @_;
  my $res = $sess->get_request($oid);
  unless(defined $res) {
    print ">>SNMP Error:".$sess->error."  OID:$oidtext($oid)\n" if($np->opts->verbose);
    return undef;
  }
  return $res->{$oid};
}

