#!/usr/bin/perl -w
# Check SMART status of ATA/SCSI disks, returning any usable metrics as perfdata.
# For usage information, run ./check_smart -h
#
# This is a modified version of the original check_smart in Public
# Domain, as released by Kurt Yoder at Feb 3, 2009.
#
# License:
#  (c) 2010,2012,2014 Instituto Superior Tecnico
#
#  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.
#
#  On Debian GNU/Linux systems, the complete text of the GNU General Public
#  License can be found in ‘/usr/share/common-licenses/GPL-2’.
#
#
# 


# This script was created under contract for the US Government and is therefore Public Domain
#
# Changes and Modifications
# =========================
# Feb 3, 2009: Kurt Yoder - initial version of script
# Jul 26, 2010: Jose Calhariz - modifications for 3ware and cciss hardware
# Oct 31, 2012: Jose Calhariz - modifications sat hardware
# Feb 26, 2014: Jose Calhariz - modifications to read Health Status from ata and scsi disks
# Feb 12, 2020: Jose Calhariz - modifications to read up to 70 megaraid devices

use strict;
use Getopt::Long;

use File::Basename qw(basename);
my $basename = basename($0);

my $revision = '$Revision: 1.0 $';

use lib '/usr/lib/nagios/plugins/';
use utils qw(%ERRORS &print_revision &support &usage);

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

use vars qw($opt_d $opt_debug $opt_h $opt_i $opt_v);
Getopt::Long::Configure('bundling');
GetOptions(
	                  "debug"       => \$opt_debug,
	"d=s" => \$opt_d, "device=s"    => \$opt_d,
	"h"   => \$opt_h, "help"        => \$opt_h,
	"i=s" => \$opt_i, "interface=s" => \$opt_i,
	"v"   => \$opt_v, "version"     => \$opt_v,
);

if ($opt_v) {
	print_revision($basename,$revision);
	exit $ERRORS{'OK'};
}

if ($opt_h) {
	print_help(); 
	exit $ERRORS{'OK'};
}

