#!/usr/bin/php -q 0 ? (($val||$last) ? ($val.' '.$word.($val!=1?'s':'').($last ?'':', ')) : '') : ''; } function my_temp($value) { global $unraid; $unit = $unraid['display']['unit']; return ($unit=='F' ? round(9/5*$value+32) : $value)." $unit"; } function my_disk($name) { return ucfirst(preg_replace('/^(disk|cache|parity)(\d+)/','$1 $2',$name)); } function my_scale($value, &$unit, $decimals=NULL, $scale=NULL) { global $unraid; $scale = $scale ?? $unraid['display']['scale']; $number = $unraid['display']['number']; $units = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']; $size = count($units); if ($scale==0 && ($decimals===NULL || $decimals<0)) { $decimals = 0; $unit = ''; } else { $base = $value ? floor(log($value, 1000)) : 0; if ($scale>0 && $base>$scale) $base = $scale; if ($base>$size) $base = $size-1; $value /= pow(1000, $base); if ($decimals===NULL) $decimals = $value>=100 ? 0 : ($value>=10 ? 1 : (round($value*100)%100===0 ? 0 : 2)); elseif ($decimals<0) $decimals = $value>=100||round($value*10)%10===0 ? 0 : abs($decimals); if ($scale<0 && round($value,-1)==1000) {$value = 1; $base++;} $unit = $units[$base]; } return number_format($value, $decimals, $number[0], $value>=10000 ? $number[1] : ''); } function my_check($time,$speed) { if (!$time) return 'unavailable (no parity-check entries logged)'; $days = floor($time/86400); $hmss = $time-$days*86400; $hour = floor($hmss/3600); $mins = $hmss/60%60; $secs = $hmss%60; return plus($days,'day',($hour|$mins|$secs)==0).plus($hour,'hour',($mins|$secs)==0).plus($mins,'minute',$secs==0).plus($secs,'second',true).". Average speed: $speed"; } function read_write_parity_log($epoch,$duration,$speed,$status,$error) { $log = '/boot/config/parity-checks.log'; $timestamp = str_replace(['.0','.'],[' ',' '],date('M.d H:i:s',$epoch)); if (file_exists($log)) { $handle = fopen($log, 'r'); while (($line = fgets($handle))!==false) { if (strpos($line,$timestamp)!==false) break; } fclose($handle); } if (empty($line)) { $year = date('Y',$epoch); $line = "$year $timestamp|$duration|$speed|$status|$error"; if ($status==0||file_exists($log)) file_put_contents($log,"$line\n",FILE_APPEND); } return str_replace("\n","",$line); } function check_temp($name,$temp,$text,$info) { global $notify,$disks,$saved,$unraid,$server; $disk = &$disks[$name]; $hot = $disk['hotTemp'] ?? $unraid['display']['hot']; $max = $disk['maxTemp'] ?? $unraid['display']['max']; $warn = $temp>=$max && $max>0 ? 'alert' : ($temp>=$hot && $hot>0 ? 'warning' : ''); $item = 'temp'; $last = $saved[$item][$name] ?? 0; if ($warn) { if ($temp>$last) { exec("$notify -e ".escapeshellarg("unRAID $text temperature")." -s ".escapeshellarg(ucfirst($warn)." [$server] - $text ".($warn=='alert'?'overheated (':'is hot (').my_temp($temp).")")." -d ".escapeshellarg("$info")." -i \"$warn\""); $saved[$item][$name] = $max>0 && $temp<=$max ? $max : $temp; } } else { if ($last) { exec("$notify -e ".escapeshellarg("unRAID $text message")." -s ".escapeshellarg("Notice [$server] - $text returned to normal temperature")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } } } function check_smart($name,$port,$text,$info) { global $var,$disks,$notify,$saved,$server,$numbers; $disk = &$disks[$name]; $select = getVal($disk,'smSelect',0); $level = getVal($disk,'smLevel',1); $events = explode('|',getVal($disk,'smEvents',$numbers)); $type = getVal($disk,'smType',''); if ($type) { $ports = []; if (!empty($disk['smDevice'])) $port = $disk['smDevice']; if (!empty($disk['smPort1'])) $ports[] = $disk['smPort1']; if (!empty($disk['smPort2'])) $ports[] = $disk['smPort2']; if (!empty($disk['smPort3'])) $ports[] = $disk['smPort3']; if ($ports) { $glue = $disk['smGlue'] ?? ','; $type .= ','.implode($glue,$ports); } } $file = "/var/local/emhttp/smart/$name"; exec("awk 'NR>7{print $1,$2,$4,$6,$9,$10}' ".escapeshellarg($file)." 2>/dev/null", $codes); $item = 'smart'; foreach ($codes as $code) { if (!$code || !is_numeric($code[0])) continue; list($id,$class,$value,$thres,$when,$raw) = explode(' ',$code); $fail = strpos($when,'FAILING_NOW')!==false; if (!$fail && !in_array($id,$events)) continue; $word = str_replace(['_',' (-)'],[' ',''],strtolower("$class ($when)")); $ack = "$name.ack"; switch ($select) { case 0: $attr = "$name.$id"; $last = ($saved[$item][$attr] ?? 0)*$level; if ($raw>0 || $fail) { if ($raw>$last) { exec("$notify -e ".escapeshellarg("unRAID $text SMART health [$id]")." -s ".escapeshellarg("Warning [$server] - $word is $raw")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$attr] = $raw; unset($saved[$item][$ack]); } } else { if ($last>0) { exec("$notify -e ".escapeshellarg("unRAID $text SMART message [$id]")." -s ".escapeshellarg("Notice [$server] - $word returned to normal value")." -d ".escapeshellarg("$info")); unset($saved[$item][$attr]); unset($saved[$item][$ack]); } } break; case 1: $attr = "$name.${id}n"; $last = $saved[$item][$attr] ?? 255; if (($thres>0 && $value<=$thres*$level) || $fail) { if ($value*($value>$thres?$level:1)<$last) { exec("$notify -e ".escapeshellarg("unRAID $text SMART health [$id]")." -s ".escapeshellarg("Warning [$server] - $word is $value")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$attr] = $value; unset($saved[$item][$ack]); } } else { if ($last<255) { exec("$notify -e ".escapeshellarg("unRAID $text SMART message [$id]")." -s ".escapeshellarg("Notice [$server] - $word returned to normal value")." -d ".escapeshellarg("$info")); unset($saved[$item][$attr]); unset($saved[$item][$ack]); } } break; } } $file .= '.ssa'; if (!file_exists($file) || (time()-filemtime($file)>=$var['poll_attributes'])) exec("smartctl -n standby -H ".escapeshellarg($type)." ".escapeshellarg("/dev/$port")."|grep -Pom1 '^SMART.*: \K[A-Z]+'|tr -d '\n' > ".escapeshellarg($file)); } function check_usage($name,$used,$text,$info) { global $notify,$disks,$saved,$unraid,$server; if ($used == -1) return; $disk = &$disks[$name]; $warning = $disk['warning'] ?? $unraid['display']['warning']; $critical = $disk['critical'] ?? $unraid['display']['critical']; $warn = $used>=$critical && $critical>0 ? 'alert' : ($used>=$warning && $warning>0 ? 'warning' : ''); $item = 'used'; $last = $saved[$item][$name] ?? 0; if ($warn) { if ($used>$last) { exec("$notify -e ".escapeshellarg("unRAID $text disk utilization")." -s ".escapeshellarg(ucfirst($warn)." [$server] - $text is ".($warn=='alert'?'low on space':'high on usage')." (${used}%)")." -d ".escapeshellarg("$info")." -i \"$warn\""); $saved[$item][$name] = $critical>0 && $used<=$critical ? $critical : $used; } } else { if ($last) { exec("$notify -e ".escapeshellarg("unRAID $text message")." -s ".escapeshellarg("Notice [$server] - $text returned to normal utilization level")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } } } // check array devices foreach ($disks as $disk) { $name = $disk['name']; if ($name=='flash' || substr($disk['status'],-3)=='_NP') continue; $text = my_disk($name).($name=='cache'||$name=='parity'?' disk':''); $info = !empty($disk['id']) ? "{$disk['id']} ({$disk['device']})" : "No device identification ({$disk['device']})"; // process disk temperature notifications check_temp($name,$disk['temp'],$text,$info); // process disk SMART notifications check_smart($name,$disk['device'],$text,$info); // process disk usage notifications check_usage($name,($disk['fsSize']?100-round(100*$disk['fsFree']/$disk['fsSize']):-1),$text,$info); // process disk operation notifications $warn = strtok($disk['color'],'-'); $item = 'disk'; $last = $saved[$item][$name] ?? ''; switch ($warn) { case 'red': if ($warn!=$last) { $status = strtolower(str_replace(['NP_','_'],['',' '],$disk['status'])); exec("$notify -e ".escapeshellarg("unRAID $text error")." -s ".escapeshellarg("Alert [$server] - $text in error state ($status)")." -d ".escapeshellarg("$info")." -i \"alert\""); $saved[$item][$name] = $warn; } break; case 'yellow': if ($warn!=$last) { $status = $name=='parity' ? "parity-sync in progress" : "drive not ready, content being reconstructed"; exec("$notify -e ".escapeshellarg("unRAID $text error")." -s ".escapeshellarg("Warning [$server] - $text, $status")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$name] = $warn; } break; default: if ($last) { exec("$notify -e ".escapeshellarg("unRAID $text message")." -s ".escapeshellarg("Notice [$server] - $text returned to normal operation")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } break;} // count disk errors if ($disk['numErrors']>0) $errors[] = "$text - $info (errors {$disk['numErrors']})"; // check file system of cache pool $item = 'pool'; if ($name=='cache' && strpos($disk['fsType'],'btrfs')) { $attr = 'missing'; if (exec("/sbin/btrfs filesystem show {$disk['uuid']} 2>/dev/null|grep -c 'missing'")>0) { if (empty($saved[$item][$attr])) { exec("$notify -e ".escapeshellarg("unRAID $text message")." -s ".escapeshellarg("Warning [$server] - Cache pool BTRFS missing device(s)")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$attr] = 1; } } elseif (isset($saved[$item][$attr])) unset($saved[$item][$attr]); $attr = 'profile'; if (exec("/sbin/btrfs filesystem df /mnt/cache 2>/dev/null|grep -c '^Data'")>1) { if (empty($saved[$item][$attr])) { exec("$notify -e ".escapeshellarg("unRAID $text message")." -s ".escapeshellarg("Warning [$server] - Cache pool BTRFS too many profiles")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$attr] = 1; } } elseif (isset($saved[$item][$attr])) unset($saved[$item][$attr]); } } // check unassigned devices $type = $var['smType'] ?? ''; foreach ($devs as $dev) { $name = $dev['device']; $smart = "/var/local/emhttp/smart/$name"; $port = substr($name,-2)!='n1' ? $name : substr($name,0,-2); if (!file_exists($smart) || (time()-filectime($smart)>=$var['poll_attributes'])) exec("smartctl -n standby -A $type ".escapeshellarg("/dev/$port")." >".escapeshellarg($smart)); $temp = exec("awk 'BEGIN{t=\"*\"} \$1==190||\$1==194{t=\$10;exit};\$1==\"Temperature:\"{t=\$2;exit} END{print t}' ".escapeshellarg($smart)." 2>/dev/null"); $text = "device $name"; $info = !empty($dev['id']) ? "{$dev['id']} ($name)": "No device identification ($name)"; // process disk temperature notifications check_temp($name,$temp,$text,$info); // process disk SMART notifications check_smart($name,$dev['device'],$text,$info); } // report array read errors $item = 'array'; $name = 'errors'; $last = $saved[$item][$name] ?? 0; $warn = count($errors); $info = "Array has $warn disk".($warn==1 ? "" : "s")." with read errors"; if ($warn>0) { if ($warn<>$last) { $message = implode('\n', $errors); exec("$notify -e \"unRAID array errors\" -s ".escapeshellarg("Warning [$server] - array has errors")." -d ".escapeshellarg("$info")." -m ".escapeshellarg("$message")." -i \"warning\""); $saved[$item][$name] = $warn; } } else { if ($last) { exec("$notify -e \"unRAID array errors\" -s ".escapeshellarg("Notice [$server] - array turned good")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } } // process parity check, parity sync and data-rebuild notifications $name = 'parity'; $last = $saved[$item][$name] ?? ''; if ($var['mdResync']>0) { if (!$last) { if (strstr($var['mdResyncAction'],"recon")) { $last = 'Parity sync / Data rebuild'; } elseif (strstr($var['mdResyncAction'],"clear")) { $last = 'Disk clear'; } elseif ($var['mdResyncAction']=="check") { $last = 'Read check'; } elseif (strstr($var['mdResyncAction'],"check")) { $last = 'Parity check'; } $info = "Size: ".my_scale($var['mdResyncSize']*1024,$unit)." $unit"; exec("$notify -e ".escapeshellarg("unRAID $last")." -s ".escapeshellarg("Notice [$server] - $last started")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$name] = $last; } } else { if ($last) { $duration = $var['sbSynced2'] - $var['sbSynced']; $status = $var['sbSyncExit']; $speed = $status==0 ? my_scale($var['mdResyncSize']*1024/$duration,$unit,1)." $unit/s" : "Unavailable"; list($entry,$duration,$speed,$status,$error) = explode('|', read_write_parity_log($var['sbSynced2'],$duration,$speed,$status,$var['sbSyncErrs'])); $info = $status==0 ? "Duration: ".my_check($duration, $speed) : ($status==-4 ? "Canceled" : "Error code: $status"); $level = ($status==0 && $var['sbSyncErrs']==0) ? "normal" : "warning"; exec("$notify -e ".escapeshellarg("unRAID $last")." -s ".escapeshellarg("Notice [$server] - $last finished ($error errors)")." -d ".escapeshellarg("$info")." -i \"$level\""); unset($saved[$item][$name]); } } // check read-write status of USB flash drive $name = 'flash'; $last = $saved[$item][$name] ?? ''; $warn = exec("grep -Pom1 '/boot \S+ \K\S{2}' /proc/mounts"); $info = "{$disks['flash']['id']} ({$disks['flash']['device']})"; if ($warn!="rw") { if ($warn!=$last) { exec("$notify -e \"USB flash drive failure\" -s ".escapeshellarg("Alert [$server] - USB drive is not read-write")." -d ".escapeshellarg("$info")." -i \"alert\""); $saved[$item][$name] = $warn; } } else { if ($last) { exec("$notify -e \"USB flash drive operation\" -s ".escapeshellarg("Notice [$server] - USB drive returned to normal operation")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } } // check docker image disk utilization system('mountpoint -q /var/lib/docker', $retval); if ($retval===0) { $item = 'system'; $name = 'docker'; $last = $saved[$item][$name] ?? ''; if (file_exists("/boot/config/docker.cfg")) { $cfg = parse_ini_file("/boot/config/docker.cfg"); $info = "Docker utilization of image file {$cfg['DOCKER_IMAGE_FILE']}"; } else $info = "Docker image file not specified"; $warn = exec("df /var/lib/docker|awk '/^\//{print $5*1}'"); if ($warn>=$high1 && $high1>0) { if ($warn>$last) { exec("$notify -e \"Docker critical image disk utilization\" -s ".escapeshellarg("Alert [$server] - Docker image disk utilization of ${warn}%")." -d ".escapeshellarg("$info")." -i \"alert\""); $saved[$item][$name] = $warn; } } elseif ($warn>=$high2 && $high2>0) { if ($warn>$last) { exec("$notify -e \"Docker high image disk utilization\" -s ".escapeshellarg("Warning [$server] - Docker image disk utilization of ${warn}%")." -d ".escapeshellarg("$info")." -i \"warning\""); $saved[$item][$name] = $warn; } } else { if ($last) { exec("$notify -e \"Docker image disk utilization\" -s ".escapeshellarg("Notice [$server] - Docker image disk utilization returned to normal level")." -d ".escapeshellarg("$info")); unset($saved[$item][$name]); } } } // save new status if ($saved) { $text = ''; foreach ($saved as $item => $block) { if ($block) $text .= "[$item]\n"; foreach ($block as $key => $value) $text .= "$key=\"$value\"\n"; } if ($text) { if ($text != @file_get_contents($ram)) file_put_contents($ram, $text); if (!file_exists($rom) || exec("diff -q $ram $rom")) file_put_contents($rom, $text); } else { @unlink($ram); @unlink($rom); } } exit(0); ?>