#!/usr/bin/env perl # Copyright (c) 2026, rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. 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 HOLDER 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. # # runSmart - Sequential SMART test execution on all system drives # # PURPOSE: # Discovers all drives in the system and executes short SMART tests sequentially # to diagnose potential hardware issues. Processes drives one at a time to minimize # system performance impact during testing. # # USAGE: # Standalone: ./runSmart # From sneakernet: Executed automatically during cleanup phase on target server # From Perl script: my $results = eval { do './runSmart' }; # # EXECUTION CONTEXT: # When executed by sneakernet on the target server: # - Script is read from transport drive, decrypted, and executed via eval() # - Summary results and errors returned via return statement for report inclusion # - When run standalone, prints results to STDOUT # # BEHAVIOR: # 1. Discovers all drives using smartctl --scan # 2. Starts short SMART test on each drive sequentially # 3. Monitors test progress with periodic status checks # 4. Collects pass/fail results in @result array # 5. Collects errors (failed/unknown tests) in @errorMessages array # 6. Returns formatted results and errors for sneakernet integration # # REQUIREMENTS: # - smartmontools package (provides smartctl command) # - Root/sudo privileges for drive access # - Drive must support SMART functionality # # OUTPUT: # STDERR: Real-time progress indicators when run standalone (separators, dots, test result details) # Return: ($resultsString, $errorsString) - formatted summary of all tests # STDOUT: Results and errors when run standalone # # Author: rodolico # # REVISION HISTORY: # Version: 1.0 # Updated: 2026-01-16 # # Version: 1.0.1 # Updated: 2026-01-23 # - Improved error handling and reporting for drives with unknown/failed tests # - Added real-time progress indicators when run standalone # - Decreased amount of output when drive passes test # # Version: 1.0.2 # Updated: 2026-02-04 # - Added verbosity level handling for sneakernet integration use strict; use warnings; use 5.010; # Get list of all drives my @drives = `smartctl --scan | awk '{print \$1}' | sed 's|/dev/||' | sort`; chomp @drives; my @result; my @errorMessages = (); my $caller = caller(); # Check if called from another script and, if not, prints updates to STDOUT # if we are being called from sneakernet, get verbosity level, otherwise just set to 5 my $verbosityLevel = ($caller && defined $ZFS_Utils::verboseLoggingLevel) ? $ZFS_Utils::verboseLoggingLevel : 5; unless (@drives) { push @errorMessages, "No drives found"; if ($caller) { return ( "", join("\n", @errorMessages) ); } else { print "\n=== CLEANUP SCRIPT ERRORS ===\n"; print join("\n", @errorMessages) . "\n"; exit 1; } } push @result, "Drives found: " . join(", ", @drives); push @result, "SMART Test Results:"; foreach my $drive (@drives) { print STDERR"Testing /dev/$drive\n" unless $caller; # Start the test (capture and discard output) system("smartctl -t short /dev/$drive >/dev/null 2>&1"); # Wait for test to complete my $completed = 0; while (!$completed) { sleep 30; my $status = `smartctl -a /dev/$drive`; my $linenumber = 0; # Check if test is still running if ($status =~ /Self-test execution status:\s+\(\s*\d+\)\s+Self-test routine in progress/i) { print STDERR "." unless $caller; } elsif ($status =~ /Self-test execution status:\s+\(\s*0\)/i) { $completed = 1; push @result, "drive $drive\tstatus => PASSED"; print STDERR "drive $drive\tstatus => PASSED" unless $caller; } else { push @result, "drive $drive\tstatus => UNKNOWN/FAILED"; push @result, "Recent Self-Test Log for /dev/$drive:" if $verbosityLevel >= 2; push @result, `smartctl -l selftest /dev/$drive | head -20` if $verbosityLevel >= 2; push @errorMessages, "drive $drive\tstatus => UNKNOWN/FAILED"; print STDERR "drive $drive\tstatus => UNKNOWN/FAILED" unless $caller; $completed = 1; } } print STDERR "\n" unless $caller; } # When run standalone, print to STDOUT chomp @result; chomp @errorMessages; if ($caller) { return ( join( "\n", @result ) . "\n", @errorMessages ? join("\n", @errorMessages) : "" ); } else { print "Results Summary:\n"; print join( "\n", @result ) . "\n"; if (@errorMessages) { print "\n=== CLEANUP SCRIPT ERRORS ===\n"; print join("\n", @errorMessages) . "\n"; } } # End of script