diff --git a/luci-app-qemu-vms/Makefile b/luci-app-qemu-vms/Makefile index a095dfb1b..644d9d532 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:=+qemu-vms +ttyd +usbutils +pciutils PKG_LICENSE:=GPLv3 PKG_VERSION:=0.0.1 -PKG_RELEASE:=1 +PKG_RELEASE:=2 include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js index 2abce9ec0..55e9ff751 100644 --- a/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js +++ b/luci-app-qemu-vms/htdocs/luci-static/resources/view/qemu-vms/passthrough.js @@ -297,8 +297,8 @@ return view.extend({ var rows = devices.map(function(dev) { var section = self.findPciSection(dev.addr); var owner = self.pciOwner(dev.addr); - var passthroughStatus = section ? _('Passthrough active (section: %s)').format(section) : _('Not passthrough'); - if (owner) passthroughStatus += ' ' + _('(attached to VM: %s)').format(owner); + var passthroughStatus = section ? _('Active
Section: %s').format(section) : _('Not passthrough'); + if (owner) passthroughStatus += ' ' + _('
Host: %s').format(owner); var actionButton; if (section) { @@ -325,11 +325,11 @@ return view.extend({ return E('table', { 'class': 'table' }, [ E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('PCI address')), + E('th', { 'class': 'th' }, _('Address')), E('th', { 'class': 'th' }, _('Vendor')), E('th', { 'class': 'th' }, _('Device')), - E('th', { 'class': 'th' }, _('Current driver')), - E('th', { 'class': 'th' }, _('Passthrough status')), + E('th', { 'class': 'th' }, _('Driver')), + E('th', { 'class': 'th', 'style': 'min-width: 140px;' }, _('Status')), E('th', { 'class': 'th' }, _('Action')) ]) ].concat(rows)); @@ -343,8 +343,8 @@ return view.extend({ var section = (vendorId && productId) ? self.findUsbSection(vendorId, productId) : null; var owners = (vendorId && productId) ? self.usbOwners(vendorId, productId) : []; - var passthroughStatus = section ? _('Passthrough active (section: %s)').format(section) : _('Not passthrough'); - if (owners.length) passthroughStatus += ' ' + _('(attached to VM: %s)').format(owners.join(', ')); + var passthroughStatus = section ? _('Active
Section: %s').format(section) : _('Not passthrough'); + if (owners.length) passthroughStatus += ' ' + _('
Host: %s').format(owners.join(', ')); var actionButton; if (section) { @@ -371,9 +371,9 @@ return view.extend({ return E('table', { 'class': 'table' }, [ E('tr', { 'class': 'tr table-titles' }, [ E('th', { 'class': 'th' }, _('Bus:Dev')), - E('th', { 'class': 'th' }, _('VendorID:ProductID')), + E('th', { 'class': 'th' }, _('VID:PID')), E('th', { 'class': 'th' }, _('Description')), - E('th', { 'class': 'th' }, _('Passthrough status')), + E('th', { 'class': 'th', 'style': 'min-width: 140px;' }, _('Status')), E('th', { 'class': 'th' }, _('Action')) ]) ].concat(rows)); 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 8db995e04..dbcc8e364 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 @@ -190,7 +190,7 @@ return view.extend({ handleLog: function(name, ev) { return callReadlog(name, 400).then(function(res) { - ui.showModal(_('Console log: %s').format(name), [ + ui.showModal(_('Instanse log: %s').format(name), [ E('pre', { 'style': 'max-height: 60vh; overflow: auto; white-space: pre-wrap; font-size: 12px;' }, res.log || _('(empty)')), @@ -493,6 +493,9 @@ return view.extend({ 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')), ' ', @@ -590,6 +593,13 @@ return view.extend({ 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; }); 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 6cf3317b7..17ed403a9 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 @@ -145,7 +145,7 @@ console_close() { readlog() { local vm="$1" local lines="${2:-200}" - local log="/var/log/qemu-$vm-console.log" + local log="/var/log/qemu-vms/$vm.log" json_init if [ -f "$log" ]; then diff --git a/qemu-vms/Makefile b/qemu-vms/Makefile index ea0c1de27..c979f7a4e 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:=1 +PKG_RELEASE:=2 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 61d614627..134ff873e 100755 --- a/qemu-vms/root/etc/init.d/qemu-vms +++ b/qemu-vms/root/etc/init.d/qemu-vms @@ -5,6 +5,23 @@ STOP=10 USE_PROCD=1 STATUS_DIR="/var/run/qemu-vms" +LOG_DIR="/var/log/qemu-vms" + +host_log() { + # Per-VM HOST-side lifecycle log: start/stop attempts, config + # warnings (missing MAC, unknown disk_bus, VNC without a port, + # passthrough bind issues), and QEMU's own stderr (KVM unavailable, + # image not found, device init failures). Deliberately separate from + # the guest's own serial console capture + # (/var/log/qemu-$vm-console.log): that one shows what the GUEST OS + # printed; this one shows whether the HOST managed to start/ + # configure the VM at all - meaningful for VNC-mode VMs too, which + # have no serial console capture at all. + local cfg="$1" + shift + mkdir -p "$LOG_DIR" + echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_DIR/$cfg.log" +} is_anonymous_section() { # UCI auto-generated names look like: cfgXXXXXX (cfg + hex) @@ -84,6 +101,27 @@ _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 + host_log "$cfg" "USB: unbound $(basename "$dev") from $driver_name" + fi + fi + fi + done +} + build_customargs() { local cfg="$1" custom_opts="" @@ -113,24 +151,30 @@ build_diskargs() { case "$bus" in virtio) - disk_opts="-drive file=$image,format=raw,if=virtio" + #disk_opts="-drive file=$image,format=raw,if=virtio" + disk_opts="-drive file=$image,if=virtio" ;; ide) # Widest guest compatibility (old Windows/DOS/legacy kernels # without virtio drivers), at some cost to I/O throughput. - disk_opts="-drive file=$image,format=raw,if=ide" + #disk_opts="-drive file=$image,format=$format,if=ide" + disk_opts="-drive file=$image,if=ide" ;; scsi) disk_controller_opts="-device virtio-scsi-pci,id=scsi0" - disk_opts="-drive file=$image,format=raw,if=none,id=drive0 -device scsi-hd,drive=drive0,bus=scsi0.0" + #disk_opts="-drive file=$image,format=$format,if=none,id=drive0 -device scsi-hd,drive=drive0,bus=scsi0.0" + disk_opts="-drive file=$image,if=none,id=drive0 -device scsi-hd,drive=drive0,bus=scsi0.0" ;; sata|ahci) disk_controller_opts="-device ahci,id=ahci0" - disk_opts="-drive file=$image,format=raw,if=none,id=drive0 -device ide-hd,drive=drive0,bus=ahci0.0" + #disk_opts="-drive file=$image,format=$format,if=none,id=drive0 -device ide-hd,drive=drive0,bus=ahci0.0" + disk_opts="-drive file=$image,if=none,id=drive0 -device ide-hd,drive=drive0,bus=ahci0.0" ;; *) logger -t qemu-vms "WARNING: VM '$cfg' has unknown disk_bus '$bus', falling back to virtio" - disk_opts="-drive file=$image,format=raw,if=virtio" + #disk_opts="-drive file=$image,format=$format,if=virtio" + disk_opts="-drive file=$image,if=virtio" + host_log "$cfg" "WARNING: unknown disk_bus '$bus', falling back to virtio" ;; esac } @@ -145,12 +189,15 @@ start_vm() { [ "$enabled" = "1" ] || return 0 local existing_pid + existing_pid="$(cat "$STATUS_DIR/$cfg.pid" 2>/dev/null)" if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null && \ grep -aq "qemu-system-x86_64" "/proc/$existing_pid/cmdline" 2>/dev/null; then logger -t qemu-vms "start_vm: '$cfg' already running (pid $existing_pid), skipping" return 0 fi + host_log "$cfg" "=== start_vm: attempting to start ===" + warn_if_anonymous "$cfg" config_get image "$cfg" image @@ -158,6 +205,7 @@ start_vm() { config_get memory "$cfg" memory 128 config_get smp "$cfg" smp 1 config_get cpu "$cfg" cpu "host" + config_list_foreach "$cfg" usb_passthrough _unbind_usb_host config_get pci_passthrough "$cfg" pci_passthrough config_get display_type "$cfg" display_type "serial" config_get vnc_port "$cfg" vnc_port @@ -176,12 +224,47 @@ start_vm() { config_get pci_id "$pci_passthrough" pci_id config_get vendor_id "$pci_passthrough" vendor_id if [ -n "$vendor_id" ] && [ -n "$pci_id" ]; then - [ -e "/sys/bus/pci/devices/0000:$pci_id/driver" ] || \ + 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 + # Test version PCI Pass + #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" + # 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" + # else + # host_log "$cfg" "ERROR: PCI passthrough: failed to bind $pci_id to vfio-pci. Check IOMMU groups and BIOS settings." + # fi + # pci_opts="-device vfio-pci,host=$pci_id" + # 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" @@ -206,10 +289,12 @@ start_vm() { #rm -f "$console_sock" "$qmp_sock" mkdir -p "$STATUS_DIR" + mkdir -p "$LOG_DIR" mkdir -p "$(dirname "$console_log")" procd_open_instance "qemu-$cfg" - procd_set_param command /usr/bin/qemu-system-x86_64 \ + procd_set_param command /usr/libexec/qemu-vms/run-with-log.sh "$LOG_DIR/$cfg.log" \ + /usr/bin/qemu-system-x86_64 \ -enable-kvm -cpu "$cpu" -smp "$smp" -m "${memory}M" \ $disk_controller_opts $disk_opts \ $device_opts $netdev_opts $pci_opts $cdrom_opts $usb_controller_opts $usb_opts $vnc_opts $custom_opts \ @@ -244,6 +329,6 @@ start_service() { fi } -service_triggers() { - procd_add_reload_trigger "qemu-vms" -} +#service_triggers() { +# procd_add_reload_trigger "qemu-vms" +#} diff --git a/qemu-vms/root/usr/libexec/qemu-vms/run-with-log.sh b/qemu-vms/root/usr/libexec/qemu-vms/run-with-log.sh new file mode 100755 index 000000000..a782b8d24 --- /dev/null +++ b/qemu-vms/root/usr/libexec/qemu-vms/run-with-log.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# /usr/libexec/qemu-vms/run-with-log.sh +# +# Usage: run-with-log.sh [args...] +# +# Appends the wrapped command's stdout/stderr to , then execs it. +# Deliberately does NOT build a shell string out of /[args...] - +# they are received and forwarded as genuine argv elements ("$@"), never +# re-parsed as shell syntax. This matters because some of the arguments +# QEMU is started with (e.g. custom_arg from the LuCI "Custom QEMU +# arguments" field) are admin-supplied free text: if this script instead +# did something like `sh -c "exec $* >>logfile"`, shell metacharacters in +# that text (;, `, $(...), |, &&) would be interpreted as shell syntax +# and executed - not just passed to qemu as inert argv text. Using "$@" +# throughout preserves the same "argv, not a string" trust model as a +# direct (unwrapped) exec. + +logfile="$1" +shift + +exec "$@" >> "$logfile" 2>&1 diff --git a/sqm-autorate/Makefile b/sqm-autorate/Makefile index 03103a547..4e3985110 100644 --- a/sqm-autorate/Makefile +++ b/sqm-autorate/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sqm-autorate PKG_VERSION:=0.1 -PKG_RELEASE:=2 +PKG_RELEASE:=3 include $(INCLUDE_DIR)/package.mk diff --git a/sqm-autorate/files/usr/share/sqm-autorate/cake-autorate.sh b/sqm-autorate/files/usr/share/sqm-autorate/cake-autorate.sh index 6c8840515..9d38f26d1 100755 --- a/sqm-autorate/files/usr/share/sqm-autorate/cake-autorate.sh +++ b/sqm-autorate/files/usr/share/sqm-autorate/cake-autorate.sh @@ -61,7 +61,7 @@ set -o pipefail export LC_ALL=C # Set PREFIX -PREFIX=/root/cake-autorate +PREFIX=/usr/share/sqm-autorate # shellcheck source=lib.sh . "${PREFIX}/lib.sh" diff --git a/sqm-autorate/files/usr/share/sqm-autorate/launcher.sh b/sqm-autorate/files/usr/share/sqm-autorate/launcher.sh index 7cb3567df..dd05d740a 100755 --- a/sqm-autorate/files/usr/share/sqm-autorate/launcher.sh +++ b/sqm-autorate/files/usr/share/sqm-autorate/launcher.sh @@ -1,6 +1,6 @@ #!/bin/bash -cake_instances=(/root/cake-autorate/config.*.sh) +cake_instances=(/usr/share/sqm-autorate/config.*.sh) cake_instance_pids=() trap kill_cake_instances INT TERM EXIT @@ -20,7 +20,7 @@ kill_cake_instances() for cake_instance in "${cake_instances[@]}" do - /root/cake-autorate/cake-autorate.sh "${cake_instance}" & + /usr/share/sqm-autorate/cake-autorate.sh "${cake_instance}" & cake_instance_pids+=(${!}) done wait