my ($device, $interface) = qw//;
if ($opt_d) {
	unless($opt_i){
		print "must specify an interface for $opt_d using -i/--interface!\n\n";
		print_help();
		exit $ERRORS{'UNKNOWN'};
	}

	if(grep {$opt_i eq $_} ('ata', 'sat', 'scsi')){

	        if (-b $opt_d){
		        $device = $opt_d;
			$interface = $opt_i;

		}
		else{
		        print "$opt_d is not a valid block device!\n\n";
		        print_help();
			exit $ERRORS{'UNKNOWN'};
		}
	}
	elsif (grep {$opt_i eq $_} ('cciss,0', 'cciss,1', 'cciss,2', 'cciss,3', 'cciss,4', 'cciss,5', 'cciss,6', 'cciss,7', 'cciss,8', 'cciss,9', 'cciss,10', 'cciss,11', 'cciss,12', 'cciss,13', 'cciss,14', 'cciss,15', 'cciss,16')){
	    	        if (-b $opt_d){
		        $device = $opt_d;
			$interface = $opt_i;

		}
		else{
		        print "$opt_d is not a valid block device!\n\n";
		        print_help();
			exit $ERRORS{'UNKNOWN'};
		}
	}
	elsif (grep {$opt_i eq $_} ('megaraid,0', 'megaraid,1', 'megaraid,2', 'megaraid,3', 'megaraid,4', 'megaraid,5', 'megaraid,6', 'megaraid,7', 'megaraid,8', 'megaraid,9', 'megaraid,10', 'megaraid,11', 'megaraid,12', 'megaraid,13', 'megaraid,14', 'megaraid,15', 'megaraid,16', 'megaraid,17', 'megaraid,18', 'megaraid,19', 'megaraid,20', 'megaraid,21', 'megaraid,22', 'megaraid,23', 'megaraid,24', 'megaraid,25', 'megaraid,26', 'megaraid,27', 'megaraid,28', 'megaraid,29', 'megaraid,30', 'megaraid,31', 'megaraid,32', 'megaraid,33', 'megaraid,34', 'megaraid,35', 'megaraid,36', 'megaraid,37', 'megaraid,38', 'megaraid,39', 'megaraid,40', 'megaraid,41', 'megaraid,42', 'megaraid,43', 'megaraid,44', 'megaraid,45', 'megaraid,46', 'megaraid,47', 'megaraid,48', 'megaraid,49', 'megaraid,50', 'megaraid,51', 'megaraid,52', 'megaraid,53', 'megaraid,54', 'megaraid,55', 'megaraid,56', 'megaraid,57', 'megaraid,58', 'megaraid,59', 'megaraid,60', 'megaraid,61', 'megaraid,62', 'megaraid,63', 'megaraid,64', 'megaraid,65', 'megaraid,66', 'megaraid,67', 'megaraid,68', 'megaraid,69', 'megaraid,70' )){
	    	        if (-b $opt_d){
		        $device = $opt_d;
			$interface = $opt_i;

		}
		else{
		        print "$opt_d is not a valid block device!\n\n";
		        print_help();
			exit $ERRORS{'UNKNOWN'};
		}
	}
	elsif(grep {$opt_i eq $_} ('3ware,0' , '3ware,1', '3ware,2', '3ware,3', '3ware,4', '3ware,5', '3ware,6', '3ware,7', '3ware,8', '3ware,9', '3ware,10', '3ware,11', '3ware,12', '3ware,13', '3ware,14', '3ware,15', '3ware,16')){
	        if (-c $opt_d){
		        $device = $opt_d;
			$interface = $opt_i;
		}
		else{
		        print "$opt_d is not a valid char device!\n\n";
		        print_help();
			exit $ERRORS{'UNKNOWN'};
		}
	}
	else{
	        print "invalid interface $opt_i for $opt_d!\n\n";
		print_help();
		exit $ERRORS{'UNKNOWN'};
	}
}
else{
	print "must specify a device!\n\n";
	print_help();
	exit $ERRORS{'UNKNOWN'};
}

my $smart_command = '/usr/bin/sudo /usr/sbin/smartctl';
my @error_messages = qw//;
my $exit_status = 'OK';


warn "###########################################################\n" if $opt_debug;
warn "(debug) CHECK 1: getting overall SMART health status\n" if $opt_debug;
warn "###########################################################\n\n\n" if $opt_debug;

my $full_command = "$smart_command -d $interface -H $device";
warn "(debug) executing:\n$full_command\n\n" if $opt_debug;

my @output = `$full_command`;
warn "(debug) output:\n@output\n\n" if $opt_debug;

# parse ata output, looking for "health status: passed"
my $found_status = 0;
my $line_str_ata = 'SMART overall-health self-assessment test result: '; # ATA SMART line
my $ok_str_ata = 'PASSED'; # ATA SMART OK string
my $line_str_scsi = 'SMART Health Status: '; # SCSI SMART line
my $ok_str_scsi = 'OK'; #SCSI SMART OK string

foreach my $line (@output){
	if($line =~ /$line_str_ata(.+)/){
		$found_status = 1;
		warn "(debug) parsing line:\n$line\n\n" if $opt_debug;
		if ($1 eq $ok_str_ata) {
			warn "(debug) found string '$ok_str_ata'; status OK\n\n" if $opt_debug;
		}
		else {
			warn "(debug) no '$ok_str_ata' status; failing\n\n" if $opt_debug;
			push(@error_messages, "Health status: $1");
			escalate_status('CRITICAL');
		}
	}
        elsif ($line =~ /$line_str_scsi(.+)/){
                $found_status = 1;
                warn "(debug) parsing line:\n$line\n\n" if $opt_debug;
                if ($1 eq $ok_str_scsi) {
			warn "(debug) found string '$ok_str_scsi'; status OK\n\n" if $opt_debug;
                }
                else {
                        warn "(debug) no '$ok_str_scsi' status; failing\n\n" if $opt_debug;
                        push(@error_messages, "Health status: $1");
                        escalate_status('CRITICAL');
                }
        }

}

