\S+) Device (?P\S+): ID (?P\S+)(?P.*)$/', $usbbusdev, $usbMatch)) { //udevadm info -a --name=/dev/bus/usb/003/002 | grep KERNEL== $udevcmd = "udevadm info -a --name=/dev/bus/usb/".$usbMatch['bus']."/".$usbMatch['dev']." | grep KERNEL=="; $physical_busid = _("None"); exec($udevcmd , $udev); if (isset($udev)) { $physical_busid = trim(substr($udev[0], 13) , '"'); if (substr($physical_busid,0,3) =='usb') { $physical_busid = substr($physical_busid,3).'-0'; } } } return($physical_busid); } /** * Enumerate SR-IOV capable PCI devices (keyed by PCI address). * * Example JSON: * { * "0000:03:00.0": { * "class": "Ethernet controller", * "class_id": "0x0200", * "name": "Intel Corporation X710 for 10GbE SFP+", * "driver": "i40e", * "module": "i40e", * "vf_param": "max_vfs", * "total_vfs": 64, * "num_vfs": 8, * "vfs": [ * {"pci": "0000:03:10.0", "iface": "enp3s0f0v0", "mac": "52:54:00:aa:00:01"} * ] * } * } */ function getSriovInfoJson(bool $includeVfDetails = true): string { $results = []; $paths = glob('/sys/bus/pci/devices/*/sriov_totalvfs') ?: []; foreach ($paths as $totalvfFile) { $devdir = dirname($totalvfFile); $pci = basename($devdir); $total_vfs = (int) @file_get_contents($totalvfFile); $num_vfs = (int) @file_get_contents("$devdir/sriov_numvfs"); // Driver/module detection $driver = $module = $vf_param = null; $driver_link = "$devdir/driver"; if (is_link($driver_link)) { $driver = basename(readlink($driver_link)); $module_link = "$driver_link/module"; $module = is_link($module_link) ? basename(readlink($module_link)) : $driver; $vf_param = detectVfParam($driver); } // Device class + numeric class + name [$class, $class_id, $name] = getPciClassNameAndId($pci); // Virtual functions $vfs = []; foreach (glob("$devdir/virtfn*") as $vf) { if (!is_link($vf)) continue; $vf_pci = basename(readlink($vf)); $vf_entry = ['pci' => $vf_pci]; if ($includeVfDetails) { // Network interface info $net = glob("/sys/bus/pci/devices/{$vf_pci}/net/*"); if ($net && isset($net[0])) { $iface = basename($net[0]); $vf_entry['iface'] = $iface; $macFile = "/sys/class/net/{$iface}/address"; if (is_readable($macFile)) { $vf_entry['mac'] = trim(file_get_contents($macFile)); } } // IOMMU group $iommu_link = "/sys/bus/pci/devices/{$vf_pci}/iommu_group"; if (is_link($iommu_link)) { $vf_entry['iommu_group'] = basename(readlink($iommu_link)); } else { $vf_entry['iommu_group'] = null; } } $vfs[] = $vf_entry; } $results[$pci] = [ 'class' => $class, 'class_id' => $class_id, 'name' => $name, 'driver' => $driver, 'module' => $module, 'vf_param' => $vf_param, 'total_vfs' => $total_vfs, 'num_vfs' => $num_vfs, 'vfs' => $vfs ]; } ksort($results, SORT_NATURAL); return json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } function detectVfParam(string $driver): ?string { if (!function_exists('shell_exec')) return null; $out = @shell_exec('modinfo ' . escapeshellarg($driver) . ' 2>/dev/null'); if (!$out) return null; $lines = explode("\n", strtolower($out)); $params = []; foreach ($lines as $line) { if (preg_match('/^parm:\s+(\S+)/', $line, $m)) $params[] = $m[1]; } foreach (['max_vfs', 'num_vfs', 'sriov_numvfs', 'sriov_vfs'] as $key) if (in_array($key, $params, true)) return $key; foreach ($params as $p) if (preg_match('/vf/', $p)) return $p; return null; } /** * Robustly get PCI class (text + numeric ID) and device name from lspci/sysfs. */ function getPciClassNameAndId(string $pci): array { $class = 'Unknown'; $class_id = null; $name = 'Unknown'; // Numeric class code from sysfs $classFile = "/sys/bus/pci/devices/{$pci}/class"; if (is_readable($classFile)) { $raw = trim(file_get_contents($classFile)); $class_id = sprintf("0x%04x", (hexdec($raw) >> 8) & 0xFFFF); } // Try lspci -mm for machine-readable info $out = trim(@shell_exec('lspci -mm -s ' . escapeshellarg($pci) . ' 2>/dev/null')); if ($out && preg_match('/"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"/', $out, $m)) { $class = $m[1]; $name = trim($m[3]); return [$class, $class_id, $name]; } // Fallback to regular lspci output $alt = trim(@shell_exec('lspci -s ' . escapeshellarg($pci) . ' 2>/dev/null')); if ($alt && preg_match('/^[\da-fA-F:.]+\s+([^:]+):\s+(.+)/', $alt, $m)) { $class = trim($m[1]); $name = trim($m[2]); } return [$class, $class_id, $name]; } /* --- CLI Entry --- */ $sriov = json_decode(getSriovInfoJson(true),true); /** * Enumerate all VFs and group them by IOMMU group * Output: associative array or JSON with keys like "IOMMU group 29" */ function getVfListByIommuGroup(): array { $groups = []; foreach (glob('/sys/bus/pci/devices/*/physfn') as $vf_physfn) { $vf_dir = dirname($vf_physfn); $vf_pci = basename($vf_dir); $iommu_link = "$vf_dir/iommu_group"; if (is_link($iommu_link)) { $iommu_group = basename(readlink($iommu_link)); } else { $iommu_group = "unknown"; } $groups[] = "IOMMU group " . $iommu_group; $groups[] = $vf_pci; } ksort($groups, SORT_NATURAL); return $groups; } $sriovvfs = getVfListByIommuGroup(); switch ($_POST['table']) { case 't1': exec('for group in $(ls /sys/kernel/iommu_groups/ -1|sort -n);do echo "IOMMU group $group";for device in $(ls -1 "/sys/kernel/iommu_groups/$group"/devices/);do echo -n $\'\t\';lspci -ns "$device"|awk \'BEGIN{ORS=" "}{print "["$3"]"}\';lspci -s "$device";done;done',$groups); if (empty($groups)) { exec('lspci -n|awk \'{print "["$3"]"}\'',$iommu); exec('lspci',$lspci); $i = 0; foreach ($lspci as $line) echo "",$iommu[$i++],"$line"; $noiommu = true; } else { $BDF_VD_REGEX = '/^[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]](\|[[:xdigit:]]{4}:[[:xdigit:]]{4})?$/'; $DBDF_VD_REGEX = '/^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]](\|[[:xdigit:]]{4}:[[:xdigit:]]{4})?$/'; $BDF_REGEX = '/^[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]$/'; $DBDF_PARTIAL_REGEX = '/[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]/'; $vfio_cfg_devices = array (); if (is_file("/boot/config/vfio-pci.cfg")) { // accepts space-separated list of or followed by an optional "|" and // example: BIND=03:00.0 0000:03:00.0 03:00.0|8086:1533 0000:03:00.0|8086:1533 // this front-end does not accept by itself, altough the underlying vfio-pci script does $file = file_get_contents("/boot/config/vfio-pci.cfg"); $file = trim(str_replace("BIND=", "", $file)); $file_contents = explode(" ", $file); foreach ($file_contents as $vfio_cfg_device) { if (preg_match($BDF_VD_REGEX, $vfio_cfg_device)) { // only was provided, assume Domain is 0000 (may be followed by optional too) $vfio_cfg_devices[] = "0000:".$vfio_cfg_device; } else if (preg_match($DBDF_VD_REGEX, $vfio_cfg_device)) { // full was provided (may be followed by optional too) $vfio_cfg_devices[] = $vfio_cfg_device; } else { // entry in wrong format, discard } } $vfio_cfg_devices = array_values(array_unique($vfio_cfg_devices, SORT_STRING)); } $DBDF_SRIOV_REGEX = '/^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]\|[[:xdigit:]]{4}:[[:xdigit:]]{4}\|[[:xdigit:]]+$/'; $DBDF_SRIOV_MAC_REGEX = '/^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]\|[[:xdigit:]]{4}:[[:xdigit:]]{4}\|([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$/'; $sriov_devices = array (); if (is_file("/boot/config/sriov.cfg")) { // accepts space-separated list of or followed by an optional "|" and // example: VFS=0000:04:00.1|8086:1521|3 0000:04:00.0|8086:1521|2 // this front-end does not accept by itself, altough the underlying vfio-pci script does $file = file_get_contents("/boot/config/sriov.cfg"); $file = trim(str_replace("VFS=", "", $file)); $file_contents = explode(" ", $file); foreach ($file_contents as $sriov_device) { if (preg_match($DBDF_SRIOV_REGEX, $sriov_device)) { // full was provided (may be followed by optional too) $sriov_devices[] = $sriov_device; } else { // entry in wrong format, discard } } $sriov_devices = array_values(array_unique($sriov_devices, SORT_STRING)); } $sriov_devices_mac = array (); if (is_file("/boot/config/sriovvfs.cfg")) { // accepts space-separated list of or followed by an optional "|" and // example:VFMAC=0000:04:11.5|8086:1520|62:00:04:11:05:01 // this front-end does not accept by itself, altough the underlying vfio-pci script does $file = file_get_contents("/boot/config/sriovvfs.cfg"); $file = trim(str_replace("VFMAC=", "", $file)); $file_contents = explode(" ", $file); foreach ($file_contents as $sriov_device_mac) { if (preg_match($DBDF_SRIOV_MAC_REGEX, $sriov_device_mac)) { // full was provided (may be followed by optional too) $sriov_devices_mac[] = $sriov_device_mac; } else { // entry in wrong format, discard } } $sriov_devices_mac = array_values(array_unique($sriov_devices_mac, SORT_STRING)); } $disks = (array)parse_ini_file('state/disks.ini',true); $devicelist = array_column($disks, 'device'); $lines = array (); foreach ($devicelist as $line) { if (!empty($line)) { exec('udevadm info --path=$(udevadm info -q path /dev/'.$line.' | cut -d / -f 1-7) --query=path',$linereturn); if(isset($linereturn[0])) { preg_match_all($DBDF_PARTIAL_REGEX, $linereturn[0], $inuse); foreach ($inuse[0] as $line) { $lines[] = $line; } } unset($inuse); unset($linereturn); } } $networks = (array)parse_ini_file('state/network.ini',true); $networklist = array_merge(array_column($networks, 'BRNICS'), array_column($networks, 'BONDNICS')); foreach ($networklist as $niclist) { if (!empty($niclist)) { $nics = explode(",", $niclist); if (!empty($nics)) { foreach ($nics as $line) { if (!empty($line)) { exec('readlink /sys/class/net/'.$line,$linereturn); if(isset($linereturn[0])) { preg_match_all($DBDF_PARTIAL_REGEX, $linereturn[0], $inuse); foreach ($inuse[0] as $line) { $lines[] = $line; } } unset($inuse); unset($linereturn); } } } } } $lines = array_values(array_unique($lines, SORT_STRING)); $iommuinuse = array (); foreach ($lines as $pciinuse){ $string = exec("ls /sys/kernel/iommu_groups/*/devices/$pciinuse -1 -d"); $string = substr($string,25,2); $iommuinuse[] = (strpos($string,'/')) ? strstr($string, '/', true) : $string; } exec('lsscsi -s',$lsscsi); // Filter for 'removed' devices $removedArr = array_filter($pci_device_diffs, function($entry) { return isset($entry['status']) && $entry['status'] === 'removed'; }); foreach ($removedArr as $removedpci => $removeddata) { $groups[] = "IOMMU "._("Removed"); $groups[] = "\tR[{$removeddata['device']['vendor_id']}:{$removeddata['device']['device_id']}] ".str_replace("0000:","",$removedpci)." ".trim($removeddata['device']['description'],"\n"); } $ackparm = ""; #echo "";var_dump($sriov, $sriovvfs);echo""; foreach ($groups as $line) { if (!$line) continue; if (in_array($line,$sriovvfs)) continue; if ($line[0]=='I') { if (isset($spacer)) echo ""; else $spacer = true; echo "$line:"; $iommu = substr($line, 12); $append = true; } else { $line = preg_replace("/^\t/","",$line); $vd = trim(explode(" ", $line)[0], "[]"); $pciaddress = explode(" ", $line)[1]; $removed = $line[0]=='R' ? true : false; if ($removed) $line=preg_replace('/R/', '', $line, 1); if (preg_match($BDF_REGEX, $pciaddress)) { // By default lspci does not output the when the only domain in the system is 0000. Add it back. $pciaddress = "0000:".$pciaddress; } if ( in_array($pciaddress,$sriovvfs)) continue; echo ($append) ? "" : ""; exec("lspci -v -s $pciaddress", $outputvfio); if (preg_grep("/vfio-pci/i", $outputvfio)) { echo ""; $isbound = "true"; } echo ""; if ((strpos($line, 'Host bridge') === false) && (strpos($line, 'PCI bridge') === false)) { if (file_exists('/sys/kernel/iommu_groups/'.$iommu.'/devices/'.$pciaddress.'/reset')) echo ""; echo ""; if (!$removed) { echo in_array($iommu, $iommuinuse) ? '| or just echo (in_array($pciaddress."|".$vd, $vfio_cfg_devices) || in_array($pciaddress, $vfio_cfg_devices)) ? " checked>" : ">"; } } else { echo ""; } echo '',$line,''; if (array_key_exists($pciaddress,$pci_device_diffs)) { echo ""; echo ""; echo _("PCI Device change"); echo " "._("Action").":".ucfirst(_($pci_device_diffs[$pciaddress]['status']))." "; $ackparm .= $pciaddress.",".$pci_device_diffs[$pciaddress]['status'].";"; if ($pci_device_diffs[$pciaddress]['status']!="removed") echo $pci_device_diffs[$pciaddress]['device']['description']; echo ""; if ($pci_device_diffs[$pciaddress]['status']=="changed") { echo ""; echo _("Differences"); foreach($pci_device_diffs[$pciaddress]['differences'] as $key => $changes){ echo " $key "._("before").":{$changes['old']} "._("after").":{$changes['new']} "; } echo ""; } } if (array_key_exists($pciaddress,$sriov) && in_array(substr($sriov[$pciaddress]['class_id'],0,4),$allowedPCIClass)) { echo ""; echo "SRIOV Available VFs:{$sriov[$pciaddress]['total_vfs']}"; $num_vfs= $sriov[$pciaddress]['num_vfs']; $matches = array_filter($sriov_devices, function($entry) use ($pciaddress) { return explode('|', $entry)[0] === $pciaddress; }); if ($matches) { $match = reset($matches); // first matching entry $file_numvfs = explode('|', $match)[2]; } else $file_numvfs = 0; echo ''; echo ''; echo " "._("Current:").$num_vfs; #sprintf(" "._("Current").":%1s",$num_vfs); echo ' '; echo ' '; if ($file_numvfs != $num_vfs) echo " ".sprintf(_("Pending action or reboot")); echo ""; foreach($sriov[$pciaddress]['vfs'] as $vrf) { $pciaddress = $vrf['pci']; if ($removed) $line=preg_replace('/R/', '', $line, 1); if (preg_match($BDF_REGEX, $pciaddress)) { // By default lspci does not output the when the only domain in the system is 0000. Add it back. $pciaddress = "0000:".$pciaddress; } echo ""; $outputvfio = $vrfline =[]; exec('lspci -ns "'.$pciaddress.'"|awk \'BEGIN{ORS=" "}{print "["$3"]"}\';lspci -s "'.$pciaddress.'"',$vrfline); $vd = trim(explode(" ", $vrfline[0])[0], "[]"); exec("lspci -v -s $pciaddress", $outputvfio); if (preg_grep("/vfio-pci/i", $outputvfio)) { echo ""; $isbound = "true"; } echo ""; if ((strpos($line, 'Host bridge') === false) && (strpos($line, 'PCI bridge') === false)) { if (file_exists('/sys/bus/pci/devices/'.$pciaddress.'/reset')) echo ""; echo ""; if (!$removed) { echo '| or just echo (in_array($pciaddress."|".$vd, $vfio_cfg_devices) || in_array($pciaddress, $vfio_cfg_devices)) ? " checked>" : ">"; } } else { echo ""; } echo 'IOMMU Group '.$vrf['iommu_group'].": ",$vrfline[0],''; if (array_key_exists($pciaddress,$pci_device_diffs)) { echo ""; echo ""; echo _("PCI Device change"); echo " "._("Action").":".ucfirst(_($pci_device_diffs[$pciaddress]['status']))." "; $ackparm .= $pciaddress.",".$pci_device_diffs[$pciaddress]['status'].";"; if ($pci_device_diffs[$pciaddress]['status']!="removed") echo $pci_device_diffs[$pciaddress]['device']['description']; echo ""; if ($pci_device_diffs[$pciaddress]['status']=="changed") { echo ""; echo _("Differences"); foreach($pci_device_diffs[$pciaddress]['differences'] as $key => $changes){ echo " $key "._("before").":{$changes['old']} "._("after").":{$changes['new']} "; } echo ""; } } $matches = array_filter($sriov_devices_mac, function($entry) use ($pciaddress) { return explode('|', $entry)[0] === $pciaddress; }); if ($matches) { $match = reset($matches); // first matching entry $mac = explode('|', $match)[2]; } else { $mac = null; } $placeholder = empty($mac) ? 'Undefined dynamic allocation' : ''; $value_attr = empty($mac) ? '' : htmlspecialchars($mac, ENT_QUOTES); echo ""; echo ''; echo ""; echo ' '; echo ' '; echo _("Current").": {$vrf['mac']}"; echo ""; } } unset($outputvfio); switch (true) { case (strpos($line, 'USB controller') !== false): if (isset($isbound)) { echo '',_('This controller is bound to vfio, connected USB devices are not visible'),'.'; } else { exec('for usb_ctrl in $(find /sys/bus/usb/devices/usb* -maxdepth 0 -type l);do path="$(realpath "${usb_ctrl}")";if [[ $path == *'.$pciaddress.'* ]];then bus="$(cat "${usb_ctrl}/busnum")";lsusb -s $bus:|sort;fi;done',$getusb); foreach($getusb as $usbdevice) { [$bus,$id] = my_explode(':',$usbdevice); $usbport = usb_physical_port($usbdevice); if (strlen($usbport) > 7 ) {$usbport .= "\t"; } else { $usbport .= "\t\t"; } echo "$bus Port $usbport",trim($id),""; } unset($getusb); } break; case (strpos($line, 'SATA controller') !== false): case (strpos($line, 'Serial Attached SCSI controller') !== false): case (strpos($line, 'RAID bus controller') !== false): case (strpos($line, 'SCSI storage controller') !== false): case (strpos($line, 'IDE interface') !== false): case (strpos($line, 'Mass storage controller') !== false): case (strpos($line, 'Non-Volatile memory controller') !== false): if (isset($isbound)) { echo '',_('This controller is bound to vfio, connected drives are not visible'),'.'; } else { exec('ls -al /sys/block/sd* /sys/block/hd* /sys/block/sr* /sys/block/nvme* 2>/dev/null | grep -i "'.$pciaddress.'"',$getsata); foreach($getsata as $satadevice) { $satadevice = substr($satadevice, strrpos($satadevice, '/', -1)+1); $search = preg_grep('/'.$satadevice.'.*/', $lsscsi); foreach ($search as $deviceline) { echo '',$deviceline,''; } } unset($search); unset($getsata); } break; } unset($isbound); $append = false; } } echo '
'; if (file_exists("/var/log/vfio-pci") && filesize("/var/log/vfio-pci")) { echo ''; } if ($ackparm == "") $ackdisable =" disabled "; else $ackdisable = ""; echo ''; echo ''; echo ''; echo ''; echo << $("#t1 input[type='checkbox']").change(function() { var matches = document.querySelectorAll("." + this.className); for (var i=0, len=matches.length|0; i EOT; } break; case 't2': $is_intel_cpu = is_intel_cpu(); $core_types = $is_intel_cpu ? get_intel_core_types() : []; exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list|sort -nu',$pairs); $i = 1; foreach ($pairs as $line) { $line2 = $line; $line = preg_replace(['/(\d+)[-,](\d+)/','/(\d+)\b/'],['$1 / $2','cpu $1'],$line); if ($is_intel_cpu && count($core_types) > 0) { [$cpu1, $cpu2] = my_preg_split('/[,-]/',$line2); $core = $cpu1; $core_type = "({$core_types[$core]})"; } else $core_type = ""; echo "".(strpos($line,'/')===false?"Single":"Pair ".$i++).":$line $core_type"; } break; case 't3': exec('lsusb|sort',$lsusb); foreach ($lsusb as $line) { [$bus,$id] = my_explode(':',$line); $usbport = usb_physical_port($line); echo "$bus Port $usbport".trim($id).""; } break; case 't4': exec('lsscsi -s',$lsscsi); foreach ($lsscsi as $line) { if (strpos($line,'/dev/')===false) continue; echo "",preg_replace('/\] +/',']',$line),""; } break; } ?>