#!/usr/local/bin/perl
#
# Check an ilom equiped sun
# So far, this means x4600s, and sort of x4100s
#
# $Header: /home/doke/work/nagios/RCS/check_ilom,v 1.15 2017/07/12 21:40:23 doke Exp $

#use strict;
#use warnings;
no warnings 'uninitialized';
use Getopt::Long;
use Net::SNMP;
use Time::HiRes qw( usleep gettimeofday );
use Data::Dumper;


my $community = 'public';
my $timeout = 30;   # these sps are slow
my $delay = 500000;    # microseconds


my $host = '';
my $verbose = 0;
my $help = 0;
my $mib2 = '1.3.6.1.2.1';
my $enterprises = '1.3.6.1.4.1';
my $sun = "$enterprises.42";
my $snmp_version = '2c';
my $use_snmp_v1 = 0;
my $use_snmp_v2c = 0;

$ENV{PATH} = "/usr/bin:/bin/:/usr/sbin";

my( @crits, @warns, @unknowns, @oks, @ignores, $sysDescr );

sub usage {
    my( $rc ) = @_;
    print "Usage: $0 [-vh] -H <host> [-C <community>]
    -H s  hostname
    -C s  snmp community [$community]
    -t n  snmp timeout in seconds [$timeout]
    -1    use snmp v1
    -2    use snmp v2c [default]
    -v    verbose
    -h    help
";
    exit $rc;
    }

Getopt::Long::Configure ("bundling");
GetOptions(
    'H=s' => \$host,
    'C=s' => \$community,
    't=i' => \$timeout,
    '1' => \$use_snmp_v1,
    '2' => \$use_snmp_v2c,
    'v+' => \$verbose,
    'h' => \$help,
    );
&usage( 0 ) if ( $help );

&usage( 0 ) if ( ! $host );

if ( $use_snmp_v2c ) { 
    $snmp_version = 'snmpv2c'; 
    }
elsif ( $use_snmp_v1 ) { 
    $snmp_version = 'snmpv1'; 
    }

&check_ilom();

my $rc = 0;
my $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 = -1 if ( $rc == 0 );
    print $sep, "Unknown ", join( ", ", @unknowns );
    }
if ( $rc == 0 ) {
    print "Ok ", join( ", ", @oks );
    $sep = '; ';
    }
if ( $#ignores >= 0 ) {
    print $sep, "Ignoring ", join( ", ", @ignores );
    }
print "\n";
exit $rc;


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