unless ($found_status) {
	push(@error_messages, 'No health status line found');
	escalate_status('UNKNOWN');
}


warn "###########################################################\n" if $opt_debug;
warn "(debug) CHECK 2: getting silent SMART health check\n" if $opt_debug;
warn "###########################################################\n\n\n" if $opt_debug;

$full_command = "$smart_command -d $interface -q silent -A $device";
warn "(debug) executing:\n$full_command\n\n" if $opt_debug;

system($full_command);
my $return_code = $?;
warn "(debug) exit code:\n$return_code\n\n" if $opt_debug;

if ($return_code & 0x01) {
	push(@error_messages, 'Commandline parse failure');
	escalate_status('UNKNOWN');
}
if ($return_code & 0x02) {
	push(@error_messages, 'Device could not be opened');
	escalate_status('UNKNOWN');
}
if ($return_code & 0x04) {
	push(@error_messages, 'Checksum failure');
	escalate_status('WARNING');
}
if ($return_code & 0x08) {
	push(@error_messages, 'Disk is failing');
	escalate_status('CRITICAL');
}
if ($return_code & 0x10) {
	push(@error_messages, 'Disk is in prefail');
	escalate_status('WARNING');
}
if ($return_code & 0x20) {
	push(@error_messages, 'Disk may be close to failure');
	escalate_status('WARNING');
}
if ($return_code & 0x40) {
	push(@error_messages, 'Error log contains errors');
	escalate_status('WARNING');
}
if ($return_code & 0x80) {
	push(@error_messages, 'Self-test log contains errors');
	escalate_status('WARNING');
}
if ($return_code && !$exit_status) {
	push(@error_messages, 'Unknown return code');
	escalate_status('CRITICAL');
}

if ($return_code) {
	warn "(debug) non-zero exit code, generating error condition\n\n" if $opt_debug;
}
else {
	warn "(debug) zero exit code, status OK\n\n" if $opt_debug;
}


warn "###########################################################\n" if $opt_debug;
warn "(debug) CHECK 3: getting detailed statistics\n" if $opt_debug;
warn "(debug) information contains a few more potential trouble spots\n" if $opt_debug;
warn "(debug) plus, we can also use the information for perfdata/graphing\n" if $opt_debug;
warn "###########################################################\n\n\n" if $opt_debug;

$full_command = "$smart_command -d $interface -A $device";
warn "(debug) executing:\n$full_command\n\n" if $opt_debug;
@output = `$full_command`;
warn "(debug) output:\n@output\n\n" if $opt_debug;
my @perfdata = qw//;

# separate metric-gathering and output analysis for ATA vs SCSI SMART output
if ($interface eq 'ata'){
	foreach my $line(@output){
		# get lines that look like this:
		#    9 Power_On_Minutes        0x0032   241   241   000    Old_age   Always       -       113h+12m
		next unless $line =~ /^\s*\d+\s(\S+)\s+(?:\S+\s+){6}(\S+)\s+(\d+)/;
		my ($attribute_name, $when_failed, $raw_value) = ($1, $2, $3);
		if ($when_failed ne '-'){
			push(@error_messages, "Attribute $attribute_name failed at $when_failed");
			escalate_status('WARNING');
			warn "(debug) parsed SMART attribute $attribute_name with error condition:\n$when_failed\n\n" if $opt_debug;
		}
		# some attributes produce questionable data; no need to graph them
		if (grep {$_ eq $attribute_name} ('Unknown_Attribute', 'Power_On_Minutes') ){
			next;
		}
		push (@perfdata, "$attribute_name=$raw_value");

		# do some manual checks
		if ( ($attribute_name eq 'Current_Pending_Sector') && $raw_value ) {
			push(@error_messages, "Sectors pending re-allocation");
			escalate_status('WARNING');
			warn "(debug) Current_Pending_Sector is non-zero ($raw_value)\n\n" if $opt_debug;
		}
	}
}
else{
	my ($current_temperature, $max_temperature, $current_start_stop, $max_start_stop) = qw//;
	foreach my $line(@output){
		if ($line =~ /Current Drive Temperature:\s+(\d+)/){
			$current_temperature = $1;
		}
		elsif ($line =~ /Drive Trip Temperature:\s+(\d+)/){
			$max_temperature = $1;
		}
		elsif ($line =~ /Current start stop count:\s+(\d+)/){
			$current_start_stop = $1;
		}
		elsif ($line =~ /Recommended maximum start stop count:\s+(\d+)/){
			$max_start_stop = $1;
		}
		elsif ($line =~ /Elements in grown defect list:\s+(\d+)/){
			push (@perfdata, "defect_list=$1");
		}
		elsif ($line =~ /Blocks sent to initiator =\s+(\d+)/){
			push (@perfdata, "sent_blocks=$1");
		}
	}
	if($current_temperature){
		if($max_temperature){
			push (@perfdata, "temperature=$current_temperature;;$max_temperature");
			if($current_temperature > $max_temperature){
				warn "(debug) Disk temperature is greater than max ($current_temperature > $max_temperature)\n\n" if $opt_debug;
				push(@error_messages, 'Disk temperature is higher than maximum');
				escalate_status('CRITICAL');
			}
		}
		else{
			push (@perfdata, "temperature=$current_temperature");
		}
	}
	if($current_start_stop){
		if($max_start_stop){
			push (@perfdata, "start_stop=$current_start_stop;$max_start_stop");
			if($current_start_stop > $max_start_stop){
				warn "(debug) Disk start_stop is greater than max ($current_start_stop > $max_start_stop)\n\n" if $opt_debug;
				push(@error_messages, 'Disk start_stop is higher than maximum');
				escalate_status('WARNING');
			}
		}
		else{
			push (@perfdata, "start_stop=$current_start_stop");
		}
	}
}
warn "(debug) gathered perfdata:\n@perfdata\n\n" if $opt_debug;
my $perf_string = join(' ', @perfdata);

warn "###########################################################\n" if $opt_debug;
warn "(debug) FINAL STATUS: $exit_status\n" if $opt_debug;
warn "###########################################################\n\n\n" if $opt_debug;

warn "(debug) final status/output:\n" if $opt_debug;

my $status_string = '';

if($exit_status ne 'OK'){
	$status_string = "$exit_status: ".join(', ', @error_messages);
}
else {
	$status_string = "OK: no SMART errors detected";
}

print "$status_string|$perf_string\n";
exit $ERRORS{$exit_status};

sub print_help {
	print_revision($basename,$revision);
	print "Usage: $basename (--device=<SMART device> --interface=(ata|sat|scsi|cciss,[0-9]+|3ware,[0-9]+)|-h|-v) [--debug]\n";
	print "  --debug: show debugging information\n";
	print "  -d/--device: a device to be SMART monitored, eg /dev/sda\n";
	print "  -i/--interface: ata, sat, scsi, cciss, 3ware, depending upon the device's interface type\n";
	print "  -h/--help: this help\n";
	print "  -v/--version: Version number\n";
	support();
}

# escalate an exit status IFF it's more severe than the previous exit status
sub escalate_status {
	my $requested_status = shift;
	# no test for 'CRITICAL'; automatically escalates upwards
	if ($requested_status eq 'WARNING') {
		return if $exit_status eq 'CRITICAL';
	}
	if ($requested_status eq 'UNKNOWN') {
		return if $exit_status eq 'WARNING';
		return if $exit_status eq 'CRITICAL';
	}
	$exit_status = $requested_status;
}
