diff --git a/emhttp/plugins/dynamix/SysDevs.page b/emhttp/plugins/dynamix/SysDevs.page index 08b4944ce1..f968171ab9 100644 --- a/emhttp/plugins/dynamix/SysDevs.page +++ b/emhttp/plugins/dynamix/SysDevs.page @@ -25,43 +25,533 @@ $(function(){ function applyCfg() { var message = "_(System Devices)_: _(A reboot is required to apply changes)_"; var string = "BIND="; + var string2 = "VFSETTINGS="; var elements = document.getElementById("vfiopci").elements; for (var i = 0, element; element = elements[i++];) { - if (element.type === "checkbox" && element.checked === true) + if (element.type === "checkbox" && element.checked === true && element.className.substring(0, 5) === "iommu") string = string + element.value + " "; + if (element.type === "checkbox" && element.className.substring(0, 7) === "vfiommu") { + var pciId = element.id.replace(/^vfvfio/, ""); + var mac = document.getElementById("vfmac" + pciId).value; + var elvfio = document.getElementById("vfvfio" + pciId).checked; + if (elvfio == true) var vfio = "1"; else vfio = "0"; + string2 = string2 + element.value + "|" + vfio + "|" + mac + " "; + } + } string = string.trim(); if (string === "BIND=") { string = ""; } - $.post( "/plugins/dynamix/include/update.vfio-pci-cfg.php", { cfg: string } ) + string2 = string2.trim(); + if (string2 === "VFSETTINGS=") { + string2 = ""; + } + + $.post( "/plugins/dynamix/include/update.vfio-pci-cfg.php", { cfg: string, vfcfg:string2} ) + .done(function(d) { + switch (d) { + case "1": + addRebootNotice(message); + document.getElementById("warning").innerHTML = "_(ALERT)_: VFIO _(Changes saved)_. _(Reboot to take effect)_."; + break; + case "2": + addRebootNotice(message); + document.getElementById("warning").innerHTML = "_(ALERT)_: SRIOV VFs _(Changes saved)_. _(Reboot to take effect)_."; + break; + case "3": + addRebootNotice(message); + document.getElementById("warning").innerHTML = "_(ALERT)_: VFIO _(and)_ SRIOV VFs _(saved)_. _(Reboot to take effect)_."; + break; + default: + removeRebootNotice(message); + document.getElementById("warning").innerHTML = "_(No changes)_."; + break; + } + $("#applycfg").attr("disabled",true); + }); +} + +function saveVFSettingsConfig(pciId,vd,interactive=1) { + var message = "_(System Devices)_: _(A reboot is required to apply changes)_"; + var mac = document.getElementById("vfmac" + pciId).value; + var vfio = document.getElementById("vfvfio" + pciId).checked; + + $.post( "/plugins/dynamix/include/update.sriov-cfg.php", { type:"sriovsettings", pciid: pciId, vd:vd, vfio:vfio, mac:mac } ) .done(function(d) { if (d==1) { - addRebootNotice(message); - document.getElementById("warning").innerHTML = "_(ALERT)_: _(Changes saved)_. _(Reboot to take effect)_."; + if (interactive == 1) + swal({ + title: _("VF Settings"), + text: _("Configuration saved") + ".", + type: "success", + timer: 3000, + showConfirmButton: false + }); + } + if (interactive == 1) $('#t1').load('/webGui/include/SysDevs.php', { table: 't1' }); + }); +} +function saveVFsConfig(pciId,vd,interactive=1) { + var message = "_(System Devices)_: _(A reboot is required to apply changes)_"; + var numvfs = document.getElementById("vf" + pciId).value; + + $.post( "/plugins/dynamix/include/update.sriov-cfg.php", { type:"sriov", pciid: pciId, vd:vd, numvfs:numvfs } ) + .done(function(d) { + if (d==1) { + if (interactive == 1) + swal({ + title: "VFs", + text: _("Configuration saved") + ".", + type: "success", + timer: 3000, + showConfirmButton: false + }); + } + if (interactive == 1) $('#t1').load('/webGui/include/SysDevs.php', { table: 't1' }); + }); + +} + +function generateMAC(pciId) { + if (!pciId) { + swal("Error", _("No PCI ID provided to generateMAC()"), "error"); + return; + } + + // Validate PCI ID format + var pciPattern = /^([0-9a-fA-F]{4}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2})\.([0-7])$/; + var match = pciId.match(pciPattern); + if (!match) { + swal(_("Invalid PCI ID"), _("Expected format: 0000:03:00.2"), "error"); + return; + } + + // Ask for host ID using Unraid swal input +swal({ + title: _("Enter Host ID"), + text: _("Provide a 2-digit hex host ID (e.g. 01)"), + type: "input", + showCancelButton: true, + closeOnConfirm: false, + inputPlaceholder: "00", + customClass: "swal-hostid-input", + onOpen: function() { + const input = document.querySelector(".sa-input"); + if (input) { + input.maxLength = 2; // Limit to 2 chars + input.style.textTransform = "uppercase"; // Auto uppercase + input.pattern = "[0-9A-Fa-f]{2}"; // Hex pattern + input.autocomplete = "off"; + + // Optional: restrict keypresses to hex characters only + input.addEventListener("keypress", function (e) { + if (!/[0-9A-Fa-f]/.test(e.key)) e.preventDefault(); + }); + } + } + }, function(hostId) { + if (hostId === false) return; // cancelled + if (!/^[0-9a-fA-F]{1,2}$/.test(hostId)) { + swal.showInputError(_("Please enter 1–2 hex digits (00–ff)")); + return false; + } + + // Extract PCI parts + var domain = match[1]; + var bus = match[2]; + var dev = match[3]; + var func = match[4]; + var dom_lo = ("0" + (parseInt(domain, 16) & 0xFF).toString(16)).slice(-2); + + // Format: 62::::: + var mac = [ + "62", + dom_lo, + bus.padStart(2, "0"), + dev.padStart(2, "0"), + func.padStart(2, "0"), + hostId.padStart(2, "0") + ].join(":").toUpperCase(); + + // Build target input ID (e.g., #vfmac0000:04:11.1) + var inputId = "vfmac" + pciId; + + // Update the field if it exists + var $field = $("#" + CSS.escape(inputId)); + if ($field.length) { + $field.val(mac); + } + + swal({ + title: _("MAC"), + text: _("Generated successfully."), + type: "success", + timer: 3000, + showConfirmButton: false + }); + }); +} + +function applyVFsConfig(pciId, vd, vfs) { + + const message = "_(System Devices)_: _(A reboot is required to apply changes)_"; + const numvfs = parseInt(document.getElementById("vf" + pciId).value, 10); + vfs = parseInt(vfs, 10); + + // --- Step 1: Check for active VMs (always returns JSON) --- + $.post("/plugins/dynamix/include/apply.sriov-cfg.php", { + type: "inuse", + pciid: pciId, + pcitype: "PF" + }, null, "json") // automatically parse JSON + .done(function (data) { + console.log("Active check result:", data); + + // If device is in use, show VM list and stop here + if (data.inuse === true) { + let vmListText = ""; + let vmCount = Array.isArray(data.vms) ? data.vms.length : 0; + + if (vmCount > 0) { + vmListText = data.vms.join("\n"); + } else { + vmListText = "No VM names returned."; + } + + const plural = vmCount === 1 ? _("VM is") : _("VMs are"); + const messageText = + _("The following") + " " + plural + " " + _("currently using this device") + ":\n\n" + + vmListText + + "\n\n" + _("Please stop") + " " + + (vmCount === 1 ? "it" : "them") + + _(" before applying changes."); + + swal({ + title: _("Active VMs Detected"), + text: messageText, + type: "warning", + confirmButtonText: _("OK"), + closeOnConfirm: true + }); + + return; + } + + // --- Step 2: Continue only if NOT in use --- + + // Case 1: VFs will be removed + if (vfs !== 0 && numvfs === 0) { + swal({ + title: _("VFs will be removed"), + text: _("Physical card will reset."), + type: "warning", + showCancelButton: true, + confirmButtonText: _("OK"), + cancelButtonText: _("Cancel"), + closeOnConfirm: true + }, function(isConfirm) { + if (isConfirm) { + setTimeout(function() { + doVFApply(pciId, vd, numvfs, message); + }, 300); + } else { + swal(_("Cancelled"), _("No changes were made."), "info"); + } + }); + return; + } + + // Case 2: Number of VFs changed + if (vfs !== numvfs && vfs !== 0) { + swal({ + title: _("Number of VFs changed"), + text: _("Will need to remove and re-add VFs."), + type: "warning", + showCancelButton: true, + confirmButtonText: _("OK"), + cancelButtonText: _("Cancel"), + closeOnConfirm: true + }, function(isConfirm) { + if (isConfirm) { + setTimeout(function() { + doVFApply(pciId, vd, numvfs, message); + }, 300); + } else { + swal(_("Cancelled"), _("No changes were made."), "info"); + } + }); + return; + } + + // Case 3: All VFs removed + if (vfs !== numvfs && vfs === 0) { + doVFApply(pciId, vd, numvfs, message); + } + }) + .fail(function (xhr, status, error) { + console.error("Active check failed:", status, error, xhr.responseText); + swal({ + title: _("Error"), + text: _("Could not verify active device state."), + type: "error", + showConfirmButton: true + }); + }); +} + +function applyVFSettings(pciId, vd, currentvfio, currentmac) { + + const message = "_(System Devices)_: _(A reboot is required to apply changes)_"; + const mac = document.getElementById("vfmac" + pciId).value; + const vfio = document.getElementById("vfvfio" + pciId).checked; + + // --- Step 1: Check for active VMs (always returns JSON) --- + $.post("/plugins/dynamix/include/apply.sriov-cfg.php", { + type: "inuse", + pciid: pciId, + pcitype: "VF" + }, null, "json") // automatically parse JSON + .done(function (data) { + console.log("Active check result:", data); + + // If device is in use, show VM list and stop here + if (data.inuse === true) { + let vmListText = ""; + let vmCount = Array.isArray(data.vms) ? data.vms.length : 0; + + if (vmCount > 0) { + vmListText = data.vms.join("\n"); + } else { + vmListText = "No VM names returned."; + } + + const plural = vmCount === 1 ? _("VM is") : _("VMs are"); + const messageText = + _("The following") + " " + plural + " " + _("currently using this device") + ":\n\n" + + vmListText + + "\n\n" + _("Please stop") + " " + + (vmCount === 1 ? "it" : "them") + + _(" before applying changes."); + + swal({ + title: _("Active VMs Detected"), + text: messageText, + type: "warning", + confirmButtonText: _("OK"), + closeOnConfirm: true + }); + + return; + } + doVFSettingApply(pciId, vd, vfio, mac, currentvfio,currentmac,message); + }) + .fail(function (xhr, status, error) { + console.error("Active check failed:", status, error, xhr.responseText); + swal({ + title: _("Error"), + text: _("Could not verify active device state."), + type: "error", + showConfirmButton: true + }); + }); +} +function doVFSettingApply(pciId, vd, vfio, mac, currentvfio, currentmac, message) { + // Show "updating" alert + swal({ + title: _("Updating..."), + text: _("Please wait while configuration is applied."), + type: "info", + showConfirmButton: false, + allowOutsideClick: false, + closeOnConfirm: false + }); + + // Perform the POST + $.post("/plugins/dynamix/include/apply.sriov-cfg.php", { + type: "sriovsettings", + pciid: pciId, + vd: vd, + vfio: vfio, + mac: mac, + currentvfio: currentvfio, + currentmac: currentmac + }) + .done(function (response) { + let data; + + // Safe JSON parse + try { + data = (typeof response === "string") ? JSON.parse(response) : response; + } catch (e) { + swal({ + title: _("Error"), + text: _("Invalid JSON response from server."), + type: "error", + showConfirmButton: true + }); + return; + } + + // SUCCESS + if (data.success) { + + saveVFSettingsConfig(pciId, vd, 0); + + // data.details may be: + // - string + // - array + // - undefined + let msg = _("Configuration successfully applied."); + + if (Array.isArray(data.details)) { + msg = data.details.join("\n"); + } else if (typeof data.details === "string") { + msg = data.details; + } + + swal({ + title: _("Update Complete"), + text: msg, + type: "success", + timer: 3000, + showConfirmButton: false + }); + } + + // FAILURE + else { + const errorMsg = data.error || _("Operation failed."); + + document.getElementById("warning").innerHTML = + "" + errorMsg + ""; + + swal({ + title: _("Operation Failed"), + text: errorMsg, + type: "error", + showConfirmButton: true + }); + } + + // Always reload the table + $('#t1').load('/webGui/include/SysDevs.php', { table: 't1' }); + }) + + // Network failure + .fail(function (xhr, status, error) { + swal({ + title: _("Network Error"), + text: _("Failed to communicate with the server.") + "\n" + (error || status), + type: "error", + showConfirmButton: true + }); + }); +} + +function doVFApply(pciId, vd, numvfs, message) { + // Show "updating" alert + swal({ + title: _("Updating..."), + text: _("Please wait while configuration is applied."), + type: "info", + showConfirmButton: false, + allowOutsideClick: false, + closeOnConfirm: false + }); + + // Perform the POST + $.post("/plugins/dynamix/include/apply.sriov-cfg.php", { + type: "sriov", + pciid: pciId, + vd: vd, + numvfs: numvfs + }) + .done(function (data) { + // Parse JSON safely + if (typeof data === "string") { + try { data = JSON.parse(data); } + catch (e) { + data = { success: false, error: _("Invalid JSON: ") + e.message }; + } + } + + // Build message + let msg = ""; + if (data.error) { + msg = _("Error") + ": " + data.error; + } else if (data.details) { + msg = formatDetailsSettings(data.details); } else { - removeRebootNotice(message); - document.getElementById("warning").innerHTML = "_(No changes)_."; + msg = _("Configuration successfully applied."); } - $("#applycfg").attr("disabled",true); + + // Show alert + const ok = data.success === true || data.success === 1; + + if (ok) saveVFsConfig(pciId, vd, 0); + + swal({ + title: ok ? _("Update Complete") : _("Update Failed"), + text: msg, + type: ok ? "success" : "error", + timer: ok ? 3000: null, + showConfirmButton: !ok + }); + + // Reload table + $('#t1').load('/webGui/include/SysDevs.php', { table: 't1' }); + }) + .fail(function (xhr, status, error) { + swal({ + title: _("Network Error"), + text: _("Failed to communicate with the server.") + "\n" + (error || status), + type: "error", + showConfirmButton: true + }); }); } + +// Helper: recursively format details Settings to plain text +function formatDetailsSettings(obj, indent = 0) { + const pad = " ".repeat(indent); + if (obj == null) return ""; + if (typeof obj === "string") return pad + obj; + if (typeof obj === "number" || typeof obj === "boolean") return pad + obj; + if (Array.isArray(obj)) return obj.map(x => formatDetailsSettings(x, indent)).join("\n"); + if (typeof obj === "object") { + return Object.entries(obj).map(([key, val]) => { + if (typeof val === "object" && val !== null) { + // Special case for VF result objects + if ("success" in val || "error" in val || "details" in val) { + const symbol = val.success ? "✔" : "✖"; + const msg = val.error || val.details || ""; + return `${pad}${key}\n${pad} ${symbol} ${msg}`; + } + return `${pad}${key}:\n${formatDetailsSettings(val, indent + 1)}`; + } + return `${pad}${key}: ${val}`; + }).join("\n"); + } + return pad + String(obj); +} + function formatFullInput(input) { - return input - .split(';') - .filter(Boolean) // remove empty trailing entry - .map(entry => { - let [pci, status] = entry.split(','); - status = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); - return `${pci} _(${status})_`; - }) - .join('
'); + return input + .split(';') + .filter(Boolean) // remove empty trailing entry + .map(entry => { + let [pci, status] = entry.split(','); + status = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + return `${pci} _(${status})_`; + }) + .join('
'); } function formatVMS(input) { - return input - .split(';') - .filter(Boolean) // remove empty trailing entry - .join('
'); + return input + .split(';') + .filter(Boolean) // remove empty trailing entry + .join('
'); } function ackPCI(pcidevice, action) { $.post('/webGui/include/PCIUpdate.php', { action: "getvm", pciid: pcidevice }).done(function(vms) { @@ -83,7 +573,7 @@ function ackPCI(pcidevice, action) { } swal({ - title: "Are you sure?", + title: _("Are you sure?"), text: swaltext, type: "warning", html: true, diff --git a/emhttp/plugins/dynamix/include/SriovHelpers.php b/emhttp/plugins/dynamix/include/SriovHelpers.php new file mode 100644 index 0000000000..b4954ef782 --- /dev/null +++ b/emhttp/plugins/dynamix/include/SriovHelpers.php @@ -0,0 +1,530 @@ + +/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)) { + $res['error'] = _("Missing physfn link"); + return $res; + } + $pf = basename(readlink($physfn)); + $vf_info = $sriov[$pf]['vfs'][$vf] ?? null; + if (!$vf_info) { + $res['error'] = _("VF not found in sriov for PF")." $pf"; + return $res; + } + + $orig_mod = $vf_info['modules'][0] ?? $sriov[$pf]['module'] ?? null; + $curr_drv = $vf_info['driver'] ?? null; + if (!$orig_mod) { + $res['error'] = _("No module info for")." $vf"; + return $res; + } + + $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 " . escapeshellarg($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; + } + $res['error'] = sprintf(_("Bound to %s instead of %s"), $bound, $new_drv); + return $res; + } + $res['error'] = _("No driver link after reprobe"); + return $res; +} + + +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; + } + + 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, $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, $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, + 'details' => [] + ]; + + if ($mac != "" && preg_match('/^([a-fA-F0-9]{2}[:\-]){5}[a-fA-F0-9]{2}$/', $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) : _("Dynamic 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 ($result['error'] === null) { + $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; +} + + +?> \ No newline at end of file diff --git a/emhttp/plugins/dynamix/include/SysDevs.php b/emhttp/plugins/dynamix/include/SysDevs.php index 1feee62d02..723e8fda63 100644 --- a/emhttp/plugins/dynamix/include/SysDevs.php +++ b/emhttp/plugins/dynamix/include/SysDevs.php @@ -1,6 +1,7 @@ "; else $spacer = true; echo "$line:"; @@ -143,6 +153,7 @@ function usb_physical_port($usbbusdev) { // 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)) { @@ -179,6 +190,112 @@ function usb_physical_port($usbbusdev) { 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']; + + if (isset($sriov_devices[$pciaddress])) + $file_numvfs = $sriov_devices[$pciaddress]['vf_count']; + else $file_numvfs = 0; + + echo ''; + echo ''; + echo " "._("Current").":".$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 (array_key_exists($pciaddress,$sriov_devices_settings) && $sriov_devices_settings[$pciaddress]['vfio'] == 1) ? " 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 ""; + } + } + if (isset($sriov_devices_settings[$pciaddress])) { + $mac = $sriov_devices_settings[$pciaddress]['mac']; + } else { + $mac = null; + } + $placeholder = empty($mac) ? _('Dynamic allocation') : ''; + $saved_mac = empty($mac) ? '' : htmlspecialchars($mac, ENT_QUOTES); + $current_mac = empty($vrf['mac']) ? '' : htmlspecialchars(strtoupper($vrf['mac']), ENT_QUOTES); + echo ""; + echo ''; + echo ""; + echo ' '; + echo ' '; + if (isset($sriov_devices_settings[$pciaddress])) { + $filevfio = $sriov_devices_settings[$pciaddress]['vfio'] == 1 ? true : false; + } else $filevfio = false; + $vfiocheck = $vrf['driver'] == "vfio-pci" ? true:false; + echo ' '; + if ($vrf['driver'] != "vfio-pci") echo _("Current").": "; + echo $vrf['driver'] == "vfio-pci" ? _("Bound to VFIO") : strtoupper($vrf['mac']); + $vfstatus =""; + if ($filevfio != $vfiocheck || (strtoupper($vrf['mac']) != $mac && $mac != null && $vrf['mac']!=null )) $vfstatus = " ".sprintf(_("Pending action or reboot")); + echo " $vfstatus"; + echo ""; + } + } + unset($outputvfio); switch (true) { case (strpos($line, 'USB controller') !== false): diff --git a/emhttp/plugins/dynamix/include/apply.sriov-cfg.php b/emhttp/plugins/dynamix/include/apply.sriov-cfg.php new file mode 100644 index 0000000000..47f6e39774 --- /dev/null +++ b/emhttp/plugins/dynamix/include/apply.sriov-cfg.php @@ -0,0 +1,284 @@ + + (bool)$success, + 'error' => $error, + 'details' => $details + ]; + + $json = json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + echo $json; + exit; +} + +function safe_file_put_contents($path, $data) +{ + # If the file exists but is not writable + if (file_exists($path) && !is_writable($path)) { + throw new RuntimeException("File not writable: $path", 1001); + } + + # If file does not exist → make sure directory is writable + $dir = dirname($path); + if (!file_exists($path) && !is_writable($dir)) { + throw new RuntimeException("Directory not writable for file creation: $dir", 1002); + } + + # Attempt the write + $result = @file_put_contents($path, $data); + + if ($result === false) { + # PHP error (rare for sysfs) + $e = error_get_last(); + if (!empty($e['message'])) { + throw new RuntimeException( + "Failed writing to $path: " . $e['message'], + 1003 + ); + } + + # Sysfs-specific: read back and check + $after = @file_get_contents($path); + if ((string)$after !== (string)$data) { + throw new RuntimeException( + "Kernel rejected write to sysfs file ($path)", + 1004 + ); + } + + throw new RuntimeException("Unknown write failure: $path", 1005); + } + + return $result; +} + + +function action_settings($pciid) +{ + $sriov = json_decode(getSriovInfoJson(), true); + $sriov_devices_settings = parseVFSettings(); + $vfs = $sriov[$pciid]['vfs'] ?? []; + $pci_device_diffs = comparePCIData(); + + + $results = []; + + foreach ($vfs as $vf) { + $vfpci = $vf['pci']; + if (array_key_exists($vfpci,$pci_device_diffs)) { + #Acknowledge PCI addition + if ($pci_device_diffs[$vfpci]['status'] == "added") acknowledge_PCI($vfpci); + } + if (!isset($sriov_devices_settings[$vfpci])) continue; + + $vfio = $sriov_devices_settings[$vfpci]['vfio']; + $mac = $sriov_devices_settings[$vfpci]['mac']; + + # Skip if no action needed + if ($vfio == 0 && $mac == "") continue; + + if ($mac == "") $mac="00:00:00:00:00:00"; + + $cmd = "/usr/local/sbin/sriov-vfsettings.sh " . + escapeshellarg($vfpci) . " " . + escapeshellarg($vf['vd']) . " " . + escapeshellarg($vfio) . " " . + escapeshellarg($mac) . " 2>&1"; # capture stderr too + + $output = []; + $ret = 0; + exec($cmd, $output, $ret); + + # Clean output: remove blank lines and trim whitespace + $output = array_filter(array_map('trim', $output)); + + if ($ret !== 0) { + # Only include relevant lines for error reporting + $results[$vfpci] = [ + 'success' => false, + 'error' => implode("\n", $output) ?: sprintf(_("Unknown error (exit code %s)"),$ret) + ]; + } else { + # Success: include minimal details or last few lines + $results[$vfpci] = [ + 'success' => true, + 'details' => _('Applied VF settings') + ]; + } + } + return $results; +} + + +$type = _var($_POST, 'type'); +$pciid = _var($_POST, 'pciid'); +$vd = _var($_POST, 'vd'); + +if (!isset($pciid) || !isset($vd)) { + json_response(false, _("Missing PCI ID or virtual device")); + exit; +} + + + +switch ($type) { + # -------------------------------------------------------- + # SR-IOV enable/disable & VF count changes + # -------------------------------------------------------- + case "sriov": + $numvfs = _var($_POST, 'numvfs'); + $currentvfs = _var($_POST, 'currentvfs'); + $filepath = "/sys/bus/pci/devices/$pciid/sriov_numvfs"; + + if (!is_writable($filepath)) { + json_response(false, _("Cannot modify VF configuration file") . ": $filepath"); + } + + try { + # Disable all VFs + if ($numvfs == 0) { + safe_file_put_contents($filepath, 0); + json_response(true, null, [_("Disabled all VFs")]); + } + + # — VF count changed + if ($numvfs != $currentvfs) { + + # Reset VFs and apply new count + safe_file_put_contents($filepath, 0); + safe_file_put_contents($filepath, $numvfs); + + # Now apply fresh VF settings + $results = action_settings($pciid); + + $all_success = array_reduce($results, fn($ok, $r) => + $ok && ($r['success'] ?? false), true + ); + + json_response(true, null, [ + sprintf(_("Changed VF count to %d"), $numvfs), + $results + ]); + } + + # No changes + json_response(true, null, [_("No changes needed")]); + + } catch (Throwable $e) { + json_response(false, _("Failed to change VF configuration") . ": " . $e->getMessage()); + } + + + # -------------------------------------------------------- + # VF driver binding, MAC changes + # -------------------------------------------------------- + case "sriovsettings": + + $mac = _var($_POST, 'mac'); + $vfio = _var($_POST, 'vfio'); + $currentvfio = _var($_POST, 'currentvfio'); + $currentmac = _var($_POST, 'currentmac'); + + # Normalize booleans + $vfio = filter_var($vfio, FILTER_VALIDATE_BOOLEAN) ? 1 : 0; + $currentvfio = filter_var($currentvfio, FILTER_VALIDATE_BOOLEAN) ? 1 : 0; + + # Normalize empty MAC to safe zero-MAC + if ($mac === "") $mac = "00:00:00:00:00:00"; + + $sriov = json_decode(getSriovInfoJson(), true); + + try { + # MAC changed AND currently bound to VFIO + if ($currentmac !== $mac) { + #Check if driver is required to change before actioning the MAC change. + $driver = ($vfio == 1) ? "vfio-pci" : "original"; + $rtn = setVfMacAddress($pciid, $sriov, $mac, $driver); + json_response( + $rtn['success'] ?? false, + $rtn['error'] ?? null, + $rtn['details'] ?? _("MAC address updated under VFIO") + ); + } + + # VFIO binding changed but MAC unchanged + if ($currentvfio !== $vfio && $currentmac === $mac) { + $driver = ($vfio == 1) ? "vfio-pci" : "original"; + $rtn = rebindVfDriver($pciid, $sriov, $driver); + json_response( + $rtn['success'] ?? false, + $rtn['error'] ?? null, + $rtn['details'] ?? _("VFIO binding updated") + ); + } + + # No changes + json_response(true, null, [_("No changes detected")]); + + } catch (Throwable $e) { + json_response(false, _("Error applying VF settings") . ": " . $e->getMessage()); + } + + break; + + + # -------------------------------------------------------- + # Check PCI device in use + # -------------------------------------------------------- + case "inuse": + $pcitype = _var($_POST, 'pcitype'); + is_pci_inuse($pciid, $pcitype); + // Note: is_pci_inuse() sends JSON response and exits + break; + + default: + json_response(false, _("Unknown request type").": $type"); + break; +} + diff --git a/emhttp/plugins/dynamix/include/update.sriov-cfg.php b/emhttp/plugins/dynamix/include/update.sriov-cfg.php new file mode 100644 index 0000000000..54cbed70f9 --- /dev/null +++ b/emhttp/plugins/dynamix/include/update.sriov-cfg.php @@ -0,0 +1,90 @@ + + $newelement) { + if (strpos($newelement,$newelement_check) !== false) { + $found = true; + if($numvfs == "0") { + unset($newexplode[$key]) ; + break; + } else { + $newexplode[$key] = $newelement_check.$numvfs; + break; + } + } + } + if (!$found && $numvfs != "0") $newexplode[] = $newelement_check.$numvfs; + $new = "VFS=".implode(" ",$newexplode); + $file = $sriov; + break; + case "sriovsettings": + $old = is_file($sriovvfs) ? rtrim(file_get_contents($sriovvfs)) : ''; + $newexplode = preg_split('/\s+/', str_replace("VFSETTINGS=","",$old), -1, PREG_SPLIT_NO_EMPTY); + $mac= _var($_POST,'mac'); + if ($mac == "") $mac = "00:00:00:00:00:00"; + $vfio= _var($_POST,'vfio'); + if ($vfio == "true") $vfio = 1; else $vfio = 0; + $found = false; + foreach($newexplode as $key => $newelement) { + if (strpos($newelement,$newelement_check) !== false) { + $found = true; + if($mac == "00:00:00:00:00:00" && $vfio == 0) { + unset($newexplode[$key]) ; + break; + } else { + $newexplode[$key] = $newelement_check.$vfio."|".$mac; + break; + } + } + } + if (!$found && ($vfio != 0 || $mac != "00:00:00:00:00:00")) $newexplode[] = $newelement_check.$vfio."|".$mac; + if ($newexplode) $new = "VFSETTINGS=".implode(" ",$newexplode);else $new = null; + $file = $sriovvfs; + break; + } +} + +$reply = 0; +if ($new != $old) { + if ($old) copy($file,"$file.bak"); + if ($new) file_put_contents($file,$new); else @unlink($file); + $reply = 1; +} + +echo $reply; +?> diff --git a/emhttp/plugins/dynamix/include/update.vfio-pci-cfg.php b/emhttp/plugins/dynamix/include/update.vfio-pci-cfg.php index 99e7e74a0f..3fb1505293 100644 --- a/emhttp/plugins/dynamix/include/update.vfio-pci-cfg.php +++ b/emhttp/plugins/dynamix/include/update.vfio-pci-cfg.php @@ -15,7 +15,67 @@ require_once "$docroot/webGui/include/Secure.php"; require_once "$docroot/webGui/include/Wrappers.php"; +function parseVF($str) +{ + $blocks = preg_split('/\s+/', trim($str)); + $result = []; + foreach ($blocks as $block) { + if ($block === '') continue; + $parts = explode('|', $block); + for ($i = 0; $i < 4; $i++) if (!isset($parts[$i])) $parts[$i] = ''; + $key = $parts[0] . '|' . $parts[1]; + $result[$key] = [$parts[2], $parts[3]]; + } + return $result; +} + +function isValidVF($fields) +{ + list($fn, $mac) = $fields; + $mac = strtolower(trim($mac)); + $isZeroMac = ($mac === '00:00:00:00:00:00'); + $hasMac = ($mac !== '' && !$isZeroMac); + if ($fn === 1) return true; + if ($fn > 1) return true; + if ($fn === 0) return $hasMac; + return $hasMac; +} + +function updateVFSettings($input, $saved) +{ + $inputParsed = parseVF($input); + $savedParsed = parseVF($saved); + $updated = []; + foreach ($savedParsed as $key => $_) { + if (isset($inputParsed[$key]) && isValidVF($inputParsed[$key])) $updated[$key] = $inputParsed[$key]; + } + foreach ($inputParsed as $key => $fields) { + if (!isset($savedParsed[$key]) && isValidVF($fields)) $updated[$key] = $fields; + } + $result = []; + foreach ($savedParsed as $key => $_) { + if (!isset($updated[$key])) continue; + list($pci,$vd) = explode('|',$key); + list($fn,$mac) = $updated[$key]; + if ($fn === '1' && ($mac === '' || $mac === null)) $mac = '00:00:00:00:00:00'; + $result[] = "$pci|$vd|$fn|$mac"; + } + foreach ($inputParsed as $key => $_) { + if (isset($savedParsed[$key])) continue; + if (!isset($updated[$key])) continue; + list($pci,$vd) = explode('|',$key); + list($fn,$mac) = $updated[$key]; + if ($fn === '1' && ($mac === '' || $mac === null)) $mac = '00:00:00:00:00:00'; + $result[] = "$pci|$vd|$fn|$mac"; + } + return implode(' ', $result); +} + + $vfio = '/boot/config/vfio-pci.cfg'; +$sriovvfs = '/boot/config/sriovvfs.cfg'; + +#Save Normal VFIOs $old = is_file($vfio) ? rtrim(file_get_contents($vfio)) : ''; $new = _var($_POST,'cfg'); @@ -23,7 +83,21 @@ if ($new != $old) { if ($old) copy($vfio,"$vfio.bak"); if ($new) file_put_contents($vfio,$new); else @unlink($vfio); - $reply = 1; + $reply |= 1; +} + +#Save SRIOV VFS +$oldvfcfg = is_file($sriovvfs) ? rtrim(file_get_contents($sriovvfs)) : ''; +$newvfcfg = _var($_POST,'vfcfg'); +$oldvfcfg_updated = updateVFSettings($newvfcfg,$oldvfcfg); +if (strpos($oldvfcfg_updated,"VFSETTINGS=") !== 0 && $oldvfcfg_updated != "") $oldvfcfg_updated = "VFSETTINGS=".$oldvfcfg_updated; + +#file_put_contents("/tmp/updatevfs",[json_encode($oldvfcfg_updated),json_encode($oldvfcfg)]); +if ($oldvfcfg_updated != $oldvfcfg) { + if ($oldvfcfg) copy($sriovvfs,"$sriovvfs.bak"); + if ($oldvfcfg_updated) file_put_contents($sriovvfs,$oldvfcfg_updated); else @unlink($sriovvfs); + $reply |= 2; } + echo $reply; ?> diff --git a/emhttp/plugins/dynamix/scripts/diagnostics b/emhttp/plugins/dynamix/scripts/diagnostics index 97563e220f..e6d3be1f83 100755 --- a/emhttp/plugins/dynamix/scripts/diagnostics +++ b/emhttp/plugins/dynamix/scripts/diagnostics @@ -776,6 +776,20 @@ if (file_exists($graphql)) { anonymize_email($graphql); } +// copy sriov log +$sriov = "/var/log/sriov"; +if (file_exists($sriov)) { + $log = "/$diag/logs/sriov.txt"; + run("todos <$sriov >".escapeshellarg($log)); +} + +// copy sriov log +$srioverrors = "/var/log/sriov-errors"; +if (file_exists($srioverrors)) { + $log = "/$diag/logs/sriov-errors.txt"; + run("todos <$srioverrors >".escapeshellarg($log)); +} + // copy vfio-pci log $vfiopci = "/var/log/vfio-pci"; if (file_exists($vfiopci)) { diff --git a/emhttp/plugins/dynamix/sheets/SysDevs.css b/emhttp/plugins/dynamix/sheets/SysDevs.css index 42597cef92..3da284b96b 100644 --- a/emhttp/plugins/dynamix/sheets/SysDevs.css +++ b/emhttp/plugins/dynamix/sheets/SysDevs.css @@ -13,3 +13,12 @@ table tr td.thin { line-height: 8px; height: 8px; } +.sweet-alert.swal-hostid-input input { + width: 60px !important; + margin: 12px auto 0 auto !important; + text-align: center !important; + display: block !important; + padding-left: 0 !important; + padding-right: 0 !important; + box-sizing: border-box !important; +} \ No newline at end of file diff --git a/etc/rc.d/rc.local b/etc/rc.d/rc.local index 292bf1fdeb..d59ea8fca3 100755 --- a/etc/rc.d/rc.local +++ b/etc/rc.d/rc.local @@ -10,10 +10,14 @@ # # LimeTech - modified for Unraid OS # Bergware - modified for Unraid OS, October 2023 +# Simon Fairweather - modified for Unraid OS, November 2025(SRIOV) # run & log functions . /etc/rc.d/rc.runlog +# LimeTech/SF - setup selected devices for sriov +/usr/local/sbin/sriov 1>/var/log/sriov 2>/var/log/sriov-errors + # import CA proxy UNPROXY=/boot/config/plugins/dynamix/outgoingproxy.cfg CAPROXY=/boot/config/plugins/community.applications/proxy.cfg diff --git a/sbin/sriov b/sbin/sriov new file mode 100755 index 0000000000..385f4cd6c7 --- /dev/null +++ b/sbin/sriov @@ -0,0 +1,75 @@ +#!/bin/bash +# limetech - wrapper for SRIOV processing +# +# Invoked early in startup before any devices are bound. +# +# Order does not matter. If both are provided, must be separated by "|". +# Multiple entries must be separated by space. +# + +# Invoke script for each device referenced via /boot/config/sriov.cfg & sriovvfs.cfg +# Accept string enclosed in quotes or not +# 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 +# accepts space-separated list of or followed by an optional "|" and | +# example:VFSETTINGS=0000:04:11.5|8086:1520|62:00:04:11:05:01 + +SRIOV_DISABLED_FILE=/boot/config/sriov_disabled +if [[ -f $SRIOV_DISABLED_FILE ]] ; then + echo 'SRIOV Processing disabled.' + exit +fi + +CFG=/boot/config/sriov.cfg + +[[ ! -f "$CFG" ]] && exit +grep -q "^VFS=" "$CFG" || exit +echo -e "Loading VFs config from $CFG\n" +cat $CFG +echo "---" + +if [[ ! "$(ls -A /sys/kernel/iommu_groups/)" ]]; then + echo "Error: IOMMU not available" + exit 1 +fi + +# Read the line properly (don’t let bash squash the spaces) +VFS_LINE=$(grep "^VFS=" "$CFG" | cut -d= -f2- | tr -d '"') +[[ -z "$VFS_LINE" ]] && exit + +for PARAM in $VFS_LINE; do + IFS='|' read -r arg1 arg2 arg3 <<< "$PARAM" + echo "Processing $arg1 $arg2 set VFs to $arg3" + /usr/local/sbin/sriov-setvfs.sh "$arg1" "$arg2" "$arg3" + echo "---" +done + +echo 'Devices VFs defined:' +ls -l /sys/bus/pci/devices/*/virtfn*| egrep [[:xdigit:]]{4}: + +printf "\n" + +CFG_VFS=/boot/config/sriovvfs.cfg + +[[ ! -f "$CFG_VFS" ]] && exit +grep -q "VFSETTINGS=" "$CFG_VFS" || exit +echo -e "Loading settings config from $CFG_VFS/n" +cat "$CFG_VFS" +echo "---" + +if [[ ! "$(ls -A /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then + echo "Error: IOMMU not available" + exit 1 +fi + +VFSETTINGS_LINE=$(grep "^VFSETTINGS=" "$CFG_VFS" | cut -d= -f2- | tr -d '"') + +for PARAM_VFS in $VFSETTINGS_LINE; do + IFS='|' read -r arg1 arg2 arg3 arg4 <<< "$PARAM_VFS" + echo "Processing $arg1 $arg2 set Mac to $arg4" + /usr/local/sbin/sriov-vfsettings.sh "$arg1" "$arg2" "$arg3" "$arg4" + echo "---" +done + + +echo "SRIOV processing complete" diff --git a/sbin/sriov-setvfs.sh b/sbin/sriov-setvfs.sh new file mode 100755 index 0000000000..13470b13a0 --- /dev/null +++ b/sbin/sriov-setvfs.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# -*- coding: utf-8 -*- +# +# ============================================================================= +# +# The MIT License (MIT) +# +# Copyright (c) 2025- Limetech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# ============================================================================= +# +# Author(s): +# Simon Fairweather based on code from: Andre Richter, +# +# ============================================================================= +# +# This script takes three parameters: +# i.e. dddd:bb:dd.f +# i.e. vvvv:dddd +# +# and then: +# +# (1) If both and were provided, +# validate that the requested exists at +# +# (2) Set numvfs to value supplied as 3rd parameter: +# + +BDF_REGEX="^[[:xdigit:]]{2}:[[:xdigit:]]{2}.[[:xdigit:]]$" +DBDF_REGEX="^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}.[[:xdigit:]]$" +VD_REGEX="^[[:xdigit:]]{4}:[[:xdigit:]]{4}$" + +if [[ $EUID -ne 0 ]]; then + echo "Error: This script must be run as root" 1>&2 + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "Error: Please provide Domain:Bus:Device.Function (dddd:bb:dd.f) and/or Vendor:Device (vvvv:dddd)" 1>&2 + exit 1 +fi + +# Check that 3 parameters are supplied +if [[ $# -ne 3 ]]; then + echo "Error: Expected 3 parameters, but got $#." 1>&2 + echo "Usage: $0 " 1>&2 + echo "Example: $0 0000:01:00.0 10de:1fb8 numvfs" 1>&2 + exit 1 +fi + +unset VD BDF NUMVFS +for arg in "$@" +do + if [[ $arg =~ $VD_REGEX ]]; then + VD=$arg + elif [[ $arg =~ $DBDF_REGEX ]]; then + BDF=$arg + elif [[ $arg =~ $BDF_REGEX ]]; then + BDF="0000:${arg}" + echo "Warning: You did not supply a PCI domain, assuming ${BDF}" 1>&2 + else + # Treat as 3rd parameter (not a PCI ID) + if [[ -z $NUMVFS ]]; then + NUMVFS=$arg + else + echo "Error: Unrecognized argument '$arg'" 1>&2 + exit 1 + fi + fi +done + +if [[ -z "$BDF" ]]; then + echo "Error: No valid Bus:Device.Function provided" 1>&2 + exit 1 +fi + +TARGET_DEV_SYSFS_PATH="/sys/bus/pci/devices/$BDF" + +if [[ ! -d $TARGET_DEV_SYSFS_PATH ]]; then + echo "Error: Device ${BDF} does not exist, unable to action VFs setting" 1>&2 + exit 1 +fi + +if [[ ! -d "$TARGET_DEV_SYSFS_PATH/iommu/" ]]; then + echo "Error: No signs of an IOMMU. Check your hardware and/or linux cmdline parameters. Use intel_iommu=on or iommu=pt iommu=1" 1>&2 + exit 1 +fi + +# validate that the correct Vendor:Device was found for this BDF +if [[ ! -z $VD ]]; then + if [[ $(lspci -n -s ${BDF} -d ${VD} 2>/dev/null | wc -l) -eq 0 ]]; then + echo "Error: Vendor:Device ${VD} not found at ${BDF}, unable to action VFs setting" 1>&2 + exit 1 + else + echo "Vendor:Device ${VD} found at ${BDF}" + fi +else + echo "Warning: You did not specify a Vendor:Device (vvvv:dddd), unable to validate ${BDF}" 1>&2 +fi + +if [[ -z "$NUMVFS" ]]; then + echo "Error: No VF count provided" 1>&2 + exit 1 +fi + +if ! [[ "$NUMVFS" =~ ^[0-9]+$ ]]; then + echo "Error: VF count must be a non-negative integer" 1>&2 + exit 1 +fi + +printf "\nSetting...\n" + +# Capture stderr output from echo into a variable +error_msg=$( (echo "$NUMVFS" > "$TARGET_DEV_SYSFS_PATH/sriov_numvfs") 2>&1 ) + +if [[ $? -ne 0 ]]; then + echo "Error: Failed to set sriov_numvfs at $TARGET_DEV_SYSFS_PATH" >&2 + clean_msg=$(echo "$error_msg" | sed -n 's/.*error: \(.*\)/\1/p') + echo "System message: $clean_msg" >&2 + exit 1 +fi + +printf "\n" +echo "Device ${VD} at ${BDF} set numvfs to ${NUMVFS}" diff --git a/sbin/sriov-vfsettings.sh b/sbin/sriov-vfsettings.sh new file mode 100755 index 0000000000..cd15e53287 --- /dev/null +++ b/sbin/sriov-vfsettings.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# -*- coding: utf-8 -*- +# +# ============================================================================= +# +# The MIT License (MIT) +# +# Copyright (c) 2025- Limetech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# ============================================================================= +# +# Author(s): +# Simon Fairweather based on code from: Andre Richter, +# +# ============================================================================= +# +# This script takes three parameters: +# i.e. dddd:bb:dd.f +# i.e. vvvv:dddd +# +# and then: +# +# (1) If both and were provided, +# validate that the requested exists at +# +# (2) Set MAC to value supplied as 3rd parameter: +# +# (3) Unbind and rebind network driver for change to take effect. +# + +BDF_REGEX="^[[:xdigit:]]{2}:[[:xdigit:]]{2}.[[:xdigit:]]$" +DBDF_REGEX="^[[:xdigit:]]{4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}.[[:xdigit:]]$" +VD_REGEX="^[[:xdigit:]]{4}:[[:xdigit:]]{4}$" + +if [[ $EUID -ne 0 ]]; then + echo "Error: This script must be run as root" 1>&2 + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "Error: Please provide Domain:Bus:Device.Function (dddd:bb:dd.f) and/or Vendor:Device (vvvv:dddd)" 1>&2 + exit 1 +fi + +# Check that 4 parameters are supplied +if [[ $# -ne 4 ]]; then + echo "Error: Expected 4 parameters, but got $#." 1>&2 + echo "Usage: $0 " 1>&2 + echo "Example: $0 0000:01:00.0 10de:1fb8 1 62:00:01:00:00:99" 1>&2 + echo "parm3 is binding to VFIO, parm 4 is mac address." + exit 1 +fi + +unset VD BDF VFIO MAC +for arg in "$@"; do + if [[ $arg =~ $VD_REGEX ]]; then + VD=$arg + elif [[ $arg =~ $DBDF_REGEX ]]; then + BDF=$arg + elif [[ $arg =~ $BDF_REGEX ]]; then + BDF="0000:${arg}" + echo "Warning: You did not supply a PCI domain, assuming ${BDF}" 1>&2 + elif [[ $arg =~ ^[01]$ ]]; then + # 3rd argument: VFIO flag (0 or 1) + VFIO=$arg + elif [[ $arg =~ ^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$ ]]; then + # 4th argument: MAC address + MAC=$arg + else + echo "Error: Unrecognized argument '$arg'" 1>&2 + exit 1 + fi +done + +if [[ -z "$BDF" ]]; then + echo "Error: No valid Bus:Device.Function provided" 1>&2 + exit 1 +fi + +TARGET_DEV_SYSFS_PATH="/sys/bus/pci/devices/$BDF" + +if [[ ! -d $TARGET_DEV_SYSFS_PATH ]]; then + echo "Error: Device ${BDF} does not exist, unable to action VFs setting" 1>&2 + exit 1 +fi + +if [[ ! -d "$TARGET_DEV_SYSFS_PATH/iommu/" ]]; then + echo "Error: No signs of an IOMMU. Check your hardware and/or linux cmdline parameters. Use intel_iommu=on or iommu=pt iommu=1" 1>&2 + exit 1 +fi + +# validate that the correct Vendor:Device was found for this BDF +if [[ ! -z $VD ]]; then + if [[ $(lspci -n -s ${BDF} -d ${VD} 2>/dev/null | wc -l) -eq 0 ]]; then + echo "Error: Vendor:Device ${VD} not found at ${BDF}, unable to action VFs setting" 1>&2 + exit 1 + else + echo "Vendor:Device ${VD} found at ${BDF}" + fi +else + echo "Warning: You did not specify a Vendor:Device (vvvv:dddd), unable to validate ${BDF}" 1>&2 +fi + +printf "\nSetting...\n" + +# Locate PF device + +VF_PCI=$BDF +VF_PATH="/sys/bus/pci/devices/$VF_PCI" +PF_PATH=$(readlink -f "$VF_PATH/physfn" 2>/dev/null) + +if [ ! -d "$PF_PATH" ]; then + echo "Error: No PF found for VF $VF_PCI" + exit 1 +fi + +# Determine PF interface name +PF_IFACE=$(basename "$(readlink -f "$PF_PATH/net"/* 2>/dev/null)") +if [ -z "$PF_IFACE" ]; then + PF_IFACE=$(basename "$(ls -d /sys/class/net/*/device 2>/dev/null | grep "$PF_PATH" | head -n1 | cut -d/ -f5)") +fi + +if [ -z "$PF_IFACE" ]; then + echo "Error: Could not determine PF interface for $VF_PCI" + exit 1 +fi + + +# Determine VF index +VF_INDEX="" +for vfdir in /sys/class/net/$PF_IFACE/device/virtfn*; do + [ -e "$vfdir" ] || continue + vf_pci=$(basename "$(readlink -f "$vfdir")") + if [ "$vf_pci" = "$VF_PCI" ]; then + VF_INDEX=${vfdir##*/virtfn} + break + fi +done + +if [ -z "$VF_INDEX" ]; then + echo "Error: VF index not found for $VF_PCI under PF $BDF" + exit 1 +fi + +if [[ -z "$MAC" ]]; then + echo "Error: No MAC address provided" 1>&2 + exit 1 +fi + +if ! [[ "$MAC" =~ ^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$ ]]; then + echo "Error: Invalid MAC address format. Expected XX:XX:XX:XX:XX:XX" 1>&2 + exit 1 +fi + +echo "Setting MAC for VF:" +echo " PF: $PF_IFACE" +echo " VF Index: $VF_INDEX" +echo " PCI: $VF_PCI" +echo " MAC: $MAC" + +# Determine current driver for this VF +VF_DRIVER=$(basename "$(readlink -f "$VF_PATH/driver" 2>/dev/null)") + +# Unbind VF from driver if loaded +if [ -n "$VF_DRIVER" ]; then + echo "Unbinding VF from driver $VF_DRIVER..." + echo "$VF_PCI" > "/sys/bus/pci/drivers/$VF_DRIVER/unbind" +fi + +# Set MAC +if ! ip link set "$PF_IFACE" vf "$VF_INDEX" mac "$MAC"; then + echo "Error: Failed to set MAC address $MAC on VF $VF_INDEX" >&2 + exit 1 +fi + +# Rebind VF to driver if it was bound before +if [ -n "$VF_DRIVER" ]; then + echo "Rebinding VF to driver $VF_DRIVER..." + echo "$VF_PCI" > "/sys/bus/pci/drivers/$VF_DRIVER/bind" +fi + +echo "MAC Address set" + +if [[ "$VFIO" == "1" ]]; then + echo "Binding VF to vfio" + /usr/local/sbin/vfio-pci-bind.sh "$BDF" "$VD" \ + 1>>/var/log/vfio-pci \ + 2>>/var/log/vfio-pci-errors +fi + +