Last active
November 1, 2025 12:12
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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}" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage example:
qvm --disk /dev/disk/by-id/ata-TOSHIBA_MG09ACA18TE_X --disk /dev/sdc --cpus 4 -m 2048Launches 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 thisExecuting: 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