From 68d255846f2bf189c970f62340ee70e7cb392bbc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Jul 2026 08:30:45 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9B=85=20Sync=202026-07-30=2008:30:45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-qemu-vms/Makefile | 2 +- .../resources/view/qemu-vms/networks.js | 25 +- .../resources/view/qemu-vms/passthrough.js | 82 ++++-- .../resources/view/qemu-vms/status.js | 243 +++++++++--------- qemu-vms/Makefile | 2 +- qemu-vms/root/etc/init.d/qemu-vms | 144 ++++++----- 6 files changed, 270 insertions(+), 228 deletions(-) diff --git a/luci-app-qemu-vms/Makefile b/luci-app-qemu-vms/Makefile index 225a0715..311bd5f7 100644 --- a/luci-app-qemu-vms/Makefile +++ b/luci-app-qemu-vms/Makefile @@ -4,7 +4,7 @@ LUCI_TITLE:=Web UI for QEMU Virtual Machine Simple (VMS) LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils PKG_LICENSE:=GPLv3 PKG_VERSION:=0.0.1 -PKG_RELEASE:=4 +PKG_RELEASE:=5 include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/networks.js b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/networks.js index 6d0b31f7..296268e7 100644 --- a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/networks.js +++ b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/networks.js @@ -29,9 +29,9 @@ return view.extend({ var m, s; m = new form.Map('qemu-vms', _('Virtual network segments'), - _('Define reusable tap interfaces that can be attached to one or more VMs. ' + - 'Interfaces are brought up by /etc/qemu-ifup at VM start; if "bridge" is set, ' + - 'the interface is also enslaved into that bridge, otherwise it is left standalone.')); + _('Define reusable tap interfaces that can be attached to one or more VMs.') + + '
' + _('Interfaces are brought up by /etc/qemu-ifup at VM start; if "bridge" is set,') + + ' ' + _('the interface is also enslaved into that bridge, otherwise it is left standalone.')); s = m.section(form.GridSection, 'network', _('Networks')); s.addremove = true; @@ -46,13 +46,13 @@ return view.extend({ return _('Invalid MAC address format (expected xx:xx:xx:xx:xx:xx)'); return true; }; - mac.write = function(section_id, value) { - if (!value) - value = randomMac(); - return form.Value.prototype.write.call(this, section_id, value); - }; + mac.rmempty = false; - mac.value(randomMac(), _('Automatically assigned')); + mac.renderWidget = function(section_id, option_index, cfgvalue) { + this.keylist = [randomMac()]; + this.vallist = [_('Automatically assigned')]; + return form.Value.prototype.renderWidget.apply(this, arguments); + }; var ifname = s.option(form.Value, 'ifname', _('tap ifname')); ifname.placeholder = 'tap0'; @@ -66,9 +66,8 @@ return view.extend({ var bridge = s.option(form.Value, 'bridge', _('Bridge (optional)')); bridge.placeholder = _('leave empty to keep the interface standalone'); - // --- Поле driver с динамическим списком --- var driver = s.option(form.ListValue, 'driver', _('Network driver')); - // Добавляем опции из загруженного списка + if (drivers.length) { drivers.forEach(function(d) { driver.value(d, d); @@ -78,8 +77,8 @@ return view.extend({ } driver.default = 'virtio-net-pci'; driver.rmempty = true; - driver.description = _('Choose the emulated network card model. If empty, virtio-net-pci is used.'); + driver.description = _('Choose the emulated network card model. Default driver: virtio-net-pci'); return m.render(); } -}); +}); \ No newline at end of file diff --git a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js index 55e9ff75..7471c20b 100644 --- a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js +++ b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js @@ -70,8 +70,6 @@ return view.extend({ return owners; }, - // --- Действия для PCI --- - createPciPassthrough: function(dev, ev) { var self = this; var addr = dev.addr; @@ -132,7 +130,6 @@ return view.extend({ ui.hideModal(); ui.addNotification(null, E('p', _('PCI passthrough section "%s" created for %s. Device is now detached from the host driver.').format(sectionName, dev.addr)), 'info'); - // Обновляем представление без перезагрузки self.updateView(); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving: ') + err.message), 'error'); @@ -165,7 +162,6 @@ return view.extend({ return uci.save().then(function() { ui.hideModal(); ui.addNotification(null, E('p', _('Passthrough removed for %s.').format(addr)), 'info'); - // Обновляем представление без перезагрузки self.updateView(); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving: ') + err.message), 'error'); @@ -176,8 +172,6 @@ return view.extend({ ]); }, - // --- Действия для USB --- - createUsbPassthrough: function(dev, ev) { var self = this; var parts = (dev.id || '').split(':'); @@ -246,7 +240,6 @@ return view.extend({ ui.hideModal(); ui.addNotification(null, E('p', _('USB passthrough section "%s" created for %s.').format(sectionName, dev.id)), 'info'); - // Обновляем представление без перезагрузки self.updateView(); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving: ') + err.message), 'error'); @@ -279,7 +272,6 @@ return view.extend({ return uci.save().then(function() { ui.hideModal(); ui.addNotification(null, E('p', _('Passthrough removed.')), 'info'); - // Обновляем представление без перезагрузки self.updateView(); }).catch(function(err) { ui.addNotification(null, E('p', _('Error saving: ') + err.message), 'error'); @@ -290,8 +282,6 @@ return view.extend({ ]); }, - // --- Рендеринг таблиц --- - renderPciTable: function(devices) { var self = this; var rows = devices.map(function(dev) { @@ -379,15 +369,11 @@ return view.extend({ ].concat(rows)); }, - // --- Метод обновления представления --- - updateView: function() { var self = this; if (!this.container) return; - // Загружаем свежие данные this.load().then(function(data) { - // Перерисовываем содержимое контейнера var newContent = self.renderContent(data); self.container.innerHTML = ''; self.container.appendChild(newContent); @@ -396,8 +382,6 @@ return view.extend({ }); }, - // --- Основной рендер --- - render: function(data) { var hw = data[0]; var container = E('div', {}); @@ -405,26 +389,76 @@ return view.extend({ var content = this.renderContent(data); container.appendChild(content); - // Сохраняем ссылку на контейнер для обновления this.container = container; return container; }, - // Вспомогательная функция для рендеринга контента (используется и в render, и в updateView) renderContent: function(data) { var hw = data[0]; - return E('div', {}, [ - E('h3', {}, _('PCI devices')), + var self = this; + + var description = E('div', { 'class': 'cbi-section' }, [ + E('h2', {}, _('Hardware Passthrough')), + E('p', { 'class': 'cbi-section-descr' }, + _('This page allows you to configure passthrough of PCI and USB devices to virtual machines. ') + + '
' + _('Changes take effect after VM restart.')) + ]); + + // tabs + var tabs = E('ul', { 'class': 'cbi-tabmenu' }, [ + E('li', { 'class': 'cbi-tab', 'data-tab': 'pci' }, [ + E('a', { + 'href': '#', + 'click': function(ev) { + ev.preventDefault(); + self.switchTab('pci'); + } + }, _('PCI devices')) + ]), + E('li', { 'class': 'cbi-tab-disabled', 'data-tab': 'usb' }, [ + E('a', { + 'href': '#', + 'click': function(ev) { + ev.preventDefault(); + self.switchTab('usb'); + } + }, _('USB devices')) + ]) + ]); + + var pciPanel = E('div', { 'class': 'cbi-tabpanel', 'id': 'pci-panel' }, [ E('p', { 'class': 'cbi-section-descr' }, _('PCI passthrough requires IOMMU enabled in the host BIOS and kernel command line (intel_iommu=on / amd_iommu=on), and the vfio-pci kernel module loaded.') + - ' ' + _('Creating a passthrough section will bind the device to vfio-pci (if possible). Removing it will attempt to re-bind to the original driver.')), - this.renderPciTable(hw.pci || []), + '
' + _('Creating a passthrough section will bind the device to vfio-pci (if possible). Removing it will attempt to re-bind to the original driver.') + + '
' + _('Note: Some devices (especially Intel network cards) may require the vfio-pci module option "disable_idle_d3=1" to prevent them from entering a low-power state.') + + '
' + _('Add "options vfio-pci disable_idle_d3=1" to /etc/modprobe.d/vfio-pci.conf if you encounter "No such device" errors.')), + this.renderPciTable(hw.pci || []) + ]); - E('h3', { 'style': 'margin-top: 2em' }, _('USB devices')), + var usbPanel = E('div', { 'class': 'cbi-tabpanel', 'id': 'usb-panel', 'style': 'display:none;' }, [ E('p', { 'class': 'cbi-section-descr' }, _('USB passthrough is based on vendor/product ID. The device will be available to any VM that includes the corresponding USB passthrough section.')), this.renderUsbTable(hw.usb || []) ]); - } + + return E('div', {}, [description, tabs, pciPanel, usbPanel]); + }, + + switchTab: function(tab) { + // all tabs + var tabs = document.querySelectorAll('li.cbi-tab, li.cbi-tab-disabled'); + tabs.forEach(function(el) { + if (el.dataset.tab === tab) { + el.className = 'cbi-tab'; + } else { + el.className = 'cbi-tab-disabled'; + } + }); + + var pciPanel = document.getElementById('pci-panel'); + var usbPanel = document.getElementById('usb-panel'); + if (pciPanel) pciPanel.style.display = (tab === 'pci') ? '' : 'none'; + if (usbPanel) usbPanel.style.display = (tab === 'usb') ? '' : 'none'; + }, }); diff --git a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/status.js b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/status.js index 7c5aa914..b71b4545 100644 --- a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/status.js +++ b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/status.js @@ -48,9 +48,6 @@ function widenModal() { } } - -// ----- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ПОСТРОЕНИЯ СЕЛЕКТОВ ----- - function buildMachineSelect(vm, machines) { var select = E('select', { 'class': 'cbi-input-select', 'id': 'edit-machine' }); machines.forEach(function(m) { @@ -212,6 +209,13 @@ return view.extend({ var self = this; var container = E('div', {}); + container.appendChild(E('div', { 'class': 'cbi-section' }, [ + E('h2', {}, _('Virtual Machines')), + E('p', { 'class': 'cbi-section-descr' }, + _('This page lists all configured virtual machines. You can start, stop, restart, open a console, view logs, edit, or delete each VM. ' + + 'Changes to VM configuration require a restart of the VM to take effect.')) + ])); + container.appendChild(E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom: 1em' }, [ E('button', { 'class': 'btn cbi-button cbi-button-add', @@ -388,7 +392,6 @@ return view.extend({ var enabledInput = E('input', { 'type': 'checkbox', 'id': 'edit-enabled', 'checked': (vm.enabled !== '0') || null }); var memoryInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-memory', 'type': 'number', 'min': '32', 'step': '32', 'value': vm.memory || '128' }); var smpInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-smp', 'type': 'number', 'min': '1', 'max': '16', 'step': '1', 'value': vm.smp || '1' }); - // cpuSelect и machineSelect переданы как аргументы var imageInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-image', 'value': vm.image || '', 'placeholder': '/mnt/disk/vm/name.img' }); var diskBusSelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-disk-bus' }, [ @@ -532,130 +535,122 @@ return view.extend({ var cpuSelect = buildCpuSelectFallback(vm); showEditModal(machineSelect, cpuSelect); }); - }, + }, + saveEdit: function(existingName, ev) { + var name = existingName || document.getElementById('edit-name').value.trim(); - saveEdit: function(existingName, ev) { - var name = existingName || document.getElementById('edit-name').value.trim(); - - if (!name) { - ui.addNotification(null, E('p', _('VM name is required')), 'error'); - return; - } - if (!existingName && uci.get('qemu-vms', name)) { - ui.addNotification(null, E('p', _('A VM with this name already exists')), 'error'); - return; - } - - if (!existingName) - uci.add('qemu-vms', 'vm', name); - - var memoryRaw = document.getElementById('edit-memory').value.trim(); - var smpRaw = document.getElementById('edit-smp').value.trim(); - - if (!/^\d+$/.test(memoryRaw)) { - ui.addNotification(null, E('p', _('Memory must be a whole number (MB)')), 'error'); - return; - } - if (!/^\d+$/.test(smpRaw)) { - ui.addNotification(null, E('p', _('SMP must be a whole number')), 'error'); - return; - } - - var memory = parseInt(memoryRaw, 10); - var smp = parseInt(smpRaw, 10); - - if (memory < 32) { - ui.addNotification(null, E('p', _('Memory must be \u2265 32 MB')), 'error'); - return; - } - if (smp < 1 || smp > 16) { - ui.addNotification(null, E('p', _('SMP must be between 1 and 16')), 'error'); - return; - } - - uci.set('qemu-vms', name, 'enabled', document.getElementById('edit-enabled').checked ? '1' : '0'); - uci.set('qemu-vms', name, 'memory', String(memory)); - uci.set('qemu-vms', name, 'smp', String(smp)); - uci.set('qemu-vms', name, 'cpu', document.getElementById('edit-cpu').value); - uci.set('qemu-vms', name, 'machine', document.getElementById('edit-machine').value || 'q35'); - uci.set('qemu-vms', name, 'image', document.getElementById('edit-image').value); - uci.set('qemu-vms', name, 'disk_bus', document.getElementById('edit-disk-bus').value); - - var displayType = document.getElementById('edit-display').value; - uci.set('qemu-vms', name, 'display_type', displayType); - - if (displayType === 'vnc') { - var vncPortRaw = document.getElementById('edit-vnc-port').value.trim(); - if (!/^\d+$/.test(vncPortRaw) || parseInt(vncPortRaw, 10) < 5900) { - ui.addNotification(null, E('p', _('VNC port must be a number \u2265 5900')), 'error'); + if (!name) { + ui.addNotification(null, E('p', _('VM name is required')), 'error'); return; } - uci.set('qemu-vms', name, 'vnc_port', vncPortRaw); - } else { - uci.unset('qemu-vms', name, 'vnc_port'); - } - - var cdrom = document.getElementById('edit-cdrom').value.trim(); - if (cdrom) - uci.set('qemu-vms', name, 'cdrom', cdrom); - else - uci.unset('qemu-vms', name, 'cdrom'); - - var customArgs = document.getElementById('edit-custom-args').value - .split('\n') - .map(function(line) { return line.trim(); }) - .filter(function(line) { return line.length > 0; }); - if (customArgs.length) - uci.set('qemu-vms', name, 'custom_arg', customArgs); - else - uci.unset('qemu-vms', name, 'custom_arg'); - - var pci = document.getElementById('edit-pci').value; - if (pci) - uci.set('qemu-vms', name, 'pci_passthrough', pci); - else - uci.unset('qemu-vms', name, 'pci_passthrough'); - - var newUsb = Array.prototype.slice.call(document.querySelectorAll('.edit-usb-check:checked')) - .map(function(el) { return el.value; }); - if (newUsb.length) - uci.set('qemu-vms', name, 'usb_passthrough', newUsb); - else - uci.unset('qemu-vms', name, 'usb_passthrough'); - - - //var nets = Array.prototype.slice.call(document.querySelectorAll('.edit-network-check:checked')) - // .map(function(el) { return el.value; }); - //uci.set('qemu-vms', name, 'network', nets); - - //var newUsb = Array.prototype.slice.call(document.querySelectorAll('.edit-usb-check:checked')) - // .map(function(el) { return el.value; }); - - var newNets = Array.prototype.slice.call(document.querySelectorAll('.edit-network-check:checked')) - .map(function(el) { return el.value; }); - - if (existingName) { - var oldNets = uci.get('qemu-vms', name, 'network') || []; - if (!Array.isArray(oldNets)) oldNets = oldNets ? [oldNets] : []; - - var sortedOld = oldNets.slice().sort(); - var sortedNew = newNets.slice().sort(); - var changed = (sortedOld.length !== sortedNew.length) || - sortedOld.some(function(v, i) { return v !== sortedNew[i]; }); - - if (changed) { - uci.set('qemu-vms', name, 'network', newNets); // только если изменился + if (!existingName && uci.get('qemu-vms', name)) { + ui.addNotification(null, E('p', _('A VM with this name already exists')), 'error'); + return; } - } else { - uci.set('qemu-vms', name, 'network', newNets); + + if (!existingName) + uci.add('qemu-vms', 'vm', name); + + var memoryRaw = document.getElementById('edit-memory').value.trim(); + var smpRaw = document.getElementById('edit-smp').value.trim(); + + if (!/^\d+$/.test(memoryRaw)) { + ui.addNotification(null, E('p', _('Memory must be a whole number (MB)')), 'error'); + return; + } + if (!/^\d+$/.test(smpRaw)) { + ui.addNotification(null, E('p', _('SMP must be a whole number')), 'error'); + return; + } + + var memory = parseInt(memoryRaw, 10); + var smp = parseInt(smpRaw, 10); + + if (memory < 32) { + ui.addNotification(null, E('p', _('Memory must be \u2265 32 MB')), 'error'); + return; + } + if (smp < 1 || smp > 16) { + ui.addNotification(null, E('p', _('SMP must be between 1 and 16')), 'error'); + return; + } + + uci.set('qemu-vms', name, 'enabled', document.getElementById('edit-enabled').checked ? '1' : '0'); + uci.set('qemu-vms', name, 'memory', String(memory)); + uci.set('qemu-vms', name, 'smp', String(smp)); + uci.set('qemu-vms', name, 'cpu', document.getElementById('edit-cpu').value); + uci.set('qemu-vms', name, 'machine', document.getElementById('edit-machine').value || 'q35'); + uci.set('qemu-vms', name, 'image', document.getElementById('edit-image').value); + uci.set('qemu-vms', name, 'disk_bus', document.getElementById('edit-disk-bus').value); + + var displayType = document.getElementById('edit-display').value; + uci.set('qemu-vms', name, 'display_type', displayType); + + if (displayType === 'vnc') { + var vncPortRaw = document.getElementById('edit-vnc-port').value.trim(); + if (!/^\d+$/.test(vncPortRaw) || parseInt(vncPortRaw, 10) < 5900) { + ui.addNotification(null, E('p', _('VNC port must be a number \u2265 5900')), 'error'); + return; + } + uci.set('qemu-vms', name, 'vnc_port', vncPortRaw); + } else { + uci.unset('qemu-vms', name, 'vnc_port'); + } + + var cdrom = document.getElementById('edit-cdrom').value.trim(); + if (cdrom) + uci.set('qemu-vms', name, 'cdrom', cdrom); + else + uci.unset('qemu-vms', name, 'cdrom'); + + var customArgs = document.getElementById('edit-custom-args').value + .split('\n') + .map(function(line) { return line.trim(); }) + .filter(function(line) { return line.length > 0; }); + if (customArgs.length) + uci.set('qemu-vms', name, 'custom_arg', customArgs); + else + uci.unset('qemu-vms', name, 'custom_arg'); + + var pci = document.getElementById('edit-pci').value; + if (pci) + uci.set('qemu-vms', name, 'pci_passthrough', pci); + else + uci.unset('qemu-vms', name, 'pci_passthrough'); + + var newUsb = Array.prototype.slice.call(document.querySelectorAll('.edit-usb-check:checked')) + .map(function(el) { return el.value; }); + if (newUsb.length) + uci.set('qemu-vms', name, 'usb_passthrough', newUsb); + else + uci.unset('qemu-vms', name, 'usb_passthrough'); + + var newNets = Array.prototype.slice.call(document.querySelectorAll('.edit-network-check:checked')) + .map(function(el) { return el.value; }); + + if (existingName) { + var oldNets = uci.get('qemu-vms', name, 'network') || []; + if (!Array.isArray(oldNets)) oldNets = oldNets ? [oldNets] : []; + + var sortedOld = oldNets.slice().sort(); + var sortedNew = newNets.slice().sort(); + var changed = (sortedOld.length !== sortedNew.length) || + sortedOld.some(function(v, i) { return v !== sortedNew[i]; }); + + if (changed) { + uci.set('qemu-vms', name, 'network', newNets); // только если изменился + } + } else { + uci.set('qemu-vms', name, 'network', newNets); + } + + return uci.save().then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', + _('Saved to configuration. Restart the VM for changes to take effect.')), 'info'); + window.location.reload(); + }); } - - return uci.save().then(function() { - ui.hideModal(); - ui.addNotification(null, E('p', - _('Saved to configuration. Restart the VM for changes to take effect.')), 'info'); - window.location.reload(); - }); } -}); +); diff --git a/qemu-vms/Makefile b/qemu-vms/Makefile index f6c3a9af..52ebfa94 100644 --- a/qemu-vms/Makefile +++ b/qemu-vms/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=qemu-vms PKG_VERSION:=0.0.1 -PKG_RELEASE:=4 +PKG_RELEASE:=5 PKG_MAINTAINER:=Konstantine Shevlakov include $(INCLUDE_DIR)/package.mk diff --git a/qemu-vms/root/etc/init.d/qemu-vms b/qemu-vms/root/etc/init.d/qemu-vms index 1e446fd0..8d97d3f0 100755 --- a/qemu-vms/root/etc/init.d/qemu-vms +++ b/qemu-vms/root/etc/init.d/qemu-vms @@ -123,36 +123,36 @@ _add_usb() { #} _unbind_usb_host() { - local usbcfg="$1" - local vendor_id product_id - config_get vendor_id "$usbcfg" vendor_id - config_get product_id "$usbcfg" product_id + local usbcfg="$1" + local vendor_id product_id + config_get vendor_id "$usbcfg" vendor_id + config_get product_id "$usbcfg" product_id logger -t qemu-vms "DEBUG unbind: usbcfg=$usbcfg vendor=$vendor_id product=$product_id" - [ -n "$vendor_id" ] && [ -n "$product_id" ] || return 0 - for dev in /sys/bus/usb/devices/*; do - if [ -f "$dev/idVendor" ] && [ -f "$dev/idProduct" ]; then - local v=$(cat "$dev/idVendor") p=$(cat "$dev/idProduct") - if [ "$v" = "$vendor_id" ] && [ "$p" = "$product_id" ]; then - local devname=$(basename "$dev") - if [ -L "$dev/driver" ]; then - local driver_path=$(readlink -f "$dev/driver") - echo -n "$devname" > "$driver_path/unbind" 2>/dev/null - host_log "$cfg" "USB: unbound $devname from $(basename "$driver_path")" - fi - # interfaces are SIBLINGS of $dev, not children - - # glob at the /sys/bus/usb/devices/ level, matching "$devname:*" - for iface in /sys/bus/usb/devices/"$devname":*; do - [ -e "$iface" ] || continue - if [ -L "$iface/driver" ]; then - local ifname=$(basename "$iface") - local idriver_path=$(readlink -f "$iface/driver") - echo -n "$ifname" > "$idriver_path/unbind" 2>/dev/null - host_log "$cfg" "USB: unbound $ifname from $(basename "$idriver_path")" - fi - done - fi - fi - done + [ -n "$vendor_id" ] && [ -n "$product_id" ] || return 0 + for dev in /sys/bus/usb/devices/*; do + if [ -f "$dev/idVendor" ] && [ -f "$dev/idProduct" ]; then + local v=$(cat "$dev/idVendor") p=$(cat "$dev/idProduct") + if [ "$v" = "$vendor_id" ] && [ "$p" = "$product_id" ]; then + local devname=$(basename "$dev") + if [ -L "$dev/driver" ]; then + local driver_path=$(readlink -f "$dev/driver") + echo -n "$devname" > "$driver_path/unbind" 2>/dev/null + host_log "$cfg" "USB: unbound $devname from $(basename "$driver_path")" + fi + # interfaces are SIBLINGS of $dev, not children - + # glob at the /sys/bus/usb/devices/ level, matching "$devname:*" + for iface in /sys/bus/usb/devices/"$devname":*; do + [ -e "$iface" ] || continue + if [ -L "$iface/driver" ]; then + local ifname=$(basename "$iface") + local idriver_path=$(readlink -f "$iface/driver") + echo -n "$ifname" > "$idriver_path/unbind" 2>/dev/null + host_log "$cfg" "USB: unbound $ifname from $(basename "$idriver_path")" + fi + done + fi + fi + done } build_customargs() { @@ -253,51 +253,65 @@ start_vm() { build_customargs "$cfg" build_diskargs "$cfg" "$image" - local pci_id="" vendor_id="" pci_opts="" - if [ -n "$pci_passthrough" ]; then - config_get pci_id "$pci_passthrough" pci_id - config_get vendor_id "$pci_passthrough" vendor_id - if [ -n "$vendor_id" ] && [ -n "$pci_id" ]; then - if [ -e "/sys/bus/pci/devices/0000:$pci_id/driver" ]; then - local current_driver - current_driver="$(basename "$(readlink -f "/sys/bus/pci/devices/0000:$pci_id/driver" 2>/dev/null)" 2>/dev/null)" - host_log "$cfg" "PCI passthrough: $pci_id already bound to driver '$current_driver'" - else - echo "$vendor_id" > /sys/bus/pci/drivers/vfio-pci/new_id 2>/dev/null - if [ -e "/sys/bus/pci/devices/0000:$pci_id/driver" ]; then - host_log "$cfg" "PCI passthrough: bound $pci_id (vendor/device $vendor_id) to vfio-pci" - else - host_log "$cfg" "WARNING: PCI passthrough: failed to bind $pci_id to vfio-pci - check IOMMU/BIOS settings" - fi - fi - pci_opts="-device vfio-pci,host=$pci_id" - else - host_log "$cfg" "WARNING: pci_passthrough='$pci_passthrough' is set but its pci_id/vendor_id are incomplete - skipping passthrough" - fi - fi - - #local pci_id="" pci_opts="" + #local pci_id="" vendor_id="" pci_opts="" #if [ -n "$pci_passthrough" ]; then # config_get pci_id "$pci_passthrough" pci_id - # if [ -n "$pci_id" ]; then - # local pci_path="/sys/bus/pci/devices/0000:$pci_id" - # if [ -e "$pci_path/driver" ]; then - # echo -n "0000:$pci_id" > "$pci_path/driver/unbind" 2>/dev/null - # host_log "$cfg" "PCI: unbound $pci_id from $(basename "$(readlink -f "$pci_path/driver")")" - # fi - # echo "vfio-pci" > "$pci_path/driver_override" 2>/dev/null - # echo "0000:$pci_id" > /sys/bus/pci/drivers/vfio-pci/bind 2>/dev/null - # if [ -e "$pci_path/driver" ] && [ "$(basename "$(readlink -f "$pci_path/driver")")" = "vfio-pci" ]; then - # host_log "$cfg" "PCI passthrough: $pci_id successfully bound to vfio-pci" + # config_get vendor_id "$pci_passthrough" vendor_id + # if [ -n "$vendor_id" ] && [ -n "$pci_id" ]; then + # if [ -e "/sys/bus/pci/devices/0000:$pci_id/driver" ]; then + # local current_driver + # current_driver="$(basename "$(readlink -f "/sys/bus/pci/devices/0000:$pci_id/driver" 2>/dev/null)" 2>/dev/null)" + # host_log "$cfg" "PCI passthrough: $pci_id already bound to driver '$current_driver'" # else - # host_log "$cfg" "ERROR: PCI passthrough: failed to bind $pci_id to vfio-pci. Check IOMMU groups and BIOS settings." + # echo "$vendor_id" > /sys/bus/pci/drivers/vfio-pci/new_id 2>/dev/null + # if [ -e "/sys/bus/pci/devices/0000:$pci_id/driver" ]; then + # host_log "$cfg" "PCI passthrough: bound $pci_id (vendor/device $vendor_id) to vfio-pci" + # else + # host_log "$cfg" "WARNING: PCI passthrough: failed to bind $pci_id to vfio-pci - check IOMMU/BIOS settings" + # fi # fi # pci_opts="-device vfio-pci,host=$pci_id" # else - # host_log "$cfg" "WARNING: pci_passthrough section exists but pci_id is empty - skipping" + # host_log "$cfg" "WARNING: pci_passthrough='$pci_passthrough' is set but its pci_id/vendor_id are incomplete - skipping passthrough" # fi #fi + local pci_id="" pci_opts="" + if [ -n "$pci_passthrough" ]; then + config_get pci_id "$pci_passthrough" pci_id + if [ -n "$pci_id" ]; then + local pci_path="/sys/bus/pci/devices/0000:$pci_id" + local iommu_group="" + if [ -L "$pci_path/iommu_group" ]; then + iommu_group="$(basename "$(readlink -f "$pci_path/iommu_group")")" + fi + if [ -n "$iommu_group" ]; then + host_log "$cfg" "PCI passthrough: device $pci_id is in IOMMU group $iommu_group" + for dev in /sys/kernel/iommu_groups/$iommu_group/devices/*; do + local dev_name="$(basename "$dev")" + local current_driver="$(basename "$(readlink -f "$dev/driver" 2>/dev/null)" 2>/dev/null)" + if [ -n "$current_driver" ] && [ "$current_driver" != "vfio-pci" ]; then + echo -n "$dev_name" > "$dev/driver/unbind" 2>/dev/null + host_log "$cfg" "PCI: unbound $dev_name from $current_driver" + fi + echo "vfio-pci" > "$dev/driver_override" 2>/dev/null + echo "$dev_name" > /sys/bus/pci/drivers/vfio-pci/bind 2>/dev/null + if [ -e "$dev/driver" ] && [ "$(basename "$(readlink -f "$dev/driver")")" = "vfio-pci" ]; then + host_log "$cfg" "PCI: successfully bound $dev_name to vfio-pci" + else + host_log "$cfg" "ERROR: failed to bind $dev_name to vfio-pci" + fi + done + sleep 3 + pci_opts="-device vfio-pci,host=$pci_id" + else + host_log "$cfg" "WARNING: Cannot determine IOMMU group for $pci_id" + fi + else + host_log "$cfg" "WARNING: pci_passthrough section exists but pci_id is empty - skipping" + fi + fi + local cdrom_opts="" [ -n "$cdrom" ] && cdrom_opts="-cdrom $cdrom"