mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-30 22:11:26 +08:00
⛅ Sync 2026-07-30 08:30:45
This commit is contained in:
parent
c7db60401c
commit
68d255846f
@ -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
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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';
|
||||
},
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user