1887 lines
72 KiB
Bash
Executable File
1887 lines
72 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
export LC_ALL=C
|
|
|
|
if ((BASH_VERSINFO[0] < 4)); then
|
|
echo "Sorry, you need bash 4.0 or newer to run this script."
|
|
exit 1
|
|
fi
|
|
|
|
function ignore_msrs_always() {
|
|
# Make sure the host has /etc/modprobe.d
|
|
if [ -d /etc/modprobe.d ]; then
|
|
# Skip if ignore_msrs is already enabled, assumes initramfs has been rebuilt
|
|
if grep -lq 'ignore_msrs=Y' /etc/modprobe.d/kvm-quickemu.conf >/dev/null 2>&1; then
|
|
echo "options kvm ignore_msrs=Y" | sudo tee /etc/modprobe.d/kvm-quickemu.conf
|
|
sudo update-initramfs -k all -u
|
|
fi
|
|
else
|
|
echo "ERROR! /etc/modprobe.d was not found, I don't know how to configure this system."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
function ignore_msrs_alert() {
|
|
local ignore_msrs=""
|
|
if [ -e /sys/module/kvm/parameters/ignore_msrs ]; then
|
|
ignore_msrs=$(cat /sys/module/kvm/parameters/ignore_msrs)
|
|
if [ "${ignore_msrs}" == "N" ]; then
|
|
echo " - MSR: WARNING! Ignoring unhandled Model-Specific Registers is disabled."
|
|
echo
|
|
echo " echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs"
|
|
echo
|
|
echo " If you are unable to run macOS or Windows VMs then run the above 👆"
|
|
echo " This will enable ignoring of unhandled MSRs until you reboot the host."
|
|
echo " You can make this change permanent by running: 'quickemu --ignore-msrs-always'"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function delete_shortcut() {
|
|
local SHORTCUT_DIR="${HOME}/.local/share/applications/"
|
|
if [ -e "${SHORTCUT_DIR}/${VMNAME}.desktop" ]; then
|
|
rm "${SHORTCUT_DIR}/${VMNAME}.desktop"
|
|
echo " - Deleted ${SHORTCUT_DIR}/${VMNAME}.desktop"
|
|
fi
|
|
}
|
|
|
|
function delete_disk() {
|
|
echo "Deleting ${VMNAME} virtual hard disk"
|
|
if [ -e "${disk_img}" ]; then
|
|
rm "${disk_img}" >/dev/null 2>&1
|
|
# Remove any EFI vars, but not for macOS
|
|
rm "${VMDIR}"/OVMF_VARS*.fd >/dev/null 2>&1
|
|
rm "${VMPATH}/${VMDIR}"/OVMF_VARS*.fd >/dev/null 2>&1
|
|
rm "${VMDIR}/${VMNAME}-vars.fd" >/dev/null 2>&1
|
|
rm "${VMPATH}/${VMDIR}/${VMNAME}-vars.fd" >/dev/null 2>&1
|
|
echo " - Deleted ${disk_img}"
|
|
delete_shortcut
|
|
else
|
|
echo " - ${disk_img} not found. Doing nothing."
|
|
fi
|
|
}
|
|
|
|
function delete_vm() {
|
|
echo "Deleting ${VMNAME} completely"
|
|
if [ -d "${VMDIR}" ]; then
|
|
rm -rf "${VMDIR}"
|
|
rm "${VM}"
|
|
echo " - Deleted ${VM} and ${VMDIR}/"
|
|
delete_shortcut
|
|
else
|
|
echo " - ${VMDIR} not found. Doing nothing."
|
|
fi
|
|
}
|
|
|
|
function kill_vm() {
|
|
echo "Killing ${VMNAME}"
|
|
if [ -z "${VM_PID}" ]; then
|
|
echo " - ${VMNAME} is not running."
|
|
rm -f "${VMDIR}/${VMNAME}.pid"
|
|
elif [ -n "${VM_PID}" ]; then
|
|
if kill -9 "${VM_PID}" > /dev/null 2>&1; then
|
|
echo " - ${VMNAME} (${VM_PID}) killed."
|
|
rm -f "${VMDIR}/${VMNAME}.pid"
|
|
else
|
|
echo " - ${VMNAME} (${VM_PID}) was not killed."
|
|
fi
|
|
elif [ ! -r "${VMDIR}/${VMNAME}.pid" ]; then
|
|
echo " - ${VMNAME} has no ${VMDIR}/${VMNAME}.pid"
|
|
fi
|
|
}
|
|
|
|
function snapshot_apply() {
|
|
echo "Snapshot apply to ${disk_img}"
|
|
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 " - 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() {
|
|
echo "Snapshotting ${disk_img}"
|
|
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 " - Created snapshot '${TAG}' for ${disk_img}"
|
|
else
|
|
echo " - ERROR! Failed to create snapshot '${TAG}' for ${disk_img}"
|
|
fi
|
|
else
|
|
echo " - NOTE! ${disk_img} not found. Doing nothing."
|
|
fi
|
|
}
|
|
|
|
function snapshot_delete() {
|
|
echo "Snapshot removal ${disk_img}"
|
|
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 " - Deleted snapshot '${TAG}' from ${disk_img}"
|
|
else
|
|
echo " - ERROR! Failed to delete snapshot '${TAG}' from ${disk_img}"
|
|
fi
|
|
else
|
|
echo " - NOTE! ${disk_img} not found. Doing nothing."
|
|
fi
|
|
}
|
|
|
|
function snapshot_info() {
|
|
echo
|
|
if [ -e "${disk_img}" ]; then
|
|
${QEMU_IMG} info "${disk_img}"
|
|
fi
|
|
}
|
|
|
|
function get_port() {
|
|
local PORT_START=$1
|
|
local PORT_RANGE=$((PORT_START+$2))
|
|
local PORT
|
|
for ((PORT = PORT_START; PORT <= PORT_RANGE; PORT++)); do
|
|
# Make sure port scans do not block too long.
|
|
timeout 0.1s bash -c "echo >/dev/tcp/127.0.0.1/${PORT}" >/dev/null 2>&1
|
|
if [ ${?} -eq 1 ]; then
|
|
echo "${PORT}"
|
|
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 [ -z "${USB_NAME}" ]; then
|
|
echo " ! USB device ${VENDOR_ID}:${PRODUCT_ID} not found. Check your configuration"
|
|
continue
|
|
elif [ -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 efi_vars() {
|
|
local VARS_IN=""
|
|
local VARS_OUT=""
|
|
VARS_IN="${1}"
|
|
VARS_OUT="${2}"
|
|
|
|
if [ ! -e "${VARS_OUT}" ]; then
|
|
if [ -e "${VARS_IN}" ]; then
|
|
cp "${VARS_IN}" "${VARS_OUT}"
|
|
else
|
|
echo "ERROR! ${VARS_IN} was not found. Please install edk2."
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function vm_boot() {
|
|
local AUDIO_DEV=""
|
|
local BALLOON="-device virtio-balloon"
|
|
local BOOT_STATUS=""
|
|
local CPU=""
|
|
local DISK_USED=""
|
|
local DISPLAY_DEVICE=""
|
|
local DISPLAY_RENDER=""
|
|
local EFI_CODE="${EFI_CODE}"
|
|
local EFI_VARS=""
|
|
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 GUEST_TWEAKS=""
|
|
local KERNEL_NAME="Unknown"
|
|
local KERNEL_NODE=""
|
|
local KERNEL_VER="?"
|
|
local LSB_DESCRIPTION="Unknown OS"
|
|
local MACHINE_TYPE="${MACHINE_TYPE:-q35}"
|
|
local MAC_BOOTLOADER=""
|
|
local MAC_MISSING=""
|
|
local MAC_DISK_DEV="${MAC_DISK_DEV:-ide-hd,bus=ahci.2}"
|
|
local NET_DEVICE="${NET_DEVICE:-virtio-net}"
|
|
local SOUND=""
|
|
local SMM="${SMM:-off}"
|
|
local TEMP_PORT=""
|
|
local USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci"
|
|
local VGA=""
|
|
local VIDEO=""
|
|
|
|
KERNEL_NAME=$(uname --kernel-name)
|
|
KERNEL_NODE="($(uname --nodename))"
|
|
KERNEL_VER=$(uname --kernel-release | cut -d'.' -f1-2)
|
|
|
|
if [ -e /etc/os-release ]; then
|
|
LSB_DESCRIPTION=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
|
|
fi
|
|
|
|
echo "Quickemu ${VERSION} using ${QEMU} v${QEMU_VER_LONG}"
|
|
echo " - Host: ${LSB_DESCRIPTION} running ${KERNEL_NAME} ${KERNEL_VER} ${KERNEL_NODE}"
|
|
|
|
HOST_CPU_CORES=$(nproc)
|
|
HOST_CPU_MODEL=$(lscpu | grep '^Model name:' | cut -d':' -f2 | sed -e 's/^[[:space:]]*//')
|
|
HOST_CPU_SOCKETS=$(lscpu | grep -E 'Socket' | cut -d':' -f2 | sed 's/ //g')
|
|
HOST_CPU_VENDOR=$(lscpu | grep -E 'Vendor' | cut -d':' -f2 | sed 's/ //g')
|
|
|
|
# A CPU with Intel VT-x / AMD SVM support is required
|
|
if [ "${HOST_CPU_VENDOR}" == "GenuineIntel" ]; 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
|
|
|
|
if [ "${guest_os}" == "macos" ] && [ "${GUEST_CPU_CORES}" -gt 10 ] || [ "${GUEST_CPU_CORES}" -eq 6 ] || [ "${GUEST_CPU_CORES}" -eq 7 ]; then
|
|
# macOS guests cannot boot with most core counts not powers of 2. This will fix the issue by rounding the core count down to a power of 2. Uses wc and factor from coreutils.
|
|
factorCPUCores=$(factor "${GUEST_CPU_CORES}")
|
|
GUEST_CPU_CORES=$(( 2 ** $(echo "${factorCPUCores#*:}" | grep -o '[0-9]' | wc -l) ))
|
|
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 " - CPU: ${HOST_CPU_MODEL}"
|
|
echo -n " - CPU VM: ${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=""
|
|
# Determine the number of gigabytes of RAM in the host by extracting the first numerical value from the output.
|
|
RAM_HOST=$(free --giga | tr ' ' '\n' | grep -m 1 [0-9])
|
|
if [ "${RAM_HOST}" -ge 128 ]; then
|
|
RAM_VM="32G"
|
|
elif [ "${RAM_HOST}" -ge 64 ]; then
|
|
RAM_VM="16G"
|
|
elif [ "${RAM_HOST}" -ge 16 ]; then
|
|
RAM_VM="8G"
|
|
elif [ "${RAM_HOST}" -ge 8 ]; then
|
|
RAM_VM="4G"
|
|
fi
|
|
else
|
|
RAM_VM="${ram}"
|
|
fi
|
|
echo ", ${RAM_VM} RAM"
|
|
|
|
if [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ]; then
|
|
if [ "${RAM_VM//G/}" -lt 4 ]; then
|
|
echo "ERROR! The guest virtual machine has been allocated insufficient RAM to run Windows."
|
|
echo " You can override the guest RAM allocation by adding 'ram=4G' to ${VM}"
|
|
exit 1
|
|
fi
|
|
elif [ "${guest_os}" == "macos" ]; then
|
|
if [ "${RAM_VM//G/}" -lt 8 ]; then
|
|
echo "ERROR! The guest virtual machine has been allocated insufficient RAM to run macOS."
|
|
echo " You can override the guest RAM allocation by adding 'ram=8G' to ${VM}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Force to lowercase.
|
|
boot=${boot,,}
|
|
guest_os=${guest_os,,}
|
|
|
|
if [ "${guest_os}" == "macos" ] || [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ]; then
|
|
# Display MSRs alert if the guest is macOS or windows
|
|
ignore_msrs_alert
|
|
fi
|
|
|
|
# Always Boot macOS using EFI
|
|
if [ "${guest_os}" == "macos" ]; then
|
|
boot="efi"
|
|
if [ -e "${VMDIR}/OVMF_CODE.fd" ] && [ -e "${VMDIR}/OVMF_VARS-1024x768.fd" ]; then
|
|
EFI_CODE="${VMDIR}/OVMF_CODE.fd"
|
|
EFI_VARS="${VMDIR}/OVMF_VARS-1024x768.fd"
|
|
elif [ -e "${VMDIR}/OVMF_CODE.fd" ] && [ -e "${VMDIR}/OVMF_VARS-1920x1080.fd" ]; then
|
|
EFI_CODE="${VMDIR}/OVMF_CODE.fd"
|
|
EFI_VARS="${VMDIR}/OVMF_VARS-1920x1080.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
|
|
BOOT_STATUS="EFI (macOS), OVMF ($(basename "${EFI_CODE}")), SecureBoot (${secureboot})."
|
|
elif [[ "${boot}" == *"efi"* ]]; then
|
|
EFI_VARS="${VMDIR}/OVMF_VARS.fd"
|
|
|
|
# Preserve backward compatibility
|
|
if [ -e "${VMDIR}/${VMNAME}-vars.fd" ]; then
|
|
mv "${VMDIR}/${VMNAME}-vars.fd" "${EFI_VARS}"
|
|
elif [ -e "${VMDIR}/OVMF_VARS_4M.fd" ]; then
|
|
mv "${VMDIR}/OVMF_VARS_4M.fd" "${EFI_VARS}"
|
|
fi
|
|
|
|
# OVMF_CODE_4M.fd is for booting guests in non-Secure Boot mode.
|
|
# While this image technically supports Secure Boot, it does so
|
|
# without requiring SMM support from QEMU
|
|
|
|
# OVMF_CODE.secboot.fd is like OVMF_CODE_4M.fd, but will abort if QEMU
|
|
# does not support SMM.
|
|
|
|
# https://bugzilla.redhat.com/show_bug.cgi?id=1929357#c5
|
|
if [ -n "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then
|
|
case ${secureboot} in
|
|
on) # shellcheck disable=SC2054,SC2140
|
|
ovmfs=("/usr/share/OVMF/OVMF_CODE_4M.secboot.fd","/usr/share/OVMF/OVMF_VARS_4M.fd" \
|
|
"/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd","/usr/share/edk2/ovmf/OVMF_VARS.fd" \
|
|
"/usr/share/OVMF/x64/OVMF_CODE.secboot.fd","/usr/share/OVMF/x64/OVMF_VARS.fd" \
|
|
"/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd","/usr/share/edk2-ovmf/OVMF_VARS.fd" \
|
|
"/usr/share/qemu/ovmf-x86_64-smm-ms-code.bin","/usr/share/qemu/ovmf-x86_64-smm-ms-vars.bin" \
|
|
"/usr/share/qemu/edk2-x86_64-secure-code.fd","/usr/share/qemu/edk2-x86_64-code.fd" \
|
|
"/usr/share/edk2-ovmf/x64/OVMF_CODE.secboot.fd","/usr/share/edk2-ovmf/x64/OVMF_VARS.fd"
|
|
);;
|
|
*) # shellcheck disable=SC2054,SC2140
|
|
ovmfs=("/usr/share/OVMF/OVMF_CODE_4M.fd","/usr/share/OVMF/OVMF_VARS_4M.fd" \
|
|
"/usr/share/edk2/ovmf/OVMF_CODE.fd","/usr/share/edk2/ovmf/OVMF_VARS.fd" \
|
|
"/usr/share/OVMF/OVMF_CODE.fd","/usr/share/OVMF/OVMF_VARS.fd" \
|
|
"/usr/share/OVMF/x64/OVMF_CODE.fd","/usr/share/OVMF/x64/OVMF_VARS.fd" \
|
|
"/usr/share/edk2-ovmf/OVMF_CODE.fd","/usr/share/edk2-ovmf/OVMF_VARS.fd" \
|
|
"/usr/share/qemu/ovmf-x86_64-4m-code.bin","/usr/share/qemu/ovmf-x86_64-4m-vars.bin" \
|
|
"/usr/share/qemu/edk2-x86_64-code.fd","/usr/share/qemu/edk2-x86_64-code.fd" \
|
|
"/usr/share/edk2-ovmf/x64/OVMF_CODE.fd","/usr/share/edk2-ovmf/x64/OVMF_VARS.fd"
|
|
);;
|
|
esac
|
|
# Attempt each EFI_CODE file one by one, selecting the corresponding code and vars
|
|
# when an existing file is found.
|
|
_IFS=$IFS
|
|
IFS=","
|
|
for f in "${ovmfs[@]}"; do
|
|
# shellcheck disable=SC2086
|
|
set -- ${f};
|
|
if [ -e "${1}" ]; then
|
|
EFI_CODE="${1}"
|
|
EFI_EXTRA_VARS="${2}"
|
|
fi
|
|
done
|
|
IFS=$_IFS
|
|
fi
|
|
if [ -z "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then
|
|
if [ "$secureboot" == "on" ]; then
|
|
echo "ERROR! SecureBoot was requested but no SecureBoot capable firmware was found."
|
|
else
|
|
echo "ERROR! EFI boot requested but no EFI firmware found."
|
|
fi
|
|
echo " Please install OVMF firmware."
|
|
exit 1
|
|
fi
|
|
if [ -n "${EFI_EXTRA_VARS}" ]; then
|
|
if [ ! -e "${EFI_EXTRA_VARS}" ]; then
|
|
echo " - EFI: ERROR! EFI_EXTRA_VARS file ${EFI_EXTRA_VARS} does not exist."
|
|
exit 1
|
|
fi
|
|
efi_vars "${EFI_EXTRA_VARS}" "${EFI_VARS}"
|
|
fi
|
|
|
|
# Make sure EFI_VARS references an actual, writeable, file
|
|
if [ ! -f "${EFI_VARS}" ] || [ ! -w "${EFI_VARS}" ]; then
|
|
echo " - EFI: ERROR! ${EFI_VARS} is not a regular file or not writeable."
|
|
echo " Deleting ${EFI_VARS}. Please re-run quickemu."
|
|
rm -f "${EFI_VARS}"
|
|
exit 1
|
|
fi
|
|
|
|
# If EFI_CODE references a symlink, resolve it to the real file.
|
|
if [ -L "${EFI_CODE}" ]; then
|
|
echo " - EFI: WARNING! ${EFI_CODE} is a symlink."
|
|
echo -n " Resolving to... "
|
|
EFI_CODE=$(realpath "${EFI_CODE}")
|
|
echo "${EFI_CODE}"
|
|
fi
|
|
BOOT_STATUS="EFI (${guest_os^}), OVMF (${EFI_CODE}), SecureBoot (${secureboot})."
|
|
else
|
|
BOOT_STATUS="Legacy BIOS (${guest_os^})"
|
|
boot="legacy"
|
|
secureboot="off"
|
|
fi
|
|
|
|
echo " - BOOT: ${BOOT_STATUS}"
|
|
|
|
# Make any OS specific adjustments
|
|
case ${guest_os} in
|
|
batocera|*bsd|freedos|haiku|linux*|*solaris)
|
|
CPU="-cpu host,kvm=on"
|
|
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then
|
|
CPU="${CPU},topoext"
|
|
fi
|
|
|
|
if [ "${guest_os}" == "freebsd" ] || [ "${guest_os}" == "ghostbsd" ]; then
|
|
mouse="usb"
|
|
elif [ "${guest_os}" == "batocera" ] || [ "${guest_os}" == "freedos" ] || [ "${guest_os}" == "haiku" ]; then
|
|
MACHINE_TYPE="pc"
|
|
NET_DEVICE="rtl8139"
|
|
fi
|
|
|
|
if [ "${guest_os}" == "freedos" ] ; then
|
|
# fix for #382
|
|
SMM="on"
|
|
sound_card="sb16"
|
|
fi
|
|
|
|
if [[ "${guest_os}" == *"solaris" ]]; then
|
|
MACHINE_TYPE="pc"
|
|
usb_controller="xhci"
|
|
sound_card="ac97"
|
|
fi
|
|
;;
|
|
kolibrios|reactos)
|
|
CPU="-cpu qemu32,kvm=on"
|
|
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then
|
|
CPU="${CPU},topoext"
|
|
fi
|
|
MACHINE_TYPE="pc"
|
|
case ${guest_os} in
|
|
kolibrios) NET_DEVICE="rtl8139";;
|
|
reactos) NET_DEVICE="e1000"
|
|
keyboard="ps2";;
|
|
esac
|
|
;;
|
|
macos)
|
|
# quickget current list: mojave catalina big-sur monterey ventura sonoma
|
|
# A CPU with fma is required for Metal support
|
|
# A CPU with invtsc is required for macOS to boot
|
|
# A CPU with SSE4.1 support is required for >= macOS Sierra
|
|
# A CPU with SSE4.2 support is required for >= macOS Catalina
|
|
# A CPU with AVX2 support is required for >= macOS Ventura
|
|
if ! check_cpu_flag fma && ! check_cpu_flag invtsc; then
|
|
echo "ERROR! macOS requires a CPU with FMA and INV TSC support."
|
|
exit 1
|
|
fi
|
|
|
|
# TODO: Investigate if hosts with an Intel CPU can just use `-cpu host`
|
|
case ${macos_release} in
|
|
ventura|sonoma)
|
|
if check_cpu_flag sse4_2 && check_cpu_flag avx2; then
|
|
CPU="-cpu Haswell-v4,kvm=on,vendor=GenuineIntel,+avx,+avx2,+sse,+sse2,+sse3,+sse4.2,vmware-cpuid-freq=on"
|
|
else
|
|
echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 and AVX2 support."
|
|
exit 1
|
|
fi;;
|
|
catalina|big-sur|monterey)
|
|
if check_cpu_flag sse4_2; then
|
|
CPU="-cpu Haswell-v4,kvm=on,vendor=GenuineIntel,+avx,+sse,+sse2,+sse3,+sse4.2,vmware-cpuid-freq=on"
|
|
else
|
|
echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 support."
|
|
exit 1
|
|
fi;;
|
|
*)
|
|
if check_cpu_flag sse4_1; then
|
|
CPU="-cpu Penryn,kvm=on,vendor=GenuineIntel,+avx,+sse,+sse2,+sse3,+sse4.1,vmware-cpuid-freq=on"
|
|
else
|
|
echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.1 support."
|
|
exit 1
|
|
fi;;
|
|
esac
|
|
|
|
for FLAG in abm adx aes amd-ssbd bmi1 bmi2 cx8 eist ept_1gb f16c fma invtsc \
|
|
mmx movbe mpx pdpe1gb popcnt smep vaes vbmi2 vpclmulqdq \
|
|
xgetbv1 xsave xsaveopt; do
|
|
if check_cpu_flag "${FLAG}"; then
|
|
CPU+=",+${FLAG}"
|
|
fi
|
|
done
|
|
|
|
# Disable S3 support in the VM to prevent macOS suspending during install
|
|
GUEST_TWEAKS="-global kvm-pit.lost_tick_policy=discard -global ICH9-LPC.disable_s3=1 -device isa-applesmc,osk=$(echo "bheuneqjbexolgurfrjbeqfthneqrqcyrnfrqbagfgrny(p)NccyrPbzchgreVap" | tr 'A-Za-z' 'N-ZA-Mn-za-m')"
|
|
|
|
# Disable High Precision Timer
|
|
if [ "${QEMU_VER_SHORT}" -ge 70 ]; then
|
|
MACHINE_TYPE+=",hpet=off"
|
|
else
|
|
GUEST_TWEAKS+=" -no-hpet"
|
|
fi
|
|
|
|
# Tune Qemu optimisations based on the macOS release, or fallback to lowest
|
|
# common supported options if none is specified.
|
|
# * VirtIO Block Media doesn't work in High Sierra (at all) or the Mojave (Recovery Image)
|
|
# * VirtIO Network is supported since Big Sur
|
|
# * VirtIO Memory Balloning is supported since Big Sur (https://pmhahn.github.io/virtio-balloon/)
|
|
# * VirtIO RNG is supported since Big Sur, but exposed to all guests by default.
|
|
case ${macos_release} in
|
|
big-sur|monterey|ventura|sonoma)
|
|
BALLOON="-device virtio-balloon"
|
|
MAC_DISK_DEV="virtio-blk-pci"
|
|
NET_DEVICE="virtio-net"
|
|
USB_HOST_PASSTHROUGH_CONTROLLER="nec-usb-xhci"
|
|
GUEST_TWEAKS="${GUEST_TWEAKS} -global nec-usb-xhci.msi=off"
|
|
sound_card="${sound_card:-usb-audio}"
|
|
usb_controller="xhci";;
|
|
*)
|
|
# Backwards compatibility if no macos_release is specified.
|
|
# Also safe catch all for High Sierra and Mojave
|
|
BALLOON=""
|
|
if [ "${macos_release}" == "catalina" ]; then
|
|
MAC_DISK_DEV="virtio-blk-pci"
|
|
else
|
|
MAC_DISK_DEV="ide-hd,bus=ahci.2"
|
|
fi
|
|
NET_DEVICE="vmxnet3"
|
|
USB_HOST_PASSTHROUGH_CONTROLLER="usb-ehci";;
|
|
esac
|
|
;;
|
|
windows|windows-server)
|
|
if [ "${QEMU_VER_SHORT}" -gt 60 ]; then
|
|
CPU="-cpu host,kvm=on,+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_passthrough"
|
|
else
|
|
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"
|
|
fi
|
|
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then
|
|
CPU="${CPU},topoext"
|
|
fi
|
|
# Disable S3 support in the VM to ensure Windows can boot with SecureBoot enabled
|
|
# - https://wiki.archlinux.org/title/QEMU#VM_does_not_boot_when_using_a_Secure_Boot_enabled_OVMF
|
|
GUEST_TWEAKS="-global kvm-pit.lost_tick_policy=discard -global ICH9-LPC.disable_s3=1"
|
|
|
|
# Disable High Precision Timer
|
|
if [ "${QEMU_VER_SHORT}" -ge 70 ]; then
|
|
MACHINE_TYPE+=",hpet=off"
|
|
else
|
|
GUEST_TWEAKS+=" -no-hpet"
|
|
fi
|
|
SMM="on"
|
|
;;
|
|
*) 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 "${disk_format}" -o lazy_refcounts=on,preallocation="${preallocation}" "${disk_img}" "${disk_size}"; then
|
|
echo "ERROR! Failed to create ${disk_img} using ${disk_format} format."
|
|
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}"
|
|
DISK_USED="no"
|
|
elif [ -e "${disk_img}" ]; then
|
|
# If the VM is not running, check for disk related issues.
|
|
if [ -z "${VM_PID}" ]; 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
|
|
fi
|
|
else
|
|
if ! ${QEMU_IMG} check -q "${disk_img}"; then
|
|
echo " Disk integrity check failed. Please run qemu-img check --help."
|
|
echo
|
|
"${QEMU_IMG}" check "${disk_img}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Only check disk image size if preallocation is off
|
|
if [ "${preallocation}" == "off" ]; then
|
|
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
|
|
DISK_USED="yes"
|
|
fi
|
|
else
|
|
DISK_USED="yes"
|
|
fi
|
|
fi
|
|
|
|
if [ "${DISK_USED}" == "yes" ] && [ "${guest_os}" != "kolibrios" ]; then
|
|
# If there is a disk image that appears to be used do not boot from installation media.
|
|
iso=""
|
|
img=""
|
|
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
|
|
|
|
# Setup the appropriate audio device based on the display output
|
|
# https://www.kraxel.org/blog/2020/01/qemu-sound-audiodev/
|
|
case ${display} in
|
|
none|spice|spice-app) AUDIO_DEV="spice,id=audio0";;
|
|
*) AUDIO_DEV="pa,id=audio0";;
|
|
esac
|
|
|
|
# Determine a sane resolution for Linux guests.
|
|
local X_RES="1280"
|
|
local Y_RES="800"
|
|
if [ -n "${width}" ] && [ -n "${height}" ]; then
|
|
local X_RES="${width}"
|
|
local Y_RES="${height}"
|
|
fi
|
|
|
|
# https://www.kraxel.org/blog/2019/09/display-devices-in-qemu/
|
|
case ${guest_os} in
|
|
*bsd) DISPLAY_DEVICE="VGA";;
|
|
linux_old|solaris) DISPLAY_DEVICE="vmware-svga";;
|
|
linux)
|
|
case ${display} in
|
|
none|spice|spice-app) DISPLAY_DEVICE="virtio-gpu";;
|
|
*) DISPLAY_DEVICE="virtio-vga";;
|
|
esac;;
|
|
macos)
|
|
# qxl-vga and VGA supports seamless mouse and sane resolutions if only
|
|
# one scanout is used. '-vga none' is added to the QEMU command line
|
|
# to avoid having two scanouts.
|
|
DISPLAY_DEVICE="VGA";;
|
|
windows|windows-server)
|
|
# virtio-gpu "works" with gtk but is limited to 1024x1024 and exhibits other issues
|
|
# https://kevinlocke.name/bits/2021/12/10/windows-11-guest-virtio-libvirt/#video
|
|
case ${display} in
|
|
gtk|none|spice) DISPLAY_DEVICE="qxl-vga";;
|
|
sdl|spice-app) DISPLAY_DEVICE="virtio-vga";;
|
|
esac;;
|
|
*) DISPLAY_DEVICE="qxl-vga";;
|
|
esac
|
|
|
|
# Map Quickemu $display to QEMU -display
|
|
case ${display} in
|
|
gtk) DISPLAY_RENDER="${display},grab-on-hover=on,zoom-to-fit=off,gl=${gl}";;
|
|
none|spice) DISPLAY_RENDER="none";;
|
|
sdl) DISPLAY_RENDER="${display},gl=${gl}";;
|
|
spice-app) DISPLAY_RENDER="${display},gl=${gl}";;
|
|
*) DISPLAY_RENDER="${display}";;
|
|
esac
|
|
|
|
# https://www.kraxel.org/blog/2021/05/virtio-gpu-qemu-graphics-update/
|
|
if [ "${gl}" == "on" ] && [ "${DISPLAY_DEVICE}" == "virtio-vga" ]; then
|
|
if [ "${QEMU_VER_SHORT}" -ge 61 ]; then
|
|
DISPLAY_DEVICE="${DISPLAY_DEVICE}-gl"
|
|
else
|
|
DISPLAY_DEVICE="${DISPLAY_DEVICE},virgl=on"
|
|
fi
|
|
echo -n " - Display: ${display^^}, ${DISPLAY_DEVICE}, GL (${gl}), VirGL (on)"
|
|
else
|
|
echo -n " - Display: ${display^^}, ${DISPLAY_DEVICE}, GL (${gl}), VirGL (off)"
|
|
fi
|
|
|
|
# Build the video configuration
|
|
VIDEO="-device ${DISPLAY_DEVICE}"
|
|
|
|
# Try and coerce the display resolution for Linux guests only.
|
|
if [ "${DISPLAY_DEVICE}" != "vmware-svga" ]; then
|
|
VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}"
|
|
echo " @ (${X_RES} x ${Y_RES})"
|
|
else
|
|
echo " "
|
|
fi
|
|
|
|
# Allocate VRAM to VGA devices
|
|
case ${DISPLAY_DEVICE} in
|
|
bochs-display) VIDEO="${VIDEO},vgamem=67108864";;
|
|
qxl|qxl-vga) VIDEO="${VIDEO},ram_size=65536,vram_size=65536,vgamem_mb=64";;
|
|
ati-vga|cirrus-vga|VGA|vmware-svga) VIDEO="${VIDEO},vgamem_mb=256";;
|
|
esac
|
|
|
|
# Configure multiscreen if max_outputs was provided in the .conf file
|
|
if [ -v max_outputs ]; then
|
|
VIDEO="${VIDEO},max_outputs=${max_outputs}"
|
|
fi
|
|
|
|
# Run QEMU with '-vga none' to avoid having two scanouts, one for VGA and
|
|
# another for virtio-vga-gl. This works around a GTK assertion failure and
|
|
# allows seamless mouse in macOS when using the qxl-vga device.
|
|
# https://www.collabora.com/news-and-blog/blog/2021/11/26/venus-on-qemu-enabling-new-virtual-vulkan-driver/
|
|
# https://github.com/quickemu-project/quickemu/issues/222
|
|
VGA="-vga none"
|
|
|
|
# Add fullscreen options
|
|
VIDEO="${VGA} ${VIDEO} ${FULLSCREEN}"
|
|
|
|
# Build the sound hardware configuration
|
|
case ${sound_card} in
|
|
ich9-intel-hda|intel-hda) SOUND="-device ${sound_card} -device ${sound_duplex},audiodev=audio0";;
|
|
usb-audio) SOUND="-device ${sound_card},audiodev=audio0";;
|
|
ac97|es1370|sb16) SOUND="-device ${sound_card},audiodev=audio0";;
|
|
none) SOUND="";;
|
|
esac
|
|
|
|
echo " - Sound: ${sound_card} (${sound_duplex})"
|
|
|
|
# Set the hostname of the VM
|
|
local NET="user,hostname=${VMNAME}"
|
|
|
|
echo -n "" > "${VMDIR}/${VMNAME}.ports"
|
|
|
|
if [ -z "${ssh_port}" ]; then
|
|
# Find a free port to expose ssh to the guest
|
|
ssh_port=$(get_port 22220 9)
|
|
fi
|
|
|
|
if [ -n "${ssh_port}" ]; then
|
|
echo "ssh,${ssh_port}" >> "${VMDIR}/${VMNAME}.ports"
|
|
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}"
|
|
NET="${NET},hostfwd=udp::${HOST_PORT}-:${GUEST_PORT}"
|
|
done
|
|
fi
|
|
|
|
if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then
|
|
local SPICE="disable-ticketing=on"
|
|
# gl=on can be use with 'spice' too, but only over local connections (not tcp ports)
|
|
if [ "${display}" == "spice-app" ]; then
|
|
SPICE+=",gl=${gl}"
|
|
fi
|
|
|
|
# TODO: Don't use ports so local-only connections can be used with gl=on
|
|
if [ -z "${spice_port}" ]; then
|
|
# Find a free port for spice
|
|
spice_port=$(get_port 5930 9)
|
|
fi
|
|
|
|
# ALLOW REMOTE ACCESS TO SPICE OVER LAN RATHER THAN JUST LOCALHOST
|
|
if [ -z "${ACCESS}" ]; then
|
|
SPICE_ADDR="127.0.0.1"
|
|
else
|
|
if [ "${ACCESS}" == "remote" ]; then
|
|
SPICE_ADDR=""
|
|
elif [ "${ACCESS}" == "local" ]; then
|
|
SPICE_ADDR="127.0.0.1"
|
|
else
|
|
SPICE_ADDR="${ACCESS}"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${spice_port}" ]; then
|
|
echo " - SPICE: All SPICE ports have been exhausted."
|
|
if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then
|
|
echo " ERROR! Requested SPICE display, but no SPICE ports are free."
|
|
exit 1
|
|
fi
|
|
else
|
|
if [ "${display}" == "spice-app" ]; then
|
|
echo " - SPICE: Enabled"
|
|
else
|
|
echo "spice,${spice_port}" >> "${VMDIR}/${VMNAME}.ports"
|
|
echo -n " - SPICE: On host: spicy --title \"${VMNAME}\" --port ${spice_port}"
|
|
if [ "${guest_os}" != "macos" ] && [ -n "${PUBLIC}" ]; then
|
|
echo -n " --spice-shared-dir ${PUBLIC}"
|
|
fi
|
|
echo "${FULLSCREEN}"
|
|
SPICE="${SPICE},port=${spice_port},addr=${SPICE_ADDR}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ -n "${PUBLIC}" ]; then
|
|
case ${guest_os} in
|
|
macos)
|
|
if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then
|
|
# Reference: https://gitlab.gnome.org/GNOME/phodav/-/issues/5
|
|
echo " - WebDAV: On guest: build spice-webdavd (https://gitlab.gnome.org/GNOME/phodav/-/merge_requests/24)"
|
|
echo " - WebDAV: On guest: Finder -> Connect to Server -> http://localhost:9843/"
|
|
fi;;
|
|
*)
|
|
echo " - WebDAV: On guest: dav://localhost:9843/";;
|
|
esac
|
|
fi
|
|
|
|
if [ "${guest_os}" != "windows" ] || [ "${guest_os}" == "windows-server" ] && [ -n "${PUBLIC}" ]; 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} ~/$(basename "${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 [ "${PUBLIC_PERMS}" != "drwxrwxrwx" ]; then
|
|
echo " - 9P: On host: chmod 777 ${PUBLIC}"
|
|
echo " Required for macOS integration 👆"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# If smbd is available and ~/Public is present export it to the guest via samba
|
|
if [[ -x "$(command -v smbd)" && -n ${PUBLIC} ]]; then
|
|
NET="${NET},smb=${PUBLIC}"
|
|
echo " - smbd: On guest: smb://10.0.2.4/qemu"
|
|
fi
|
|
|
|
enable_usb_passthrough
|
|
|
|
echo "#!/usr/bin/env bash" > "${VMDIR}/${VMNAME}.sh"
|
|
|
|
# Start TPM
|
|
if [ "${tpm}" == "on" ]; then
|
|
local tpm_args=()
|
|
# shellcheck disable=SC2054
|
|
tpm_args+=(socket
|
|
--ctrl type=unixio,path="${VMDIR}/${VMNAME}.swtpm-sock"
|
|
--terminate
|
|
--tpmstate dir="${VMDIR}"
|
|
--tpm2)
|
|
echo "${SWTPM} ${tpm_args[*]} &" >> "${VMDIR}/${VMNAME}.sh"
|
|
${SWTPM} "${tpm_args[@]}" >> "${VMDIR}/${VMNAME}.log" &
|
|
echo " - TPM: ${VMDIR}/${VMNAME}.swtpm-sock (${!})"
|
|
sleep 0.25
|
|
fi
|
|
|
|
# Boot the VM
|
|
local args=()
|
|
|
|
# shellcheck disable=SC2054,SC2206,SC2140
|
|
args+=(-name ${VMNAME},process=${VMNAME} -pidfile "${VMDIR}/${VMNAME}.pid"
|
|
-enable-kvm -machine ${MACHINE_TYPE},smm=${SMM},vmport=off ${GUEST_TWEAKS}
|
|
${CPU} ${SMP}
|
|
-m ${RAM_VM} ${BALLOON}
|
|
${VIDEO} -display ${DISPLAY_RENDER}
|
|
-rtc base=localtime,clock=host,driftfix=slew)
|
|
|
|
# Only enable SPICE is using SPICE display
|
|
if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-spice "${SPICE}"
|
|
-device virtio-serial-pci
|
|
-chardev socket,id=agent0,path="${VMDIR}/${VMNAME}-agent.sock",server=on,wait=off
|
|
-device virtserialport,chardev=agent0,name=org.qemu.guest_agent.0
|
|
-chardev spicevmc,id=vdagent0,name=vdagent
|
|
-device virtserialport,chardev=vdagent0,name=com.redhat.spice.0
|
|
-chardev spiceport,id=webdav0,name=org.spice-space.webdav.0
|
|
-device virtserialport,chardev=webdav0,name=org.spice-space.webdav.0)
|
|
fi
|
|
|
|
# shellcheck disable=SC2054
|
|
args+=(-device virtio-rng-pci,rng=rng0
|
|
-object rng-random,id=rng0,filename=/dev/urandom
|
|
-device "${USB_HOST_PASSTHROUGH_CONTROLLER}",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 pci-ohci,id=smartpass
|
|
-device usb-ccid
|
|
)
|
|
|
|
if "${QEMU}" -chardev spicevmc,id=ccid,name= 2>&1 | grep -q smartcard; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-chardev spicevmc,id=ccid,name=smartcard
|
|
-device ccid-card-passthru,chardev=ccid)
|
|
else
|
|
echo "WARNING! ${QEMU} was not compiled with support for smartcard devices"
|
|
fi
|
|
|
|
# setup usb-controller
|
|
if [ "${usb_controller}" == "ehci" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device usb-ehci,id=input)
|
|
elif [ "${usb_controller}" == "xhci" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device qemu-xhci,id=input)
|
|
elif [ "${usb_controller}" == "none" ]; then
|
|
# add nothing
|
|
:
|
|
else
|
|
echo "WARNING! Unknown usb-controller value: '${usb_controller}'"
|
|
fi
|
|
|
|
# setup keyboard
|
|
# @INFO: must be set after usb-controller
|
|
if [ "${keyboard}" == "usb" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device usb-kbd,bus=input.0)
|
|
elif [ "${keyboard}" == "virtio" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device virtio-keyboard)
|
|
elif [ "${keyboard}" == "ps2" ]; then
|
|
# add nothing, default is ps/2 keyboard
|
|
:
|
|
else
|
|
echo "WARNING! Unknown keyboard value: '${keyboard}'; Fallback to ps2"
|
|
fi
|
|
|
|
# setup keyboard_layout
|
|
# @INFO: When using the VNC display, you must use the -k parameter to set the keyboard layout if you are not using en-us.
|
|
if [ -n "${keyboard_layout}" ]; then
|
|
args+=(-k "${keyboard_layout}")
|
|
fi
|
|
|
|
# FIXME: Check for device availability. qemu will fail to start otherwise
|
|
if [ -n "${BRAILLE}" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-chardev braille,id=brltty
|
|
-device usb-braille,id=usbbrl,chardev=brltty)
|
|
fi
|
|
|
|
# setup mouse
|
|
# @INFO: must be set after usb-controller
|
|
if [ "${mouse}" == "usb" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device usb-mouse,bus=input.0)
|
|
elif [ "${mouse}" == "tablet" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device usb-tablet,bus=input.0)
|
|
elif [ "${mouse}" == "virtio" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-device virtio-mouse)
|
|
elif [ "${mouse}" == "ps2" ]; then
|
|
# add nothing, default is ps/2 mouse
|
|
:
|
|
else
|
|
echo "WARNING! Unknown mouse value: '${mouse}; Fallback to ps2'"
|
|
fi
|
|
|
|
# setup audio
|
|
# @INFO: must be set after usb-controller; in case usb-audio is used
|
|
# shellcheck disable=SC2206
|
|
args+=(-audiodev ${AUDIO_DEV} ${SOUND})
|
|
|
|
# $bridge backwards compatibility for Quickemu <= 4.0
|
|
if [ -n "${bridge}" ]; then
|
|
network="${bridge}"
|
|
fi
|
|
|
|
if [ "${network}" == "none" ]; then
|
|
# Disable all networking
|
|
echo " - Network: Disabled"
|
|
args+=(-nic none)
|
|
elif [ "${network}" == "restrict" ]; then
|
|
echo " - Network: Restricted (${NET_DEVICE})"
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device ${NET_DEVICE},netdev=nic -netdev ${NET},restrict=y,id=nic)
|
|
elif [ -n "${network}" ]; then
|
|
# Enable bridge mode networking
|
|
echo " - Network: Bridged (${network})"
|
|
|
|
# If a persistent MAC address is provided, use it.
|
|
local MAC=""
|
|
if [ -n "${macaddr}" ]; then
|
|
MAC=",mac=${macaddr}"
|
|
fi
|
|
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-nic bridge,br=${network},model=virtio-net-pci${MAC})
|
|
else
|
|
echo " - Network: User (${NET_DEVICE})"
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device ${NET_DEVICE},netdev=nic -netdev ${NET},id=nic)
|
|
fi
|
|
|
|
# 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+=(-global driver=cfi.pflash01,property=secure,value=on
|
|
-drive if=pflash,format=raw,unit=0,file="${EFI_CODE}",readonly=on
|
|
-drive if=pflash,format=raw,unit=1,file="${EFI_VARS}")
|
|
fi
|
|
|
|
if [ -n "${iso}" ] && [ "${guest_os}" == "freedos" ]; then
|
|
# FreeDOS reboots after partitioning the disk, and QEMU tries to boot from disk after first restart
|
|
# This flag sets the boot order to cdrom,disk. It will persist until powering down the VM
|
|
args+=(-boot order=dc)
|
|
elif [ -n "${iso}" ] && [ "${guest_os}" == "kolibrios" ]; then
|
|
# Since there is bug (probably) in KolibriOS: cdrom indexes 0 or 1 make system show an extra unexisting iso, so we use index=2
|
|
# shellcheck disable=SC2054
|
|
args+=(-drive media=cdrom,index=2,file="${iso}")
|
|
iso=""
|
|
elif [ -n "${iso}" ] && [ "${guest_os}" == "reactos" ]; then
|
|
# https://reactos.org/wiki/QEMU
|
|
# shellcheck disable=SC2054
|
|
args+=(-boot order=d
|
|
-drive if=ide,index=2,media=cdrom,file="${iso}")
|
|
iso=""
|
|
elif [ -n "${iso}" ] && [ "${guest_os}" == "windows" ] && [ -e "${VMDIR}/unattended.iso" ]; then
|
|
# Attach the unattended configuration to Windows guests when booting from ISO
|
|
# shellcheck disable=SC2054
|
|
args+=(-drive media=cdrom,index=2,file="${VMDIR}/unattended.iso")
|
|
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,bootindex=0
|
|
-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}")
|
|
fi
|
|
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device ${MAC_DISK_DEV},drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO})
|
|
elif [ "${guest_os}" == "kolibrios" ]; then
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device ahci,id=ahci
|
|
-device ide-hd,bus=ahci.0,drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO})
|
|
|
|
elif [ "${guest_os}" == "batocera" ] ; then
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device virtio-blk-pci,drive=BootDisk
|
|
-drive id=BootDisk,if=none,format=raw,file="${img}"
|
|
-device virtio-blk-pci,drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO})
|
|
|
|
elif [ "${guest_os}" == "reactos" ]; then
|
|
# https://reactos.org/wiki/QEMU
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-drive if=ide,index=0,media=disk,file="${disk_img}")
|
|
|
|
elif [ "${guest_os}" == "windows-server" ]; then
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device ide-hd,drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO})
|
|
|
|
else
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device virtio-blk-pci,drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}" ${STATUS_QUO})
|
|
fi
|
|
|
|
# https://wiki.qemu.org/Documentation/9psetup
|
|
# https://askubuntu.com/questions/772784/9p-libvirt-qemu-share-modes
|
|
if [ "${guest_os}" != "windows" ] || [ "${guest_os}" == "windows-server" ] && [ -n "${PUBLIC}" ]; 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
|
|
|
|
if [ "${tpm}" == "on" ] && [ -S "${VMDIR}/${VMNAME}.swtpm-sock" ]; then
|
|
# shellcheck disable=SC2054
|
|
args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock"
|
|
-tpmdev emulator,id=tpm0,chardev=chrtpm
|
|
-device tpm-tis,tpmdev=tpm0)
|
|
fi
|
|
|
|
if [ -n "${monitor_telnet_port}" ] && ! is_numeric "${monitor_telnet_port}"; then
|
|
echo "ERROR: telnet port must be a number!"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${monitor}" == "none" ]; then
|
|
args+=(-monitor none)
|
|
echo " - Monitor: (off)"
|
|
elif [ "${monitor}" == "telnet" ]; then
|
|
# Find a free port to expose monitor-telnet to the guest
|
|
TEMP_PORT="$(get_port "${monitor_telnet_port}" 9)"
|
|
if [ -z "${TEMP_PORT}" ]; then
|
|
echo " - Monitor: All Monitor-Telnet ports have been exhausted."
|
|
else
|
|
monitor_telnet_port="${TEMP_PORT}"
|
|
# shellcheck disable=SC2054
|
|
args+=(-monitor telnet:"${monitor_telnet_host}:${monitor_telnet_port}",server,nowait)
|
|
echo " - Monitor: On host: telnet ${monitor_telnet_host} ${monitor_telnet_port}"
|
|
echo "monitor-telnet,${monitor_telnet_port},${monitor_telnet_host}" >> "${VMDIR}/${VMNAME}.ports"
|
|
fi
|
|
elif [ "${monitor}" == "socket" ]; then
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-monitor unix:${SOCKET_MONITOR},server,nowait)
|
|
echo " - Monitor: On host: nc -U \"${SOCKET_MONITOR}\""
|
|
echo " or : socat -,echo=0,icanon=0 unix-connect:${SOCKET_MONITOR}"
|
|
else
|
|
echo "ERROR! \"${monitor}\" is an unknown monitor option."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "${serial_telnet_port}" ] && ! is_numeric "${serial_telnet_port}"; then
|
|
echo "ERROR: serial port must be a number!"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${serial}" == "none" ]; then
|
|
args+=(-serial none)
|
|
echo " - Serial: (off)"
|
|
elif [ "${serial}" == "telnet" ]; then
|
|
# Find a free port to expose serial-telnet to the guest
|
|
TEMP_PORT="$(get_port "${serial_telnet_port}" 9)"
|
|
if [ -z "${TEMP_PORT}" ]; then
|
|
echo " - Serial: All Serial Telnet ports have been exhausted."
|
|
else
|
|
serial_telnet_port="${TEMP_PORT}"
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-serial telnet:${serial_telnet_host}:${serial_telnet_port},server,nowait)
|
|
echo " - Serial: On host: telnet ${serial_telnet_host} ${serial_telnet_port}"
|
|
echo "serial-telnet,${serial_telnet_port},${serial_telnet_host}" >> "${VMDIR}/${VMNAME}.ports"
|
|
fi
|
|
elif [ "${serial}" == "socket" ]; then
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-serial unix:${SOCKET_SERIAL},server,nowait)
|
|
echo " - Serial: On host: nc -U \"${SOCKET_SERIAL}\""
|
|
echo " or : socat -,echo=0,icanon=0 unix-connect:${SOCKET_SERIAL}"
|
|
else
|
|
echo "ERROR! \"${serial}\" is an unknown serial option."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "${extra_args}" ]; then
|
|
# shellcheck disable=SC2206
|
|
args+=(${extra_args})
|
|
fi
|
|
|
|
# The OSK parameter contains parenthesis, they need to be escaped in the shell
|
|
# scripts. The vendor name, Quickemu Project, contains a space. It needs to be
|
|
# double-quoted.
|
|
SHELL_ARGS="${args[*]}"
|
|
SHELL_ARGS="${SHELL_ARGS//\(/\\(}"
|
|
SHELL_ARGS="${SHELL_ARGS//)/\\)}"
|
|
SHELL_ARGS="${SHELL_ARGS//Quickemu Project/\"Quickemu Project\"}"
|
|
|
|
if [ -z "${VM_PID}" ]; then
|
|
# Enable grab-on-hover for SDL: https://github.com/quickemu-project/quickemu/issues/541
|
|
case "${display}" in
|
|
sdl) export SDL_MOUSE_FOCUS_CLICKTHROUGH=1;;
|
|
esac
|
|
echo "${QEMU}" "${SHELL_ARGS}" "2>/dev/null" >> "${VMDIR}/${VMNAME}.sh"
|
|
sed -i -e 's/ -/ \\\n -/g' "${VMDIR}/${VMNAME}.sh"
|
|
${QEMU} "${args[@]}" &> "${VMDIR}/${VMNAME}.log" &
|
|
local VM_PID=$!
|
|
sleep 0.25
|
|
if kill -0 "${VM_PID}" 2>/dev/null; then
|
|
echo " - Process: Started ${VM} as ${VMNAME} (${VM_PID})"
|
|
else
|
|
echo " - Process: ERROR! Failed to start ${VM} as ${VMNAME}"
|
|
rm -f "${VMDIR}/${VMNAME}.pid"
|
|
echo && cat "${VMDIR}/${VMNAME}.log"
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function start_viewer {
|
|
errno=0
|
|
if [ "${viewer}" != "none" ]; then
|
|
|
|
# If output is 'none' then SPICE was requested.
|
|
if [ "${display}" == "spice" ]; then
|
|
if [ "${viewer}" == "remote-viewer" ]; then
|
|
# show via viewer: remote-viewer
|
|
if [ -n "${PUBLIC}" ]; then
|
|
echo " - Viewer: ${viewer} --title \"${VMNAME}\" --spice-shared-dir \"${PUBLIC}\" ${FULLSCREEN} \"spice://localhost:${spice_port}\" >/dev/null 2>&1 &"
|
|
${viewer} --title "${VMNAME}" --spice-shared-dir "${PUBLIC}" ${FULLSCREEN} "spice://localhost:${spice_port}" >/dev/null 2>&1 &
|
|
errno=$?
|
|
else
|
|
echo " - Viewer: ${viewer} --title \"${VMNAME}\" ${FULLSCREEN} \"spice://localhost:${spice_port}\" >/dev/null 2>&1 &"
|
|
${viewer} --title "${VMNAME}" ${FULLSCREEN} "spice://localhost:${spice_port}" >/dev/null 2>&1 &
|
|
errno=$?
|
|
fi
|
|
|
|
elif [ "${viewer}" == "spicy" ]; then
|
|
# show via viewer: spicy
|
|
if [ -n "${PUBLIC}" ]; then
|
|
echo " - Viewer: ${viewer} --title \"${VMNAME}\" --port \"${spice_port}\" --spice-shared-dir \"${PUBLIC}\" \"${FULLSCREEN}\" >/dev/null 2>&1 &"
|
|
${viewer} --title "${VMNAME}" --port "${spice_port}" --spice-shared-dir "${PUBLIC}" "${FULLSCREEN}" >/dev/null 2>&1 &
|
|
errno=$?
|
|
else
|
|
echo " - Viewer: ${viewer} --title \"${VMNAME}\" --port \"${spice_port}\" \"${FULLSCREEN}\" >/dev/null 2>&1 &"
|
|
${viewer} --title "${VMNAME}" --port "${spice_port}" "${FULLSCREEN}" >/dev/null 2>&1 &
|
|
errno=$?
|
|
fi
|
|
fi
|
|
if [ ${errno} -ne 0 ]; then
|
|
echo "WARNING! Could not start viewer (${viewer}) Err: ${errno}"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function shortcut_create {
|
|
local dirname="${HOME}/.local/share/applications"
|
|
local filename="${HOME}/.local/share/applications/${VMNAME}.desktop"
|
|
echo "Creating ${VMNAME} desktop shortcut file"
|
|
|
|
if [ ! -d "${dirname}" ]; then
|
|
mkdir -p "${dirname}"
|
|
fi
|
|
cat << EOF > "${filename}"
|
|
[Desktop Entry]
|
|
Version=1.0
|
|
Type=Application
|
|
Terminal=false
|
|
Exec=${0} --vm ${VM}
|
|
Path=${VMPATH}
|
|
Name=${VMNAME}
|
|
Icon=/usr/share/icons/hicolor/scalable/apps/qemu.svg
|
|
EOF
|
|
echo " - ${filename} created."
|
|
}
|
|
|
|
function usage() {
|
|
echo " _ _"
|
|
echo " __ _ _ _(_) ___| | _____ _ __ ___ _ _"
|
|
echo " / _' | | | | |/ __| |/ / _ \ '_ ' _ \| | | |"
|
|
echo "| (_| | |_| | | (__| < __/ | | | | | |_| |"
|
|
echo " \__, |\__,_|_|\___|_|\_\___|_| |_| |_|\__,_|"
|
|
echo " |_| v${VERSION}, using qemu ${QEMU_VER_LONG}"
|
|
echo "--------------------------------------------------------------------------------"
|
|
echo " Project - https://github.com/quickemu-project/quickemu"
|
|
echo " Discord - https://wimpysworld.io/discord"
|
|
echo "--------------------------------------------------------------------------------"
|
|
echo
|
|
echo "Usage"
|
|
echo " ${LAUNCHER} --vm ubuntu.conf <arguments>"
|
|
echo
|
|
echo "Arguments"
|
|
echo " --access : Enable remote spice access support. 'local' (default), 'remote', 'clientipaddress'"
|
|
echo " --braille : Enable braille support. Requires SDL."
|
|
echo " --delete-disk : Delete the disk image and EFI variables"
|
|
echo " --delete-vm : Delete the entire VM and its configuration"
|
|
echo " --display : Select display backend. 'sdl' (default), 'gtk', 'none', 'spice' or 'spice-app'"
|
|
echo " --fullscreen : Starts VM in full screen mode (Ctl+Alt+f to exit)"
|
|
echo " --ignore-msrs-always : Configure KVM to always ignore unhandled machine-specific registers"
|
|
echo " --kill : Kill the VM process if it is running"
|
|
echo " --offline : Override all network settings and start the VM offline"
|
|
echo " --shortcut : Create a desktop shortcut"
|
|
echo " --snapshot apply <tag> : Apply/restore a snapshot."
|
|
echo " --snapshot create <tag> : Create a snapshot."
|
|
echo " --snapshot delete <tag> : Delete a snapshot."
|
|
echo " --snapshot info : Show disk/snapshot info."
|
|
echo " --status-quo : Do not commit any changes to disk/snapshot."
|
|
echo " --viewer <viewer> : Choose an alternative viewer. @Options: 'spicy' (default), 'remote-viewer', 'none'"
|
|
echo " --width <width> : Set VM screen width; requires '--height'"
|
|
echo " --height <height> : Set VM screen height; requires '--width'"
|
|
echo " --ssh-port <port> : Set SSH port manually"
|
|
echo " --spice-port <port> : Set SPICE port manually"
|
|
echo " --public-dir <path> : Expose share directory. @Options: '' (default: xdg-user-dir PUBLICSHARE), '<directory>', 'none'"
|
|
echo " --monitor <type> : Set monitor connection type. @Options: 'socket' (default), 'telnet', 'none'"
|
|
echo " --monitor-telnet-host <ip/host> : Set telnet host for monitor. (default: 'localhost')"
|
|
echo " --monitor-telnet-port <port> : Set telnet port for monitor. (default: '4440')"
|
|
echo " --monitor-cmd <cmd> : Send command to monitor if available. (Example: system_powerdown)"
|
|
echo " --serial <type> : Set serial connection type. @Options: 'socket' (default), 'telnet', 'none'"
|
|
echo " --serial-telnet-host <ip/host> : Set telnet host for serial. (default: 'localhost')"
|
|
echo " --serial-telnet-port <port> : Set telnet port for serial. (default: '6660')"
|
|
echo " --keyboard <type> : Set keyboard. @Options: 'usb' (default), 'ps2', 'virtio'"
|
|
echo " --keyboard_layout <layout> : Set keyboard layout: 'en-us' (default)"
|
|
echo " --mouse <type> : Set mouse. @Options: 'tablet' (default), 'ps2', 'usb', 'virtio'"
|
|
echo " --usb-controller <type> : Set usb-controller. @Options: 'ehci' (default), 'xhci', 'none'"
|
|
echo " --sound-card <type> : Set sound card. @Options: 'intel-hda' (default), 'ac97', 'es1370', 'sb16', 'usb-audio', 'none'"
|
|
echo " --sound-duplex <type> : Set sound card duplex. @Options: 'hda-micro' (default: speaker/mic), 'hda-duplex' (line-in/line-out), 'hda-output' (output-only)"
|
|
echo " --extra_args <arguments> : Pass additional arguments to qemu"
|
|
echo " --version : Print version"
|
|
exit 1
|
|
}
|
|
|
|
function display_param_check() {
|
|
if [ "${display}" != "gtk" ] && [ "${display}" != "none" ] && [ "${display}" != "sdl" ] && [ "${display}" != "spice" ] && [ "${display}" != "spice-app" ]; then
|
|
echo "ERROR! Requested output '${display}' is not recognised."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
function sound_card_param_check() {
|
|
if [ "${sound_card}" != "ac97" ] && [ "${sound_card}" != "es1370" ] && [ "${sound_card}" != "ich9-intel-hda" ] && [ "${sound_card}" != "intel-hda" ] && [ "${sound_card}" != "sb16" ] && [ "${sound_card}" != "usb-audio" ] && [ "${sound_card}" != "none" ]; then
|
|
echo "ERROR! Requested sound card '${sound_card}' is not recognised."
|
|
exit 1
|
|
fi
|
|
|
|
# USB audio requires xhci controller
|
|
if [ "${sound_card}" == "usb-audio" ]; then
|
|
usb_controller="xhci";
|
|
fi
|
|
|
|
#name "hda-duplex", bus HDA, desc "HDA Audio Codec, duplex (line-out, line-in)"
|
|
#name "hda-micro", bus HDA, desc "HDA Audio Codec, duplex (speaker, microphone)"
|
|
#name "hda-output", bus HDA, desc "HDA Audio Codec, output-only (line-out)"
|
|
if [ "${sound_duplex}" != "hda-duplex" ] && [ "${sound_duplex}" != "hda-micro" ] && [ "${sound_duplex}" != "hda-output" ]; then
|
|
echo "ERROR! Requested sound duplex '${sound_duplex}' is not recognised."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
function viewer_param_check() {
|
|
if [ "${viewer}" != "none" ] && [ "${viewer}" != "spicy" ] && [ "${viewer}" != "remote-viewer" ]; then
|
|
echo "ERROR! Requested viewer '${viewer}' is not recognised."
|
|
exit 1
|
|
fi
|
|
if [ "${viewer}" == "spicy" ] && ! command -v spicy &>/dev/null; then
|
|
echo "ERROR! Requested 'spicy' as viewer, but 'spicy' is not installed."
|
|
exit 1
|
|
elif [ "${viewer}" == "remote-viewer" ] && ! command -v remote-viewer &>/dev/null; then
|
|
echo "ERROR! Requested 'remote-viewer' as viewer, but 'remote-viewer' is not installed."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
function parse_ports_from_file {
|
|
local FILE="${VMDIR}/${VMNAME}.ports"
|
|
local host_name=""
|
|
local port_name=""
|
|
local port_number=""
|
|
|
|
# Loop over each line in the file
|
|
while IFS= read -r CONF || [ -n "${CONF}" ]; do
|
|
# parse ports
|
|
port_name=$(echo "${CONF}" | cut -d',' -f 1)
|
|
port_number=$(echo "${CONF}" | cut -d',' -f 2)
|
|
host_name=$(echo "${CONF}" | awk 'FS="," {print $3,"."}')
|
|
|
|
if [ "${port_name}" == "ssh" ]; then
|
|
ssh_port="${port_number}"
|
|
elif [ "${port_name}" == "spice" ]; then
|
|
spice_port="${port_number}"
|
|
elif [ "${port_name}" == "monitor-telnet" ]; then
|
|
monitor_telnet_port="${port_number}"
|
|
monitor_telnet_host="${host_name}"
|
|
elif [ "${port_name}" == "serial-telnet" ]; then
|
|
serial_telnet_port="${port_number}"
|
|
serial_telnet_host="${host_name}"
|
|
fi
|
|
done < "${FILE}"
|
|
}
|
|
|
|
function is_numeric {
|
|
[[ "$1" =~ ^[0-9]+$ ]]
|
|
}
|
|
|
|
function monitor_send_cmd {
|
|
local MSG="${1}"
|
|
|
|
if [ -z "${MSG}" ]; then
|
|
echo "WARNING! Send to QEMU-Monitor: Message empty!"
|
|
return 1
|
|
fi
|
|
|
|
case "${monitor}" in
|
|
socket)
|
|
echo -e " - Sending: via socket ${MSG}"
|
|
echo -e "${MSG}" | socat -,shut-down unix-connect:"${SOCKET_MONITOR}" > /dev/null 2>&1;;
|
|
telnet)
|
|
echo -e " - Sending: via telnet ${MSG}"
|
|
echo -e "${MSG}" | socat - tcp:"${monitor_telnet_host}":"${monitor_telnet_port}" > /dev/null 2>&1;;
|
|
*)
|
|
echo "WARNING! No qemu-monitor channel available - Couldn't send message to monitor!"
|
|
return 1;;
|
|
esac
|
|
|
|
return 0
|
|
}
|
|
|
|
### MAIN
|
|
|
|
# Lowercase variables are used in the VM config file only
|
|
boot="efi"
|
|
cpu_cores=""
|
|
disk_format="${disk_format:-qcow2}"
|
|
disk_img="${disk_img:-}"
|
|
disk_size="${disk_size:-16G}"
|
|
display="${display:-sdl}"
|
|
extra_args="${extra_args:-}"
|
|
fixed_iso=""
|
|
floppy=""
|
|
guest_os="linux"
|
|
img=""
|
|
iso=""
|
|
macaddr=""
|
|
macos_release=""
|
|
network=""
|
|
port_forwards=()
|
|
preallocation="off"
|
|
ram=""
|
|
secureboot="off"
|
|
tpm="off"
|
|
usb_devices=()
|
|
viewer="${viewer:-spicy}"
|
|
width="${width:-}"
|
|
height="${height:-}"
|
|
ssh_port="${ssh_port:-}"
|
|
spice_port="${spice_port:-}"
|
|
public_dir=""
|
|
monitor="${monitor:-socket}"
|
|
monitor_telnet_port="${monitor_telnet_port:-4440}"
|
|
monitor_telnet_host="${monitor_telnet_host:-localhost}"
|
|
serial="${serial:-socket}"
|
|
serial_telnet_port="${serial_telnet_port:-6660}"
|
|
serial_telnet_host="${serial_telnet_host:-localhost}"
|
|
# options: ehci (USB2.0), xhci (USB3.0)
|
|
usb_controller="${usb_controller:-ehci}"
|
|
keyboard="${keyboard:-usb}"
|
|
keyboard_layout="${keyboard_layout:-en-us}"
|
|
mouse="${mouse:-tablet}"
|
|
sound_card="${sound_card:-intel-hda}"
|
|
sound_duplex="${sound_duplex:-hda-micro}"
|
|
|
|
ACCESS=""
|
|
ACTIONS=()
|
|
BRAILLE=""
|
|
FULLSCREEN=""
|
|
MONITOR_CMD=""
|
|
PUBLIC=""
|
|
PUBLIC_PERMS=""
|
|
PUBLIC_TAG=""
|
|
SNAPSHOT_ACTION=""
|
|
SNAPSHOT_TAG=""
|
|
SOCKET_MONITOR=""
|
|
SOCKET_SERIAL=""
|
|
STATUS_QUO=""
|
|
USB_PASSTHROUGH=""
|
|
VM=""
|
|
VMDIR=""
|
|
VMNAME=""
|
|
VMPATH=""
|
|
|
|
# shellcheck disable=SC2155
|
|
readonly LAUNCHER=$(basename "${0}")
|
|
readonly DISK_MIN_SIZE=$((197632 * 8))
|
|
readonly VERSION="4.9.5"
|
|
|
|
# TODO: Make this run the native architecture binary
|
|
QEMU=$(command -v qemu-system-x86_64)
|
|
QEMU_IMG=$(command -v qemu-img)
|
|
if [ ! -e "${QEMU}" ] || [ ! -e "${QEMU_IMG}" ]; then
|
|
echo "ERROR! QEMU not found. Please make install qemu-system-x86_64 and qemu-img"
|
|
exit 1
|
|
fi
|
|
|
|
QEMU_VER_LONG=$(${QEMU} -version | head -n1 | cut -d' ' -f4 | cut -d'(' -f1)
|
|
QEMU_VER_SHORT=$(${QEMU} -version | head -n1 | cut -d' ' -f4 | cut -d'(' -f1 | sed 's/\.//g' | cut -c1-2)
|
|
if [ "${QEMU_VER_SHORT}" -lt 60 ]; then
|
|
echo "ERROR! Qemu 6.0.0 or newer is required, detected ${QEMU_VER_LONG}."
|
|
exit 1
|
|
fi
|
|
|
|
# Take command line arguments
|
|
if [ $# -lt 1 ]; then
|
|
usage
|
|
else
|
|
while [ $# -gt 0 ]; do
|
|
case "${1}" in
|
|
-access|--access)
|
|
ACCESS="${2}"
|
|
shift 2;;
|
|
-braille|--braille)
|
|
BRAILLE="on"
|
|
shift;;
|
|
-delete|--delete|-delete-disk|--delete-disk)
|
|
ACTIONS+=(delete_disk)
|
|
shift;;
|
|
-delete-vm|--delete-vm)
|
|
ACTIONS+=(delete_vm)
|
|
shift;;
|
|
-display|--display)
|
|
display="${2}"
|
|
display_param_check
|
|
shift 2;;
|
|
-fullscreen|--fullscreen|-full-screen|--full-screen)
|
|
FULLSCREEN="--full-screen"
|
|
shift;;
|
|
-ignore-msrs-always|--ignore-msrs-always)
|
|
ignore_msrs_always
|
|
exit;;
|
|
-kill|--kill)
|
|
ACTIONS+=(kill_vm)
|
|
shift;;
|
|
-offline|--offline)
|
|
network="none"
|
|
shift;;
|
|
-snapshot|--snapshot)
|
|
if [ -z "${2}" ]; then
|
|
echo "ERROR! '--snapshot' needs an action to perform."
|
|
exit 1
|
|
fi
|
|
SNAPSHOT_ACTION="${2}"
|
|
if [ -z "${3}" ] && [ "${SNAPSHOT_ACTION}" != "info" ]; then
|
|
echo "ERROR! '--snapshot ${SNAPSHOT_ACTION}' needs a tag."
|
|
exit 1
|
|
fi
|
|
SNAPSHOT_TAG="${3}"
|
|
if [ "${SNAPSHOT_ACTION}" == "info" ]; then
|
|
shift 2
|
|
else
|
|
shift 3
|
|
fi;;
|
|
-status-quo|--status-quo)
|
|
STATUS_QUO="-snapshot"
|
|
shift;;
|
|
-shortcut|--shortcut)
|
|
ACTIONS+=(shortcut_create)
|
|
shift;;
|
|
-vm|--vm)
|
|
VM="${2}"
|
|
shift 2;;
|
|
-viewer|--viewer)
|
|
viewer="${2}"
|
|
shift 2;;
|
|
-width|--width)
|
|
width="${2}"
|
|
shift 2;;
|
|
-height|--height)
|
|
height="${2}"
|
|
shift 2;;
|
|
-ssh-port|--ssh-port)
|
|
ssh_port="${2}"
|
|
shift 2;;
|
|
-spice-port|--spice-port)
|
|
spice_port="${2}"
|
|
shift 2;;
|
|
-public-dir|--public-dir)
|
|
PUBLIC="${2}"
|
|
shift 2;;
|
|
-monitor|--monitor)
|
|
monitor="${2}"
|
|
shift 2;;
|
|
-monitor-cmd|--monitor-cmd)
|
|
MONITOR_CMD="${2}"
|
|
shift 2;;
|
|
-monitor-telnet-host|--monitor-telnet-host)
|
|
monitor_telnet_host="${2}"
|
|
shift 2;;
|
|
-monitor-telnet-port|--monitor-telnet-port)
|
|
monitor_telnet_port="${2}"
|
|
shift 2;;
|
|
-serial|--serial)
|
|
serial="${2}"
|
|
shift 2;;
|
|
-serial-telnet-host|--serial-telnet-host)
|
|
serial_telnet_host="${2}"
|
|
shift 2;;
|
|
-serial-telnet-port|--serial-telnet-port)
|
|
serial_telnet_port="${2}"
|
|
shift 2;;
|
|
-keyboard|--keyboard)
|
|
keyboard="${2}"
|
|
shift 2;;
|
|
-keyboard_layout|--keyboard_layout)
|
|
keyboard_layout="${2}"
|
|
shift 2;;
|
|
-mouse|--mouse)
|
|
mouse="${2}"
|
|
shift 2;;
|
|
-usb-controller|--usb-controller)
|
|
usb_controller="${2}"
|
|
shift 2;;
|
|
-extra_args|--extra_args)
|
|
extra_args+="${2}"
|
|
shift 2;;
|
|
-sound-card|--sound-card)
|
|
sound_card="${2}"
|
|
shift 2;;
|
|
-sound-duplex|--sound-duplex)
|
|
sound_duplex="${2}"
|
|
shift 2;;
|
|
-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}"
|
|
|
|
VMDIR=$(dirname "${disk_img}") # directory the VM disk and state files are stored
|
|
VMNAME=$(basename "${VM}" .conf) # name of the VM
|
|
VMPATH=$(realpath "$(dirname "${VM}")") # path to the top-level VM directory
|
|
SOCKET_MONITOR="${VMDIR}/${VMNAME}-monitor.socket"
|
|
SOCKET_SERIAL="${VMDIR}/${VMNAME}-serial.socket"
|
|
|
|
# if not disk_img is configured, do the right thing.
|
|
if [ -z "${disk_img}" ]; then
|
|
disk_img="${VMDIR}/disk.${disk_format}"
|
|
fi
|
|
|
|
# Iterate over any actions and exit.
|
|
if [ ${#ACTIONS[@]} -ge 1 ]; then
|
|
for ACTION in "${ACTIONS[@]}"; do
|
|
${ACTION}
|
|
done
|
|
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)
|
|
echo "Snapshot information ${disk_img}"
|
|
snapshot_info
|
|
exit;;
|
|
*)
|
|
echo "ERROR! \"${SNAPSHOT_ACTION}\" is not a supported snapshot action."
|
|
usage;;
|
|
esac
|
|
fi
|
|
|
|
# Braille support requires SDL. Override $display if braille was requested.
|
|
if [ -n "${BRAILLE}" ]; then
|
|
display="sdl"
|
|
fi
|
|
|
|
display_param_check
|
|
sound_card_param_check
|
|
viewer_param_check
|
|
|
|
# Set the default 3D acceleration.
|
|
if [ -z "${gl}" ]; then
|
|
if command -v glxinfo &>/dev/null; then
|
|
GLSL_VER=$(glxinfo | grep "OpenGL ES GLSL" | awk '{print $NF}')
|
|
case ${GLSL_VER} in
|
|
1*|2*) gl="off";;
|
|
*) gl="on";;
|
|
esac
|
|
else
|
|
gl="on"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${PUBLIC}" ]; then
|
|
PUBLIC="${public_dir}"
|
|
fi
|
|
|
|
if [ "${PUBLIC}" == "none" ]; then
|
|
PUBLIC=""
|
|
else
|
|
# PUBLICSHARE is the only directory exposed to guest VMs for file
|
|
# sharing via 9P, spice-webdavd and Samba. This path is not configurable.
|
|
if [ -z "${PUBLIC}" ]; then
|
|
if command -v xdg-user-dir &>/dev/null; then
|
|
PUBLIC=$(xdg-user-dir PUBLICSHARE)
|
|
fi
|
|
fi
|
|
|
|
if [ ! -d "${PUBLIC}" ]; then
|
|
echo "ERROR! Public directory: '${PUBLIC}' doesn't exist!"
|
|
exit 1
|
|
fi
|
|
|
|
PUBLIC_TAG="Public-${USER,,}"
|
|
# shellcheck disable=SC2012
|
|
PUBLIC_PERMS=$(ls -ld "${PUBLIC}" | cut -d' ' -f1)
|
|
fi
|
|
|
|
if [ -n "${ssh_port}" ] && ! is_numeric "${ssh_port}"; then
|
|
echo "ERROR: ssh_port must be a number!"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "${spice_port}" ] && ! is_numeric "${spice_port}"; then
|
|
echo "ERROR: spice_port must be a number!"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if vm is already run
|
|
VM_PID=""
|
|
if [ -r "${VMDIR}/${VMNAME}.pid" ]; then
|
|
VM_PID=$(head -1 "${VMDIR}/${VMNAME}.pid")
|
|
if ! kill -0 "${VM_PID}" > /dev/null 2>&1; then
|
|
# VM is not running, cleaning up.
|
|
VM_PID=""
|
|
rm -f "${VMDIR}/${VMNAME}.pid"
|
|
fi
|
|
fi
|
|
|
|
if [ "${tpm}" == "on" ]; then
|
|
SWTPM=$(command -v swtpm)
|
|
if [ ! -e "${SWTPM}" ]; then
|
|
echo "ERROR! TPM is enabled, but swtpm was not found."
|
|
exit 1
|
|
fi
|
|
fi
|
|
else
|
|
echo "ERROR! Virtual machine configuration not found."
|
|
usage
|
|
fi
|
|
|
|
if [ -z "${VM_PID}" ]; then
|
|
#TODO: double quote the args array to prevent word splitting and this can be removed
|
|
# Fix failing to start VM with spaces in the path
|
|
# https://github.com/quickemu-project/quickemu/pull/875
|
|
if [ ! -f "${disk_img}" ]; then
|
|
pushd "${VMPATH}" || exit
|
|
fi
|
|
|
|
vm_boot
|
|
# If the VM being started is an uninstalled Windows VM then auto-skip the press-any key prompt.
|
|
if [ -n "${iso}" ] && [ "${guest_os}" == "windows" ]; then
|
|
sleep 3.5
|
|
monitor_send_cmd "sendkey ret"
|
|
fi
|
|
if [ -n "${iso}" ] && [ "${guest_os}" == "windows-server" ]; then
|
|
sleep 7
|
|
monitor_send_cmd "sendkey ret"
|
|
fi
|
|
start_viewer
|
|
else
|
|
echo "${VMNAME}"
|
|
echo " - Process: Already running ${VM} as ${VMNAME} (${VM_PID})"
|
|
parse_ports_from_file
|
|
start_viewer
|
|
fi
|
|
|
|
if [ -n "${MONITOR_CMD}" ]; then
|
|
monitor_send_cmd "${MONITOR_CMD}"
|
|
fi
|
|
|
|
# vim:tabstop=4:shiftwidth=4:expandtab
|