From 93288fe344bfad5ca3d1bfd0563b07f7ca701ecd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jul 2026 20:42:56 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Sync=202026-07-29=2020:42:56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ddns-go/Makefile | 4 +- luci-app-easytier/Makefile | 2 +- luci-app-easytier/root/etc/init.d/easytier | 18 +- luci-app-qemu-vms/Makefile | 2 +- .../resources/view/qemu-vms/networks.js | 37 +- .../resources/view/qemu-vms/status.js | 458 +++++++++--------- .../root/usr/libexec/rpcd/luci.qemu-vms | 36 ++ .../share/rpcd/acl.d/luci-app-qemu-vms.json | 3 + qemu-vms/Makefile | 2 +- qemu-vms/root/etc/init.d/qemu-vms | 75 ++- sing-box/Makefile | 4 +- 11 files changed, 384 insertions(+), 257 deletions(-) diff --git a/ddns-go/Makefile b/ddns-go/Makefile index 7d4dcbdc..3946bc8a 100644 --- a/ddns-go/Makefile +++ b/ddns-go/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ddns-go -PKG_VERSION:=6.17.2 -PKG_RELEASE:=19 +PKG_VERSION:=6.17.3 +PKG_RELEASE:=20 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? diff --git a/luci-app-easytier/Makefile b/luci-app-easytier/Makefile index c4b65d76..11f43369 100644 --- a/luci-app-easytier/Makefile +++ b/luci-app-easytier/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk -include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.4) -PKG_RELEASE:=11 +PKG_RELEASE:=12 LUCI_TITLE:=LuCI support for EasyTier LUCI_DEPENDS:=+kmod-tun +easytier +luci-compat diff --git a/luci-app-easytier/root/etc/init.d/easytier b/luci-app-easytier/root/etc/init.d/easytier index 0bfa70dc..d0c016ed 100755 --- a/luci-app-easytier/root/etc/init.d/easytier +++ b/luci-app-easytier/root/etc/init.d/easytier @@ -181,9 +181,11 @@ check_config() { LOG_SIZE=\$(ls -l /tmp/easytier.log | awk '{print int(\$5/1024)}') if [ \$LOG_SIZE -gt 5120 ]; then tail -n 500 /tmp/easytier.log > /tmp/easytier.log.tmp - mv /tmp/easytier.log.tmp /tmp/easytier.log + : > /tmp/easytier.log + cat /tmp/easytier.log.tmp >> /tmp/easytier.log + rm -f /tmp/easytier.log.tmp fi - sleep 300 + sleep 60 done } & LOG_MANAGEMENT_PID=\$! @@ -604,9 +606,11 @@ EOF LOG_SIZE=\$(ls -l /tmp/easytier.log | awk '{print int(\$5/1024)}') if [ \$LOG_SIZE -gt 5120 ]; then tail -n 500 /tmp/easytier.log > /tmp/easytier.log.tmp - mv /tmp/easytier.log.tmp /tmp/easytier.log + : > /tmp/easytier.log + cat /tmp/easytier.log.tmp >> /tmp/easytier.log + rm -f /tmp/easytier.log.tmp fi - sleep 300 + sleep 60 done } & LOG_MANAGEMENT_PID=\$! @@ -753,9 +757,11 @@ start_web() { LOG_SIZE=\$(ls -l /tmp/easytierweb.log | awk '{print int(\$5/1024)}') if [ \$LOG_SIZE -gt 5120 ]; then tail -n 500 /tmp/easytierweb.log > /tmp/easytierweb.log.tmp - mv /tmp/easytierweb.log.tmp /tmp/easytierweb.log + : > /tmp/easytierweb.log + cat /tmp/easytierweb.log.tmp >> /tmp/easytierweb.log + rm -f /tmp/easytierweb.log.tmp fi - sleep 300 + sleep 60 done } & LOG_MANAGEMENT_PID=\$! diff --git a/luci-app-qemu-vms/Makefile b/luci-app-qemu-vms/Makefile index 37e16700..225a0715 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:=3 +PKG_RELEASE:=4 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 21cb296b..6d0b31f7 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 @@ -2,17 +2,30 @@ 'require form'; 'require view'; 'require uci'; +'require rpc'; + +var callListNetDrivers = rpc.declare({ + object: 'luci.qemu-vms', + method: 'list_net_drivers', + params: [] +}); function randomMac() { - // OUI 52:54:00:31 is our virtual-cluster prefix (matches manually - // assigned MACs used elsewhere in this config). var b1 = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); var b2 = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); return '52:54:00:31:%s:%s'.format(b1, b2); } return view.extend({ - render: function() { + load: function() { + return Promise.all([ + callListNetDrivers(), + uci.load('qemu-vms') + ]); + }, + + render: function(data) { + var drivers = data[0].drivers || []; var m, s; m = new form.Map('qemu-vms', _('Virtual network segments'), @@ -26,10 +39,9 @@ return view.extend({ s.nodescriptions = true; var mac = s.option(form.Value, 'mac', _('MAC address')); - //mac.placeholder = _('leave empty to auto-generate 52:54:00:31:xx:xx'); mac.validate = function(section_id, value) { if (!value) - return true; // filled in by write() below if left empty + return true; if (!/^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(value)) return _('Invalid MAC address format (expected xx:xx:xx:xx:xx:xx)'); return true; @@ -41,7 +53,6 @@ return view.extend({ }; mac.rmempty = false; mac.value(randomMac(), _('Automatically assigned')); - //mac.editable = true; var ifname = s.option(form.Value, 'ifname', _('tap ifname')); ifname.placeholder = 'tap0'; @@ -55,6 +66,20 @@ 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); + }); + } else { + driver.value('', _('No drivers found')); + } + driver.default = 'virtio-net-pci'; + driver.rmempty = true; + driver.description = _('Choose the emulated network card model. If empty, virtio-net-pci is used.'); + return m.render(); } }); 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 dbcc8e36..7c5aa914 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 @@ -12,6 +12,18 @@ var callStatus = rpc.declare({ params: ['vm_name'] }); +var callListMachines = rpc.declare({ + object: 'luci.qemu-vms', + method: 'list_machines', + params: [] +}); + +var callListCpus = rpc.declare({ + object: 'luci.qemu-vms', + method: 'list_cpus', + params: [] +}); + var callStart = rpc.declare({ object: 'luci.qemu-vms', method: 'start', params: ['vm_name'] }); var callStop = rpc.declare({ object: 'luci.qemu-vms', method: 'stop', params: ['vm_name'] }); var callRestart = rpc.declare({ object: 'luci.qemu-vms', method: 'restart', params: ['vm_name'] }); @@ -36,6 +48,83 @@ function widenModal() { } } + +// ----- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ПОСТРОЕНИЯ СЕЛЕКТОВ ----- + +function buildMachineSelect(vm, machines) { + var select = E('select', { 'class': 'cbi-input-select', 'id': 'edit-machine' }); + machines.forEach(function(m) { + var opt = E('option', { 'value': m }); + if (m === (vm.machine || 'q35')) opt.selected = true; + opt.textContent = m; + select.appendChild(opt); + }); + if (vm.machine && !machines.includes(vm.machine)) { + var opt = E('option', { 'value': vm.machine, 'selected': true }); + opt.textContent = vm.machine + ' (custom)'; + select.appendChild(opt); + } + return select; +} + +function buildCpuSelect(vm, cpus) { + var baseCpus = ['host', 'max', 'qemu64', 'Haswell', 'Skylake-Client']; + var select = E('select', { 'class': 'cbi-input-select', 'id': 'edit-cpu' }); + + baseCpus.forEach(function(c) { + var opt = E('option', { 'value': c }); + if (c === (vm.cpu || 'host')) opt.selected = true; + opt.textContent = c; + select.appendChild(opt); + }); + + cpus.forEach(function(c) { + if (baseCpus.indexOf(c) === -1) { + var opt = E('option', { 'value': c }); + if (c === vm.cpu) opt.selected = true; + opt.textContent = c; + select.appendChild(opt); + } + }); + + if (vm.cpu && !baseCpus.includes(vm.cpu) && !cpus.includes(vm.cpu)) { + var opt = E('option', { 'value': vm.cpu, 'selected': true }); + opt.textContent = vm.cpu + ' (custom)'; + select.appendChild(opt); + } + return select; +} + +function buildMachineSelectFallback(vm) { + var select = E('select', { 'class': 'cbi-input-select', 'id': 'edit-machine' }, [ + E('option', { 'value': 'q35', 'selected': (vm.machine || 'q35') === 'q35' || null }, 'q35 (recommended)'), + E('option', { 'value': 'pc', 'selected': vm.machine === 'pc' || null }, 'pc (legacy)') + ]); + if (vm.machine && vm.machine !== 'q35' && vm.machine !== 'pc') { + var opt = E('option', { 'value': vm.machine, 'selected': true }); + opt.textContent = vm.machine + ' (custom)'; + select.appendChild(opt); + } + return select; +} + +function buildCpuSelectFallback(vm) { + var baseCpus = ['host', 'max', 'qemu64', 'Haswell', 'Skylake-Client']; + var select = E('select', { 'class': 'cbi-input-select', 'id': 'edit-cpu' }); + baseCpus.forEach(function(c) { + var opt = E('option', { 'value': c }); + if (c === (vm.cpu || 'host')) opt.selected = true; + opt.textContent = c; + select.appendChild(opt); + }); + if (vm.cpu && !baseCpus.includes(vm.cpu)) { + var opt = E('option', { 'value': vm.cpu, 'selected': true }); + opt.textContent = vm.cpu + ' (custom)'; + select.appendChild(opt); + } + return select; +} + return view.extend({ load: function() { return Promise.all([ @@ -283,233 +372,166 @@ return view.extend({ }, handleEdit: function(name, ev) { + var self = this; var isNew = !name; var existingNameForReset = isNew ? null : name; var vm = isNew ? {} : (uci.get('qemu-vms', name) || {}); - var allNetworks = uci.sections('qemu-vms', 'network').map(function(s) { return s['.name']; }); - var allPci = uci.sections('qemu-vms', 'pci-passthrough').map(function(s) { return s['.name']; }); - var allUsb = uci.sections('qemu-vms', 'usb-passthrough').map(function(s) { return s['.name']; }); - var attachedNetworks = [].concat(vm.network || []); - var attachedUsb = [].concat(vm.usb_passthrough || []); + function showEditModal(machineSelect, cpuSelect) { + var allNetworks = uci.sections('qemu-vms', 'network').map(function(s) { return s['.name']; }); + var allPci = uci.sections('qemu-vms', 'pci-passthrough').map(function(s) { return s['.name']; }); + var allUsb = uci.sections('qemu-vms', 'usb-passthrough').map(function(s) { return s['.name']; }); + var attachedNetworks = [].concat(vm.network || []); + var attachedUsb = [].concat(vm.usb_passthrough || []); - var nameInput = E('input', { - 'class': 'cbi-input-text', - 'id': 'edit-name', - 'value': name || '', - 'disabled': isNew ? null : true, - 'placeholder': isNew ? _('e.g. ap, test, vlan') : null - }); + var nameInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-name', 'value': name || '', 'disabled': isNew ? null : true, 'placeholder': isNew ? _('e.g. ap, test, vlan') : null }); + 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 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' - }); - - var cpuSelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-cpu' }, [ - E('option', { 'value': 'host', 'selected': (vm.cpu || 'host') === 'host' || null }, 'host'), - E('option', { 'value': 'max', 'selected': vm.cpu === 'max' || null }, 'max'), - E('option', { 'value': 'qemu64', 'selected': vm.cpu === 'qemu64' || null }, 'qemu64'), - E('option', { 'value': 'Haswell', 'selected': vm.cpu === 'Haswell' || null }, 'Haswell'), - E('option', { 'value': 'Skylake-Client', 'selected': vm.cpu === 'Skylake-Client' || null }, 'Skylake-Client') - ]); - - 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' }, [ - E('option', { 'value': 'virtio', 'selected': (vm.disk_bus || 'virtio') === 'virtio' || null }, 'virtio (default, best performance, needs guest drivers)'), - E('option', { 'value': 'ide', 'selected': vm.disk_bus === 'ide' || null }, 'IDE (widest compatibility \u2014 old Windows/DOS/legacy kernels)'), - E('option', { 'value': 'sata', 'selected': vm.disk_bus === 'sata' || null }, 'SATA/AHCI'), - E('option', { 'value': 'scsi', 'selected': vm.disk_bus === 'scsi' || null }, 'virtio-SCSI') - ]); - - var cdromInput = E('input', { - 'class': 'cbi-input-text', - 'id': 'edit-cdrom', - 'value': vm.cdrom || '', - 'placeholder': _('optional, e.g. /mnt/disk/iso/installer.iso') - }); - - var existingCustomArgs = [].concat(vm.custom_arg || []); - var customArgsInput = E('textarea', { - 'class': 'cbi-input-textarea', - 'id': 'edit-custom-args', - 'style': 'width: 100%; height: 6em; font-family: monospace;', - 'placeholder': _('one raw QEMU argument per line, e.g.\n-device usb-tablet\n-boot order=d') - }, existingCustomArgs.join('\n')); - - var displaySelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-display' }, [ - E('option', { 'value': 'serial', 'selected': (vm.display_type || 'serial') === 'serial' || null }, _('Serial console (ttyd)')), - E('option', { 'value': 'vnc', 'selected': vm.display_type === 'vnc' || null }, _('VNC (noVNC)')) - ]); - - var usedPorts = uci.sections('qemu-vms', 'vm') - .map(function(s) { return parseInt(s.vnc_port, 10); }) - .filter(function(p) { return !isNaN(p); }); - var suggestedPort = usedPorts.length ? Math.max.apply(null, usedPorts) + 1 : 5901; - - var vncPortInput = E('input', { - 'class': 'cbi-input-text', - 'id': 'edit-vnc-port', - 'type': 'number', - 'min': '5900', - 'value': vm.vnc_port || suggestedPort - }); - - var vncPortRow = E('div', { 'class': 'cbi-value', 'id': 'edit-vnc-port-row' }, [ - E('label', { 'class': 'cbi-value-title' }, _('VNC port (RFB, 5900+)')), - E('div', { 'class': 'cbi-value-field' }, [ - vncPortInput, - E('p', { 'class': 'cbi-value-description' }, - _('Serves noVNC over websocket on port %s.').format('+1000')) - ]) - ]); - - displaySelect.addEventListener('change', function() { - vncPortRow.style.display = (displaySelect.value === 'vnc') ? '' : 'none'; - }); - vncPortRow.style.display = ((vm.display_type || 'serial') === 'vnc') ? '' : 'none'; - - var pciSelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-pci' }, [ - E('option', { 'value': '', 'selected': !vm.pci_passthrough || null }, _('-- none --')) - ].concat(allPci.map(function(p) { - return E('option', { 'value': p, 'selected': vm.pci_passthrough === p || null }, p); - }))); - - var networkChecks = allNetworks.map(function(net) { - return E('div', {}, [ - E('label', {}, [ - E('input', { - 'type': 'checkbox', - 'class': 'edit-network-check', - 'value': net, - 'checked': attachedNetworks.indexOf(net) !== -1 || null - }), - ' ' + net - ]) + 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' }, [ + E('option', { 'value': 'virtio', 'selected': (vm.disk_bus || 'virtio') === 'virtio' || null }, 'virtio (default)'), + E('option', { 'value': 'ide', 'selected': vm.disk_bus === 'ide' || null }, 'IDE'), + E('option', { 'value': 'sata', 'selected': vm.disk_bus === 'sata' || null }, 'SATA/AHCI'), + E('option', { 'value': 'scsi', 'selected': vm.disk_bus === 'scsi' || null }, 'virtio-SCSI') ]); - }); + var cdromInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-cdrom', 'value': vm.cdrom || '', 'placeholder': _('optional, e.g. /mnt/disk/iso/installer.iso') }); + var customArgsInput = E('textarea', { 'class': 'cbi-input-textarea', 'id': 'edit-custom-args', 'style': 'width: 100%; height: 6em; font-family: monospace;', 'placeholder': _('one raw QEMU argument per line') }, ([].concat(vm.custom_arg || [])).join('\n')); - if (!allNetworks.length) - networkChecks = [E('em', {}, _('No networks defined yet \u2014 add some on the Networks tab.'))]; - - var usbSelect = E('div', { - 'style': 'display: flex; flex-wrap: wrap; gap: 0.3em 1.2em;' - }, allUsb.map(function(usb) { - return E('label', { 'style': 'white-space: nowrap; font-weight: normal;' }, [ - E('input', { - 'type': 'checkbox', - 'class': 'edit-usb-check', - 'value': usb, - 'checked': attachedUsb.indexOf(usb) !== -1 || null - }), - ' ' + usb + var displaySelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-display' }, [ + E('option', { 'value': 'serial', 'selected': (vm.display_type || 'serial') === 'serial' || null }, _('Serial console (ttyd)')), + E('option', { 'value': 'vnc', 'selected': vm.display_type === 'vnc' || null }, _('VNC (noVNC)')) ]); - })); - if (!allUsb.length) - usbSelect = E('em', {}, _('No USB passthrough sections defined yet \u2014 add some on the Hardware passthrough tab.')); - - - ui.showModal(isNew ? _('Add VM') : _('Edit %s').format(name), [ - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Name')), - E('div', { 'class': 'cbi-value-field' }, nameInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Enabled')), - E('div', { 'class': 'cbi-value-field' }, enabledInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Memory (MB)')), - E('div', { 'class': 'cbi-value-field' }, memoryInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('SMP (vCPUs)')), - E('div', { 'class': 'cbi-value-field' }, smpInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('CPU model')), - E('div', { 'class': 'cbi-value-field' }, cpuSelect) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Disk image path')), - E('div', { 'class': 'cbi-value-field' }, imageInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Disk bus')), - E('div', { 'class': 'cbi-value-field' }, diskBusSelect) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('CD-ROM (ISO)')), - E('div', { 'class': 'cbi-value-field' }, cdromInput) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Custom QEMU arguments')), + var usedPorts = uci.sections('qemu-vms', 'vm').map(function(s) { return parseInt(s.vnc_port, 10); }).filter(function(p) { return !isNaN(p); }); + var suggestedPort = usedPorts.length ? Math.max.apply(null, usedPorts) + 1 : 5901; + var vncPortInput = E('input', { 'class': 'cbi-input-text', 'id': 'edit-vnc-port', 'type': 'number', 'min': '5900', 'value': vm.vnc_port || suggestedPort }); + var vncPortRow = E('div', { 'class': 'cbi-value', 'id': 'edit-vnc-port-row' }, [ + E('label', { 'class': 'cbi-value-title' }, _('VNC port (RFB, 5900+)')), E('div', { 'class': 'cbi-value-field' }, [ - customArgsInput, - E('p', { 'class': 'cbi-value-description' }, - _('Escape hatch for flags with no dedicated field \u2014 appended verbatim to the qemu-system-x86_64 command line.')) + vncPortInput, + E('p', { 'class': 'cbi-value-description' }, _('Serves noVNC over websocket on port %s.').format('+1000')) ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Console type')), - E('div', { 'class': 'cbi-value-field' }, displaySelect) - ]), - vncPortRow, - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('PCI passthrough')), - E('div', { 'class': 'cbi-value-field' }, pciSelect) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('USB passthrough')), - E('div', { 'class': 'cbi-value-field' }, usbSelect) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Attached networks')), - E('div', { 'class': 'cbi-value-field' }, networkChecks) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('p', { 'class': 'cbi-value-description' }, _('Changes will take effect after restarting the VM.')) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 1em' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn', - 'click': ui.createHandlerFn(this, 'handleEdit', existingNameForReset) - }, _('Reset')), - ' ', - E('button', { - 'class': 'btn cbi-button-positive', - 'click': ui.createHandlerFn(this, 'saveEdit', isNew ? null : name) - }, _('Save')) - ]) - ]); + ]); + displaySelect.addEventListener('change', function() { + vncPortRow.style.display = (displaySelect.value === 'vnc') ? '' : 'none'; + }); + vncPortRow.style.display = ((vm.display_type || 'serial') === 'vnc') ? '' : 'none'; + + var pciSelect = E('select', { 'class': 'cbi-input-select', 'id': 'edit-pci' }, [ + E('option', { 'value': '', 'selected': !vm.pci_passthrough || null }, _('-- none --')) + ].concat(allPci.map(function(p) { + return E('option', { 'value': p, 'selected': vm.pci_passthrough === p || null }, p); + }))); + + var networkChecks = allNetworks.map(function(net) { + return E('div', {}, [ + E('label', {}, [ + E('input', { 'type': 'checkbox', 'class': 'edit-network-check', 'value': net, 'checked': attachedNetworks.indexOf(net) !== -1 || null }), + ' ' + net + ]) + ]); + }); + if (!allNetworks.length) networkChecks = [E('em', {}, _('No networks defined yet'))]; + + var usbSelect = E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.3em 1.2em;' }, allUsb.map(function(usb) { + return E('label', { 'style': 'white-space: nowrap; font-weight: normal;' }, [ + E('input', { 'type': 'checkbox', 'class': 'edit-usb-check', 'value': usb, 'checked': attachedUsb.indexOf(usb) !== -1 || null }), + ' ' + usb + ]); + })); + if (!allUsb.length) usbSelect = E('em', {}, _('No USB passthrough sections defined yet')); + + ui.showModal(isNew ? _('Add VM') : _('Edit %s').format(name), [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Name')), + E('div', { 'class': 'cbi-value-field' }, nameInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Enabled')), + E('div', { 'class': 'cbi-value-field' }, enabledInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Memory (MB)')), + E('div', { 'class': 'cbi-value-field' }, memoryInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('SMP (vCPUs)')), + E('div', { 'class': 'cbi-value-field' }, smpInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('CPU model')), + E('div', { 'class': 'cbi-value-field' }, cpuSelect) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Machine type')), + E('div', { 'class': 'cbi-value-field' }, machineSelect) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Disk image path')), + E('div', { 'class': 'cbi-value-field' }, imageInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Disk bus')), + E('div', { 'class': 'cbi-value-field' }, diskBusSelect) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('CD-ROM (ISO)')), + E('div', { 'class': 'cbi-value-field' }, cdromInput) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Custom QEMU arguments')), + E('div', { 'class': 'cbi-value-field' }, [ + customArgsInput, + E('p', { 'class': 'cbi-value-description' }, _('Escape hatch for flags with no dedicated field')) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Console type')), + E('div', { 'class': 'cbi-value-field' }, displaySelect) + ]), + vncPortRow, + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('PCI passthrough')), + E('div', { 'class': 'cbi-value-field' }, pciSelect) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('USB passthrough')), + E('div', { 'class': 'cbi-value-field' }, usbSelect) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Attached networks')), + E('div', { 'class': 'cbi-value-field' }, networkChecks) + ]), + E('p', { 'class': 'cbi-section-descr', 'style': 'margin-top: 1em;' }, _('Changes will take effect after restarting the VM.')), + E('div', { 'class': 'right', 'style': 'margin-top: 1em' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'handleEdit', existingNameForReset) }, _('Reset')), + ' ', + E('button', { 'class': 'btn cbi-button-positive', 'click': ui.createHandlerFn(self, 'saveEdit', isNew ? null : name) }, _('Save')) + ]) + ]); + } + + Promise.all([callListMachines(), callListCpus()]) + .then(function(results) { + var machines = results[0].machines || []; + var cpus = results[1].cpus || []; + + var machineSelect = buildMachineSelect(vm, machines); + var cpuSelect = buildCpuSelect(vm, cpus); + + showEditModal(machineSelect, cpuSelect); + }) + .catch(function(err) { + console.warn('Failed to load machine/CPU lists, using fallback:', err.message); + var machineSelect = buildMachineSelectFallback(vm); + var cpuSelect = buildCpuSelectFallback(vm); + showEditModal(machineSelect, cpuSelect); + }); }, saveEdit: function(existingName, ev) { @@ -555,6 +577,7 @@ return view.extend({ 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); @@ -605,8 +628,8 @@ return view.extend({ // .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 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; }); @@ -635,3 +658,4 @@ return view.extend({ }); } }); + diff --git a/luci-app-qemu-vms/root/usr/libexec/rpcd/luci.qemu-vms b/luci-app-qemu-vms/root/usr/libexec/rpcd/luci.qemu-vms index 17ed403a..3be81a0d 100755 --- a/luci-app-qemu-vms/root/usr/libexec/rpcd/luci.qemu-vms +++ b/luci-app-qemu-vms/root/usr/libexec/rpcd/luci.qemu-vms @@ -309,7 +309,10 @@ case "$1" in "readlog": { "vm_name": "", "lines": 0 }, "console_open": { "vm_name": "" }, "console_close": { "vm_name": "" }, + "list_cpus": { }, "list_hardware": { }, + "list_machines": { }, + "list_net_drivers": { }, "delete_vm": { "vm_name": "" } }' ;; @@ -366,6 +369,39 @@ case "$1" in list_hardware) list_hardware ;; + list_machines) + machines=$(/usr/bin/qemu-system-x86_64 -M help 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+' | grep -v Supported | awk '{print $1}') + json_init + json_add_array "machines" + for m in $machines; do + json_add_string "" "$m" + done + json_close_array + json_dump + ;; + list_cpus) + cpus=$(/usr/bin/qemu-system-x86_64 -cpu help 2>/dev/null | \ + awk '/^[[:space:]]*$/ || /Recognized/ {exit} {print $1}') + json_init + json_add_array "cpus" + for c in $cpus; do + json_add_string "" "$c" + done + json_close_array + json_dump + ;; + list_net_drivers) + drivers=$(/usr/bin/qemu-system-x86_64 -device help 2>/dev/null | \ + awk '/^Network devices:/{flag=1; next} /^[[:space:]]*$/{if(flag) exit} flag && /^name "[^"]+"/{print $2}' | \ + sed -e 's/"//g' -e 's/,//g' ) + json_init + json_add_array "drivers" + for d in $drivers; do + json_add_string "" "$d" + done + json_close_array + json_dump + ;; delete_vm) stop_vm_instance "$vm_name" console_close "$vm_name" >/dev/null diff --git a/luci-app-qemu-vms/root/usr/share/rpcd/acl.d/luci-app-qemu-vms.json b/luci-app-qemu-vms/root/usr/share/rpcd/acl.d/luci-app-qemu-vms.json index 3aef9eb5..a8516706 100644 --- a/luci-app-qemu-vms/root/usr/share/rpcd/acl.d/luci-app-qemu-vms.json +++ b/luci-app-qemu-vms/root/usr/share/rpcd/acl.d/luci-app-qemu-vms.json @@ -19,6 +19,9 @@ "restart", "console_open", "console_close", + "list_machines", + "list_cpus", + "list_net_drivers", "delete_vm" ] }, diff --git a/qemu-vms/Makefile b/qemu-vms/Makefile index 6338f973..f6c3a9af 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:=3 +PKG_RELEASE:=4 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 134ff873..1e446fd0 100755 --- a/qemu-vms/root/etc/init.d/qemu-vms +++ b/qemu-vms/root/etc/init.d/qemu-vms @@ -56,6 +56,7 @@ _add_net() { config_get mac "$netcfg" mac config_get ifname "$netcfg" ifname config_get bridge "$netcfg" bridge + config_get driver "$netcfg" driver "virtio-net-pci" local id="net${idx}" @@ -78,7 +79,7 @@ _add_net() { local mac_opt="" [ -n "$mac" ] && mac_opt=",mac=$mac" - device_opts="$device_opts -device virtio-net-pci${mac_opt},netdev=$id" + device_opts="$device_opts -device ${driver}${mac_opt},netdev=$id" idx=$((idx + 1)) } @@ -101,25 +102,57 @@ _add_usb() { usb_needed=1 } +#_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 +# [ -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 +# if [ -L "$dev/driver" ]; then +# local driver_path=$(readlink -f "$dev/driver") +# local driver_name=$(basename "$driver_path") +# echo -n "$(basename "$dev")" > "$driver_path/unbind" 2>/dev/null +# fi +# fi +# fi +# done +#} + _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 - [ -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 - if [ -L "$dev/driver" ]; then - local driver_path=$(readlink -f "$dev/driver") - local driver_name=$(basename "$driver_path") - echo -n "$(basename "$dev")" > "$driver_path/unbind" 2>/dev/null - host_log "$cfg" "USB: unbound $(basename "$dev") from $driver_name" - fi - fi - fi - done + 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 } build_customargs() { @@ -209,6 +242,7 @@ start_vm() { config_get pci_passthrough "$cfg" pci_passthrough config_get display_type "$cfg" display_type "serial" config_get vnc_port "$cfg" vnc_port + config_get machine "$cfg" machine "q35" local console_sock="/var/run/qemu-$cfg.sock" local console_log="/var/log/qemu-$cfg-console.log" @@ -242,7 +276,6 @@ start_vm() { fi fi - # Test version PCI Pass #local pci_id="" pci_opts="" #if [ -n "$pci_passthrough" ]; then # config_get pci_id "$pci_passthrough" pci_id @@ -299,7 +332,7 @@ start_vm() { $disk_controller_opts $disk_opts \ $device_opts $netdev_opts $pci_opts $cdrom_opts $usb_controller_opts $usb_opts $vnc_opts $custom_opts \ -device virtio-rng-pci \ - -machine q35 \ + -machine $machine \ -chardev socket,id=con0,path="$console_sock",server=on,wait=off,logfile="$console_log",logappend=on \ -serial chardev:con0 \ -chardev socket,id=qmp0,path="$qmp_sock",server=on,wait=off \ diff --git a/sing-box/Makefile b/sing-box/Makefile index 780d8258..02e50857 100644 --- a/sing-box/Makefile +++ b/sing-box/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.13.14 -PKG_RELEASE:=22 +PKG_VERSION:=1.13.15 +PKG_RELEASE:=23 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?