Sync 2026-07-30 08:30:45

This commit is contained in:
github-actions[bot] 2026-07-30 08:30:45 +08:00
parent c7db60401c
commit 68d255846f
6 changed files with 270 additions and 228 deletions

View File

@ -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

View File

@ -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.') +
'<br />' + _('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: <code>virtio-net-pci</code>');
return m.render();
}
});
});

View File

@ -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. ') +
'<br />' + _('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 || []),
'<br />' + _('Creating a passthrough section will bind the device to vfio-pci (if possible). Removing it will attempt to re-bind to the original driver.') +
'<br />' + _('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.') +
'<br />' + _('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';
},
});

View File

@ -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();
});
}
});
);

View File

@ -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 <shevlakov@132lan.ru>
include $(INCLUDE_DIR)/package.mk

View File

@ -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"