mirror of
https://github.com/unraid/webgui.git
synced 2026-01-07 18:19:54 -06:00
517 lines
18 KiB
PHP
517 lines
18 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.
|
|
*/
|
|
?>
|
|
<?
|
|
|
|
$allowedPCIClass = ['0x02'];
|
|
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
|
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt.php";
|
|
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
|
|
|
|
/**
|
|
* Get available kernel modules for this PCI device based on its modalias
|
|
*/
|
|
function getModulesFromModalias(string $pci): array {
|
|
$modaliasFile = "/sys/bus/pci/devices/{$pci}/modalias";
|
|
if (!is_readable($modaliasFile)) return [];
|
|
$alias = trim(file_get_contents($modaliasFile));
|
|
$cmd = sprintf('modprobe -R %s 2>/dev/null', escapeshellarg($alias));
|
|
$out = trim(shell_exec($cmd));
|
|
return $out ? preg_split('/\s+/', $out) : [];
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// Vendor:Device formatted string
|
|
$vendorFile = "/sys/bus/pci/devices/{$vf_pci}/vendor";
|
|
$deviceFile = "/sys/bus/pci/devices/{$vf_pci}/device";
|
|
$vendor = is_readable($vendorFile) ? trim(file_get_contents($vendorFile)) : null;
|
|
$device = is_readable($deviceFile) ? trim(file_get_contents($deviceFile)) : null;
|
|
$vf_entry['vd'] = ($vendor && $device) ? sprintf('%s:%s', substr($vendor, 2), substr($device, 2)) : null;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// --- Current driver ---
|
|
$driver_link = "/sys/bus/pci/devices/{$vf_pci}/driver";
|
|
if (is_link($driver_link)) {
|
|
$vf_entry['driver'] = basename(readlink($driver_link));
|
|
} else {
|
|
$vf_entry['driver'] = null; // no driver bound
|
|
}
|
|
// Kernel modules (from modalias)
|
|
$vf_entry['modules'] = getModulesFromModalias($vf_pci);
|
|
}
|
|
$vfs[$vf_pci] = $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 rebindVfDriver($vf, $sriov, $target = 'original')
|
|
{
|
|
$res = ['pci'=>$vf,'success'=>false,'error'=>null,'details'=>[]];
|
|
$vf_path = "/sys/bus/pci/devices/$vf";
|
|
$physfn = "$vf_path/physfn";
|
|
if (!is_link($physfn)) return $res + ['error'=>_("Missing physfn link")];
|
|
$pf = basename(readlink($physfn));
|
|
$vf_info = $sriov[$pf]['vfs'][$vf] ?? null;
|
|
if (!$vf_info) return $res + ['error'=>_("VF not found in sriov for PF")." $pf"];
|
|
|
|
$orig_mod = $vf_info['modules'][0] ?? $sriov[$pf]['module'] ?? null;
|
|
$curr_drv = $vf_info['driver'] ?? null;
|
|
if (!$orig_mod) return $res + ['error'=>_("No module info for")." $vf"];
|
|
|
|
$drv_override = "$vf_path/driver_override";
|
|
|
|
// Determine target driver
|
|
$new_drv = ($target === 'vfio-pci') ? 'vfio-pci' : $orig_mod;
|
|
|
|
// Step 1: Unbind current driver
|
|
$curr_unbind = "/sys/bus/pci/drivers/$curr_drv/unbind";
|
|
if (is_writable($curr_unbind))
|
|
@file_put_contents($curr_unbind, $vf);
|
|
|
|
// Step 2: Load target driver if needed
|
|
$target_bind = "/sys/bus/pci/drivers/$new_drv/bind";
|
|
if (!file_exists($target_bind))
|
|
exec("modprobe $new_drv 2>/dev/null");
|
|
|
|
// Step 3: Override driver binding
|
|
if (is_writable($drv_override))
|
|
@file_put_contents($drv_override, "$new_drv");
|
|
$probe_path = "/sys/bus/pci/drivers_probe";
|
|
if (is_writable($probe_path))
|
|
@file_put_contents($probe_path, $vf);
|
|
if (is_writable($drv_override))
|
|
@file_put_contents($drv_override, "\n");
|
|
|
|
// Step 4: Verify binding
|
|
$drv_link = "$vf_path/driver";
|
|
if (is_link($drv_link)) {
|
|
$bound = basename(readlink($drv_link));
|
|
if ($bound === $new_drv) {
|
|
$res['success'] = true;
|
|
$res['details'][] = _("Bound to")." $new_drv";
|
|
return $res;
|
|
}
|
|
return $res + ['error'=> sprintf(_("Bound to %s instead of %s"),$bound,$new_drv)];
|
|
}
|
|
return $res + ['error'=>_("No driver link after reprobe")];
|
|
}
|
|
|
|
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Enumerate all VFs and group them by IOMMU group
|
|
* Output: Flat array of groups and pci vfpci as separate elemements
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// ----------------------
|
|
// Parse SR-IOV VF counts
|
|
// ----------------------
|
|
function parseVFvalues() {
|
|
$sriov_devices = [];
|
|
$DBDF_SRIOV_REGEX = '/^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]\|[[:xdigit:]]{4}:[[:xdigit:]]{4}\|[[:digit:]]+$/';
|
|
if (is_file("/boot/config/sriov.cfg")) {
|
|
$file = trim(file_get_contents("/boot/config/sriov.cfg"));
|
|
$file = preg_replace('/^VFS=/', '', $file); // Remove prefix
|
|
$entries = preg_split('/\s+/', $file, -1, PREG_SPLIT_NO_EMPTY);
|
|
|
|
foreach ($entries as $entry) {
|
|
if (preg_match($DBDF_SRIOV_REGEX, $entry)) {
|
|
// Format: <DBDF>|<Vendor:Device>|<VF_count>
|
|
[$dbdf, $ven_dev, $vf_count] = explode('|', $entry);
|
|
$sriov_devices[$dbdf] = [
|
|
'dbdf' => $dbdf,
|
|
'vendor' => $ven_dev,
|
|
'vf_count' => (int)$vf_count,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
return $sriov_devices;
|
|
}
|
|
|
|
// ---------------------------------
|
|
// Parse SR-IOV VF settings (VFIO+MAC)
|
|
// ---------------------------------
|
|
function parseVFSettings() {
|
|
$sriov_devices_settings = [];
|
|
$DBDF_SRIOV_SETTINGS_REGEX = '/^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]\|[[:xdigit:]]{4}:[[:xdigit:]]{4}\|[01]\|([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$/';
|
|
if (is_file("/boot/config/sriovvfs.cfg")) {
|
|
$file = trim(file_get_contents("/boot/config/sriovvfs.cfg"));
|
|
$file = preg_replace('/^VFSETTINGS=/', '', $file); // Remove prefix
|
|
$entries = preg_split('/\s+/', $file, -1, PREG_SPLIT_NO_EMPTY);
|
|
|
|
foreach ($entries as $entry) {
|
|
if (preg_match($DBDF_SRIOV_SETTINGS_REGEX, $entry)) {
|
|
// Format: <DBDF>|<Vendor:Device>|<VFIO_flag>|<MAC>
|
|
[$dbdf, $ven_dev, $vfio_flag, $mac] = explode('|', $entry);
|
|
if ($mac == "00:00:00:00:00:00") $mac = "";
|
|
$sriov_devices_settings[$dbdf] = [
|
|
'dbdf' => $dbdf,
|
|
'vendor' => $ven_dev,
|
|
'vfio' => (int)$vfio_flag,
|
|
'mac' => strtoupper($mac),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
return $sriov_devices_settings;
|
|
}
|
|
|
|
/**
|
|
* Safely set a MAC address for a VF.
|
|
* Automatically detects PF, interface, and VF index.
|
|
*
|
|
* @param string $vf_pci PCI ID of VF (e.g. 0000:02:00.2)
|
|
* @param string $mac MAC address to assign
|
|
* @param string|null $rebindDriver Driver to bind after change:
|
|
* - null → rebind to original driver
|
|
* - 'none' → leave unbound
|
|
* - 'vfio-pci' → bind to vfio-pci
|
|
*
|
|
* @return array Result info (for JSON or logs)
|
|
*/
|
|
function setVfMacAddress(string $vf_pci, array $sriov, string $mac, ?string $rebindDriver = null): array {
|
|
$vf_path = "/sys/bus/pci/devices/{$vf_pci}";
|
|
$result = [
|
|
'vf_pci' => $vf_pci,
|
|
'mac' => $mac,
|
|
'pf_pci' => null,
|
|
'pf_iface' => null,
|
|
'vf_index' => null,
|
|
'driver_before' => null,
|
|
'driver_after' => null,
|
|
'unbind' => false,
|
|
'mac_set' => false,
|
|
'rebind' => false,
|
|
'error' => null
|
|
];
|
|
|
|
if ($mac != "" && preg_match('/([a-fA-F0-9]{2}[:|\-]?){6}/', $mac) != 1) {
|
|
$result['error'] = _("MAC format is invalid.");
|
|
return $result;
|
|
}
|
|
|
|
if (!is_dir($vf_path)) {
|
|
$result['error'] = _("VF path not found").": $vf_path";
|
|
return $result;
|
|
}
|
|
|
|
// --- Find parent PF (Physical Function) ---
|
|
$pf_link = "$vf_path/physfn";
|
|
if (!is_link($pf_link)) {
|
|
$result['error'] = sprintf("No PF link for %s (not an SR-IOV VF?)",$vf_pci);
|
|
return $result;
|
|
}
|
|
$pf_pci = basename(readlink($pf_link));
|
|
$result['pf_pci'] = $pf_pci;
|
|
|
|
// --- Detect PF network interface name ---
|
|
$pf_net = glob("/sys/bus/pci/devices/{$pf_pci}/net/*");
|
|
$pf_iface = ($pf_net && isset($pf_net[0])) ? basename($pf_net[0]) : null;
|
|
if (!$pf_iface) {
|
|
$result['error'] = _("Could not detect PF interface for")." $pf_pci";
|
|
return $result;
|
|
}
|
|
$result['pf_iface'] = $pf_iface;
|
|
|
|
// --- Detect VF index ---
|
|
$vf_index = getVfIndex($pf_pci, $vf_pci);
|
|
if ($vf_index === null) {
|
|
$result['error'] = sprintf(_("Could not determine VF index for %s under %s"),$vf_pci,$pf_pci);
|
|
return $result;
|
|
}
|
|
$result['vf_index'] = $vf_index;
|
|
|
|
// --- Detect current driver ---
|
|
$driver_link = "$vf_path/driver";
|
|
$vf_driver = is_link($driver_link) ? basename(readlink($driver_link)) : null;
|
|
$result['driver_before'] = $vf_driver;
|
|
|
|
// --- Unbind from current driver ---
|
|
if ($vf_driver) {
|
|
$unbind_path = "/sys/bus/pci/drivers/{$vf_driver}/unbind";
|
|
if (is_writable($unbind_path)) {
|
|
file_put_contents($unbind_path, $vf_pci);
|
|
$result['unbind'] = true;
|
|
} else {
|
|
$result['error'] = sprintf(_("Cannot unbind VF %s from %s (permissions)"),$vf_pci,$vf_driver);
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
// --- Set MAC ---
|
|
if ($mac=="") $mac="00:00:00:00:00:00";
|
|
$cmd = sprintf(
|
|
'ip link set %s vf %d mac %s 2>&1',
|
|
escapeshellarg($pf_iface),
|
|
$vf_index,
|
|
escapeshellarg($mac)
|
|
);
|
|
exec($cmd, $output, $ret);
|
|
|
|
if ($ret === 0) {
|
|
$result['mac_set'] = true;
|
|
$result['details'] = [sprintf(_("MAC address set to %s"),($mac != "00:00:00:00:00:00") ? strtoupper($mac) : _("Dyanamic allocation"))];
|
|
} else {
|
|
$result['error'] = _("Failed to set MAC").": " . implode("; ", $output);
|
|
}
|
|
|
|
// --- Rebind logic ---
|
|
if ($rebindDriver !== "none") {
|
|
$result2 = rebindVfDriver($vf_pci,$sriov,$rebindDriver);
|
|
if (isset($result2['error'])) $result['error'] = $result2['error'];
|
|
elseif (isset($result2['details'])) $result["details"] = array_merge($result['details'] , $result2['details']);
|
|
}
|
|
if (!isset($result['error'])) $result['success'] = true;
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Helper: Determine VF index from PF/VF relationship
|
|
*/
|
|
function getVfIndex(string $pf_pci, string $vf_pci): ?int {
|
|
$pf_path = "/sys/bus/pci/devices/{$pf_pci}";
|
|
foreach (glob("$pf_path/virtfn*") as $vf_link) {
|
|
if (is_link($vf_link)) {
|
|
$target = basename(readlink($vf_link));
|
|
if ($target === $vf_pci) {
|
|
return (int)preg_replace('/[^0-9]/', '', basename($vf_link));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function build_pci_active_vm_map() {
|
|
global $lv, $libvirt_running;
|
|
if ($libvirt_running !== "yes") return [];
|
|
$pcitovm = [];
|
|
|
|
$vms = $lv->get_domains();
|
|
foreach ($vms as $vm) {
|
|
$vmpciids = $lv->domain_get_vm_pciids($vm);
|
|
$res = $lv->get_domain_by_name($vm);
|
|
$dom = $lv->domain_get_info($res);
|
|
$state = $lv->domain_state_translate($dom['state']);
|
|
if ($state === 'shutoff') continue;
|
|
|
|
foreach ($vmpciids as $pciid => $pcidetail) {
|
|
$pcitovm["0000:" . $pciid][$vm] = $state;
|
|
}
|
|
}
|
|
|
|
return $pcitovm;
|
|
}
|
|
|
|
function is_pci_inuse($pciid, $type) {
|
|
$actives = build_pci_active_vm_map();
|
|
$sriov = json_decode(getSriovInfoJson(true), true);
|
|
$inuse = false;
|
|
$vms = [];
|
|
|
|
switch ($type) {
|
|
case "VF":
|
|
if (isset($actives[$pciid])) {
|
|
$inuse = true;
|
|
$vms = array_keys($actives[$pciid]);
|
|
}
|
|
break;
|
|
|
|
case "PF":
|
|
if (isset($sriov[$pciid])) {
|
|
$vfs = $sriov[$pciid]['vfs'] ?? [];
|
|
foreach ($vfs as $vf) {
|
|
$vf_pci = $vf['pci'];
|
|
if (isset($actives[$vf_pci])) {
|
|
$inuse = true;
|
|
$vms = array_merge($vms, array_keys($actives[$vf_pci]));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Remove duplicate VM names (in case multiple VFs from same VM)
|
|
$vms = array_values(array_unique($vms));
|
|
|
|
// Output consistent JSON structure
|
|
$result = [
|
|
"inuse" => $inuse,
|
|
"vms" => $vms
|
|
];
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode($result, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
|
|
?>
|