#!/usr/bin/perl
#
# check_ipmi_sel -- Nagios plugin to check ipmitool sel list for errors.
#
# Written by Darren Patterson <darrenp1@stanford.edu>
# Modified by Michael Goll <mgoll@stanford.edu>
# Copyright 2011, 2013
#     The Board of Trustees of the Leland Stanford Junior University

use strict;
use warnings;

our $CONFIG = '/etc/nagios-plugins/ipmi_sel.conf';

# Report a configuration error and exit.  We do this via stdout in order to
# satisfy the Nagios plugin output requirements, but also report a more
# conventional error via stderr in case people are calling this outside of
# Nagios.
sub error {
    print "IPMI UNKNOWN - ", join ('', @_), "\n";
    warn "$0: ", join ('', @_), "\n";
    exit 3;
}

open (FILE, $CONFIG) || error ("Could not open config file: $!");
my @contents = grep { ! /^(#|\s+$)/ } <FILE>; # parse out comments in conf
close (FILE);

my @ignore;
my @results;
my (@statusmatch, @statusclear);
my (@alertmatch, @alertclear);

# parse options
foreach (@contents) {
    if (/^ignore\s*=\s*(.*?)\s*$/) {
        push (@ignore, $1);
    } elsif ( /^statusclear\s*=\s*(.)([^\1]*)\1([^\1]*)\1$/ ) {
        push (@statusmatch, $2);
        push (@statusclear, $3);
    } elsif ( /^alertclear\s*=\s*(.)([^\1]*)\1([^\1]*)\1$/ ) {
        push (@alertmatch, $2);
        push (@alertclear, $3);
    }
}

chomp @ignore;

# exit nicely if running in vmware.
# TODO: take param for detecting VM from config?
if ( -x '/usr/sbin/dmidecode' && `/usr/sbin/dmidecode 2>&1` =~ /vmware/i) {
    print "IPMI OK - VM detected so not running ipmitool.\n";
    exit 0;
}

# Run ipmitool.
unless (-x '/usr/bin/ipmitool') {
    error ('/usr/bin/ipmitool not found');
}

my $regex = join ('|', map { "(?:$_)" } @ignore);
my @raw_results = grep { ! /$regex/ } `/usr/bin/ipmitool sel list 2>&1`;

RAW_RESULT: for (my $i=0; $i < @raw_results; $i++) {
    $raw_results[$i] =~ /^[^|]*\|[^|]*\|[^|]*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*(?:\|\s*([^|]*?)\s*)?$/;
    my $what = '';
    my $alert = '';
    my $status = '';
    my $alertclearer = '';
    my $statusclearer = '';
    $what = $1 if ($1);
    $alert = $2 if ($2);
    $status = $3 if ($3);

    # skip both alert and status clears
    foreach (@alertclear) {
        if ( $alert =~ /$_/ ) { next RAW_RESULT }
    }
    foreach (@statusclear) {
        if ( $status =~ /$_/ ) { next RAW_RESULT }
    }

    # lookup the expected alert to clear this alert
    for ( my $i=0; $i<@alertmatch; $i++ ) {
        if ( $alert =~ /$alertmatch[$i]/ ) {
            $alertclearer = $alertclear[$i];
            last;
        }
    }

    # lookup the expected status to clear this alert
    for ( my $i=0; $i<@statusmatch; $i++ ) {
        if ( $status =~ /$statusmatch[$i]/ ) {
            $statusclearer = $statusclear[$i];
            last;
        }
    }

    my @remaining = @raw_results[$i..@raw_results-1];

    # check if there was a subsequent matching clear
    if ( not &check_for_clear($what, $alert, $alertclearer, $statusclearer, \@remaining) ) {
        unshift (@results,$raw_results[$i]);  # nope
    }
}

chomp $results[0] if ($results[0]);
$results[0] =~ s%\|%//%g if (@results >= 1);

if (@results == 0) {
    print "IPMI OK\n";
} elsif (@results == 1) {
    print "IPMI CRITICAL - $results[0]\n";
    exit 2;
} else {
    my $more = scalar (@results) - 1;
    print "IPMI CRITICAL - $results[0] (and $more more)\n";
    exit 2;
}

sub check_for_clear() {
    my $parm_what = shift;
    my $parm_alert = shift;
    my $alertclearer = shift;
    my $statusclearer = shift;
    my ($array) =@_;
    foreach (@$array) {
        /^[^|]*\|[^|]*\|[^|]*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*(?:\|\s*([^|]*?)\s*)?$/;
        my $what = '';
        my $alert = '';
        my $status = '';
        $what = $1 if ($1);
        $alert = $2 if ($2);
        $status = $3 if ($3);
        if ( $parm_what eq $what && (
             ( $parm_alert eq $alert && $statusclearer ne '' && $status =~ /$statusclearer/ ) ||
             ( $alertclearer ne '' && $alert =~ /$alertclearer/ )
         )) {
            return 1
        }
    }
    # if we fall through there was no clear
    return 0
}

__END__

=for stopwords
IPMI Nagios ipmitool remctl sel util Goll REGEXs alertREGEX alertclear
clearREGEX statusREGEX syntaxes 4th 5th

=head1 NAME

check_ipmi_sel - Nagios plugin to check ipmitool sel list for errors

=head1 SYNOPSIS

B<check_ipmi_sel>

=head1 DESCRIPTION

This Nagios plugin calls C<ipmitool sel list> and filters the results
using strings/regexes from the config file at
F</etc/nagios-plugins/ipmi_sel.conf>.  It will attempt to match alerts
with the corresponding clears and drop them by using the alertclear and
status clear options.  It's meant to be called via remctl from Nagios as
part of routine system monitoring.

The configuration file supports lines with three syntaxes:

    ignore = REGEX

where REGEX is taken to be a regular expression and all lines matching
that regular expression are ignored (not treated as errors).

    alertclear = /alertREGEX/clearREGEX/
    alertclear = /Redundancy Lost/Fully Redundant/

where 'clearREGEX' corresponds to the clear for alert 'alertREGEX' in the
4th "|" delimited trimmed field of the ipmitool sel list line.

    statusclear = /statusREGEX/clearREGEX/
    statusclear = /Asserted/Deasserted/

where 'clearREGEX' corresponds to the clear for status 'statusREGEX' in
the 5th "|" delimited trimmed field of the ipmitool sel list line.

The delimiter "/" can be any character not appearing in the REGEXs.

=head1 EXIT STATUS

B<check_ipmi_sel> follows the standard Nagios exit status requirements.
This means that it will exit with status 0 if there are no problems or
with status 2 if there are errors in IPMI.  For other errors, such as
invalid syntax, B<check_ipmi_sel> will exit with status 3.

=head1 BUGS

The standard Nagios plugin command-line options are not supported.

=head1 CAVEATS

This script does not use the Nagios util library or any of the defaults
that it provides, which makes it somewhat deficient as a Nagios plugin.

=head1 AUTHORS

Darren Patterson <darrenp1@stanford.edu>
Michael Goll <mgoll@stanford.edu>

=head1 SEE ALSO

ipmitool(1)

=cut