sub check_ilom {
    my( $session, $error, $result, @oids, $row, $model,
	$cmd, $pingout, $fwrev, $sysName,
	$rows, $oid, $alarms, $alarm, $val, %entities, $alias,
	$memory, $ncpus, $scale, $msg, $sn, $val2, $entName, $entDescr,
	$mac, @rate_units, @base_units, $minrow, $maxrow );

    # open the snmp session with the service processor 
    $verbose && print "opening snmp session to $host\n";
    ( $session, $error ) = Net::SNMP->session(
        -version => $snmp_version,    # caution, ilom can get overloaded by 2c
        -hostname => $host,
        -community => $community,
        -timeout => $timeout,
        #-debug => 0x02
        );
    if ( ! defined( $session ) ) {
	$verbose && print "snmp setup error: $error\n";
        push @crits, "snmp setup error: $error";
        return;
        }
    $session->translate( [ '-octetstring' => 0 ] );
    #$session->translate( '-all' => 0 );
    #$session->translate( '-unsigned' => 1 );



    # get the sysDescr and sysName to verify it's an ilom
    my $sysDescr_oid = "$mib2.1.1.0";
    my $sysName_oid = "$mib2.1.5.0";
    @oids = ( $sysDescr_oid, $sysName_oid );
    $verbose && print "snmp_get sysDescr and sysName\n";
    $result = $session->get_request( -varbindlist => \@oids );
    if ( ! defined( $result ) ) { 
	$verbose && print "snmp error ", $session->error(), "\n";
	push @crits, "couldn't get sysDescr " . $session->error();

	# we can't snmp to it, let's see if we can ping it
	if ( -x "/usr/sbin/sun" ) { 
	    # Solaris 
	    $cmd = "/usr/sbin/ping $host 1 2>&1";
	    }
	else { 
	    # Linux
	    $cmd = "ping -c 2 -W 2 $host>&1";
	    }
	$verbose && print "+ $cmd\n";
	if ( open( pH, '-|', $cmd ) ) { 
	    $pingout = '';
	    while ( <pH> ) {
		$verbose && print ">$_";
		chomp;
		$pingout .= $_ . ', ';
		}
	    close pH;
	    $rc = $? >> 8;
	    if ( $rc ) {
		$pingout =~ s/, $//;
		push @crits, "ping failed: $pingout";
		}
	    else {
		push @crits, "ping worked, snmp may not be configured\n";
		}
	    }
	else {
	    push @crits, "unable to run ping: $!\n";
	    }
	return;
	}
    $sysDescr = $result->{ $sysDescr_oid };
    $sysName = $result->{ $sysName_oid };

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


    usleep( $delay );
    $fwrev = snmp_get_one( $session, 'ilomCtrlFirmwareMgmtVersion', "$sun.2.175.102.6.1.0" );
    if ( ! defined $fwrev || ! $fwrev || $fwrev eq '--' ) { 
	$fwrev = guess_firmware();
	}

    if ( ! $fwrev && $sysDescr !~ m/Linux (SUNSP|Virtual).*(ppc|armv5tejl)/i ) {
	push @unknowns, "device doesn't look like an ilom, no firmware revision, and sysDescr $sysDescr $sysName";
        return 0;
        }

    if ( $fwrev ) { 
	$verbose && print "fwrev $fwrev\n";
	push @oks, "ilom firmware $fwrev";
	}

    # looks like an ilom, good


    # sunPlatEquipmentAlarmStatus
    # critical(1),
    # major(2),
    # minor(3),
    # indeterminate(4),
    # warning(5),
    # pending(6),
    # cleared(7)
    #
    my @alarm_levels = ( '', 'critical', 'major', 'minor', 'indeterminate',
	'warning', 'pending', 'cleared' );
    usleep( $delay );
    $result = snmp_walk( $session, 'sunPlatEquipmentAlarmStatus', "$sun.2.70.101.1.1.2.1.3" );
    $alarms = $rows = 0;
    $minrow = 99999;
    $maxrow = 0;
    foreach $oid ( keys %$result ) {
	$rows++;
	if ( $oid =~ m/.*\.(\d+)$/ ) {
	    $row = $1;
	    $alarm = $result->{ $oid };
	    $entities{ sunPlatEquipmentAlarmStatus }[ $row ] = $alarm;
	    if ( $alarm == 1 || $alarm == 2 || $alarm == 3 || $alarm == 5 ) {
		$alarms ++;
		}
	    if ( $row < $minrow ) { 
		$minrow = $row;
		}
	    elsif ( $row > $maxrow ) { 
		$maxrow = $row;
		}
	    }
	}
    if ( $rows == 0 ) { 
	$minrow = 0;
	}
    elsif ( $rows == 1 ) { 
	$maxrow = $minrow;
	}
    $verbose && print "sunPlatEquipmentAlarmStatus column has $rows rows, $minrow - $maxrow, $alarms alarms\n";

    if ( ! $rows ) { 
	push @unknowns, "no hardware info in mib";
	return 0;
	}

    if ( $alarms )  {
	# There were some alarms, 
	# We don't try to understand them, we just print them out.
	# This means pulling some additional data first.

	foreach $row ( 1 .. $#{@{$entities{ sunPlatEquipmentAlarmStatus }}} ) {
	    $alarm = $entities{ sunPlatEquipmentAlarmStatus }[ $row ];
	    next if ( ! defined $alarm );
	    if ( $alarm == 1 || $alarm == 2 || $alarm == 3 || $alarm == 5 ) {

		# have to get all of these seperately, because some of them may not exist.

		# not all iloms have entPhysicalDescr, even when they're the same version.  Huh?
		$val = snmp_get_one( $session, 'entPhysicalDescr', "$mib2.47.1.1.1.1.2.$row" );
		$val =~ tr/\040-\176//cd;
		$entities{ entPhysicalDescr }[ $row ] = $val;

		$val = snmp_get_one( $session, 'entPhysicalClass', "$mib2.47.1.1.1.1.5.$row" );
		$entities{ entPhysicalClass }[ $row ] = $val;

		$val = snmp_get_one( $session, 'entPhysicalName', "$mib2.47.1.1.1.1.7.$row" );
		$entities{ entPhysicalName }[ $row ] = $val;

		$val = snmp_get_one( $session, 'sunPlatNumericSensorCurrent', "$sun.2.70.101.1.1.8.1.4.$row" );
		if ( defined $val )  { 
		    $entities{ sunPlatNumericSensorCurrent }[ $row ] = $val;

		    # sunPlatNumericSensorBaseUnits
		    # other(1),
		    # unknown(2),
		    # degC(3),
		    # degF(4),
		    # degK(5),
		    # volts(6),
		    # amps(7),
		    # watts(8),
		    # joules(9),
		    # coulombs(10),
		    # va(11),
		    # nits(12),
		    # lumens(13),
		    # lux(14),
		    # candelas(15),
		    # kPa(16),
		    # psi(17),
		    # newtons(18),
		    # cfm(19),
		    # rpm(20),
		    # hertz(21),
		    # seconds(22),
		    # minutes(23),
		    # hours(24),
		    # days(25),
		    # weeks(26),
		    # mils(27),
		    # inches(28),
		    # feet(29),
		    # cubicInches(30),
		    # cubicFeet(31),
		    # meters(32),
		    # cubicCentimeters(33),
		    # cubicMeters(34),
		    # liters(35),
		    # fluidOunces(36),
		    # radians(37),
		    # steradians(38),
		    # revolutions(39),
		    # cycles(40),
		    # gravities(41),
		    # ounces(42),
		    # pounds(43),
		    # footPounds(44),
		    # ounceInches(45),
		    # gauss(46),
		    # gilberts(47),
		    # henries(48),
		    # farads(49),
		    # ohms(50),
		    # siemens(51),
		    # moles(52),
		    # becquerels(53),
		    # ppm(54),
		    # decibels(55),
		    # dBA(56),
		    # dbC(57),
		    # grays(58),
		    # sieverts(59),
		    # colorTemperatureDegK(60),
		    # bits(61),
		    # bytes(62),
		    # words(63),
		    # doubleWords(64),
		    # quadWords(65),
		    # percentage(66)
		    # 
		    @base_units = ( 'other', 'unknown', 'degC', 'degF', 'degK',
			'volts', 'amps', 'watts', 'joules', 'coulombs', 'va', 'nits',
			'lumens', 'lux', 'candelas', 'kPa', 'psi', 'newtons', 'cfm', 'rpm',
			'hertz', 'seconds', 'minutes', 'hours', 'days', 'weeks', 'mils',
			'inches', 'feet', 'cubicInches', 'cubicFeet', 'meters',
			'cubicCentimeters', 'cubicMeters', 'liters', 'fluidOunces',
			'radians', 'steradians', 'revolutions', 'cycles', 'gravities',
			'ounces', 'pounds', 'footPounds', 'ounceInches', 'gauss',
			'gilberts', 'henries', 'farads', 'ohms', 'siemens', 'moles',
			'becquerels', 'ppm', 'decibels', 'dBA', 'dbC', 'grays', 'sieverts',
			'colorTemperatureDegK', 'bits', 'bytes', 'words', 'doubleWords',
			'quadWords', 'percentage' );

		    $val = snmp_get_one( $session, 'sunPlatNumericSensorBaseUnits', "$sun.2.70.101.1.1.8.1.1.$row" );
		    $entities{ sunPlatNumericSensorBaseUnits }[ $row ] = $val;

		    $val = snmp_get_one( $session, 'sunPlatNumericSensorExponent', "$sun.2.70.101.1.1.8.1.2.$row" );
		    $entities{ sunPlatNumericSensorExponent }[ $row ] = $val;


		    # sunPlatNumericSensorRateUnits
		    # none(1),
		    # perMicroSecond(2),
		    # perMilliSecond(3),
		    # perSecond(4),
		    # perMinute(5),
		    # perHour(6),
		    # perDay(7),
		    # perWeek(8),
		    # perMonth(9),
		    # perYear(10)
		    #
		    @rate_units = ( '', '', 'usec', 'msec', 'sec', 'min', 'hour', 'day', 'week', 'month', 'year' );
		    $val = snmp_get_one( $session, 'sunPlatNumericSensorRateUnits', "$sun.2.70.101.1.1.8.1.3.$row" );
		    $entities{ sunPlatNumericSensorRateUnits }[ $row ] = $val;

		    }


		$msg =  sprintf( "%s %s in %s alarm",
		    $entities{ entPhysicalDescr }[ $row ],
		    $entities{ entPhysicalName }[ $row ],
		    $alarm_levels[ $alarm ]
		    );
		if ( defined $entities{ sunPlatNumericSensorCurrent }[ $row ] ) {
		    $scale = 10 ** $entities{ sunPlatNumericSensorExponent }[ $row ];
		    $verbose && print "scale $scale\n";
		    $msg .= sprintf( " curval %0.2f %s",
			$entities{ sunPlatNumericSensorCurrent }[ $row ] * $scale,
			$base_units[ $entities{ sunPlatNumericBaseUnits }[ $row ] ]
			);
		    if ( defined $entities{ sunPlatNumericRateUnits }[ $row ] 
			    && $rate_units[ $entities{ sunPlatNumericRateUnits }[ $row ] ] ) { 
			$msg .= "/" . $rate_units[ $entities{ sunPlatNumericRateUnits }[ $row ] ];
			}
		    }
		$verbose && print "msg $msg\n";

		if ( $alarm == 1 || $alarm == 2 ) {
		    push @crits, $msg;
		    }
		elsif ( $alarm == 3 || $alarm == 5 ) {
		    # wacko false alarms
		    if ( $entities{ entPhysicalDescr }[ $row ] 
				eq 'Presence Sensor'
			    && $entities{ entPhysicalName }[ $row ] 
				=~ m!(?:/SYS/SASBP/HDD\d+/PRSNT|/SYS/MB/P\d+/PRSNT|/SYS/PS\d+/PRSNT)!i ) { 
			push @ignores, $msg;
			}
		    else { 
			push @warns, $msg;
			}
		    }
		}
	    }
	}

    else {
	# no alarms

	# Pull enough data to describe the device well enough, so we can be
	# sure we're monitoring the right thing.

	# retarded ilom sometimes won't let us walk the entPhysicalDescr
	# column, nor any other single column in that table

	foreach $row ( 1 .. $#{@{$entities{ sunPlatEquipmentAlarmStatus }}} ) {

	    # not all iloms have entPhysicalDescr, even when they're the same version.  Huh?
	    $val = snmp_get_one( $session, 'entPhysicalDescr', "$mib2.47.1.1.1.1.2.$row" );
	    $val =~ tr/\040-\176//cd;
	    $entities{ entPhysicalDescr }[ $row ] = $val;

	    $val = snmp_get_one( $session, 'entPhysicalClass', "$mib2.47.1.1.1.1.5.$row" );
	    $entities{ entPhysicalClass }[ $row ] = $val;

	    $val = snmp_get_one( $session, 'entPhysicalName', "$mib2.47.1.1.1.1.7.$row" );
	    $entities{ entPhysicalName }[ $row ] = $val;

	    # This is weird.  Sometimes there isn't an entry in entPhysicalName, but there is 
	    # in sunPlatEquipmentLocationName.
	    $val = snmp_get_one( $session, 'sunPlatEquipmentLocationName', "$sun.2.70.101.1.1.2.1.5" );
	    $entities{ sunPlatEquipmentLocationName }[ $row ] = $val;
	    if ( ! defined $entities{ entPhysicalName }[ $row ] ) { 
		$entities{ entPhysicalName }[ $row ] = $val;
		}
	    elsif ( $val ne $entities{ entPhysicalName }[ $row ] ) { 
		# they don't match?
		# this happens a lot on X4100 its-a-trap 
		# FIXME: now what?
		}

	    $val = snmp_get_one( $session, 'entPhysicalAlias', "$mib2.47.1.1.1.1.14" );
	    $entities{ entPhysicalAlias }[ $row ] = $val;

	    }
	


	$memory = 0;
	$ncpus = 0;
	foreach $row ( 1 .. $#{@{$entities{ entPhysicalName }}} ) {
	    $entName = $entities{ entPhysicalName }[ $row ] || '';
	    $entDescr = $entities{ entPhysicalDescr }[ $row ] || '';

	    $verbose > 1 && printf "row %d, %s %s\n", $row, $entDescr, $entName; 

	    # motherboard
	    if ( $entName eq '/SYS/MB' || $entName eq '/SYS/MB/MOTHERBOARD' ) {
		$alias = $entities{ entPhysicalAlias }[ $row ]; 
		$verbose && printf "row %d, %s %s %s\n", $row, $entDescr, $entName, $alias; 

		$fwrev = snmp_get_one( $session, 
		    "entPhysicalFirmwareRev.$row",
		    "$mib2.47.1.1.1.1.9.$row" );
		if ( defined $fwrev && $fwrev && $fwrev ne '--' ) {
		    $verbose && print "mb fwrev $fwrev\n";
		    push @oks, "mb firmware $fwrev";
		    }

		# motherboard serial number
		$sn = snmp_get_one( $session, "entPhysicalSerialNum.$row", 
		    "$mib2.47.1.1.1.1.11.$row" );
		if ( defined $sn && $sn && $sn ne '--' ) {
		    $verbose && print "sn $sn\n";
		    push @oks, "mb sn $sn";
		    }

		if ( $alias =~ m/ASSY,MOTHERBOARD,X4600,REV F/ ) {
		    $model = "x4600m2";   # is this always true?
		    $verbose && print "model $model\n";
		    }
		}

	    elsif ( $entDescr  eq 'Blade FRU' ) {
		$alias = $entities{ entPhysicalAlias }[ $row ]; 
		$verbose && printf "row %d, %s %s %s\n", $row, $entDescr, $entName, $alias; 

		if ( $alias =~ m/ASSY,CPU BD,8 DIMM,X4600 M2/i ) {
		    $model = "x4600m2";
		    $verbose && print "model $model\n";
		    }
		}

	    elsif ( $entDescr  eq 'DIMM' ) {
		$alias = $entities{ entPhysicalAlias }[ $row ]; 
		$verbose && printf "row %d, %s %s %s\n", $row, $entDescr, $entName, $alias; 
		# 2048MB DDR-II 666 (PC2 5300) ADDRESS/COMMAND PARITY/ECC
		if ( $alias =~ m/(\d+)MB DDR/i ) {
		    $memory += $1;
		    $verbose && print "memory $memory\n";
		    }
		}

	    elsif ( $entName =~ m!^/SYS/P(\d+)$! || $entName =~ m!^/SYS/MB/P(\d+)/CPU$! ) {
		$alias = $entities{ entPhysicalAlias }[ $row ]; 
		$verbose && printf "row %d, %s %s %s\n", $row, $entDescr, $entName, $alias; 

		# DUAL-CORE AMD OPTERON(TM) PROCESSOR 8220

		$ncpus ++;
		$verbose && print "ncpus $ncpus\n";
		}

	    elsif ( $entDescr eq 'Network Interface' ) {
		$alias = $entities{ entPhysicalAlias }[ $row ]; 
		$verbose && printf "row %d, %s %s %s\n", $row, $entDescr, $entName, $alias; 

		$mac = lc snmp_get_one( $session, "entPhysicalSerialNum.$row", 
		    "$mib2.47.1.1.1.1.11.$row" );
		$verbose && print "mac $mac\n";
		if ( defined $mac && $mac =~ m/^(?:[\da-f]{2}:){5}[\da-f]{2}$/i ) {
		    if ( $entName =~ m/NET(\d+)$/i ) { 
			push @oks, "mac$1 $mac";
			}
		    else { 
			push @oks, "mac $mac";
			}
		    }
		}
	    }

	if ( $model ) {
	    push @oks, "model $model";
	    }
	if ( $memory ) {
	    push @oks, sprintf( "memory %d GB", $memory / 1024 );
	    }
	if ( $ncpus ) {
	    push @oks, "ncpus $ncpus";
	    }
	if ( ! defined $fwrev || $fwrev eq '--' ) { 
	    $fwrev = guess_firmware();
	    if ( $fwrev ) { 
		$verbose && print "fwrev $fwrev\n";
		push @oks, "ilom firmware $fwrev";
		}
	    }
	}
    }



