#!/usr/bin/env bash export LC_ALL=C function disk_delete() { if [ -e "${disk_img}" ]; then rm "${disk_img}" echo "SUCCESS! Deleted ${disk_img}" else echo "NOTE! ${disk_img} not found. Doing nothing." fi local VMNAME="" VMNAME=$(basename "${VM}" .conf) local SHORTCUT_DIR="${HOME}/.local/share/applications/" if [ -e "${SHORTCUT_DIR}/${VMNAME}.desktop" ]; then rm "${SHORTCUT_DIR}/${VMNAME}.desktop" echo "Deleted ${VM} desktop shortcut" fi } function snapshot_apply() { local TAG="${1}" if [ -z "${TAG}" ]; then echo "ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -a "${TAG}" "${disk_img}"; then echo "SUCCESS! Applied snapshot ${TAG} to ${disk_img}" else echo "ERROR! Failed to apply snapshot ${TAG} to ${disk_img}" fi else echo "NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_create() { local TAG="${1}" if [ -z "${TAG}" ]; then echo "ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -c "${TAG}" "${disk_img}"; then echo "SUCCESS! Created snapshot ${TAG} of ${disk_img}" else echo "ERROR! Failed to create snapshot ${TAG} of ${disk_img}" fi else echo "NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_delete() { local TAG="${1}" if [ -z "${TAG}" ]; then echo "ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -d "${TAG}" "${disk_img}"; then echo "SUCCESS! Deleted snapshot ${TAG} of ${disk_img}" else echo "ERROR! Failed to delete snapshot ${TAG} of ${disk_img}" fi else echo "NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_info() { if [ -e "${disk_img}" ]; then ${QEMU_IMG} info "${disk_img}" fi } function get_port() { local PORT_START=$1 local PORT_RANGE=$2 while true; do local CANDIDATE=$((PORT_START + (RANDOM % PORT_RANGE))) (echo "" >/dev/tcp/127.0.0.1/${CANDIDATE}) >/dev/null 2>&1 if [ ${?} -ne 0 ]; then echo "${CANDIDATE}" break fi done } function enable_usb_passthrough() { local DEVICE="" local USB_BUS="" local USB_DEV="" local USB_NAME="" local VENDOR_ID="" local PRODUCT_ID="" local USB_NOT_READY=0 # Have any USB devices been requested for pass-through? if (( ${#usb_devices[@]} )); then echo " - USB: Host pass-through requested:" for DEVICE in "${usb_devices[@]}"; do VENDOR_ID=$(echo "${DEVICE}" | cut -d':' -f1) PRODUCT_ID=$(echo "${DEVICE}" | cut -d':' -f2) USB_BUS=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f2) USB_DEV=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f4 | cut -d':' -f1) USB_NAME=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f7-) if [ -w "/dev/bus/usb/${USB_BUS}/${USB_DEV}" ]; then echo " o ${USB_NAME} on bus ${USB_BUS} device ${USB_DEV} is accessible." else echo " x ${USB_NAME} on bus ${USB_BUS} device ${USB_DEV} needs permission changes:" echo " sudo chown -v root:${USER} /dev/bus/usb/${USB_BUS}/${USB_DEV}" USB_NOT_READY=1 fi USB_PASSTHROUGH="${USB_PASSTHROUGH} -device usb-host,bus=hostpass.0,vendorid=0x${VENDOR_ID},productid=0x${PRODUCT_ID}" done if [ "${USB_NOT_READY}" -eq 1 ]; then echo " ERROR! USB permission changes are required 👆" exit 1 fi fi } function check_cpu_flag() { local HOST_CPU_FLAG="${1}" if lscpu | grep -o "^Flags\b.*: .*\b${HOST_CPU_FLAG}\b" > /dev/null; then return 0 else return 1 fi } function vm_boot() { local VMNAME="" VMNAME=$(basename "${VM}" .conf) local VMDIR="" VMDIR=$(dirname "${disk_img}") local BALLOON="-device virtio-balloon" local CPU="" local GUEST_CPU_CORES="" local GUEST_CPU_LOGICAL_CORES="" local GUEST_CPU_THREADS="" local HOST_CPU_CORES="" local HOST_CPU_SMT="" local HOST_CPU_SOCKETS="" local HOST_CPU_VENDOR="" local DISPLAY_DEVICE="" local GL="on" local GUEST_TWEAKS="" local MAC_BOOTLOADER="" local MAC_MISSING="" local MAC_DISK_DEV="ide-hd,bus=ahci.2" local NET_DEVICE="virtio-net" local OSK="" local QEMU_VER="" local USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci" local VIDEO="" QEMU_VER=$(${QEMU} -version | head -n1 | cut -d' ' -f4 | cut -d'(' -f1) echo "Quickemu ${VERSION} starting ${VM}" echo " - QEMU: ${QEMU} v${QEMU_VER}" # Force to lowercase. boot=${boot,,} guest_os=${guest_os,,} # Always Boot macOS using EFI if [ "${guest_os}" == "macos" ]; then boot="efi" echo " - BOOT: EFI (${guest_os})" if [ -e "${VMDIR}/OVMF_CODE.fd" ] && [ -e "${VMDIR}/OVMF_VARS-1024x768.fd" ]; then local EFI_CODE="${VMDIR}/OVMF_CODE.fd" local EFI_VARS="${VMDIR}/OVMF_VARS-1024x768.fd" else MAC_MISSING="Firmware" fi if [ -e "${VMDIR}/OpenCore.qcow2" ]; then MAC_BOOTLOADER="${VMDIR}/OpenCore.qcow2" elif [ -e "${VMDIR}/ESP.qcow2" ]; then # Backwards compatibility for Clover MAC_BOOTLOADER="${VMDIR}/ESP.qcow2" else MAC_MISSING="Bootloader" fi if [ -n "${MAC_MISSING}" ]; then echo "ERROR! macOS ${MAC_MISSING} was not found." echo " Use 'quickget' to download the required files." exit 1 fi elif [[ "${boot}" == *"efi"* ]]; then if [ -e "/usr/share/OVMF/OVMF_CODE_4M.fd" ]; then echo " - BOOT: EFI (${guest_os})" local EFI_CODE="/usr/share/OVMF/OVMF_CODE_4M.fd" local EFI_VARS="${VMDIR}/OVMF_VARS_4M.fd" if [ -e "${VMDIR}/${VMNAME}-vars.fd" ]; then # Preserve backward compatibility mv "${VMDIR}/${VMNAME}-vars.fd" "${EFI_VARS}" elif [ ! -e "${EFI_VARS}" ]; then cp "/usr/share/OVMF/OVMF_VARS_4M.fd" "${EFI_VARS}" fi else boot="legacy" echo " - BOOT: Legacy BIOS (${guest_os}) - EFI requested but no EFI firmware found." fi else echo " - BOOT: Legacy BIOS (${guest_os})" fi HOST_CPU_CORES=$(nproc --all) HOST_CPU_VENDOR=$(lscpu | grep -E 'Vendor' | cut -d':' -f2 | sed 's/ //g') HOST_CPU_SOCKETS=$(lscpu | grep -E 'Socket' | cut -d':' -f2 | sed 's/ //g') # A CPU with Intel VT-x / AMD SVM support is required if [ "${HOST_CPU_VENDOR}" == "AuthenticIntel" ]; then if ! check_cpu_flag vmx; then echo "ERROR! Intel VT-x support is required." exit 1 fi elif [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then if ! check_cpu_flag svm; then echo "ERROR! AMD SVM support is required." exit 1 fi fi if [ -z "${cpu_cores}" ]; then if [ "${HOST_CPU_CORES}" -ge 32 ]; then GUEST_CPU_CORES="16" elif [ "${HOST_CPU_CORES}" -ge 16 ]; then GUEST_CPU_CORES="8" elif [ "${HOST_CPU_CORES}" -ge 8 ]; then GUEST_CPU_CORES="4" elif [ "${HOST_CPU_CORES}" -ge 4 ]; then GUEST_CPU_CORES="2" else GUEST_CPU_CORES="1" fi else GUEST_CPU_CORES="${cpu_cores}" fi # Account for Hyperthreading/SMT. if [ -e /sys/devices/system/cpu/smt/control ] && [ "${GUEST_CPU_CORES}" -ge 2 ]; then HOST_CPU_SMT=$(cat /sys/devices/system/cpu/smt/control) case ${HOST_CPU_SMT} in on) GUEST_CPU_THREADS=2 GUEST_CPU_LOGICAL_CORES=$(( GUEST_CPU_CORES / GUEST_CPU_THREADS )) ;; *) GUEST_CPU_THREADS=1 GUEST_CPU_LOGICAL_CORES=${GUEST_CPU_CORES} ;; esac else GUEST_CPU_THREADS=1 GUEST_CPU_LOGICAL_CORES=${GUEST_CPU_CORES} fi local SMP="-smp cores=${GUEST_CPU_LOGICAL_CORES},threads=${GUEST_CPU_THREADS},sockets=${HOST_CPU_SOCKETS}" echo -n " - CPU: ${HOST_CPU_SOCKETS} Socket(s), ${GUEST_CPU_LOGICAL_CORES} Core(s), ${GUEST_CPU_THREADS} Thread(s)" local RAM_VM="2G" if [ -z "${ram}" ]; then local RAM_HOST="" RAM_HOST=$(free --mega -h | grep Mem | cut -d':' -f2 | cut -d'G' -f1 | sed 's/ //g') #Round up - https://github.com/wimpysworld/quickemu/issues/11 RAM_HOST=$(printf '%.*f\n' 0 "${RAM_HOST}") if [ "${RAM_HOST}" -ge 256 ]; then RAM_VM="32G" elif [ "${RAM_HOST}" -ge 128 ]; then RAM_VM="16G" elif [ "${RAM_HOST}" -ge 64 ]; then RAM_VM="8G" elif [ "${RAM_HOST}" -ge 32 ]; then RAM_VM="4G" elif [ "${RAM_HOST}" -ge 16 ]; then RAM_VM="3G" fi else RAM_VM="${ram}" fi echo ", ${RAM_VM} RAM" # Make any OS specific adjustments case ${guest_os} in linux) CPU="-cpu host,kvm=on" if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then CPU="${CPU},topoext" fi disk_size="16G" ;; macos) #https://www.nicksherlock.com/2020/06/installing-macos-big-sur-on-proxmox/ # A CPU with SSE4.1 support is required for >= macOS Sierra # A CPU with AVX2 support is required for >= macOS Mojave if check_cpu_flag sse4_1 && check_cpu_flag avx2; then case ${HOST_CPU_VENDOR} in AuthenticIntel) CPU="-cpu host,kvm=on,vendor=GenuineIntel,+hypervisor,+invtsc,+kvm_pv_eoi,+kvm_pv_unhalt";; AuthenticAMD|*) # Used in past versions: +movbe,+smep,+xgetbv1,+xsavec # Warn on AMD: +fma4,+pcid CPU="-cpu Penryn,kvm=on,vendor=GenuineIntel,+aes,+avx,+avx2,+bmi1,+bmi2,+fma,+hypervisor,+invtsc,+kvm_pv_eoi,+kvm_pv_unhalt,+popcnt,+ssse3,+sse4.2,vmware-cpuid-freq=on,+xsave,+xsaveopt,check";; esac else echo "ERROR! macOS requires a CPU with SSE 4.1 and AVX2 support." exit 1 fi # Tune Qemu optimisations based on the macOS release, or fallback to lowest # common supported options if none is specificed. # * VirtIO Block Media doesn't work in High Sierra (at all) or the Mojave (Recovery Image) # * VirtIO Network is supported in Big Sur # * VirtIO Memory Balloning is supported in Big Sur (https://pmhahn.github.io/virtio-balloon/) # * VirtIO RNG is supported in Big Sur, but exposed to all guests. case ${macos_release} in catalina) BALLOON="" MAC_DISK_DEV="virtio-blk-pci" NET_DEVICE="vmxnet3" USB_HOST_PASSTHROUGH_CONTROLLER="usb-ehci" ;; big-sur) BALLOON="-device virtio-balloon" MAC_DISK_DEV="virtio-blk-pci" NET_DEVICE="virtio-net" USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci" ;; *) # Backwards compatibility if no macos_release is specified. # Also safe catch all for High Sierra and Mojave BALLOON="" MAC_DISK_DEV="ide-hd,bus=ahci.2" NET_DEVICE="vmxnet3" USB_HOST_PASSTHROUGH_CONTROLLER="usb-ehci" ;; esac OSK=$(echo "bheuneqjbexolgurfrjbeqfthneqrqcyrnfrqbagfgrny(p)NccyrPbzchgreVap" | tr 'A-Za-z' 'N-ZA-Mn-za-m') GUEST_TWEAKS="-device isa-applesmc,osk=${OSK} -no-hpet -global kvm-pit.lost_tick_policy=discard" disk_size="64G" ;; windows) CPU="-cpu host,kvm=on,+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_frequencies,kvm_pv_unhalt,hv_reenlightenment,hv_relaxed,hv_spinlocks=8191,hv_stimer,hv_synic,hv_time,hv_vapic,hv_vendor_id=1234567890ab,hv_vpindex" if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then CPU="${CPU},topoext" fi GUEST_TWEAKS="-no-hpet -global kvm-pit.lost_tick_policy=discard" disk_size="64G" ;; *) CPU="-cpu host,kvm=on" NET_DEVICE="rtl8139" echo "WARNING! Unrecognised guest OS: ${guest_os}" ;; esac echo " - Disk: ${disk_img} (${disk_size})" if [ ! -f "${disk_img}" ]; then # If there is no disk image, create a new image. mkdir -p "${VMDIR}" 2>/dev/null case ${preallocation} in off|metadata|falloc|full) true;; *) echo "ERROR! ${preallocation} is an unsupported disk preallocation option." exit 1;; esac # https://blog.programster.org/qcow2-performance if ! ${QEMU_IMG} create -q -f qcow2 -o lazy_refcounts=on,preallocation="${preallocation}" "${disk_img}" "${disk_size}"; then echo "ERROR! Failed to create ${disk_img}" exit 1 fi if [ -z "${iso}" ] && [ -z "${img}" ]; then echo "ERROR! You haven't specified a .iso or .img image to boot from." exit 1 fi echo " Just created, booting from ${iso}${img}" elif [ -e "${disk_img}" ]; then # Check there isn't already a process attached to the disk image. if ! ${QEMU_IMG} info "${disk_img}" >/dev/null; then echo " Failed to get \"write\" lock. Is another process using the disk?" exit 1 else DISK_CURR_SIZE=$(stat -c%s "${disk_img}") if [ "${DISK_CURR_SIZE}" -le "${DISK_MIN_SIZE}" ]; then echo " Looks unused, booting from ${iso}${img}" if [ -z "${iso}" ] && [ -z "${img}" ]; then echo "ERROR! You haven't specified a .iso or .img image to boot from." exit 1 fi else # If there is a disk image, assume there is an install and do not boot # from installation media. iso="" img="" fi fi fi # Has the status quo been requested? if [ "${STATUS_QUO}" == "-snapshot" ]; then if [ -z "${img}" ] && [ -z "${iso}" ]; then echo " Existing disk state will be preserved, no writes will be committed." fi fi if [ -n "${iso}" ] && [ -e "${iso}" ]; then echo " - Boot ISO: ${iso}" elif [ -n "${img}" ] && [ -e "${img}" ]; then echo " - Recovery: ${img}" fi if [ -n "${fixed_iso}" ] && [ -e "${fixed_iso}" ]; then echo " - CD-ROM: ${fixed_iso}" fi local X_RES=1152 local Y_RES=648 if [ "${XDG_SESSION_TYPE}" == "x11" ]; then local LOWEST_WIDTH="" if [ -z "${SCREEN}" ]; then LOWEST_WIDTH=$(xrandr --listmonitors | grep -v Monitors | cut -d' ' -f4 | cut -d'/' -f1 | sort | head -n1) else LOWEST_WIDTH=$(xrandr --listmonitors | grep -v Monitors | grep "^ ${SCREEN}:" | cut -d' ' -f4 | cut -d'/' -f1 | head -n1) fi if [ "${FULLSCREEN}" ]; then if [ -z "${SCREEN}" ]; then X_RES=$(xrandr --listmonitors | grep -v Monitors | cut -d' ' -f4 | cut -d'/' -f1 | sort | head -n1) Y_RES=$(xrandr --listmonitors | grep -v Monitors | cut -d' ' -f4 | cut -d'/' -f2 | cut -d'x' -f2 | sort | head -n1) else X_RES=$(xrandr --listmonitors | grep -v Monitors | grep "^ ${SCREEN}:" | cut -d' ' -f4 | cut -d'/' -f1 | head -n1) Y_RES=$(xrandr --listmonitors | grep -v Monitors | grep "^ ${SCREEN}:" | cut -d' ' -f4 | cut -d'/' -f2 | cut -d'x' -f2 | head -n1) fi elif [ "${LOWEST_WIDTH}" -ge 3840 ]; then X_RES=3200 Y_RES=1800 elif [ "${LOWEST_WIDTH}" -ge 2560 ]; then X_RES=2048 Y_RES=1152 elif [ "${LOWEST_WIDTH}" -ge 1920 ]; then X_RES=1664 Y_RES=936 elif [ "${LOWEST_WIDTH}" -ge 1280 ]; then X_RES=1152 Y_RES=648 fi fi if [ "${guest_os}" != "macos" ]; then echo " - Screen: ${X_RES}x${Y_RES}" fi # https://www.kraxel.org/blog/2019/09/display-devices-in-qemu/ if [ "${guest_os}" == "linux" ]; then case ${OUTPUT} in spice) DISPLAY_DEVICE="qxl-vga";; *) DISPLAY_DEVICE="virtio-vga";; esac elif [ "${guest_os}" == "macos" ]; then # Tweak video device based on the guest macOS release. # Displays in System Preferences can be used to select a resolution if: # - qxl is used on Big Sur and Catalina # - VGA is used on Mojave, although available resolutions are all 4:3 # - High Sierra will run at the default 1920x1080 only. case ${macos_release} in catalina|big-sur) DISPLAY_DEVICE="qxl";; *) DISPLAY_DEVICE="VGA";; esac elif [ "${guest_os}" == "windows" ]; then DISPLAY_DEVICE="qxl-vga" else DISPLAY_DEVICE="qxl-vga" fi echo -n " - Display: ${OUTPUT^^}, ${DISPLAY_DEVICE}" if [ "${OUTPUT}" == "spice" ]; then OUTPUT="none" fi # Build the video configuration VIDEO="-device ${DISPLAY_DEVICE}" # Do not try and coerce the display resolution for macOS if [ "${guest_os}" != "macos" ]; then VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}" fi # Allocate VRAM to VGA devices if [ "${DISPLAY_DEVICE}" == "qxl-vga" ] || [ "${DISPLAY_DEVICE}" == "VGA" ]; then VIDEO="${VIDEO},vgamem_mb=128" fi VIDEO="${VIDEO} ${FULLSCREEN}" if [ "${OUTPUT}" == "gtk" ]; then OUTPUT="${OUTPUT},grab-on-hover=on,zoom-to-fit=off" # GL is not working with GTK and virtio-vga if [ "${DISPLAY_DEVICE}" == "virtio-vga" ]; then GL="off" fi fi if [ "${OUTPUT}" != "none" ]; then OUTPUT="${OUTPUT},gl=${GL}" fi if [ "${GL}" == "on" ] && [[ "${DISPLAY_DEVICE}" == *"virtio"* ]]; then DISPLAY_DEVICE="${DISPLAY_DEVICE},virgl=on" echo ", GL (${GL}), VirGL (on)" else echo ", GL (${GL}), VirGL (off)" fi # Set the hostname of the VM local NET="user,hostname=${VMNAME}" # Find a free port to expose ssh to the guest local SSH_PORT="" SSH_PORT=$(get_port 22220 9) if [ -n "${SSH_PORT}" ]; then NET="${NET},hostfwd=tcp::${SSH_PORT}-:22" echo " - ssh: On host: ssh user@localhost -p ${SSH_PORT}" else echo " - ssh: All ssh ports have been exhausted." fi # Have any port forwards been requested? if (( ${#port_forwards[@]} )); then echo " - PORTS: Port forwards requested:" for FORWARD in "${port_forwards[@]}"; do HOST_PORT=$(echo "${FORWARD}" | cut -d':' -f1) GUEST_PORT=$(echo "${FORWARD}" | cut -d':' -f2) echo " - ${HOST_PORT} => ${GUEST_PORT}" NET="${NET},hostfwd=tcp::${HOST_PORT}-:${GUEST_PORT}" done fi # Find a free port for spice local SPICE="disable-ticketing=on" local SPICE_PORT="" SPICE_PORT=$(get_port 5930 9) if [ -z "${SPICE_PORT}" ]; then echo " - SPICE: All SPICE ports have been exhausted." if [ "${OUTPUT}" == "none" ] || [ "${OUTPUT}" == "spice-app" ]; then echo " ERROR! Requested SPICE display, but no SPICE ports are free." exit 1 fi else if [ "${OUTPUT}" == "spice-app" ]; then echo " - SPICE: Enabled" else echo -n " - SPICE: On host: spicy --title \"${VMNAME}\" --port ${SPICE_PORT}" if [ "${guest_os}" != "macos" ]; then echo -n " --spice-shared-dir ${PUBLIC}" fi echo "${FULLSPICY}" SPICE="${SPICE},port=${SPICE_PORT}" fi # Reference: https://gitlab.gnome.org/GNOME/phodav/-/issues/5 if [ "${guest_os}" != "macos" ]; then echo " - WebDAV: On guest: dav://localhost:9843/" fi if [ "${guest_os}" != "windows" ]; then echo -n " - 9P: On guest: " if [ "${guest_os}" == "linux" ]; then echo "sudo mount -t 9p -o trans=virtio,version=9p2000.L,msize=104857600 ${PUBLIC_TAG} ~/Public" elif [ "${guest_os}" == "macos" ]; then # PUBLICSHARE needs to be world writeable for seamless integration with # macOS. Test if it is world writeable, and prompt what to do if not. echo "sudo mount_9p ${PUBLIC_TAG}" if [ -n "${PUBLIC}" ] && [ "${PUBLIC_PERMS}" != "drwxrwxrwx" ]; then echo " - 9P: On host: chmod 777 ${PUBLIC}" echo " Required for macOS integration 👆" fi fi fi fi enable_usb_passthrough # Boot the VM local args=() # shellcheck disable=SC2054,SC2206,SC2140 args+=(-name ${VMNAME},process=${VMNAME} -enable-kvm -machine q35,vmport=off ${GUEST_TWEAKS} ${CPU} ${SMP} -m ${RAM_VM} ${BALLOON} -smbios type=2,manufacturer="Wimpys World",product="Quickemu",version="${VERSION}",serial="jvzclfjbeyq.pbz",location="wimpysworld.com",asset="${VMNAME}" ${VIDEO} -display ${OUTPUT} -device usb-ehci,id=input -device usb-kbd,bus=input.0 -device usb-tablet,bus=input.0 -device ${NET_DEVICE},netdev=nic -netdev ${NET},id=nic -audiodev pa,id=audio0,out.mixing-engine=off,out.stream-name=${LAUNCHER}-${VMNAME},in.stream-name=${LAUNCHER}-${VMNAME} -device intel-hda -device hda-duplex,audiodev=audio0 -rtc base=localtime,clock=host,driftfix=slew -spice ${SPICE} -device virtio-serial-pci -chardev spicevmc,id=vdagent0,name=vdagent -device virtserialport,chardev=vdagent0,name=com.redhat.spice.0 -device virtio-rng-pci,rng=rng0 -object rng-random,id=rng0,filename=/dev/urandom -monitor none -serial mon:stdio) # Add the disks # - https://turlucode.com/qemu-disk-io-performance-comparison-native-or-threads-windows-10-version/ if [[ "${boot}" == *"efi"* ]]; then # shellcheck disable=SC2054 args+=(-drive if=pflash,format=raw,file="${EFI_CODE}",readonly=on -drive if=pflash,format=raw,file="${EFI_VARS}") fi if [ -n "${floppy}" ]; then # shellcheck disable=SC2054 args+=(-drive if=floppy,format=raw,file="${floppy}") fi if [ -n "${iso}" ]; then # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=0,file="${iso}") fi if [ -n "${fixed_iso}" ]; then # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=1,file="${fixed_iso}") fi if [ "${guest_os}" == "macos" ]; then # shellcheck disable=SC2054 args+=(-device ahci,id=ahci -device ide-hd,bus=ahci.0,drive=BootLoader -drive id=BootLoader,if=none,format=qcow2,file="${MAC_BOOTLOADER}") if [ -n "${img}" ]; then # shellcheck disable=SC2054 args+=(-device ide-hd,bus=ahci.1,drive=RecoveryImage -drive id=RecoveryImage,if=none,format=raw,file="${img}",cache=none,aio=native) fi # shellcheck disable=SC2054,SC2206 args+=(-device ${MAC_DISK_DEV},drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",cache=none,aio=native ${STATUS_QUO}) else # shellcheck disable=SC2054,SC2206 args+=(-device virtio-blk-pci,drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",cache=none,aio=native ${STATUS_QUO} -device qemu-xhci,id=spicepass -chardev spicevmc,id=usbredirchardev1,name=usbredir -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 -chardev spicevmc,id=usbredirchardev2,name=usbredir -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 -chardev spicevmc,id=usbredirchardev3,name=usbredir -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 -device usb-ccid -chardev spicevmc,id=ccid,name=smartcard -device ccid-card-passthru,chardev=ccid -device virtio-serial-pci -chardev spiceport,id=webdav0,name=org.spice-space.webdav.0 -device virtserialport,chardev=webdav0,name=org.spice-space.webdav.0) fi # https://wiki.qemu.org/Documentation/9psetup # https://askubuntu.com/questions/772784/9p-libvirt-qemu-share-modes if [ "${guest_os}" != "windows" ]; then # shellcheck disable=SC2054 args+=(-fsdev local,id=fsdev0,path="${PUBLIC}",security_model=mapped-xattr -device virtio-9p-pci,fsdev=fsdev0,mount_tag="${PUBLIC_TAG}") fi if [ -n "${USB_PASSTHROUGH}" ]; then # shellcheck disable=SC2054,SC2206 args+=(-device ${USB_HOST_PASSTHROUGH_CONTROLLER},id=hostpass ${USB_PASSTHROUGH}) fi # The OSK parameter contains parenthesis, they need to be escaped in the shell scripts # The vendor name, Wimpys World, contains a space. It needs to be double-quoted. SHELL_ARGS="${args[*]}" SHELL_ARGS="${SHELL_ARGS//(/\\(}" SHELL_ARGS="${SHELL_ARGS//)/\\)}" SHELL_ARGS="${SHELL_ARGS//Wimpys World/\"Wimpys World\"}" echo "#!/usr/bin/env bash" > "${VMDIR}/${VMNAME}.sh" echo "${QEMU}" "${SHELL_ARGS}" >> "${VMDIR}/${VMNAME}.sh" ${QEMU} "${args[@]}" > "${VMDIR}/${VMNAME}.log" & echo -e " - Process: On guest: killall ${VMNAME}\t(if a forced kill is required)" # If output is 'none' then SPICE was requested. if [ ${OUTPUT} == "none" ]; then spicy --title "${VMNAME}" --port "${SPICE_PORT}" --spice-shared-dir "${PUBLIC}" "${FULLSPICY}" >/dev/null 2>&1 & fi } function shortcut_create { local VMNAME="" VMNAME=$(basename "${VM}" .conf) local LAUNCHER_DIR="" LAUNCHER_DIR="$(dirname "$(realpath "$0")")" local filename="${HOME}/.local/share/applications/${VMNAME}.desktop" cat << EOF > "${filename}" [Desktop Entry] Version=1.0 Type=Application Terminal=true Exec=${LAUNCHER_DIR}/${LAUNCHER} --vm ${VM} Name=${VMNAME} Icon=/usr/share/icons/hicolor/scalable/apps/qemu.svg EOF echo "Created ${VMNAME}.desktop file" } function usage() { echo echo "Usage" echo " ${LAUNCHER} --vm ubuntu.conf" echo echo "You can also pass optional parameters" echo " --delete : Delete the disk image." echo " --display : Select display backend. 'sdl' (default), 'gtk' or 'spice'" echo " --shortcut : Create a desktop shortcut" echo " --snapshot apply : Apply/restore a snapshot." echo " --snapshot create : Create a snapshot." echo " --snapshot delete : Delete a snapshot." echo " --snapshot info : Show disk/snapshot info." echo " --status-quo : Do not commit any changes to disk/snapshot." echo " --fullscreen : Starts VM in full screen mode (Ctl+Alt+f to exit)" echo " --screen : Use specified screen to determine the window size." echo " --version : Print version" exit 1 } # Lowercase variables are used in the VM config file only boot="efi" cpu_cores="" disk_img="" disk_size="32G" fixed_iso="" floppy="" guest_os="linux" img="" iso="" macos_release="" port_forwards=() preallocation="metadata" ram="" usb_devices=() DELETE=0 FULLSCREEN="" FULLSPICY="" OUTPUT="sdl" PUBLIC="" PUBLIC_PERMS="" PUBLIC_TAG="" SCREEN="" SHORTCUT=0 SNAPSHOT_ACTION="" SNAPSHOT_TAG="" STATUS_QUO="" USB_PASSTHROUGH="" VM="" readonly LAUNCHER=$(basename "${0}") readonly DISK_MIN_SIZE=$((197632 * 8)) readonly VERSION="2.1.0" # PUBLICSHARE is the only directory exposed to guest VMs for file # sharing via 9P and spice-webdavd. This path is not configurable. PUBLIC=$(xdg-user-dir PUBLICSHARE) if [ "${PUBLIC}" != ${HOME} ]; then if [ ! -d "${PUBLIC}" ]; then mkdir -p "${PUBLIC}" fi PUBLIC_TAG=$(basename ${PUBLIC})-${USER,,} PUBLIC_PERMS=$(ls -ld ${PUBLIC} | cut -d' ' -f1) else PUBLIC="" fi # TODO: Make this run the native architecture binary QEMU=$(which qemu-system-x86_64) QEMU_IMG=$(which qemu-img) if [ ! -e "${QEMU}" ] && [ ! -e "${QEMU_IMG}" ]; then echo "ERROR! qemu not found. Please install qemu." exit 1 fi QEMU_VER=$(${QEMU} -version | head -n1 | cut -d' ' -f4 | cut -d'(' -f1 | sed 's/\.//g' | cut -c1-2) if [ "${QEMU_VER}" -lt 60 ]; then echo "ERROR! Qemu 6.0.0 or newer is required, detected $(${QEMU} -version | head -n1 | cut -d' ' -f4 | cut -d'(' -f1)." exit 1 fi # Take command line arguments if [ $# -lt 1 ]; then usage exit 0 else while [ $# -gt 0 ]; do case "${1}" in -delete|--delete) DELETE=1 shift;; -display|--display) OUTPUT="${2}" if [ "${OUTPUT}" != "gtk" ] && [ "${OUTPUT}" != "sdl" ] && [ "${OUTPUT}" != "spice" ]; then echo "ERROR! Requested output '${OUTPUT}' is not recognised." exit 1 elif [ "${OUTPUT}" == "spice" ] && [ ! "$(which spicy)" ]; then echo "ERROR! Requested SPICE display, but 'spicy' is not installed." exit 1 fi shift shift;; -snapshot|--snapshot) SNAPSHOT_ACTION="${2}" if [ -z "${SNAPSHOT_ACTION}" ]; then echo "ERROR! No snapshot action provided." exit 1 fi shift SNAPSHOT_TAG="${2}" if [ -z "${SNAPSHOT_TAG}" ] && [ "${SNAPSHOT_ACTION}" != "info" ]; then echo "ERROR! No snapshot tag provided." exit 1 fi shift shift;; -status-quo|--status-quo) STATUS_QUO="-snapshot" shift;; -fullscreen|--fullscreen|-full-screen|--full-screen) FULLSCREEN="-full-screen" FULLSPICY="--full-screen" shift;; -vm|--vm) VM="${2}" shift shift;; -screen|--screen) SCREEN="${2}" shift shift;; -shortcut|--shortcut) SHORTCUT=1 shift;; -version|--version) echo "${VERSION}" exit;; -h|--h|-help|--help) usage;; *) echo "ERROR! \"${1}\" is not a supported parameter." usage;; esac done fi if [ -n "${VM}" ] && [ -e "${VM}" ]; then # shellcheck source=/dev/null source "${VM}" if [ -z "${disk_img}" ]; then echo "ERROR! No disk_img defined." exit 1 fi # Backwards compatibility for ${driver_iso} if [ -n "${driver_iso}" ] && [ -z "${fixed_iso}" ]; then fixed_iso="${driver_iso}" fi # Backwards compatibility for ${disk} (size) if [ -n "${disk}" ]; then disk_size="${disk}" fi else echo "ERROR! Virtual machine configuration not found." usage fi if [ ${DELETE} -eq 1 ]; then disk_delete exit fi if [ -n "${SNAPSHOT_ACTION}" ]; then case ${SNAPSHOT_ACTION} in apply) snapshot_apply "${SNAPSHOT_TAG}" snapshot_info exit;; create) snapshot_create "${SNAPSHOT_TAG}" snapshot_info exit;; delete) snapshot_delete "${SNAPSHOT_TAG}" snapshot_info exit;; info) snapshot_info exit;; *) echo "ERROR! \"${SNAPSHOT_ACTION}\" is not a supported snapshot action." usage;; esac fi if [ ${SHORTCUT} -eq 1 ]; then shortcut_create exit fi vm_boot