930 lines
27 KiB
Perl
930 lines
27 KiB
Perl
#!/usr/bin/env perl
|
|
#Copyright (c) 2024, Zane C. Bowers-Hadley
|
|
#All rights reserved.
|
|
#
|
|
#Redistribution and use in source and binary forms, with or without modification,
|
|
#are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
#THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
=for comment
|
|
|
|
Add this to snmpd.conf like below.
|
|
|
|
extend smart /etc/snmp/smart
|
|
|
|
Then add to root's cron tab, if you have more than a few disks.
|
|
|
|
*/5 * * * * /etc/snmp/extends/smart -u
|
|
|
|
You will also need to create the config file, which defaults to the same path as the script,
|
|
but with .config appended. So if the script is located at /etc/snmp/smart, the config file
|
|
will be /etc/snmp/extends/smart.config. Alternatively you can also specific a config via -c.
|
|
|
|
Anything starting with a # is comment. The format for variables is $variable=$value. Empty
|
|
lines are ignored. Spaces and tabes at either the start or end of a line are ignored. Any
|
|
line with out a matched variable or # are treated as a disk.
|
|
|
|
#This is a comment
|
|
cache=/var/cache/smart
|
|
smartctl=/usr/local/sbin/smartctl
|
|
useSN=0
|
|
ada0
|
|
da5 /dev/da5 -d sat
|
|
twl0,0 /dev/twl0 -d 3ware,0
|
|
twl0,1 /dev/twl0 -d 3ware,1
|
|
twl0,2 /dev/twl0 -d 3ware,2
|
|
|
|
The variables are as below.
|
|
|
|
cache = The path to the cache file to use. Default: /var/cache/smart
|
|
smartctl = The path to use for smartctl. Default: /usr/bin/env smartctl
|
|
useSN = If set to 1, it will use the disks SN for reporting instead of the device name.
|
|
1 is the default. 0 will use the device name.
|
|
|
|
A disk line is can be as simple as just a disk name under /dev/. Such as in the config above
|
|
The line "ada0" would resolve to "/dev/ada0" and would be called with no special argument. If
|
|
a line has a space in it, everything before the space is treated as the disk name and is what
|
|
used for reporting and everything after that is used as the argument to be passed to smartctl.
|
|
|
|
If you want to guess at the configuration, call it with -g and it will print out what it thinks
|
|
it should be.
|
|
|
|
|
|
Switches:
|
|
|
|
-c <config> The config file to use.
|
|
-u Update
|
|
-p Pretty print the JSON.
|
|
-Z GZip+Base64 compress the results.
|
|
|
|
-g Guess at the config and print it to STDOUT
|
|
-C Enable manual checking for guess and cciss.
|
|
-S Set useSN to 0 when using -g
|
|
-t <test> Run the specified smart self test on all the devices.
|
|
-U When calling cciss_vol_status, call it with -u.
|
|
-G <modes> Guess modes to use. This is a comma seperated list.
|
|
Default :: scan-open,cciss-vol-status
|
|
|
|
Guess Modes:
|
|
|
|
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
|
|
|
|
- scan-open :: Call smartctl with "--scan-open".
|
|
|
|
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
|
|
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
|
|
and then optionally checking for disks via smrtctl if -C is given. Should be noted
|
|
though that -C will not find drives that are currently missing/failed. If -U is given,
|
|
cciss_vol_status will be called with -u.
|
|
|
|
=cut
|
|
|
|
##
|
|
## You should not need to touch anything below here.
|
|
##
|
|
use warnings;
|
|
use strict;
|
|
use Getopt::Std;
|
|
use JSON;
|
|
use MIME::Base64;
|
|
use IO::Compress::Gzip qw(gzip $GzipError);
|
|
|
|
my $cache = '/var/cache/smart';
|
|
my $smartctl = '/usr/bin/env smartctl';
|
|
my @disks;
|
|
my $useSN = 1;
|
|
|
|
$Getopt::Std::STANDARD_HELP_VERSION = 1;
|
|
|
|
sub main::VERSION_MESSAGE {
|
|
print "SMART SNMP extend 0.3.2\n";
|
|
}
|
|
|
|
sub main::HELP_MESSAGE {
|
|
&VERSION_MESSAGE;
|
|
print "\n" . "-u Update '" . $cache . "'\n" . '-g Guess at the config and print it to STDOUT
|
|
-c <config> The config file to use.
|
|
-p Pretty print the JSON.
|
|
-Z GZip+Base64 compress the results.
|
|
-C Enable manual checking for guess and cciss.
|
|
-S Set useSN to 0 when using -g
|
|
-t <test> Run the specified smart self test on all the devices.
|
|
-U When calling cciss_vol_status, call it with -u.
|
|
-G <modes> Guess modes to use. This is a comma seperated list.
|
|
Default :: scan-open,cciss-vol-status
|
|
|
|
|
|
Scan Modes:
|
|
|
|
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
|
|
|
|
- scan-open :: Call smartctl with "--scan-open".
|
|
|
|
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
|
|
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
|
|
and then optionally checking for disks via smrtctl if -C is given. Should be noted
|
|
though that -C will not find drives that are currently missing/failed. If -U is given,
|
|
cciss_vol_status will be called with -u.
|
|
';
|
|
|
|
} ## end sub main::HELP_MESSAGE
|
|
|
|
#gets the options
|
|
my %opts = ();
|
|
getopts( 'ugc:pZhvCSGt:U', \%opts );
|
|
|
|
if ( $opts{h} ) {
|
|
&HELP_MESSAGE;
|
|
exit;
|
|
}
|
|
if ( $opts{v} ) {
|
|
&VERSION_MESSAGE;
|
|
exit;
|
|
}
|
|
|
|
#
|
|
# figure out what scan modes to use if -g specified
|
|
#
|
|
my $scan_modes = {
|
|
'scan-open' => 0,
|
|
'scan' => 0,
|
|
'cciss_vol_status' => 0,
|
|
};
|
|
if ( $opts{g} ) {
|
|
if ( !defined( $opts{G} ) ) {
|
|
$opts{G} = 'scan-open,cciss_vol_status';
|
|
}
|
|
$opts{G} =~ s/[\ \t]//g;
|
|
my @scan_modes_split = split( /,/, $opts{G} );
|
|
foreach my $mode (@scan_modes_split) {
|
|
if ( !defined $scan_modes->{$mode} ) {
|
|
die( '"' . $mode . '" is not a recognized scan mode' );
|
|
}
|
|
$scan_modes->{$mode} = 1;
|
|
}
|
|
} ## end if ( $opts{g} )
|
|
|
|
# configure JSON for later usage
|
|
# only need to do this if actually running as in -g is not specified
|
|
my $json;
|
|
if ( !$opts{g} ) {
|
|
|
|
$json = JSON->new->allow_nonref->canonical(1);
|
|
if ( $opts{p} ) {
|
|
$json->pretty;
|
|
}
|
|
}
|
|
|
|
#
|
|
#
|
|
# guess if asked
|
|
#
|
|
#
|
|
if ( defined( $opts{g} ) ) {
|
|
|
|
#get what path to use for smartctl
|
|
$smartctl = `which smartctl`;
|
|
chomp($smartctl);
|
|
if ( $? != 0 ) {
|
|
warn("'which smartctl' failed with a exit code of $?");
|
|
exit 1;
|
|
}
|
|
|
|
#try to touch the default cache location and warn if it can't be done
|
|
system( 'touch ' . $cache . '>/dev/null' );
|
|
if ( $? != 0 ) {
|
|
$cache = '#Could not touch ' . $cache . "You will need to manually set it\n" . "cache=?\n";
|
|
} else {
|
|
system( 'rm -f ' . $cache . '>/dev/null' );
|
|
$cache = 'cache=' . $cache . "\n";
|
|
}
|
|
|
|
my $drive_lines = '';
|
|
|
|
#
|
|
#
|
|
# scan-open and scan guess mode handling
|
|
#
|
|
#
|
|
if ( $scan_modes->{'scan-open'} || $scan_modes->{'scan'} ) {
|
|
# used for checking if a disk has been found more than once
|
|
my %found_disks_names;
|
|
my @argumentsA;
|
|
|
|
# use scan-open if it is set, overriding scan if it is also set
|
|
my $mode = 'scan';
|
|
if ( $scan_modes->{'scan-open'} ) {
|
|
$mode = 'scan-open';
|
|
}
|
|
|
|
#have smartctl scan and see if it finds anythings not get found
|
|
my $scan_output = `$smartctl --$mode`;
|
|
my @scan_outputA = split( /\n/, $scan_output );
|
|
|
|
# remove non-SMART devices sometimes returned
|
|
@scan_outputA = grep( !/ses[0-9]/, @scan_outputA ); # not a disk, but may or may not have SMART attributes
|
|
@scan_outputA = grep( !/pass[0-9]/, @scan_outputA ); # very likely a duplicate and a disk under another name
|
|
@scan_outputA = grep( !/cd[0-9]/, @scan_outputA ); # CD drive
|
|
if ( $^O eq 'freebsd' ) {
|
|
@scan_outputA = grep( !/sa[0-9]/, @scan_outputA ); # tape drive
|
|
@scan_outputA = grep( !/ctl[0-9]/, @scan_outputA ); # CAM target layer
|
|
} elsif ( $^O eq 'linux' ) {
|
|
@scan_outputA = grep( !/st[0-9]/, @scan_outputA ); # SCSI tape drive
|
|
@scan_outputA = grep( !/ht[0-9]/, @scan_outputA ); # ATA tape drive
|
|
}
|
|
|
|
# make the first pass, figuring out what all we have and trimming comments
|
|
foreach my $arguments (@scan_outputA) {
|
|
my $name = $arguments;
|
|
|
|
$arguments =~ s/ \#.*//; # trim the comment out of the argument
|
|
$name =~ s/ .*//;
|
|
$name =~ s/\/dev\///;
|
|
if ( defined( $found_disks_names{$name} ) ) {
|
|
$found_disks_names{$name}++;
|
|
} else {
|
|
$found_disks_names{$name} = 0;
|
|
}
|
|
|
|
push( @argumentsA, $arguments );
|
|
|
|
} ## end foreach my $arguments (@scan_outputA)
|
|
|
|
# second pass, putting the lines together
|
|
my %current_disk;
|
|
foreach my $arguments (@argumentsA) {
|
|
my $not_virt = 1;
|
|
|
|
# check to see if we have a virtual device
|
|
my @virt_check = split( /\n/, `smartctl -i $arguments 2> /dev/null` );
|
|
foreach my $virt_check_line (@virt_check) {
|
|
if ( $virt_check_line =~ /(?i)Product\:.*LOGICAL VOLUME/ ) {
|
|
$not_virt = 0;
|
|
}
|
|
}
|
|
|
|
my $name = $arguments;
|
|
$name =~ s/ .*//;
|
|
$name =~ s/\/dev\///;
|
|
|
|
# only add it if not a virtual RAID drive
|
|
# HP RAID virtual disks will show up with very basical but totally useless smart data
|
|
if ($not_virt) {
|
|
if ( $found_disks_names{$name} == 0 ) {
|
|
# If no other devices, just name it after the base device.
|
|
$drive_lines = $drive_lines . $name . " " . $arguments . "\n";
|
|
} else {
|
|
# if more than one, start at zero and increment, apennding comma number to the base device name
|
|
if ( defined( $current_disk{$name} ) ) {
|
|
$current_disk{$name}++;
|
|
} else {
|
|
$current_disk{$name} = 0;
|
|
}
|
|
$drive_lines = $drive_lines . $name . "," . $current_disk{$name} . " " . $arguments . "\n";
|
|
}
|
|
} ## end if ($not_virt)
|
|
|
|
} ## end foreach my $arguments (@argumentsA)
|
|
} ## end if ( $scan_modes->{'scan-open'} || $scan_modes...)
|
|
|
|
#
|
|
#
|
|
# scan mode handler for cciss_vol_status
|
|
# /dev/sg* devices for cciss on Linux
|
|
# /dev/ccis* devices for cciss on FreeBSD
|
|
#
|
|
#
|
|
if ( $scan_modes->{'cciss_vol_status'} && ( $^O eq 'linux' || $^O eq 'freebsd' ) ) {
|
|
my $cciss;
|
|
if ( $^O eq 'freebsd' ) {
|
|
$cciss = 'ciss';
|
|
} elsif ( $^O eq 'linux' ) {
|
|
$cciss = 'sg';
|
|
}
|
|
|
|
my $uarg = '';
|
|
if ( $opts{U} ) {
|
|
$uarg = '-u';
|
|
}
|
|
|
|
# generate the initial device path that will be checked
|
|
my $sg_int = 0;
|
|
my $device = '/dev/' . $cciss . $sg_int;
|
|
|
|
my $sg_process = 1;
|
|
if ( -e $device ) {
|
|
my $output = `which cciss_vol_status 2> /dev/null`;
|
|
if ( $? != 0 && !$opts{C} ) {
|
|
$sg_process = 0;
|
|
$drive_lines
|
|
= $drive_lines
|
|
. "# -C not given, but "
|
|
. $device
|
|
. " exists and cciss_vol_status is not present\n"
|
|
. "# in path or 'ccis_vol_status -V "
|
|
. $device
|
|
. "' is failing\n";
|
|
} ## end if ( $? != 0 && !$opts{C} )
|
|
} ## end if ( -e $device )
|
|
my $seen_lines = {};
|
|
my $ignore_lines = {};
|
|
while ( -e $device && $sg_process ) {
|
|
my $output = `cciss_vol_status -V $uarg $device 2> /dev/null`;
|
|
if ( $? != 0 && $output eq '' && !$opts{C} ) {
|
|
# just empty here as we just want to skip it if it fails and there is no C
|
|
# warning is above
|
|
} elsif ( $? != 0 && $output eq '' && $opts{C} ) {
|
|
my $drive_count = 0;
|
|
my $continue = 1;
|
|
while ($continue) {
|
|
my $output = `$smartctl -i $device -d cciss,$drive_count 2> /dev/null`;
|
|
if ( $? != 0 ) {
|
|
$continue = 0;
|
|
} else {
|
|
my $add_it = 0;
|
|
my $id;
|
|
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
|
|
$id = $1;
|
|
$id =~ s/^\s+|\s+$//g;
|
|
}
|
|
if ( defined($id) && !defined( $seen_lines->{$id} ) ) {
|
|
$add_it = 1;
|
|
$seen_lines->{$id} = 1;
|
|
}
|
|
if ( $continue && $add_it ) {
|
|
$drive_lines
|
|
= $drive_lines
|
|
. $cciss . '0-'
|
|
. $drive_count . ' '
|
|
. $device
|
|
. ' -d cciss,'
|
|
. $drive_count . "\n";
|
|
}
|
|
} ## end else [ if ( $? != 0 ) ]
|
|
$drive_count++;
|
|
} ## end while ($continue)
|
|
} else {
|
|
my $drive_count = 0;
|
|
# count the connector lines, this will make sure failed are founded as well
|
|
my $seen_conectors = {};
|
|
while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g ) {
|
|
my $cciss_drive_line = $1;
|
|
my $connector = $cciss_drive_line;
|
|
$connector =~ s/(.*\ bay +\d+).*/$1/;
|
|
if ( !defined( $seen_lines->{$cciss_drive_line} )
|
|
&& !defined( $seen_conectors->{$connector} )
|
|
&& !defined( $ignore_lines->{$cciss_drive_line} ) )
|
|
{
|
|
$seen_lines->{$cciss_drive_line} = 1;
|
|
$seen_conectors->{$connector} = 1;
|
|
$drive_count++;
|
|
} else {
|
|
# going to be a connector we've already seen
|
|
# which will happen when it is processing replacement drives
|
|
# so save this as a device to ignore
|
|
$ignore_lines->{$cciss_drive_line} = 1;
|
|
}
|
|
} ## end while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g)
|
|
my $drive_int = 0;
|
|
while ( $drive_int < $drive_count ) {
|
|
$drive_lines
|
|
= $drive_lines
|
|
. $cciss
|
|
. $sg_int . '-'
|
|
. $drive_int . ' '
|
|
. $device
|
|
. ' -d cciss,'
|
|
. $drive_int . "\n";
|
|
|
|
$drive_int++;
|
|
} ## end while ( $drive_int < $drive_count )
|
|
} ## end else [ if ( $? != 0 && $output eq '' && !$opts{C})]
|
|
|
|
$sg_int++;
|
|
$device = '/dev/' . $cciss . $sg_int;
|
|
} ## end while ( -e $device && $sg_process )
|
|
} ## end if ( $scan_modes->{'cciss_vol_status'} && ...)
|
|
|
|
my $useSN = 1;
|
|
if ( $opts{S} ) {
|
|
$useSN = 0;
|
|
}
|
|
|
|
print '# scan_modes='
|
|
. $opts{G}
|
|
. "\nuseSN="
|
|
. $useSN . "\n"
|
|
. 'smartctl='
|
|
. $smartctl . "\n"
|
|
. $cache
|
|
. $drive_lines;
|
|
|
|
exit 0;
|
|
} ## end if ( defined( $opts{g} ) )
|
|
|
|
#get which config file to use
|
|
my $config = $0 . '.config';
|
|
if ( defined( $opts{c} ) ) {
|
|
$config = $opts{c};
|
|
}
|
|
|
|
#reads the config file, optionally
|
|
my $config_file = '';
|
|
open( my $readfh, "<", $config ) or die "Can't open '" . $config . "'";
|
|
read( $readfh, $config_file, 1000000 );
|
|
close($readfh);
|
|
|
|
#
|
|
#
|
|
# parse the config file and remove comments and empty lines
|
|
#
|
|
#
|
|
my @configA = split( /\n/, $config_file );
|
|
@configA = grep( !/^$/, @configA );
|
|
@configA = grep( !/^\#/, @configA );
|
|
@configA = grep( !/^[\s\t]*$/, @configA );
|
|
my $configA_int = 0;
|
|
while ( defined( $configA[$configA_int] ) ) {
|
|
my $line = $configA[$configA_int];
|
|
chomp($line);
|
|
$line =~ s/^[\t\s]+//;
|
|
$line =~ s/[\t\s]+$//;
|
|
|
|
my ( $var, $val ) = split( /=/, $line, 2 );
|
|
|
|
my $matched;
|
|
if ( $var eq 'cache' ) {
|
|
$cache = $val;
|
|
$matched = 1;
|
|
}
|
|
|
|
if ( $var eq 'smartctl' ) {
|
|
$smartctl = $val;
|
|
$matched = 1;
|
|
}
|
|
|
|
if ( $var eq 'useSN' ) {
|
|
$useSN = $val;
|
|
$matched = 1;
|
|
}
|
|
|
|
if ( !defined($val) ) {
|
|
push( @disks, $line );
|
|
}
|
|
|
|
$configA_int++;
|
|
} ## end while ( defined( $configA[$configA_int] ) )
|
|
|
|
#
|
|
#
|
|
# run the specified self test on all disks if asked
|
|
#
|
|
#
|
|
if ( defined( $opts{t} ) ) {
|
|
|
|
# make sure we have something that atleast appears sane for the test name
|
|
my $valid_tesks = {
|
|
'offline' => 1,
|
|
'short' => 1,
|
|
'long' => 1,
|
|
'conveyance' => 1,
|
|
'afterselect,on' => 1,
|
|
};
|
|
if ( !defined( $valid_tesks->{ $opts{t} } ) && $opts{t} !~ /select,(\d+[\-\+]\d+|next|next\+\d+|redo\+\d+)/ ) {
|
|
print '"' . $opts{t} . "\" does not appear to be a valid test\n";
|
|
exit 1;
|
|
}
|
|
|
|
print "Running the SMART $opts{t} on all devices in the config...\n\n";
|
|
|
|
foreach my $line (@disks) {
|
|
my $disk;
|
|
my $name;
|
|
if ( $line =~ /\ / ) {
|
|
( $name, $disk ) = split( /\ /, $line, 2 );
|
|
} else {
|
|
$disk = $line;
|
|
$name = $line;
|
|
}
|
|
if ( $disk !~ /\// ) {
|
|
$disk = '/dev/' . $disk;
|
|
}
|
|
|
|
print "\n------------------------------------------------------------------\nDoing "
|
|
. $smartctl . ' -t '
|
|
. $opts{t} . ' '
|
|
. $disk
|
|
. " ...\n\n";
|
|
print `$smartctl -t $opts{t} $disk` . "\n";
|
|
|
|
} ## end foreach my $line (@disks)
|
|
|
|
exit 0;
|
|
} ## end if ( defined( $opts{t} ) )
|
|
|
|
#if set to 1, no cache will be written and it will be printed instead
|
|
my $noWrite = 0;
|
|
|
|
#
|
|
#
|
|
# if no -u, it means we are being called from snmped
|
|
#
|
|
#
|
|
if ( !defined( $opts{u} ) ) {
|
|
# if the cache file exists, print it, otherwise assume one is not being used
|
|
if ( -f $cache ) {
|
|
my $old = '';
|
|
open( my $readfh, "<", $cache ) or die "Can't open '" . $cache . "'";
|
|
read( $readfh, $old, 1000000 );
|
|
close($readfh);
|
|
print $old;
|
|
exit 0;
|
|
} else {
|
|
$opts{u} = 1;
|
|
$noWrite = 1;
|
|
}
|
|
} ## end if ( !defined( $opts{u} ) )
|
|
|
|
#
|
|
#
|
|
# Process each disk
|
|
#
|
|
#
|
|
my $to_return = {
|
|
data => { disks => {}, exit_nonzero => 0, unhealthy => 0, useSN => $useSN },
|
|
version => 1,
|
|
error => 0,
|
|
errorString => '',
|
|
};
|
|
foreach my $line (@disks) {
|
|
my $disk;
|
|
my $name;
|
|
if ( $line =~ /\ / ) {
|
|
( $name, $disk ) = split( /\ /, $line, 2 );
|
|
} else {
|
|
$disk = $line;
|
|
$name = $line;
|
|
}
|
|
if ( $disk !~ /\// ) {
|
|
$disk = '/dev/' . $disk;
|
|
}
|
|
|
|
my $output = `$smartctl -A $disk`;
|
|
my %IDs = (
|
|
'5' => 'null',
|
|
'10' => 'null',
|
|
'173' => 'null',
|
|
'177' => 'null',
|
|
'183' => 'null',
|
|
'184' => 'null',
|
|
'187' => 'null',
|
|
'188' => 'null',
|
|
'190' => 'null',
|
|
'194' => 'null',
|
|
'196' => 'null',
|
|
'197' => 'null',
|
|
'198' => 'null',
|
|
'199' => 'null',
|
|
'231' => 'null',
|
|
'232' => 'null',
|
|
'233' => 'null',
|
|
'9' => 'null',
|
|
'disk' => $disk,
|
|
'serial' => undef,
|
|
'selftest_log' => undef,
|
|
'health_pass' => 0,
|
|
max_temp => 'null',
|
|
exit => $?,
|
|
);
|
|
$IDs{'disk'} =~ s/^\/dev\///;
|
|
|
|
# if polling exited non-zero above, no reason running the rest of the checks
|
|
my $disk_id = $name;
|
|
if ( $IDs{exit} != 0 ) {
|
|
$to_return->{data}{exit_nonzero}++;
|
|
} else {
|
|
my @outputA;
|
|
|
|
if ( $output =~ /NVMe Log/ ) {
|
|
# we have an NVMe drive with annoyingly different output
|
|
my %mappings = (
|
|
'Temperature' => 194,
|
|
'Power Cycles' => 12,
|
|
'Power On Hours' => 9,
|
|
'Percentage Used' => 231,
|
|
);
|
|
foreach ( split( /\n/, $output ) ) {
|
|
if (/:/) {
|
|
my ( $key, $val ) = split(/:/);
|
|
$val =~ s/^\s+|\s+$|\D+//g;
|
|
if ( exists( $mappings{$key} ) ) {
|
|
if ( $mappings{$key} == 231 ) {
|
|
$IDs{ $mappings{$key} } = 100 - $val;
|
|
} else {
|
|
$IDs{ $mappings{$key} } = $val;
|
|
}
|
|
}
|
|
} ## end if (/:/)
|
|
} ## end foreach ( split( /\n/, $output ) )
|
|
|
|
} else {
|
|
@outputA = split( /\n/, $output );
|
|
my $outputAint = 0;
|
|
while ( defined( $outputA[$outputAint] ) ) {
|
|
my $line = $outputA[$outputAint];
|
|
$line =~ s/^ +//;
|
|
$line =~ s/ +/ /g;
|
|
|
|
if ( $line =~ /^[0123456789]+ / ) {
|
|
my @lineA = split( /\ /, $line, 10 );
|
|
my $raw = $lineA[9];
|
|
my $normalized = $lineA[3];
|
|
my $id = $lineA[0];
|
|
|
|
# Crucial SSD
|
|
# 202, Percent_Lifetime_Remain, same as 231, SSD Life Left
|
|
if ( $id == 202
|
|
&& $line =~ /Percent_Lifetime_Remain/ )
|
|
{
|
|
$IDs{231} = $raw;
|
|
}
|
|
|
|
# single int raw values
|
|
if ( ( $id == 5 )
|
|
|| ( $id == 10 )
|
|
|| ( $id == 173 )
|
|
|| ( $id == 183 )
|
|
|| ( $id == 184 )
|
|
|| ( $id == 187 )
|
|
|| ( $id == 196 )
|
|
|| ( $id == 197 )
|
|
|| ( $id == 198 )
|
|
|| ( $id == 199 ) )
|
|
{
|
|
my @rawA = split( /\ /, $raw );
|
|
$IDs{$id} = $rawA[0];
|
|
} ## end if ( ( $id == 5 ) || ( $id == 10 ) || ( $id...))
|
|
|
|
# single int normalized values
|
|
if ( ( $id == 177 )
|
|
|| ( $id == 230 )
|
|
|| ( $id == 231 )
|
|
|| ( $id == 232 )
|
|
|| ( $id == 233 ) )
|
|
{
|
|
# annoying non-standard disk
|
|
# WDC WDS500G2B0A
|
|
# 230 Media_Wearout_Indicator 0x0032 100 100 --- Old_age Always - 0x002e000a002e
|
|
# 232 Available_Reservd_Space 0x0033 100 100 004 Pre-fail Always - 100
|
|
# 233 NAND_GB_Written_TLC 0x0032 100 100 --- Old_age Always - 9816
|
|
|
|
if ( $id == 230
|
|
&& $line =~ /Media_Wearout_Indicator/ )
|
|
{
|
|
$IDs{233} = int($normalized);
|
|
} elsif ( $id == 232
|
|
&& $line =~ /Available_Reservd_Space/ )
|
|
{
|
|
$IDs{232} = int($normalized);
|
|
} else {
|
|
# only set 233 if it has not been set yet
|
|
# if it was set already then the above did it and we don't want
|
|
# to overwrite it
|
|
if ( $id == 233 && $IDs{233} eq "null" ) {
|
|
$IDs{$id} = int($normalized);
|
|
} elsif ( $id != 233 ) {
|
|
$IDs{$id} = int($normalized);
|
|
}
|
|
} ## end else [ if ( $id == 230 && $line =~ /Media_Wearout_Indicator/)]
|
|
} ## end if ( ( $id == 177 ) || ( $id == 230 ) || (...))
|
|
|
|
# 9, power on hours
|
|
if ( $id == 9 ) {
|
|
my @runtime = split( /[\ h]/, $raw );
|
|
$IDs{$id} = $runtime[0];
|
|
}
|
|
|
|
# 188, Command_Timeout
|
|
if ( $id == 188 ) {
|
|
my $total = 0;
|
|
my @rawA = split( /\ /, $raw );
|
|
my $rawAint = 0;
|
|
while ( defined( $rawA[$rawAint] ) ) {
|
|
$total = $total + $rawA[$rawAint];
|
|
$rawAint++;
|
|
}
|
|
$IDs{$id} = $total;
|
|
} ## end if ( $id == 188 )
|
|
|
|
# 190, airflow temp
|
|
# 194, temp
|
|
if ( ( $id == 190 )
|
|
|| ( $id == 194 ) )
|
|
{
|
|
my ($temp) = split( /\ /, $raw );
|
|
$IDs{$id} = $temp;
|
|
}
|
|
} ## end if ( $line =~ /^[0123456789]+ / )
|
|
|
|
# SAS Wrapping
|
|
# Section by Cameron Munroe (munroenet[at]gmail.com)
|
|
|
|
# Elements in Grown Defect List.
|
|
# Marking as 5 Reallocated_Sector_Ct
|
|
if ( $line =~ "Elements in grown defect list:" ) {
|
|
|
|
my @lineA = split( /\ /, $line, 10 );
|
|
my $raw = $lineA[5];
|
|
|
|
# Reallocated Sector Count ID
|
|
$IDs{5} = $raw;
|
|
|
|
}
|
|
|
|
# Current Drive Temperature
|
|
# Marking as 194 Temperature_Celsius
|
|
if ( $line =~ "Current Drive Temperature:" ) {
|
|
|
|
my @lineA = split( /\ /, $line, 10 );
|
|
my $raw = $lineA[3];
|
|
|
|
# Temperature C ID
|
|
$IDs{194} = $raw;
|
|
|
|
}
|
|
|
|
# End of SAS Wrapper
|
|
|
|
$outputAint++;
|
|
} ## end while ( defined( $outputA[$outputAint] ) )
|
|
} ## end else [ if ( $output =~ /NVMe Log/ ) ]
|
|
|
|
#get the selftest logs
|
|
$output = `$smartctl -l selftest $disk`;
|
|
@outputA = split( /\n/, $output );
|
|
my @completed = grep( /Completed/, @outputA );
|
|
$IDs{'completed'} = scalar @completed;
|
|
my @interrupted = grep( /Interrupted/, @outputA );
|
|
$IDs{'interrupted'} = scalar @interrupted;
|
|
my @read_failure = grep( /read failure/, @outputA );
|
|
$IDs{'read_failure'} = scalar @read_failure;
|
|
my @read_failure2 = grep( /Failed in segment/, @outputA );
|
|
$IDs{'read_failure'} = $IDs{'read_failure'} + scalar @read_failure2;
|
|
my @unknown_failure = grep( /unknown failure/, @outputA );
|
|
$IDs{'unknown_failure'} = scalar @unknown_failure;
|
|
my @extended = grep( /\d.*\ ([Ee]xtended|[Ll]ong).*(?![Dd]uration)/, @outputA );
|
|
$IDs{'extended'} = scalar @extended;
|
|
my @short = grep( /[Ss]hort/, @outputA );
|
|
$IDs{'short'} = scalar @short;
|
|
my @conveyance = grep( /[Cc]onveyance/, @outputA );
|
|
$IDs{'conveyance'} = scalar @conveyance;
|
|
my @selective = grep( /[Ss]elective/, @outputA );
|
|
$IDs{'selective'} = scalar @selective;
|
|
my @offline = grep( /(\d|[Bb]ackground|[Ff]oreground)+\ +[Oo]ffline/, @outputA );
|
|
$IDs{'offline'} = scalar @offline;
|
|
|
|
# if we have logs, actually grab the log output
|
|
if ( $IDs{'completed'} > 0
|
|
|| $IDs{'interrupted'} > 0
|
|
|| $IDs{'read_failure'} > 0
|
|
|| $IDs{'extended'} > 0
|
|
|| $IDs{'short'} > 0
|
|
|| $IDs{'conveyance'} > 0
|
|
|| $IDs{'selective'} > 0
|
|
|| $IDs{'offline'} > 0 )
|
|
{
|
|
my @headers = grep( /(Num\ +Test.*LBA| Description .*[Hh]ours)/, @outputA );
|
|
|
|
my @log_lines;
|
|
push( @log_lines, @extended, @short, @conveyance, @selective, @offline );
|
|
$IDs{'selftest_log'} = join( "\n", @headers, sort(@log_lines) );
|
|
} ## end if ( $IDs{'completed'} > 0 || $IDs{'interrupted'...})
|
|
|
|
# get the drive serial number, if needed
|
|
$disk_id = $name;
|
|
$output = `$smartctl -i $disk`;
|
|
# generally upper case, HP branded drives seem to report with lower case n
|
|
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
|
|
$IDs{'serial'} = $1;
|
|
$IDs{'serial'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
if ($useSN) {
|
|
$disk_id = $IDs{'serial'};
|
|
}
|
|
|
|
while ( $output =~ /(?i)Model Family:(.*)/g ) {
|
|
$IDs{'model_family'} = $1;
|
|
$IDs{'model_family'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
while ( $output =~ /(?i)Device Model:(.*)/g ) {
|
|
$IDs{'device_model'} = $1;
|
|
$IDs{'device_model'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
while ( $output =~ /(?i)Model Number:(.*)/g ) {
|
|
$IDs{'model_number'} = $1;
|
|
$IDs{'model_number'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
while ( $output =~ /(?i)Firmware Version:(.*)/g ) {
|
|
$IDs{'fw_version'} = $1;
|
|
$IDs{'fw_version'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
# mainly HP drives
|
|
while ( $output =~ /(?i)Vendor:(.*)/g ) {
|
|
$IDs{'vendor'} = $1;
|
|
$IDs{'vendor'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
# mainly HP drives
|
|
while ( $output =~ /(?i)Product:(.*)/g ) {
|
|
$IDs{'product'} = $1;
|
|
$IDs{'product'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
# mainly HP drives
|
|
while ( $output =~ /(?i)Revision:(.*)/g ) {
|
|
$IDs{'revision'} = $1;
|
|
$IDs{'revision'} =~ s/^\s+|\s+$//g;
|
|
}
|
|
|
|
# figure out what to use for the max temp, if there is one
|
|
if ( $IDs{'190'} =~ /^\d+$/ ) {
|
|
$IDs{max_temp} = $IDs{'190'};
|
|
} elsif ( $IDs{'194'} =~ /^\d+$/ ) {
|
|
$IDs{max_temp} = $IDs{'194'};
|
|
}
|
|
if ( $IDs{'194'} =~ /^\d+$/ && defined( $IDs{max_temp} ) && $IDs{'194'} > $IDs{max_temp} ) {
|
|
$IDs{max_temp} = $IDs{'194'};
|
|
}
|
|
|
|
$output = `$smartctl -H $disk`;
|
|
if ( $output =~ /SMART\ overall\-health\ self\-assessment\ test\ result\:\ PASSED/ ) {
|
|
$IDs{'health_pass'} = 1;
|
|
} elsif ( $output =~ /SMART\ Health\ Status\:\ OK/ ) {
|
|
$IDs{'health_pass'} = 1;
|
|
}
|
|
|
|
if ( !$IDs{'health_pass'} ) {
|
|
$to_return->{data}{unhealthy}++;
|
|
}
|
|
} ## end else [ if ( $IDs{exit} != 0 ) ]
|
|
|
|
# only bother to save this if useSN is not being used
|
|
if ( !$useSN ) {
|
|
$to_return->{data}{disks}{$disk_id} = \%IDs;
|
|
} elsif ( $IDs{exit} == 0 && defined($disk_id) ) {
|
|
$to_return->{data}{disks}{$disk_id} = \%IDs;
|
|
}
|
|
|
|
# smartctl will in some cases exit zero when it can't pull data for cciss
|
|
# so if we get a zero exit, but no serial then it means something errored
|
|
# and the device is likely dead
|
|
if ( $IDs{exit} == 0 && !defined( $IDs{serial} ) ) {
|
|
$to_return->{data}{unhealthy}++;
|
|
}
|
|
} ## end foreach my $line (@disks)
|
|
|
|
my $toReturn = $json->encode($to_return);
|
|
|
|
if ( !$opts{p} ) {
|
|
$toReturn = $toReturn . "\n";
|
|
}
|
|
|
|
if ( $opts{Z} ) {
|
|
my $toReturnCompressed;
|
|
gzip \$toReturn => \$toReturnCompressed;
|
|
my $compressed = encode_base64($toReturnCompressed);
|
|
$compressed =~ s/\n//g;
|
|
$compressed = $compressed . "\n";
|
|
if ( length($compressed) < length($toReturn) ) {
|
|
$toReturn = $compressed;
|
|
}
|
|
} ## end if ( $opts{Z} )
|
|
|
|
if ( !$noWrite ) {
|
|
open( my $writefh, ">", $cache ) or die "Can't open '" . $cache . "'";
|
|
print $writefh $toReturn;
|
|
close($writefh);
|
|
} else {
|
|
print $toReturn;
|
|
}
|