mirror of
https://github.com/unraid/webgui.git
synced 2025-12-30 22:20:23 -06:00
990 lines
33 KiB
PHP
990 lines
33 KiB
PHP
<?PHP
|
|
/* Copyright 2005-2025, Lime Technology
|
|
* Copyright 2012-2025, Bergware International.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License version 2,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*/
|
|
?>
|
|
<?
|
|
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
|
require_once "$docroot/webGui/include/Wrappers.php";
|
|
require_once "$docroot/webGui/include/Secure.php";
|
|
|
|
// Helper functions
|
|
function my_scale($value, &$unit, $decimals=NULL, $scale=NULL, $kilo=1000) {
|
|
global $display, $language;
|
|
$scale = $scale ?? $display['scale'];
|
|
$number = _var($display,'number','.,');
|
|
$units = explode(' ', ' '.($kilo==1000 ? ($language['prefix_SI'] ?? 'K M G T P E Z Y') : ($language['prefix_IEC'] ?? 'Ki Mi Gi Ti Pi Ei Zi Yi')));
|
|
$size = count($units);
|
|
if ($scale == 0 && ($decimals === NULL || $decimals < 0)) {
|
|
$decimals = 0;
|
|
$unit = '';
|
|
} else {
|
|
$base = $value ? intval(floor(log($value, $kilo))) : 0;
|
|
if ($scale > 0 && $base > $scale) $base = $scale;
|
|
if ($base > $size) $base = $size - 1;
|
|
$value /= pow($kilo, $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]._('B');
|
|
}
|
|
return number_format($value, $decimals, $number[0], $value > 9999 ? $number[1] : '');
|
|
}
|
|
|
|
function my_number($value) {
|
|
global $display;
|
|
$number = _var($display,'number','.,');
|
|
return number_format($value, 0, $number[0], ($value >= 10000 ? $number[1] : ''));
|
|
}
|
|
|
|
function my_time($time, $fmt=NULL) {
|
|
global $display;
|
|
if (!$fmt) $fmt = _var($display,'date').(_var($display,'date')!='%c' ? ", "._var($display,'time') : "");
|
|
return $time ? my_date($fmt, $time) : _('unknown');
|
|
}
|
|
|
|
function my_temp($value) {
|
|
global $display;
|
|
$unit = _var($display,'unit','C');
|
|
$number = _var($display,'number','.,');
|
|
return is_numeric($value) ? (($unit == 'F' ? fahrenheit($value) : str_replace('.', $number[0], $value)).' °'.$unit) : $value;
|
|
}
|
|
|
|
function my_disk($name, $raw=false) {
|
|
global $display;
|
|
return _var($display,'raw') || $raw ? $name : ucfirst(preg_replace('/(\d+)$/',' $1',$name));
|
|
}
|
|
|
|
function my_disks($disk) {
|
|
return strpos(_var($disk,'status'),'_NP') === false;
|
|
}
|
|
|
|
function my_hyperlink($text, $link) {
|
|
return str_replace(['[',']'],["<a href=\"$link\">","</a>"],$text);
|
|
}
|
|
|
|
function main_only($disk) {
|
|
return _var($disk,'type') == 'Parity' || _var($disk,'type') == 'Data';
|
|
}
|
|
|
|
function parity_only($disk) {
|
|
return _var($disk,'type') == 'Parity';
|
|
}
|
|
|
|
function data_only($disk) {
|
|
return _var($disk,'type') == 'Data';
|
|
}
|
|
|
|
function cache_only($disk) {
|
|
return _var($disk,'type') == 'Cache';
|
|
}
|
|
|
|
function luks_only($disk) {
|
|
return _var($disk,'type') == 'Data' || _var($disk,'type') == 'Cache';
|
|
}
|
|
|
|
function main_filter($disks) {
|
|
return array_filter($disks, 'main_only');
|
|
}
|
|
|
|
function parity_filter($disks) {
|
|
return array_filter($disks, 'parity_only');
|
|
}
|
|
|
|
function data_filter($disks) {
|
|
return array_filter($disks, 'data_only');
|
|
}
|
|
|
|
function cache_filter($disks) {
|
|
return array_filter($disks, 'cache_only');
|
|
}
|
|
|
|
function luks_filter($disks) {
|
|
return array_filter($disks, 'luks_only');
|
|
}
|
|
|
|
function pools_filter($disks) {
|
|
return array_unique(array_map('prefix', array_keys(cache_filter($disks))));
|
|
}
|
|
|
|
function my_id($id) {
|
|
global $display;
|
|
$len = strlen($id);
|
|
$wwn = substr($id,-18);
|
|
return (_var($display,'wwn') || substr($wwn,0,2) != '_3' || preg_match('/.[_-]/',$wwn)) ? $id : substr($id,0,$len-18);
|
|
}
|
|
|
|
function my_word($num) {
|
|
$words = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen','twenty','twenty-one','twenty-two','twenty-three','twenty-four','twenty-five','twenty-six','twenty-seven','twenty-eight','twenty-nine','thirty'];
|
|
return $num < count($words) ? _($words[$num],1) : $num;
|
|
}
|
|
|
|
function my_usage() {
|
|
global $disks, $var, $display;
|
|
$arraysize = 0;
|
|
$arrayfree = 0;
|
|
foreach ($disks as $disk) {
|
|
if (strpos(_var($disk,'name'),'disk') !== false) {
|
|
$arraysize += _var($disk,'sizeSb',0);
|
|
$arrayfree += _var($disk,'fsFree',0);
|
|
}
|
|
}
|
|
if (_var($var,'fsNumMounted',0) > 0) {
|
|
$used = $arraysize ? 100-round(100*$arrayfree/$arraysize) : 0;
|
|
echo "<div class='usage-bar'><span style='width:{$used}%' class='".usage_color($display,$used,false)."'>{$used}%</span></div>";
|
|
} else {
|
|
echo "<div class='usage-bar'><span style='text-align:center'>".($var['fsState']=='Started'?'Maintenance':'off-line')."</span></div>";
|
|
}
|
|
}
|
|
|
|
function usage_color(&$disk, $limit, $free) {
|
|
global $display;
|
|
if (_var($display,'text',0) == 1 || intval(_var($display,'text',0)/10) == 1) return '';
|
|
$critical = _var($disk,'critical') >= 0 ? $disk['critical'] : (_var($display,'critical') >= 0 ? $display['critical'] : 0);
|
|
$warning = _var($disk,'warning') >= 0 ? $disk['warning'] : (_var($display,'warning') >= 0 ? $display['warning'] : 0);
|
|
if (!$free) {
|
|
if ($critical > 0 && $limit >= $critical) return 'redbar';
|
|
if ($warning > 0 && $limit >= $warning) return 'orangebar';
|
|
return 'greenbar';
|
|
} else {
|
|
if ($critical > 0 && $limit <= 100-$critical) return 'redbar';
|
|
if ($warning > 0 && $limit <= 100-$warning) return 'orangebar';
|
|
return 'greenbar';
|
|
}
|
|
}
|
|
|
|
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 = floor($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').": ".(is_numeric($speed) ? my_scale($speed,$unit,1)." $unit/s" : $speed);
|
|
}
|
|
|
|
function my_error($code) {
|
|
switch ($code) {
|
|
case -4:
|
|
return "<em>"._('aborted')."</em>";
|
|
default:
|
|
return "<strong>$code</strong>";
|
|
}
|
|
}
|
|
|
|
function mk_option($select, $value, $text, $extra="") {
|
|
return "<option value='$value'".($value == $select ? " selected" : "").(strlen($extra) ? " $extra" : "").">$text</option>";
|
|
}
|
|
|
|
function mk_option_check($name, $value, $text="") {
|
|
if ($text) {
|
|
$checked = in_array($value,explode(',',$name)) ? " selected" : "";
|
|
return "<option value='$value'$checked>$text</option>";
|
|
}
|
|
if (strpos($name,'disk') !== false) {
|
|
$checked = in_array($name,explode(',',$value)) ? " selected" : "";
|
|
return "<option value='$name'$checked>".my_disk($name)."</option>";
|
|
}
|
|
}
|
|
|
|
function mk_option_luks($name, $value, $luks) {
|
|
if (strpos($name,'disk') !== false) {
|
|
$checked = in_array($name,explode(',',$value)) ? " selected" : "";
|
|
return "<option luks='$luks' value='$name'$checked>".my_disk($name)."</option>";
|
|
}
|
|
}
|
|
|
|
function day_count($time) {
|
|
global $var;
|
|
if (!$time) return;
|
|
$datetz = new DateTimeZone($var['timeZone']);
|
|
$date = new DateTime("now", $datetz);
|
|
$offset = $datetz->getOffset($date);
|
|
$now = new DateTime("@".intval((time()+$offset)/86400)*86400);
|
|
$last = new DateTime("@".intval(($time+$offset)/86400)*86400);
|
|
$days = date_diff($last,$now)->format('%a');
|
|
switch (true) {
|
|
case ($days < 0):
|
|
return;
|
|
case ($days == 0):
|
|
return " <span class='green-text'>("._('today').")</span>";
|
|
case ($days == 1):
|
|
return " <span class='green-text'>("._('yesterday').")</span>";
|
|
case ($days <= 31):
|
|
return " <span class='green-text'>(".sprintf(_('%s days ago'),my_word($days)).")</span>";
|
|
case ($days <= 61):
|
|
return " <span class='orange-text'>(".sprintf(_('%s days ago'),$days).")</span>";
|
|
case ($days > 61):
|
|
return " <span class='red-text'>(".sprintf(_('%s days ago'),$days).")</span>";
|
|
}
|
|
}
|
|
|
|
function plus($val, $word, $last) {
|
|
return $val > 0 ? (($val || $last) ? ($val.' '._($word.($val != 1 ? 's' : '')).($last ? '' : ', ')) : '') : '';
|
|
}
|
|
|
|
function compress($name, $size=18, $end=6) {
|
|
return mb_strlen($name) <= $size ? $name : mb_substr($name, 0, $size-($end ? $end+3 : 0)).'...'.($end ? mb_substr($name,-$end) : '');
|
|
}
|
|
|
|
function escapestring($name) {
|
|
return "\"$name\"";
|
|
}
|
|
|
|
function tail($file, $rows=1) {
|
|
$file = new SplFileObject($file);
|
|
$file->seek(PHP_INT_MAX);
|
|
$file->seek($file->key()-$rows);
|
|
$echo = [];
|
|
while (!$file->eof()) {
|
|
$echo[] = $file->current();
|
|
$file->next();
|
|
}
|
|
return implode($echo);
|
|
}
|
|
|
|
/* Get the last parity check from the parity history. */
|
|
function last_parity_log() {
|
|
$log = '/boot/config/parity-checks.log';
|
|
if (file_exists($log)) {
|
|
[$date, $duration, $speed, $status, $error, $action, $size] = my_explode('|', tail($log), 7);
|
|
} else {
|
|
[$date, $duration, $speed, $status, $error, $action, $size] = array_fill(0, 7, 0);
|
|
}
|
|
if ($date) {
|
|
[$y, $m, $d, $t] = my_preg_split('/ +/', $date, 4);
|
|
$date = strtotime("$d-$m-$y $t");
|
|
}
|
|
return [$date, $duration, $speed, $status, $error, $action, $size];
|
|
}
|
|
|
|
|
|
/* Get the last parity check from Unraid. */
|
|
function last_parity_check() {
|
|
global $var;
|
|
/* Files for the latest parity check. */
|
|
$stamps = '/var/tmp/stamps.ini';
|
|
$resync = '/var/tmp/resync.ini';
|
|
/* Get the latest parity information from Unraid. */
|
|
$synced = file_exists($stamps) ? explode(',',file_get_contents($stamps)) : [];
|
|
$sbSynced = array_shift($synced) ?: _var($var,'sbSynced',0);
|
|
$idle = [];
|
|
while (count($synced) > 1) {
|
|
$idle[] = array_pop($synced) - array_pop($synced);
|
|
}
|
|
$action = _var($var, 'mdResyncAction');
|
|
$size = _var($var, 'mdResyncSize', 0);
|
|
if (file_exists($resync)) {
|
|
list($action, $size) = my_explode(',', file_get_contents($resync));
|
|
}
|
|
$duration = $var['sbSynced2']-$sbSynced-array_sum($idle);
|
|
$status = _var($var,'sbSyncExit');
|
|
$speed = $status==0 ? round($size*1024/$duration) : 0;
|
|
$error = _var($var,'sbSyncErrs',0);
|
|
return [$duration, $speed, $status, $error, $action, $size];
|
|
}
|
|
|
|
function urlencode_path($path) {
|
|
return str_replace("%2F", "/", urlencode($path));
|
|
}
|
|
|
|
function check_deprecated_filesystem($disk) {
|
|
$fsType = _var($disk, 'fsType', '');
|
|
$name = _var($disk, 'name', '');
|
|
$warnings = [];
|
|
|
|
// Check for ReiserFS
|
|
if (stripos($fsType, 'reiserfs') !== false) {
|
|
$warnings[] = [
|
|
'type' => 'reiserfs',
|
|
'severity' => 'critical',
|
|
'message' => _('ReiserFS is deprecated and will not be supported in future Unraid releases')
|
|
];
|
|
}
|
|
|
|
// Check for XFS v4 (lacks CRC checksums)
|
|
if (stripos($fsType, 'xfs') !== false) {
|
|
// Check if disk is mounted to determine XFS version
|
|
$mountPoint = "/mnt/$name";
|
|
if (is_dir($mountPoint) && exec("mountpoint -q " . escapeshellarg($mountPoint) . " 2>/dev/null", $output, $ret) && $ret == 0) {
|
|
// Check for crc=0 which indicates XFS v4
|
|
$xfsInfo = shell_exec("xfs_info " . escapeshellarg($mountPoint) . " 2>/dev/null");
|
|
if ($xfsInfo && strpos($xfsInfo, 'crc=0') !== false) {
|
|
$warnings[] = [
|
|
'type' => 'xfs_v4',
|
|
'severity' => 'critical',
|
|
'message' => _('XFS v4 is deprecated and will not be supported in future Unraid releases. Please migrate to XFS v5 immediately')
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $warnings;
|
|
}
|
|
|
|
function get_filesystem_warning_icon($warnings) {
|
|
if (empty($warnings)) return '';
|
|
|
|
$hasCritical = false;
|
|
$messages = [];
|
|
|
|
foreach ($warnings as $warning) {
|
|
if ($warning['severity'] == 'critical') {
|
|
$hasCritical = true;
|
|
}
|
|
$messages[] = $warning['message'];
|
|
}
|
|
|
|
$icon = $hasCritical ? 'exclamation-triangle' : 'exclamation-circle';
|
|
$color = $hasCritical ? 'red-text' : 'orange-text';
|
|
$tooltip = implode('. ', $messages);
|
|
|
|
return " <i class='fa fa-$icon $color' title='$tooltip'></i>";
|
|
}
|
|
|
|
function pgrep($process_name, $escape_arg=true) {
|
|
$pid = exec('pgrep --ns $$ '.($escape_arg ? escapeshellarg($process_name) : $process_name), $output, $retval);
|
|
return $retval == 0 ? $pid : false;
|
|
}
|
|
|
|
function is_block($path) {
|
|
return (@filetype(realpath($path)) == 'block');
|
|
}
|
|
|
|
function autov($file, $ret=false) {
|
|
global $docroot;
|
|
$path = $docroot.$file;
|
|
clearstatcache(true, $path);
|
|
$time = file_exists($path) ? filemtime($path) : 'autov_fileDoesntExist';
|
|
$newFile = "$file?v=".$time;
|
|
if ($ret)
|
|
return $newFile;
|
|
else
|
|
echo $newFile;
|
|
}
|
|
|
|
function transpose_user_path($path) {
|
|
if (strpos($path,'/mnt/user/') === 0 && file_exists($path)) {
|
|
$realdisk = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg($path)." 2>/dev/null"));
|
|
if (!empty($realdisk))
|
|
$path = str_replace('/mnt/user/', "/mnt/$realdisk/", $path);
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
function cpu_list() {
|
|
exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list|sort -nu', $cpus);
|
|
return $cpus;
|
|
}
|
|
|
|
function my_explode($split, $text, $count=2) {
|
|
return array_pad(explode($split, $text??"", $count), $count, '');
|
|
}
|
|
|
|
function my_preg_split($split, $text, $count=2) {
|
|
return array_pad(preg_split($split, $text, $count), $count, '');
|
|
}
|
|
|
|
function delete_file(...$file) {
|
|
array_map('unlink', array_filter($file,'file_exists'));
|
|
}
|
|
|
|
function my_mkdir($dirname, $permissions=0777, $recursive=false, $own="nobody", $grp="users") {
|
|
write_logging("Check if dir exists\n");
|
|
if (is_dir($dirname)) {write_logging("Dir exists\n"); return(false);}
|
|
write_logging("Dir does not exist\n");
|
|
$parent = $dirname;
|
|
write_logging("Getting $parent\n");
|
|
while (!is_dir($parent)){
|
|
if (!is_dir($parent)) write_logging("Not parent $parent\n"); else write_logging("Parent $parent is\n");
|
|
if (!$recursive) return(false);
|
|
$pathinfo2 = pathinfo($parent);
|
|
$parent = $pathinfo2["dirname"];
|
|
}
|
|
write_logging("Parent $parent\n");
|
|
if (strpos($dirname,'/mnt/user/') === 0) {
|
|
write_logging("Getting real disks\n");
|
|
$realdisk = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg($parent)." 2>/dev/null"));
|
|
if (!empty($realdisk)) {
|
|
$dirname = str_replace('/mnt/user/', "/mnt/$realdisk/", $dirname);
|
|
$parent = str_replace('/mnt/user/', "/mnt/$realdisk/", $parent);
|
|
}
|
|
}
|
|
$fstype = trim(shell_exec(" stat -f -c '%T' $parent"));
|
|
$rtncode = false;
|
|
write_logging("fstype:$fstype parent $parent dir name $dirname\n");
|
|
switch ($fstype) {
|
|
case "zfs":
|
|
if (is_dir($parent.'/.zfs')) {
|
|
write_logging("ZFS Volume\n");
|
|
$zfsdataset = trim(shell_exec("zfs list -H -o name $parent"));
|
|
write_logging("Shell $zfsdataset\n");
|
|
$zfsdataset .= str_replace($parent,"",$dirname);
|
|
write_logging("Dataset $zfsdataset\n");
|
|
$zfsoutput = array();
|
|
if ($recursive) exec("zfs create -p \"$zfsdataset\"",$zfsoutput,$rtncode);else exec("zfs create \"$zfsdataset\"", $zfsoutput, $rtncode);
|
|
write_logging("Output: {$zfsoutput[0]} $rtncode");
|
|
if ($rtncode == 0) write_logging( " ZFS Command OK\n"); else write_logging( "ZFS Command Fail\n");
|
|
} else {write_logging("Not ZFS dataset\n");$rtncode = 1;}
|
|
if ($rtncode > 0) { mkdir($dirname, $permissions, $recursive); write_logging( "created dir:$dirname\n");} else chmod($zfsdataset, $permissions);
|
|
break;
|
|
case "btrfs":
|
|
$btrfsoutput = array();
|
|
if ($recursive) exec("btrfs subvolume create --parents \"$dirname\"",$btrfsoutput,$rtncode); else exec("btrfs subvolume create \"$dirname\"", $btrfsoutput, $rtncode);
|
|
if ($rtncode > 0) mkdir($dirname, $permissions, $recursive); else chmod($dirname, $permissions);
|
|
break;
|
|
default:
|
|
mkdir($dirname, $permissions, $recursive);
|
|
break;
|
|
}
|
|
chown($dirname, $own);
|
|
chgrp($dirname, $grp);
|
|
return($rtncode);
|
|
}
|
|
|
|
function my_rmdir($dirname) {
|
|
if (!is_dir("$dirname")) {
|
|
$return = [
|
|
'rtncode' => "false",
|
|
'type' => "NoDir",
|
|
];
|
|
return($return);
|
|
}
|
|
if (strpos($dirname,'/mnt/user/') === 0) {
|
|
$realdisk = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg($dirname)." 2>/dev/null"));
|
|
if (!empty($realdisk)) {
|
|
$dirname = str_replace('/mnt/user/', "/mnt/$realdisk/", "$dirname");
|
|
}
|
|
}
|
|
$fstype = trim(shell_exec(" stat -f -c '%T' ".escapeshellarg($dirname)));
|
|
$rtncode = false;
|
|
switch ($fstype) {
|
|
case "zfs":
|
|
$zfsoutput = array();
|
|
$zfsdataset = trim(shell_exec("zfs list -H -o name ".escapeshellarg($dirname))) ;
|
|
$cmdstr = "zfs destroy \"$zfsdataset\" 2>&1 ";
|
|
$error = exec($cmdstr,$zfsoutput,$rtncode);
|
|
$return = [
|
|
'rtncode' => $rtncode,
|
|
'output' => $zfsoutput,
|
|
'dataset' => $zfsdataset,
|
|
'type' => $fstype,
|
|
'cmd' => $cmdstr,
|
|
'error' => $error,
|
|
];
|
|
break;
|
|
case "btrfs":
|
|
default:
|
|
$rtncode = rmdir($dirname);
|
|
$return = [
|
|
'rtncode' => $rtncode,
|
|
'type' => $fstype,
|
|
];
|
|
break;
|
|
}
|
|
return($return);
|
|
}
|
|
|
|
function get_realvolume($path) {
|
|
if (strpos($path,"/mnt/user/",0) === 0)
|
|
$reallocation = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg($path)." 2>/dev/null"));
|
|
else {
|
|
$realexplode = explode("/",str_replace("/mnt/","",$path));
|
|
$reallocation = $realexplode[0];
|
|
}
|
|
return $reallocation;
|
|
}
|
|
|
|
function write_logging($value) {
|
|
$debug = is_file("/tmp/my_mkdir_debug");
|
|
if (!$debug) return;
|
|
file_put_contents('/tmp/my_mkdir_output', $value, FILE_APPEND);
|
|
}
|
|
|
|
function device_exists($name) {
|
|
global $disks, $devs;
|
|
return (array_key_exists($name, $disks) && !str_contains(_var($disks[$name],'status'),'_NP')) || (array_key_exists($name, $devs));
|
|
}
|
|
|
|
# Check for process Core Types.
|
|
function parse_cpu_ranges($file) {
|
|
if (!is_file($file)) return null;
|
|
$ranges = file_get_contents($file);
|
|
$ranges = trim($ranges);
|
|
if ($ranges === '') return null;
|
|
$cores = [];
|
|
foreach (explode(',', $ranges) as $range) {
|
|
if (strpos($range, '-') !== false) {
|
|
list($start, $end) = explode('-', $range);
|
|
$cores = array_merge($cores, range((int)$start, (int)$end));
|
|
} else {
|
|
$cores[] = (int)$range;
|
|
}
|
|
}
|
|
return $cores;
|
|
}
|
|
|
|
function get_intel_core_types() {
|
|
$core_types = array();
|
|
$cpu_core_file = "/sys/devices/cpu_core/cpus";
|
|
$cpu_atom_file = "/sys/devices/cpu_atom/cpus";
|
|
$p_cores = parse_cpu_ranges($cpu_core_file);
|
|
$e_cores = parse_cpu_ranges($cpu_atom_file);
|
|
if ($p_cores) {
|
|
foreach ($p_cores as $core) {
|
|
$core_types[$core] = _("P-Core");
|
|
}
|
|
}
|
|
if ($e_cores) {
|
|
foreach ($e_cores as $core) {
|
|
$core_types[$core] = _("E-Core");
|
|
}
|
|
}
|
|
return $core_types;
|
|
}
|
|
|
|
function dmidecode($key, $n, $all=true) {
|
|
$entries = array_filter(explode($key, shell_exec("dmidecode -qt$n")??""));
|
|
$properties = [];
|
|
foreach ($entries as $entry) {
|
|
$property = [];
|
|
foreach (explode("\n",$entry) as $line) if (strpos($line,': ') !== false) {
|
|
[$key, $value] = my_explode(': ',trim($line));
|
|
$property[$key] = $value;
|
|
}
|
|
$properties[] = $property;
|
|
}
|
|
return $all ? $properties : $properties[0] ?? null;
|
|
}
|
|
|
|
function is_intel_cpu() {
|
|
$cpu_vendor_check = exec("grep -Pom1 '^model name\s+:\s*\K.+' /proc/cpuinfo") ?? "";
|
|
return stripos($cpu_vendor_check, "intel") !== false;
|
|
}
|
|
|
|
// Load saved PCI data
|
|
function loadSavedData($filename) {
|
|
if (file_exists($filename)) {
|
|
$saveddata = file_get_contents($filename);
|
|
} else $saveddata = "";
|
|
return json_decode($saveddata, true);
|
|
}
|
|
|
|
// Run lspci -Dmn to get the current devices
|
|
function loadCurrentPCIData() {
|
|
$output = shell_exec('lspci -Dmn');
|
|
$devices = [];
|
|
if (file_exists("/boot/config/current.json")) {
|
|
$devices = loadSavedData("/boot/config/current.json");
|
|
} else {
|
|
foreach (explode("\n", trim($output)) as $line) {
|
|
$parts = explode(" ", $line);
|
|
if (count($parts) < 6) continue; // Skip malformed lines
|
|
$description_str = shell_exec(("lspci -s ".$parts[0]));
|
|
$description = preg_replace('/^\S+\s+/', '', $description_str);
|
|
$device = [
|
|
'class' => trim($parts[1], '"'),
|
|
'vendor_id' => trim($parts[2], '"'),
|
|
'device_id' => trim($parts[3], '"'),
|
|
'description' => trim($description,'"'),
|
|
];
|
|
$devices[$parts[0]] = $device;
|
|
}
|
|
}
|
|
return $devices;
|
|
}
|
|
|
|
// Compare the saved and current data
|
|
function comparePCIData() {
|
|
$changes = [];
|
|
$saved = loadSavedData("/boot/config/savedpcidata.json");
|
|
if (!$saved) return [];
|
|
$current = loadCurrentPCIData();
|
|
// Compare saved devices with current devices
|
|
foreach ($saved as $pci_id => $saved_device) {
|
|
if (!isset($current[$pci_id])) {
|
|
// Device has been removed
|
|
$changes[$pci_id] = [
|
|
'status' => 'removed',
|
|
'device' => $saved_device
|
|
];
|
|
} else {
|
|
// Device exists in both, check for modifications
|
|
$current_device = $current[$pci_id];
|
|
$differences = [];
|
|
// Compare fields
|
|
foreach (['vendor_id', 'device_id', 'class'] as $field) {
|
|
if (isset($saved_device[$field]) && isset($current_device[$field]) && $saved_device[$field] !== $current_device[$field]) {
|
|
$differences[$field] = [
|
|
'old' => $saved_device[$field],
|
|
'new' => $current_device[$field]
|
|
];
|
|
}
|
|
}
|
|
if (!empty($differences)) {
|
|
$changes[$pci_id] = [
|
|
'status' => 'changed',
|
|
'device' => $current_device,
|
|
'differences' => $differences
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// Check for added devices
|
|
foreach ($current as $pci_id => $current_device) {
|
|
if (!isset($saved[$pci_id])) {
|
|
// Device has been added
|
|
$changes[$pci_id] = [
|
|
'status' => 'added',
|
|
'device' => $current_device
|
|
];
|
|
}
|
|
}
|
|
return $changes;
|
|
}
|
|
|
|
function clone_list($disk) {
|
|
global $pools;
|
|
return strpos($disk['status'],'_NP') === false && ($disk['type'] == 'Data' || in_array($disk['name'], $pools));
|
|
}
|
|
|
|
// Deprecated filesystem detection and display functions
|
|
|
|
// Core function to check a single disk for deprecated filesystems
|
|
function check_disk_for_deprecated_fs($disk) {
|
|
$deprecated = [];
|
|
$fsType = strtolower(_var($disk, 'fsType', ''));
|
|
|
|
// Check for ReiserFS
|
|
if (strpos($fsType, 'reiserfs') !== false) {
|
|
$deprecated[] = [
|
|
'name' => _var($disk, 'name'),
|
|
'fsType' => 'ReiserFS',
|
|
'severity' => 'critical',
|
|
'message' => 'ReiserFS is deprecated and will not be supported in future Unraid releases'
|
|
];
|
|
}
|
|
|
|
// Check for XFS v4 (lacks CRC checksums)
|
|
if (strpos($fsType, 'xfs') !== false) {
|
|
$name = _var($disk, 'name');
|
|
$mountPoint = "/mnt/$name";
|
|
|
|
// Check if disk is mounted
|
|
if (is_dir($mountPoint)) {
|
|
exec("mountpoint -q " . escapeshellarg($mountPoint) . " 2>/dev/null", $output, $ret);
|
|
if ($ret == 0) {
|
|
// Get XFS info to check for crc=0 which indicates XFS v4
|
|
$xfsInfo = shell_exec("xfs_info " . escapeshellarg($mountPoint) . " 2>/dev/null");
|
|
if ($xfsInfo && strpos($xfsInfo, 'crc=0') !== false) {
|
|
$deprecated[] = [
|
|
'name' => $name,
|
|
'fsType' => 'XFS v4',
|
|
'severity' => 'notice',
|
|
'message' => 'XFS v4 is deprecated and will not be supported in future Unraid releases. You have until 2030 to migrate to XFS v5.'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $deprecated;
|
|
}
|
|
|
|
// Generate inline warning HTML for a single disk
|
|
function get_inline_fs_warnings($disk) {
|
|
$warnings = check_disk_for_deprecated_fs($disk);
|
|
$html = '';
|
|
|
|
foreach ($warnings as $warning) {
|
|
if ($warning['severity'] === 'critical') {
|
|
// ReiserFS - critical warning
|
|
$html .= '<span id="reiserfs" class="warning"><i class="fa fa-exclamation-triangle"></i> ' .
|
|
htmlspecialchars(_($warning['message'])) . '</span>';
|
|
} else {
|
|
// XFS v4 - notice (without .notice class to avoid duplicate icon)
|
|
$html .= '<div id="xfsv4" style="color:#0066cc; margin: 5px 0; line-height: 1.5;">' .
|
|
'<i class="fa fa-info-circle"></i> ' .
|
|
htmlspecialchars(_($warning['message'])) . '</div>';
|
|
}
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
// Check array of disks for deprecated filesystems (used by Main page)
|
|
function check_deprecated_filesystems_array($disks, $filter_function) {
|
|
$deprecated = [];
|
|
|
|
foreach ($filter_function($disks) as $disk) {
|
|
if (substr($disk['status'],0,7) != 'DISK_NP') {
|
|
$disk_warnings = check_disk_for_deprecated_fs($disk);
|
|
$deprecated = array_merge($deprecated, $disk_warnings);
|
|
}
|
|
}
|
|
|
|
return $deprecated;
|
|
}
|
|
|
|
function display_deprecated_filesystem_warning($deprecated_disks, $type = 'array') {
|
|
if (empty($deprecated_disks)) return '';
|
|
|
|
// Separate warnings by severity
|
|
$critical_disks = [];
|
|
$notice_disks = [];
|
|
|
|
foreach ($deprecated_disks as $disk) {
|
|
if (_var($disk, 'severity', 'critical') === 'critical') {
|
|
$critical_disks[] = $disk;
|
|
} else {
|
|
$notice_disks[] = $disk;
|
|
}
|
|
}
|
|
|
|
$html = '';
|
|
|
|
// Critical warnings (ReiserFS) - severe styling, reappears on every page load
|
|
if (!empty($critical_disks)) {
|
|
$id = $type === 'array' ? 'array-critical-warning' : 'pool-critical-warning';
|
|
$title = htmlspecialchars($type === 'array' ? 'Critical: Deprecated Filesystem' : 'Critical: Pool Deprecated Filesystem');
|
|
$description = htmlspecialchars($type === 'array' ?
|
|
'The following array devices are using deprecated filesystems:' :
|
|
'The following pool devices are using deprecated filesystems:');
|
|
|
|
$diskList = '';
|
|
foreach ($critical_disks as $disk) {
|
|
$name = htmlspecialchars($disk['name']);
|
|
$fsType = htmlspecialchars($disk['fsType']);
|
|
$message = htmlspecialchars($disk['message']);
|
|
$diskList .= "<li><strong>{$name}:</strong> {$fsType} - {$message}</li>\n";
|
|
}
|
|
|
|
$html .= <<<HTML
|
|
<div id="{$id}" style="margin: 20px 0;">
|
|
<div style="background: #feefb3; border: 1px solid #ff8c2f; border-radius: 4px; padding: 15px; position: relative;">
|
|
<button onclick="$('#{$id}').fadeOut();"
|
|
style="position: absolute; right: 10px; top: 10px; background: transparent; border: none; color: #ff8c2f; cursor: pointer; font-size: 1.2em;">
|
|
<i class="fa fa-times"></i>
|
|
</button>
|
|
<div style="display: flex; align-items: start;">
|
|
<i class="fa fa-exclamation-triangle" style="color: #ff8c2f; margin-right: 10px; font-size: 1.2em;"></i>
|
|
<div style="flex: 1; color: #000;">
|
|
<div style="font-weight: bold; margin-bottom: 10px; color: #ff8c2f;">
|
|
{$title}
|
|
</div>
|
|
<div style="margin-bottom: 10px;">
|
|
{$description}
|
|
</div>
|
|
<ul style="margin: 10px 0 10px 20px;">
|
|
{$diskList}
|
|
</ul>
|
|
<div style="margin-top: 10px;">
|
|
<strong>Action Required:</strong> Migrate to a supported filesystem (XFS v5, BTRFS, or ZFS).
|
|
<a href="https://docs.unraid.net/go/convert-reiser-and-xfs"
|
|
target="_blank" style="color: #ff8c2f;">View migration guide →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
HTML;
|
|
}
|
|
|
|
// Notice warnings (XFS v4) - less severe styling, dismissible until reboot via sessionStorage
|
|
if (!empty($notice_disks)) {
|
|
$id = $type === 'array' ? 'array-notice-warning' : 'pool-notice-warning';
|
|
$title = htmlspecialchars($type === 'array' ? 'Notice: Filesystem Update Available' : 'Notice: Pool Filesystem Update Available');
|
|
$description = htmlspecialchars($type === 'array' ?
|
|
'The following array devices are using older filesystem versions:' :
|
|
'The following pool devices are using older filesystem versions:');
|
|
|
|
$diskList = '';
|
|
foreach ($notice_disks as $disk) {
|
|
$name = htmlspecialchars($disk['name']);
|
|
$fsType = htmlspecialchars($disk['fsType']);
|
|
$message = htmlspecialchars($disk['message']);
|
|
$diskList .= "<li><strong>{$name}:</strong> {$fsType} - {$message}</li>\n";
|
|
}
|
|
|
|
$html .= <<<HTML
|
|
<script>
|
|
// Check if XFS warning was dismissed this session
|
|
if (!sessionStorage.getItem('xfs-{$id}-dismissed')) {
|
|
document.write(`
|
|
<div id="{$id}" style="margin: 20px 0;">
|
|
<div style="background: #e7f3ff; border: 1px solid #0066cc; border-radius: 4px; padding: 15px; position: relative;">
|
|
<button onclick="sessionStorage.setItem('xfs-{$id}-dismissed', 'true'); $('#{$id}').fadeOut();"
|
|
style="position: absolute; right: 10px; top: 10px; background: transparent; border: none; color: #0066cc; cursor: pointer; font-size: 1.2em;"
|
|
title="Dismiss until reboot">
|
|
<i class="fa fa-times"></i>
|
|
</button>
|
|
<div style="display: flex; align-items: start;">
|
|
<i class="fa fa-info-circle" style="color: #0066cc; margin-right: 10px; font-size: 1.2em;"></i>
|
|
<div style="flex: 1; color: #000;">
|
|
<div style="font-weight: bold; margin-bottom: 10px; color: #0066cc;">
|
|
{$title}
|
|
</div>
|
|
<div style="margin-bottom: 10px;">
|
|
{$description}
|
|
</div>
|
|
<ul style="margin: 10px 0 10px 20px;">
|
|
{$diskList}
|
|
</ul>
|
|
<div style="margin-top: 10px;">
|
|
<strong>Recommendation:</strong> Plan to migrate to XFS v5, BTRFS, or ZFS within the next 5 years.
|
|
<a href="https://docs.unraid.net/go/convert-reiser-and-xfs"
|
|
target="_blank" style="color: #0066cc;">View migration guide →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
</script>
|
|
HTML;
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
function get_cpu_packages(string $separator = ','): array {
|
|
$packages = [];
|
|
foreach (glob("/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list") as $path) {
|
|
$pkg_id = (int)file_get_contents(dirname($path) . "/physical_package_id");
|
|
$siblings = str_replace(",", $separator, trim(file_get_contents($path)));
|
|
if (!in_array($siblings, $packages[$pkg_id] ?? [])) {
|
|
$packages[$pkg_id][] = $siblings;
|
|
}
|
|
}
|
|
foreach ($packages as &$list) {
|
|
$keys = array_map(fn($s) => (int)explode($separator, $s)[0], $list);
|
|
array_multisort($keys, SORT_ASC, SORT_NUMERIC, $list);
|
|
}
|
|
unset($list);
|
|
return $packages;
|
|
}
|
|
|
|
function getIpAddressesByPci(string $pciAddress): array
|
|
{
|
|
$base = "/sys/bus/pci/devices/$pciAddress/net";
|
|
|
|
if (!is_dir($base)) {
|
|
return []; // PCI address not found or not a network device
|
|
}
|
|
|
|
$interfaces = scandir($base);
|
|
$ips = [];
|
|
|
|
foreach ($interfaces as $iface) {
|
|
if ($iface === '.' || $iface === '..') continue;
|
|
|
|
// Use `ip` command for IPv4 + IPv6
|
|
$cmd = sprintf('ip -o addr show dev %s 2>/dev/null', escapeshellarg($iface));
|
|
$output = shell_exec($cmd);
|
|
|
|
if (!$output) continue;
|
|
|
|
foreach (explode("\n", trim($output)) as $line) {
|
|
|
|
// Match IPv4 like: inet 192.168.1.195/24
|
|
if (preg_match('/inet\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+)/', $line, $m)) {
|
|
$ips[$iface][] = $m[1];
|
|
}
|
|
|
|
// Match IPv6 like: inet6 fe80::1234:abcd/64
|
|
if (preg_match('/inet6\s+([0-9a-fA-F:]+\/[0-9]+)/', $line, $m)) {
|
|
$ips[$iface][] = $m[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $ips;
|
|
}
|
|
|
|
function getSystemNumaNodeCount() {
|
|
return count(glob("/sys/devices/system/node/node*"));
|
|
}
|
|
|
|
function normalizeNumaNode($node, $numNodes) {
|
|
// If system has only 1 node, interpret -1 as node 0
|
|
if ($numNodes === 1 && $node === -1) {
|
|
return 0;
|
|
}
|
|
return $node;
|
|
}
|
|
|
|
function getCpuNumaInfo($numNodes) {
|
|
$cpus = [];
|
|
|
|
foreach (glob("/sys/devices/system/cpu/cpu[0-9]*") as $cpuPath) {
|
|
$cpu = basename($cpuPath);
|
|
|
|
$nodes = glob("$cpuPath/node*");
|
|
$node = -1;
|
|
|
|
if (!empty($nodes)) {
|
|
$node = intval(str_replace("node", "", basename($nodes[0])));
|
|
}
|
|
|
|
$node = normalizeNumaNode($node, $numNodes);
|
|
|
|
$cpus[$cpu] = [
|
|
"cpu_id" => intval(str_replace("cpu", "", $cpu)),
|
|
"numa_node" => $node
|
|
];
|
|
}
|
|
|
|
return $cpus;
|
|
}
|
|
|
|
function getPciNumaInfo($numNodes) {
|
|
$pci = [];
|
|
|
|
foreach (glob("/sys/bus/pci/devices/*") as $devPath) {
|
|
$dev = basename($devPath);
|
|
|
|
$numaNodeFile = "$devPath/numa_node";
|
|
$node = file_exists($numaNodeFile) ? intval(trim(file_get_contents($numaNodeFile))) : -1;
|
|
|
|
$node = normalizeNumaNode($node, $numNodes);
|
|
|
|
$desc = trim(shell_exec("lspci -mm -s $dev 2>/dev/null"));
|
|
|
|
$pci[$dev] = [
|
|
"pci_address" => $dev,
|
|
"numa_node" => $node,
|
|
"description" => $desc
|
|
];
|
|
}
|
|
|
|
return $pci;
|
|
}
|
|
|
|
function getNumaInfo() {
|
|
$numNodes = getSystemNumaNodeCount();
|
|
|
|
$result = [
|
|
"system" => [
|
|
"numa_nodes" => $numNodes
|
|
],
|
|
"cpus" => getCpuNumaInfo($numNodes),
|
|
"pci_devices" => getPciNumaInfo($numNodes)
|
|
];
|
|
|
|
if (is_file("/tmp/numain")) {
|
|
$numain = file_get_contents("/tmp/numain");
|
|
$result = json_decode($numain,true);
|
|
}
|
|
return $result;
|
|
|
|
}
|
|
?>
|