🎨 Sync 2026-07-29 20:42:56

This commit is contained in:
github-actions[bot] 2026-07-29 20:42:56 +08:00
parent fc21fd8c5e
commit 93288fe344
11 changed files with 384 additions and 257 deletions

View File

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

View File

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

View File

@ -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=\$!

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:=3
PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk

View File

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

View File

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

View File

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

View File

@ -19,6 +19,9 @@
"restart",
"console_open",
"console_close",
"list_machines",
"list_cpus",
"list_net_drivers",
"delete_vm"
]
},

View File

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

View File

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

View File

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