sub guess_firmware { 
    my( $fwrev );

    if ( $sysDescr =~ m/Tue Sep 13 12:42:45 PDT 2005/ ) { 
	$fwrev = "1.0"; 
	}
    elsif ( $sysDescr =~ m/Sat Apr 21 11:36:10 PDT 2007/ ) { 
	$fwrev = "1.1.8"; 
	}
    elsif ( $sysDescr =~ m/Thu Sep 6 16:58:31 PDT 2007/ ) { 
	$fwrev = "2.0.2.1"; 
	}
    else { 
	$fwrev = 'unknown';
	}
    return $fwrev;
    }






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

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

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

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




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

    $verbose && print "walking $name\n";
    usleep( $delay );
    $result = $session->get_table( -baseoid => $baseoid );
    #print "session error ", $session->error(), "\n";
    if ( ! defined( $result )
            && $session->error() !~ m/Requested table is empty/ ) {
        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;
    }



sub walk_table {
    my( $session, $name, $baseoid ) = @_;
    my( $result, $rows, $oid, $val, $col, $row, @data );

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

    $rows = 0;
    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[$col][$row] = $val;
	    $rows = $row if ( $row > $rows );
	    }
	}

    if ( ! $rows ) {
        push @unknowns, sprintf "no rows in $name table on %s: %s",
            $session->hostname, $session->error();
	return undef;
        }

    return @data;
    }



