294 lines
6.2 KiB
PHP
294 lines
6.2 KiB
PHP
|
|
||
|
<?php
|
||
|
|
||
|
|
||
|
// Change directory to here
|
||
|
$thisDir=DirName(__FILE__);
|
||
|
|
||
|
// Make sure we're root
|
||
|
$whoami=Trim(Shell_Exec("whoami"));
|
||
|
if($whoami!='root'){
|
||
|
die("Please run as root (Currently: $whoami)");
|
||
|
}
|
||
|
|
||
|
// First parameter should be path to config file
|
||
|
$config_path=$argv[1];
|
||
|
if(!File_Exists($config_path)){
|
||
|
die("\nInvalid config path: {$config_path}; Current directory: ".GetCWD()."\n");
|
||
|
}
|
||
|
if(!Is_File($config_path)){
|
||
|
die("\nConfig file ... isn't a file?: {$config_path}\n");
|
||
|
}
|
||
|
$config_path=RealPath($config_path);
|
||
|
|
||
|
// Second could be a test type (optional)
|
||
|
$bStartTests_short=false;
|
||
|
$bStartTests_long=false;
|
||
|
if(IsSet($argv[2])){
|
||
|
if($argv[2]=='start-tests-short'){
|
||
|
$bStartTests_short=true;
|
||
|
}
|
||
|
else if($argv[2]=='start-tests-long'){
|
||
|
$bStartTests_long=true;
|
||
|
}
|
||
|
else{
|
||
|
DoError("Unknown test type: {$argv[2]}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Load config
|
||
|
$config=LoadConfig($config_path);
|
||
|
|
||
|
// Show information about each drive
|
||
|
foreach($config['devices'] as $name=>$device){
|
||
|
|
||
|
//
|
||
|
$match=true;
|
||
|
|
||
|
//
|
||
|
print "\n".Str_Repeat('*',30);
|
||
|
print "\n*** Entry: {$name}";
|
||
|
print "\nPath={$device['path']}; Type={$device['device']}";
|
||
|
|
||
|
//
|
||
|
$device_safe=EscapeShellArg($device['device']);
|
||
|
$path_safe=EscapeShellArg($device['path']);
|
||
|
|
||
|
// If the path doesn't exist, possibly fail
|
||
|
Exec("[ -e {$path_safe} ]", $output_lines, $return_code);
|
||
|
if($return_code!=0){
|
||
|
print "\nPath does not exist!";
|
||
|
if($device['bRequired']){
|
||
|
DoError("Path doesn't exist but was required: {$device['path']}");
|
||
|
}
|
||
|
print "\n";
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Is this even a block device?
|
||
|
Exec("[ -b {$path_safe} ]", $output_lines, $return_code);
|
||
|
if($return_code!=0){
|
||
|
DoError("Path doesn't seem to point to a block device?");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Ask smartctl for status
|
||
|
$command="smartctl --all --device {$device_safe} {$path_safe}";
|
||
|
$output_lines=false;
|
||
|
Exec($command,$output_lines,$smart_return_code);
|
||
|
$lines=Implode("\n",$output_lines);
|
||
|
|
||
|
// Output any messages indicated by smartctl
|
||
|
$messages=CheckNamedBits($smart_return_code);
|
||
|
foreach($messages as $message){
|
||
|
print "\nMESSAGE: {$message}";
|
||
|
}
|
||
|
|
||
|
// Test Status:
|
||
|
if(Preg_Match('@^Self-test execution status.+?(?P<remainder>[0-9]+\% of test remaining)\.@smi',$lines,$match)){
|
||
|
print "\n{$match['remainder']}";
|
||
|
}
|
||
|
else if(Preg_Match('@^Self-test execution status.+?(completed\s+without\s+error)@smi',$lines,$match)){
|
||
|
|
||
|
//
|
||
|
print "\nNo test seems to be running";
|
||
|
|
||
|
// Start short test?
|
||
|
if($bStartTests_short){
|
||
|
print "; Will start short test now!\n";
|
||
|
Exec("smartctl --test=short --device {$device_safe} {$path_safe}");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Start long test?
|
||
|
else if($bStartTests_long){
|
||
|
print "; Will start long test now!\n";
|
||
|
Exec("smartctl --test=long --device {$device_safe} {$path_safe}");
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
else{
|
||
|
DoError("Could not locate execution status information");
|
||
|
}
|
||
|
|
||
|
// Check for terrible states
|
||
|
if(CheckBitPosition($smart_return_code,3)){
|
||
|
$error_message=Str_Repeat('!',10)." ".Str_Repeat('*',10)." CATASTROPHY WARNING!!! DISK IS FAILING ".Str_Repeat('*',10)." ".Str_Repeat('!',10);
|
||
|
DoError($error_message);
|
||
|
}
|
||
|
if(CheckBitPosition($smart_return_code,4)){
|
||
|
$error_message=Str_Repeat('!',10)." ".Str_Repeat('*',10)." DISK IS IN PREFAIL STATE ".Str_Repeat('*',10)." ".Str_Repeat('!',10);
|
||
|
DoError($error_message);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
print "\n";
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function LoadConfig($config_path)
|
||
|
{
|
||
|
//
|
||
|
$config=Array(
|
||
|
'global'=>Array(),
|
||
|
'devices'=>Array()
|
||
|
);
|
||
|
|
||
|
// Load config
|
||
|
$ini=Parse_Ini_File($config_path,true);
|
||
|
if(!$ini){
|
||
|
DoError("Unable to parse config file: {$config_path}");
|
||
|
}
|
||
|
|
||
|
// Booleans
|
||
|
foreach($ini as $section_name=>&$section){
|
||
|
foreach($section as $key=>&$value){
|
||
|
|
||
|
//
|
||
|
$v=StrToLower($value);
|
||
|
switch($v)
|
||
|
{
|
||
|
//
|
||
|
case 'true':
|
||
|
case '1':
|
||
|
case 'on':
|
||
|
case 'yes':
|
||
|
$value=true;
|
||
|
break;
|
||
|
|
||
|
//
|
||
|
case 'false':
|
||
|
case '0':
|
||
|
case 'off':
|
||
|
case 'no':
|
||
|
$value=false;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
}Unset($value);
|
||
|
}Unset($section);
|
||
|
|
||
|
// Pull out global config
|
||
|
if(IsSet($ini['global'])){
|
||
|
$config['global']=$ini['global'];
|
||
|
Unset($ini['global']);
|
||
|
}
|
||
|
|
||
|
// The remainder are devices
|
||
|
$config['devices']=$ini;
|
||
|
|
||
|
return $config;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function CheckNamedBits($return_code)
|
||
|
{
|
||
|
//
|
||
|
$messages=Array();
|
||
|
|
||
|
//
|
||
|
$map=Array(
|
||
|
'0'=>"Command line did not parse.",
|
||
|
'1'=>"Device open failed, device did not return an IDENTIFY DEVICE structure, or device is in a low-power mode",
|
||
|
'2'=>"Some SMART or other ATA command to the disk failed, or there was a checksum error in a SMART data structure",
|
||
|
'3'=>"SMART status check returned \"DISK FAILING\".",
|
||
|
'4'=>"We found prefail Attributes <= threshold.",
|
||
|
'5'=>"SMART status check returned \"DISK OK\" but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past.",
|
||
|
'6'=>"The device error log contains records of errors.",
|
||
|
'7'=>"The device self-test log contains records of errors. [ATA only] Failed self-tests outdated by a newer successful extended self-test are ignored."
|
||
|
);
|
||
|
|
||
|
//
|
||
|
foreach($map as $bitIndex=>$message){
|
||
|
if(CheckBitPosition($return_code,$bitIndex)){
|
||
|
$messages[]=$message;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $messages;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function CheckBitPosition($toCheck,$index)
|
||
|
{
|
||
|
//
|
||
|
$value=Pow(2,$index);
|
||
|
|
||
|
//
|
||
|
$bIsSet=false;
|
||
|
if($toCheck == ($toCheck|$value)){
|
||
|
$bIsSet=true;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
//print "\n toCheck=$toCheck, index=$index, value=$value, IsSet=".($bIsSet?'true':'false');
|
||
|
|
||
|
return $bIsSet;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function DoError($message)
|
||
|
{
|
||
|
//print "\nError: {$message}";
|
||
|
LogError($message);
|
||
|
MailError($message);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function LogError($message)
|
||
|
{
|
||
|
//
|
||
|
$prefix="[".(BaseName(__FILE__))."] ";
|
||
|
|
||
|
//
|
||
|
$stderr = fOpen('php://stderr', 'w');
|
||
|
fWrite($stderr,"\n{$message}");
|
||
|
fClose($stderr);
|
||
|
|
||
|
//
|
||
|
$message_safe=EscapeShellArg("{$prefix}{$message}");
|
||
|
Shell_Exec("logger {$message_safe}");
|
||
|
}
|
||
|
|
||
|
//
|
||
|
function MailError($message)
|
||
|
{
|
||
|
//
|
||
|
global $config;
|
||
|
|
||
|
//
|
||
|
$hostname=Shell_Exec("hostname");
|
||
|
|
||
|
//
|
||
|
if(!IsSet($config['global']['mailtos'])){
|
||
|
return false;
|
||
|
}
|
||
|
$mailtos=Explode(",",$config['global']['mailtos']);
|
||
|
|
||
|
//
|
||
|
foreach($mailtos as $mailto){
|
||
|
|
||
|
//
|
||
|
$mailto=Trim($mailto);
|
||
|
|
||
|
//
|
||
|
if(Filter_Var($mailto,FILTER_VALIDATE_EMAIL)===FALSE){
|
||
|
LogError("This doesn't seem to be a valid email address: {$mailto}");
|
||
|
continue;
|
||
|
}
|
||
|
if(!mail(
|
||
|
$mailto,
|
||
|
"{$hostname} - smartctl error",
|
||
|
$message
|
||
|
))
|
||
|
{
|
||
|
LogError("PHP failed to send email to {$mailto}, for some reason");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
?>
|
||
|
|
||
|
Done
|
||
|
|