#!/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 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 Run the specified smart self test on all the devices. -U When calling cciss_vol_status, call it with -u. -G 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 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 Run the specified smart self test on all the devices. -U When calling cciss_vol_status, call it with -u. -G 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; }