Skip to content

Instantly share code, notes, and snippets.

@thiscantbeserious
Last active November 1, 2025 12:12
Show Gist options
  • Save thiscantbeserious/8e693e979cf5c6e43a214e6ca02f0020 to your computer and use it in GitHub Desktop.
Save thiscantbeserious/8e693e979cf5c6e43a214e6ca02f0020 to your computer and use it in GitHub Desktop.
qvm command: unmanaged QEMU launcher with multi raw --disk support for TrueNAS Scale
#!/bin/zsh
# using this in my .zshrc file of the truenas_scale admin as an aliased command
qvm() {
local DISKS=()
local ISO=""
local BRIDGE=""
local VNC_DISP="2"
local VNC_BIND="0.0.0.0"
local MEM="1024"
local CPUS="1"
local BOOT="c"
local SYS_DISK="/mnt/main/vms/debian-root.qcow2"
local LOGFILE="/var/log/qemu-debian-btrfs.log"
local PIDFILE="/run/qemu-debian-btrfs.pid"
local WANT_KEYMAP="de"
while [[ $# -gt 0 ]]; do
case "$1" in
-D|--disk) DISKS+=("$2"); shift 2 ;;
-i|--iso) ISO="$2"; shift 2 ;;
-m|--mem) MEM="$2"; shift 2 ;;
-c|--cpus) CPUS="$2"; shift 2 ;;
-v|--vnc) VNC_DISP="$2"; shift 2 ;;
--vnc-bind) VNC_BIND="$2"; shift 2 ;;
-B|--boot) BOOT="$2"; shift 2 ;; # c or d
--bridge) BRIDGE="$2"; shift 2 ;; # e.g. br0
--sysdisk) SYS_DISK="$2"; shift 2 ;;
--log) LOGFILE="$2"; shift 2 ;;
--pidfile) PIDFILE="$2"; shift 2 ;;
--keymap) WANT_KEYMAP="$2"; shift 2 ;;
--stop)
if [[ -f "$PIDFILE" ]] && sudo kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
sudo kill "$(cat "$PIDFILE")"
echo "Stopped QEMU PID $(cat "$PIDFILE")"
else
echo "No running QEMU via $PIDFILE"
fi
return 0 ;;
-h|--help)
cat <<'EOF'
qvm (multi-disk)
-D, --disk PATH Add raw/qcow2 disk (repeatable) -> vdb, vdc, ...
-i, --iso PATH Installer ISO; use -B d to boot from it
-B, --boot c|d Boot from disk (c) or cdrom (d). Default: c
-m, --mem MiB RAM (default 1024)
-c, --cpus N vCPUs (default 1)
-v, --vnc N VNC display (default 2 -> TCP 5902)
--vnc-bind ADDR 0.0.0.0 (LAN) or "localhost"
--bridge brN Bridge iface (default: user/NAT)
--sysdisk PATH qcow2 for OS (default /mnt/main/vms/debian-root.qcow2)
--log PATH QEMU log (default /var/log/qemu-debian-btrfs.log)
--pidfile PATH PID file (default /run/qemu-debian-btrfs.pid)
--keymap LAYOUT Keyboard layout for VNC (default de)
--stop Stop VM started by qvm
EOF
return 0 ;;
*) echo "Unknown arg: $1" >&2; return 1 ;;
esac
done
(( ${#DISKS[@]} == 0 )) && { echo "Error: at least one --disk required" >&2; return 1; }
[[ "$BOOT" != "c" && "$BOOT" != "d" ]] && { echo "Error: --boot must be c or d" >&2; return 1; }
if [[ ! -f "$SYS_DISK" ]]; then
sudo mkdir -p "$(dirname "$SYS_DISK")"
sudo qemu-img create -f qcow2 "$SYS_DISK" 32G
fi
local NET=()
if [[ -n "$BRIDGE" ]]; then
sudo mkdir -p /etc/qemu
if [[ ! -f /etc/qemu/bridge.conf ]] || ! grep -q "allow $BRIDGE" /etc/qemu/bridge.conf 2>/dev/null; then
echo "allow $BRIDGE" | sudo tee /etc/qemu/bridge.conf >/dev/null
sudo chmod 0644 /etc/qemu/bridge.conf
fi
NET=( -nic "bridge,br=${BRIDGE},model=virtio-net-pci" )
else
NET=( -nic "user,model=virtio-net-pci" )
fi
local VNC_ADDR
if [[ "$VNC_BIND" == "localhost" ]]; then
VNC_ADDR=":${VNC_DISP}"
else
VNC_ADDR="${VNC_BIND}:${VNC_DISP}"
fi
local USE_K="no"
if /usr/bin/qemu-system-x86_64 -h 2>&1 | grep -qE '(^|[[:space:]])-k[[:space:]]'; then
USE_K="yes"
fi
local VNC_OPT=()
if [[ "$USE_K" == "yes" ]]; then
VNC_OPT=( -vnc "$VNC_ADDR" -k "$WANT_KEYMAP" )
else
VNC_OPT=( -vnc "${VNC_ADDR},keymap=${WANT_KEYMAP}" )
fi
local CDROM=()
[[ -n "$ISO" ]] && CDROM=( -cdrom "$ISO" )
local DRIVE_ARGS=( -drive "if=virtio,file=${SYS_DISK},format=qcow2" )
local d fmt
for d in "${DISKS[@]}"; do
if [[ ! -b "$d" && ! -f "$d" ]]; then
echo "Warning: $d not found, skipping" >&2
continue
fi
fmt="raw"; [[ "$d" == *.qcow2 ]] && fmt="qcow2"
DRIVE_ARGS+=( -drive "if=virtio,file=${d},format=${fmt}" )
done
sudo mkdir -p /var/log /run
local CMD=( sudo /usr/bin/qemu-system-x86_64
-enable-kvm -cpu host -smp "$CPUS" -m "$MEM"
"${DRIVE_ARGS[@]}"
"${CDROM[@]}"
-boot "order=${BOOT}"
-display none
"${VNC_OPT[@]}"
"${NET[@]}"
-D "$LOGFILE" -d guest_errors
)
echo -n "Executing: "
printf '%q ' "${CMD[@]}"
echo
"${CMD[@]}" &
local pid=$!
echo $pid | sudo tee "$PIDFILE" >/dev/null
echo "QEMU PID $pid started; VNC ${VNC_BIND}:$((5900+VNC_DISP)) (disp :$VNC_DISP), keymap=${WANT_KEYMAP}"
}
@thiscantbeserious
Copy link
Author

thiscantbeserious commented Nov 1, 2025

Usage example:

qvm --disk /dev/disk/by-id/ata-TOSHIBA_MG09ACA18TE_X --disk /dev/sdc --cpus 4 -m 2048

Launches it in the background so that its not bound to current shell session / ssh

Will create a 32GiB SYS-Disk by default under /mnt/main/vms/debian-root.qcow2 - I used debian 13 / trixi netboot for this

Executing: sudo /usr/bin/qemu-system-x86_64 -enable-kvm -cpu host -smp 4 -m 2048 -drive if=virtio,file=/mnt/main/vms/debian-root.qcow2,format=qcow2 -drive if=virtio,file=/dev/disk/by-id/ata-TOSHIBA_MG09ACA18TE_X,format=raw -drive if=virtio,file=/dev/sdc,format=raw -boot order=c -display none -vnc 0.0.0.0:2 -k de -nic user,model=virtio-net-pci -D /var/log/qemu-debian-btrfs.log -d guest_errors 
[2] 581467
QEMU PID 581467 started; VNC 0.0.0.0:5902 (disp :2), keymap=de

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment