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"