Created
August 1, 2022 02:07
-
-
Save pythoninthegrass/43c93c556d1ae3cc1a92db15e4e73ba1 to your computer and use it in GitHub Desktop.
Patched quickemu from @trollkarlen's macOS fork
This file contains 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
#!/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 | |
# Check for gnu tools on MacOS | |
UNAME="uname" | |
if command -v guname >/dev/null; then | |
UNAME="guname" | |
fi | |
STAT="stat" | |
if command -v gstat >/dev/null; then | |
STAT="gstat" | |
fi | |
MACOS=0 | |
if [ "$("$UNAME")" == "Darwin" ]; then | |
MACOS=1 | |
CPU_KVM="" | |
CPU_KVM_EXTRA="" | |
KVM_GUEST_TWEAKS=() | |
else | |
CPU_KVM=",kvm=on" | |
CPU_KVM_EXTRA=",+kvm_pv_eoi,+kvm_pv_unhalt" | |
CPU_KVM_EXTRA_UNHALT=",+kvm_pv_unhalt" | |
KVM_GUEST_TWEAKS=(-global "kvm-pit.lost_tick_policy=discard") | |
fi | |
# Multi platform get cpu info | |
function get_cpu_info() { | |
local INFO_NAME="$1" | |
if [[ "$MACOS" == "1" ]]; then | |
if [[ "^Model name:" == "$INFO_NAME" ]]; then | |
sysctl -n machdep.cpu.brand_string | sed 's/ //g' | |
elif [[ "Socket" == "$INFO_NAME" ]]; then | |
sysctl -n hw.packages | |
elif [[ "Vendor" == "$INFO_NAME" ]]; then | |
sysctl -n machdep.cpu.vendor | sed 's/ //g' | |
else | |
>&2 echo "Could not find translation for MacOS for $INFO_NAME" | |
exit 23 | |
fi | |
else | |
lscpu | grep "$INFO_NAME" | cut -d':' -f2 | sed 's/ //g' | |
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 permenant 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 ${VM} desktop shortcut" | |
fi | |
} | |
function delete_disk() { | |
if [ -e "${disk_img}" ]; then | |
rm "${disk_img}" | |
# 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 "SUCCESS! Deleted ${disk_img}" | |
delete_shortcut | |
else | |
echo "NOTE! ${disk_img} not found. Doing nothing." | |
fi | |
} | |
function delete_vm() { | |
if [ -d "${VMDIR}" ]; then | |
rm -rf "${VMDIR}" | |
rm "${VM}" | |
echo "SUCCESS! Deleted ${VM} and ${VMDIR}" | |
delete_shortcut | |
else | |
echo "NOTE! ${VMDIR} not found. Doing nothing." | |
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=$((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 [ -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 command -v lscpu >/dev/null; then | |
if lscpu | grep -o "^Flags\b.*: .*\b${HOST_CPU_FLAG}\b" > /dev/null; then | |
return 0 | |
else | |
return 1 | |
fi | |
elif command -v sysctl >/dev/null; then # MacOS | |
if sysctl -n machdep.cpu.features | grep -i "${HOST_CPU_FLAG}" >/dev/null; then | |
return 0 | |
else | |
return 1 | |
fi | |
else | |
>&2 echo "Cant get cpu flags for this platform or the right tools is not installed." | |
exit 2 | |
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="" | |
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 GL="on" | |
local GUEST_TWEAKS="" | |
local KERNEL_NAME="Unknown" | |
local KERNEL_NODE="" | |
local KERNEL_VER="?" | |
local LSB_DESCRIPTION="Unknown OS" | |
local MACHINE_TYPE="q35" | |
local MAC_BOOTLOADER="" | |
local MAC_MISSING="" | |
local MAC_DISK_DEV="ide-hd,bus=ahci.2" | |
local MOUSE="usb-tablet" | |
local NET_DEVICE="virtio-net" | |
local OSK="" | |
local SMM="off" | |
local USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci" | |
local VIDEO="" | |
KERNEL_NAME=$("$UNAME" --kernel-name) | |
KERNEL_NODE="($("$UNAME" --nodename))" | |
KERNEL_VER=$("$UNAME" --kernel-release | cut -d'.' -f1-2) | |
if command -v lsb_release &>/dev/null; then | |
LSB_DESCRIPTION=$(lsb_release --description --short) | |
elif [ -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 --all) | |
HOST_CPU_MODEL=$(get_cpu_info '^Model name:') | |
HOST_CPU_SOCKETS=$(get_cpu_info 'Socket') | |
HOST_CPU_VENDOR=$(get_cpu_info 'Vendor') | |
# 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 | |
# 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="" | |
if [[ $MACOS == "1" ]]; then | |
RAM_HOST=$(($(sysctl -n hw.memsize) / (1048576*1024))) | |
else | |
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}") | |
fi | |
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}" == "macos" ] || [ "${guest_os}" == "windows" ]; then | |
if [ "${RAM_VM//G/}" -lt 4 ]; then | |
echo "ERROR! You have insufficient RAM to run ${guest_os} in a VM" | |
exit 1 | |
fi | |
fi | |
# Force to lowercase. | |
boot=${boot,,} | |
guest_os=${guest_os,,} | |
# 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" | |
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 | |
if [[ "$MACOS" == "1" ]]; then | |
QEMU_SHARE_PATH="$(brew --prefix qemu)/share/qemu" | |
else | |
QEMU_SHARE_PATH="/usr/share/qemu" | |
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 | |
case ${secureboot} in | |
on) | |
if [ -e "/usr/share/OVMF/OVMF_CODE_4M.secboot.fd" ]; then | |
EFI_CODE="/usr/share/OVMF/OVMF_CODE_4M.secboot.fd" | |
efi_vars "/usr/share/OVMF/OVMF_VARS_4M.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd" ]; then | |
EFI_CODE="/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd" | |
efi_vars "/usr/share/edk2/ovmf/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/OVMF/x64/OVMF_CODE.secboot.fd" ]; then | |
EFI_CODE="/usr/share/OVMF/x64/OVMF_CODE.secboot.fd" | |
efi_vars "/usr/share/OVMF/x64/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd" ]; then | |
EFI_CODE="/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd" | |
efi_vars "/usr/share/edk2-ovmf/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "${QEMU_SHARE_PATH}/ovmf-x86_64-smm-ms-code.bin" ]; then | |
EFI_CODE="${QEMU_SHARE_PATH}/ovmf-x86_64-smm-ms-code.bin" | |
efi_vars "${QEMU_SHARE_PATH}/ovmf-x86_64-smm-ms-vars.bin" "${EFI_VARS}" | |
elif [ -e "${QEMU_SHARE_PATH}/edk2-x86_64-secure-code.fd" ]; then | |
EFI_CODE="${QEMU_SHARE_PATH}/edk2-x86_64-secure-code.fd" | |
efi_vars "${QEMU_SHARE_PATH}/edk2-x86_64-code.fd" "${EFI_VARS}" | |
else | |
echo "ERROR! SecureBoot was requested but no SecureBoot capable firmware was found." | |
echo " Please install OVMF firmware." | |
exit 1 | |
fi | |
;; | |
*) | |
if [ -e "/usr/share/OVMF/OVMF_CODE_4M.fd" ]; then | |
EFI_CODE="/usr/share/OVMF/OVMF_CODE_4M.fd" | |
efi_vars "/usr/share/OVMF/OVMF_VARS_4M.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/edk2/ovmf/OVMF_CODE.fd" ]; then | |
EFI_CODE="/usr/share/edk2/ovmf/OVMF_CODE.fd" | |
efi_vars "/usr/share/edk2/ovmf/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/OVMF/OVMF_CODE.fd" ]; then | |
EFI_CODE="/usr/share/OVMF/OVMF_CODE.fd" | |
efi_vars "/usr/share/OVMF/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/OVMF/x64/OVMF_CODE.fd" ]; then | |
EFI_CODE="/usr/share/OVMF/x64/OVMF_CODE.fd" | |
efi_vars "/usr/share/OVMF/x64/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "/usr/share/edk2-ovmf/OVMF_CODE.fd" ]; then | |
EFI_CODE="/usr/share/edk2-ovmf/OVMF_CODE.fd" | |
efi_vars "/usr/share/edk2-ovmf/OVMF_VARS.fd" "${EFI_VARS}" | |
elif [ -e "${QEMU_SHARE_PATH}/ovmf-x86_64-4m-code.bin" ]; then | |
EFI_CODE="${QEMU_SHARE_PATH}/ovmf-x86_64-4m-code.bin" | |
efi_vars "${QEMU_SHARE_PATH}/ovmf-x86_64-4m-vars.bin" "${EFI_VARS}" | |
elif [ -e "${QEMU_SHARE_PATH}/edk2-x86_64-code.fd" ]; then | |
EFI_CODE="${QEMU_SHARE_PATH}/edk2-x86_64-code.fd" | |
efi_vars "${QEMU_SHARE_PATH}/edk2-x86_64-code.fd" "${EFI_VARS}" | |
else | |
echo "ERROR! EFI boot requested but no EFI firmware found." | |
echo " Please install OVMF firmware." | |
exit 1 | |
fi | |
;; | |
esac | |
# 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 | |
*bsd|haiku|freedos|linux) | |
CPUS="host${CPU_KVM}" | |
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then | |
CPUS="${CPUS},topoext" | |
fi | |
CPU=(-cpu "$CPUS") | |
if [ "${guest_os}" == "freebsd" ] || [ "${guest_os}" == "ghostbsd" ]; then | |
MOUSE="usb-mouse" | |
elif [ "${guest_os}" == "haiku" ] || [ "${guest_os}" == "freedos" ]; then | |
MACHINE_TYPE="pc" | |
NET_DEVICE="rtl8139" | |
fi | |
if [ "${guest_os}" == "freedos" ] ; then | |
# fix for #382 | |
SMM="on" | |
fi | |
if [ -z "${disk_size}" ]; then | |
disk_size="16G" | |
fi | |
;; | |
batocera) | |
CPU="-cpu host,kvm=on" | |
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then | |
CPU="${CPU},topoext" | |
fi | |
MACHINE_TYPE="pc" | |
NET_DEVICE="rtl8139" | |
disk_size="8G" | |
;; | |
kolibrios) | |
CPUS="qemu32${CPU_KVM}" | |
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then | |
CPUS="${CPUS},topoext" | |
fi | |
CPU=(-cpu "$CPUS") | |
MACHINE_TYPE="pc" | |
NET_DEVICE="rtl8139" | |
;; | |
macos) | |
#https://www.nicksherlock.com/2020/06/installing-macos-big-sur-on-proxmox/ | |
# A CPU with SSE4.1 support is required for >= macOS Sierra | |
if check_cpu_flag sse4_1; then | |
case ${HOST_CPU_VENDOR} in | |
GenuineIntel) | |
CPU=(-cpu "host${CPU_KVM},vendor=GenuineIntel,+hypervisor,+invtsc${CPU_KVM_EXTRA}");; | |
AuthenticAMD|*) | |
# Used in past versions: +movbe,+smep,+xgetbv1,+xsavec,+avx2 | |
# Warn on AMD: +fma4,+pcid | |
CPU=(-cpu "Penryn${CPU_KVM},vendor=GenuineIntel,+aes,+avx,+bmi1,+bmi2,+fma,+hypervisor,+invtsc${CPU_KVM_EXTRA},+popcnt,+ssse3,+sse4.2,vmware-cpuid-freq=on,+xsave,+xsaveopt,check");; | |
esac | |
else | |
echo "ERROR! macOS requires a CPU with SSE 4.1 support." | |
exit 1 | |
fi | |
OSK=$(echo "bheuneqjbexolgurfrjbeqfthneqrqcyrnfrqbagfgrny(p)NccyrPbzchgreVap" | tr 'A-Za-z' 'N-ZA-Mn-za-m') | |
# Disable S3 support in the VM to prevent macOS suspending during install | |
GUEST_TWEAKS="-no-hpet ${KVM_GUEST_TWEAKS[*]} -global ICH9-LPC.disable_s3=1 -device isa-applesmc,osk=${OSK}" | |
# 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 | |
catalina) | |
BALLOON=() | |
MAC_DISK_DEV="virtio-blk-pci" | |
NET_DEVICE="vmxnet3" | |
USB_HOST_PASSTHROUGH_CONTROLLER="usb-ehci" | |
;; | |
big-sur|monterey) | |
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" | |
;; | |
*) | |
# 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 | |
if [ -z "${disk_size}" ]; then | |
disk_size="96G" | |
fi | |
;; | |
windows) | |
CPUS="host${CPU_KVM},+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_frequencies${CPU_KVM_EXTRA_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 | |
CPUS="${CPUS},topoext" | |
fi | |
CPU=(-cpu "$CPUS") | |
GUEST_TWEAKS="-no-hpet ${KVM_GUEST_TWEAKS[*]}" | |
# 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="${GUEST_TWEAKS} -global ICH9-LPC.disable_s3=1" | |
if [ -z "${disk_size}" ]; then | |
disk_size="64G" | |
fi | |
SMM="on" | |
;; | |
*) | |
CPU=(-cpu "host${CPU_KVM}") | |
NET_DEVICE="rtl8139" | |
if [ -z "${disk_size}" ]; then | |
disk_size="32G" | |
fi | |
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}" | |
DISK_USED="no" | |
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 | |
# 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 | |
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 | |
if [ "${guest_os}" == "macos" ] || [ "${guest_os}" == "windows" ]; then | |
# Display MSRs alert if the guest is macOS | |
ignore_msrs_alert | |
fi | |
# Has the status quo been requested? | |
if [ "${STATUS_QUO[0]}" == "-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 | |
case ${OUTPUT} in | |
spice|spice-app|none) AUDIO_DEV="spice,id=audio0";; | |
*) AUDIO_DEV="pa,id=audio0,out.mixing-engine=off,out.stream-name=${LAUNCHER}-${VMNAME},in.stream-name=${LAUNCHER}-${VMNAME}";; | |
esac | |
# Determine a sane resolution for Linux guests. | |
if [ "${guest_os}" == "linux" ]; then | |
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 | |
fi | |
# https://www.kraxel.org/blog/2019/09/display-devices-in-qemu/ | |
if [ "${guest_os}" == "linux" ]; then | |
case ${OUTPUT} in | |
none|spice) DISPLAY_DEVICE="qxl-vga";; | |
*) DISPLAY_DEVICE="virtio-vga";; | |
esac | |
elif [ "${guest_os}" == "macos" ]; then | |
# Displays in System Preferences can be used to select a resolution if: | |
# - Mojave only offers 4:3 resolutions | |
# - High Sierra will run at the default 1920x1080 only. | |
# QXL prevents seamless mouse working with a SPICE client | |
# - https://github.com/wimpysworld/quickemu/issues/222 | |
DISPLAY_DEVICE="VGA" | |
elif [ "${guest_os}" == "windows" ]; then | |
DISPLAY_DEVICE="qxl-vga" | |
else | |
DISPLAY_DEVICE="qxl-vga" | |
fi | |
echo -n " - Display: ${OUTPUT^^}, ${DISPLAY_DEVICE}" | |
# Map Quickemu OUTPUT to QEMU -display | |
case ${OUTPUT} in | |
gtk) | |
DISPLAY_RENDER="${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 | |
;; | |
none|spice) | |
DISPLAY_RENDER="none";; | |
cocoa) | |
DISPLAY_RENDER="${OUTPUT}";; | |
*) | |
DISPLAY_RENDER="${OUTPUT},gl=${GL}";; | |
esac | |
if [ "${GL}" == "on" ] && [[ "${DISPLAY_DEVICE}" == *"virtio"* ]]; then | |
if [[ "$MACOS" != "1" ]]; then | |
if [ "${QEMU_VER_SHORT}" -ge 61 ]; then | |
DISPLAY_DEVICE="${DISPLAY_DEVICE}-gl" | |
else | |
DISPLAY_DEVICE="${DISPLAY_DEVICE},virgl=on" | |
fi | |
echo ", GL (${GL}), VirGL (on)" | |
else | |
GL=off | |
echo ", GL (off), VirGL (??)" | |
fi | |
else | |
echo ", GL (${GL}), VirGL (off)" | |
fi | |
# Build the video configuration | |
VIDEO="-device ${DISPLAY_DEVICE}" | |
# Try and coerce the display resolution for Linux guests only. | |
if [ "${guest_os}" == "linux" ]; then | |
VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}" | |
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) VIDEO="${VIDEO},vgamem_mb=64";; | |
esac | |
# Add fullscreen options | |
VIDEO="${VIDEO} ${FULLSCREEN}" | |
# Set the hostname of the VM | |
local NET="user,hostname=${VMNAME}" | |
echo -n "" > "${VMDIR}/${VMNAME}.ports" | |
# Find a free port to expose ssh to the guest | |
local SSH_PORT="" | |
SSH_PORT=$(get_port 22220 9) | |
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}" | |
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" ] || [ "${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 "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 "${FULLSPICY}" | |
SPICE="${SPICE},port=${SPICE_PORT}" | |
fi | |
if [ -n "${PUBLIC}" ]; then | |
case ${guest_os} in | |
macos) | |
# 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/" | |
;; | |
*) | |
echo " - WebDAV: On guest: dav://localhost:9843/";; | |
esac | |
fi | |
fi | |
if [ "${guest_os}" != "windows" ] && [ -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 [[ -e "/usr/sbin/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=() | |
if [[ "$MACOS" != "1" ]]; then | |
# 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[@]}" | |
-smbios type=2,manufacturer="Quickemu Project",product="Quickemu",version="${VERSION}",serial="0xDEADBEEF",location="quickemu.com",asset="${VMNAME}" | |
${VIDEO} -display ${DISPLAY_RENDER} | |
-device usb-ehci,id=input | |
-device usb-kbd,bus=input.0 | |
-device ${MOUSE},bus=input.0 | |
-audiodev ${AUDIO_DEV} | |
-device intel-hda -device hda-duplex,audiodev=audio0 | |
-rtc base=localtime,clock=host,driftfix=slew | |
-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 | |
-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 | |
-chardev spicevmc,id=ccid,name=smartcard | |
-device ccid-card-passthru,chardev=ccid | |
-monitor none | |
-serial mon:stdio) | |
else | |
# for MacOS host | |
args+=(-name "${VMNAME}" -pidfile "${VMDIR}/${VMNAME}.pid" | |
-accel hvf \ | |
-cpu host \ | |
"${CPU[@]}" "${SMP[@]}" | |
-m "${RAM_VM}" "${BALLOON[@]}" | |
-usb \ | |
-device usb-tablet \ | |
-vga virtio \ | |
-display "default,show-cursor=on" \ | |
-device "virtio-net,netdev=vmnic" -netdev "user,id=vmnic" \ | |
-audiodev "coreaudio,id=coreaudio" \ | |
-device ich9-intel-hda -device "hda-output,audiodev=coreaudio" \ | |
-serial mon:stdio) | |
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 | |
if [ -n "${bridge}" ]; then | |
# Enable bridge mode networking | |
# shellcheck disable=SC2054,SC2206 | |
args+=(-nic bridge,br=${bridge},model=virtio-net-pci) | |
else | |
# 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 "${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 [ -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}" == "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 [ "${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 | |
# virtio-blk-pci does not work on MacOS host | |
if [[ "$MACOS" == "1" ]] && [[ "${MAC_DISK_DEV}" == "virtio-blk-pci" ]]; then | |
MAC_DISK_DEV="virtio-blk" | |
fi | |
args+=(-device "${MAC_DISK_DEV},drive=SystemDisk" | |
-drive "id=SystemDisk,if=none,format=qcow2,file=${disk_img}") | |
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}") | |
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}) | |
else | |
# shellcheck disable=SC2054,SC2206 | |
if [[ "$MACOS" != "1" ]]; then | |
args+=(-device "virtio-blk-pci,drive=SystemDisk" | |
-drive "id=SystemDisk,if=none,format=qcow2,file=${disk_img}") | |
else | |
args+=(-device "virtio-blk,drive=SystemDisk" | |
-drive "id=SystemDisk,if=none,format=qcow2,file=${disk_img}") | |
fi | |
fi | |
args+=("${STATUS_QUO[@]}") | |
# https://wiki.qemu.org/Documentation/9psetup | |
# https://askubuntu.com/questions/772784/9p-libvirt-qemu-share-modes | |
if [ "${guest_os}" != "windows" ] && [ -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 [ -z "${MONITOR}" ]; then | |
MONITOR="${monitor:-none}" | |
fi | |
if [ -z "${MONITOR_TELNET_HOST}" ]; then | |
MONITOR_TELNET_HOST="${monitor_telnet_host:-localhost}" | |
fi | |
if [ -z "${MONITOR_TELNET_PORT}" ]; then | |
MONITOR_TELNET_PORT="${monitor_telnet_port}" | |
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 | |
local 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}" | |
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 | |
args+=(-monitor unix:${VM_MONITOR_SOCKETPATH},server,nowait) | |
echo " - Monitor: On host: nc -U \"${VM_MONITOR_SOCKETPATH}\"" | |
echo " or : socat -,echo=0,icanon=0 unix-connect:${VM_MONITOR_SOCKETPATH}" | |
else | |
echo "ERROR! \"${MONITOR}\" is an unknown monitor option." | |
exit 1 | |
fi | |
if [ -z "${SERIAL}" ]; then | |
SERIAL="${serial:-none}" | |
fi | |
if [ -z "${SERIAL_TELNET_HOST}" ]; then | |
SERIAL_TELNET_HOST="${serial_telnet_host:-localhost}" | |
fi | |
if [ -z "${SERIAL_TELNET_PORT}" ]; then | |
SERIAL_TELNET_PORT="${serial_telnet_port}" | |
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) | |
elif [ "${SERIAL}" == "telnet" ]; then | |
# Find a free port to expose serial-telnet to the guest | |
local 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}" | |
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 | |
args+=(-serial unix:${VM_SERIAL_SOCKETPATH},server,nowait) | |
echo " - Serial: On host: nc -U \"${VM_SERIAL_SOCKETPATH}\"" | |
echo " or : socat -,echo=0,icanon=0 unix-connect:${VM_SERIAL_SOCKETPATH}" | |
else | |
echo "ERROR! \"${SERIAL}\" is an unknown serial option." | |
exit 1 | |
fi | |
if [ -z "${EXTRA_ARGS}" ]; then | |
EXTRA_ARGS="${extra_args}" | |
fi | |
if [ -n "${EXTRA_ARGS}" ]; then | |
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 [ ${VM_UP} -eq 0 ]; then | |
echo "${QEMU}" "${SHELL_ARGS}" >> "${VMDIR}/${VMNAME}.sh" | |
${QEMU} "${args[@]}" > "${VMDIR}/${VMNAME}.log" & | |
sleep 0.25 | |
fi | |
echo " - Process: Starting ${VM} as ${VMNAME} ($(cat "${VMDIR}/${VMNAME}.pid"))" | |
} | |
function start_viewer { | |
errno=0 | |
if [ "${VIEWER}" != "none" ]; then | |
echo "---" | |
# If output is 'none' then SPICE was requested. | |
if [ "${OUTPUT}" == "spice" ]; then | |
if [ "${VIEWER}" == "remote-viewer" ]; then | |
# show via viewer: remote-viewer | |
if [ -n "${PUBLIC}" ]; then | |
echo " - Start viewer: ${VIEWER} --title \"${VMNAME}\" --spice-shared-dir \"${PUBLIC}\" ${FULLSPICY} \"spice://localhost:${SPICE_PORT}\" >/dev/null 2>&1 &" | |
${VIEWER} --title "${VMNAME}" --spice-shared-dir "${PUBLIC}" ${FULLSPICY} "spice://localhost:${SPICE_PORT}" >/dev/null 2>&1 & | |
errno=$? | |
else | |
echo " - Start viewer: ${VIEWER} --title \"${VMNAME}\" ${FULLSPICY} \"spice://localhost:${SPICE_PORT}\" >/dev/null 2>&1 &" | |
${VIEWER} --title "${VMNAME}" ${FULLSPICY} "spice://localhost:${SPICE_PORT}" >/dev/null 2>&1 & | |
errno=$? | |
fi | |
elif [ "${VIEWER}" == "spicy" ]; then | |
# show via viewer: spicy | |
if [ -n "${PUBLIC}" ]; then | |
echo " - Start viewer: ${VIEWER} --title \"${VMNAME}\" --port \"${SPICE_PORT}\" --spice-shared-dir \"${PUBLIC}\" \"${FULLSPICY}\" >/dev/null 2>&1 &" | |
${VIEWER} --title "${VMNAME}" --port "${SPICE_PORT}" --spice-shared-dir "${PUBLIC}" "${FULLSPICY}" >/dev/null 2>&1 & | |
errno=$? | |
else | |
echo " - Start viewer: ${VIEWER} --title \"${VMNAME}\" --port \"${SPICE_PORT}\" \"${FULLSPICY}\" >/dev/null 2>&1 &" | |
${VIEWER} --title "${VMNAME}" --port "${SPICE_PORT}" "${FULLSPICY}" >/dev/null 2>&1 & | |
errno=$? | |
fi | |
fi | |
if [ $errno -ne 0 ]; then | |
echo "WARNING! Could not start viewer(${VIEWER}) Err: $errno" | |
fi | |
fi | |
fi | |
sleep 0.25 | |
echo " - Process: Starting ${VM} as ${VMNAME} ($(cat "${VMDIR}/${VMNAME}.pid"))" | |
} | |
function shortcut_create { | |
local dirname="${HOME}/.local/share/applications" | |
local filename="${HOME}/.local/share/applications/${VMNAME}.desktop" | |
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 "Created ${VMNAME}.desktop file" | |
} | |
function usage() { | |
echo | |
echo "Usage" | |
echo " ${LAUNCHER} --vm ubuntu.conf" | |
echo | |
echo "You can also pass optional parameters" | |
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 it's configuration" | |
echo " --display : Select display backend. 'sdl' (default), 'gtk', 'none', or 'spice'" | |
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 " --screen <screen> : Use specified screen to determine the window size." | |
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', 'cocoa', 'none'" | |
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." | |
echo " --mouse <type> : Set mouse. @Options: 'tablet' (default), 'ps2', 'usb', 'virtio'" | |
echo " --usb-controller <type> : Set usb-controller. @Options: 'ehci' (default), 'xhci', 'none'" | |
echo " --extra_args <arguments> : Pass additional arguments to qemu" | |
echo " --version : Print version" | |
exit 1 | |
} | |
function display_param_check() { | |
# @ASK: accept "spice-app" as output ? | |
if [ "${OUTPUT}" != "gtk" ] && [ "${OUTPUT}" != "none" ] && [ "${OUTPUT}" != "sdl" ] && [ "${OUTPUT}" != "spice" ] && [ "${OUTPUT}" != "cocoa" ]; then | |
echo "ERROR! Requested output '${OUTPUT}' 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 | |
# Check for right outout for MacOS | |
elif [[ "$MACOS" == "1" ]] && [[ "${OUTPUT}" != "cocoa" ]]; then | |
>&2 echo "ERROR! Requested ${OUTPUT} display, but only cocoa avalible on MacOS." | |
exit 1 | |
fi | |
} | |
function parse_ports_from_file { | |
local FILE="${VMDIR}/${VMNAME}.ports" | |
# parse ports | |
local port_name=( $(cat "$FILE" | cut -d, -f1) ) | |
local port_number=( $(cat "$FILE" | cut -d, -f2) ) | |
local host_name=( $(cat "$FILE" | gawk 'FS="," {print $3,"."}') ) | |
for ((i=0; i<${#port_name[@]}; i++)); do | |
if [ "${port_name[$i]}" == "ssh" ]; then | |
SSH_PORT="${port_number[$i]}" | |
elif [ "${port_name[$i]}" == "spice" ]; then | |
SPICE_PORT="${port_number[$i]}" | |
elif [ "${port_name[$i]}" == "monitor-telnet" ]; then | |
MONITOR_TELNET_PORT="${port_number[$i]}" | |
MONITOR_TELNET_HOST="${host_name[$i]}" | |
elif [ "${port_name[$i]}" == "serial-telnet" ]; then | |
SERIAL_TELNET_PORT="${port_number[$i]}" | |
SERIAL_TELNET_HOST="${host_name[$i]}" | |
fi | |
done | |
} | |
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 | |
# Determine monitor channel | |
local monitor_channel="" | |
if [ -S "${VMDIR}/${VMNAME}-monitor.socket" ]; then | |
monitor_channel="socket" | |
elif [ -n "${MONITOR_TELNET_PORT}" ] && [ -n "${MONITOR_TELNET_HOST}" ]; then | |
monitor_channel="telnet" | |
else | |
echo "WARNING! No qemu-monitor channel available - Couldn't send message to monitor!" | |
return | |
fi | |
case "${monitor_channel}" in | |
socket) | |
echo -e " - MON-SEND: ${MSG}" | |
echo -e "${MSG}" | socat -,shut-down unix-connect:"${VM_MONITOR_SOCKETPATH}" 2>&1 > /dev/null | |
;; | |
telnet) | |
echo -e " - MON-SEND: ${MSG}" | |
echo -e "${MSG}" | socat - tcp:"${MONITOR_TELNET_HOST}":"${MONITOR_TELNET_PORT}" 2>&1 > /dev/null | |
;; | |
*) | |
echo "ERROR! This should never happen!" | |
exit 1 | |
;; | |
esac | |
return 0 | |
} | |
### MAIN | |
# Lowercase variables are used in the VM config file only | |
boot="efi" | |
bridge="" | |
cpu_cores="" | |
disk_img="" | |
disk_size="" | |
extra_args="" | |
fixed_iso="" | |
floppy="" | |
guest_os="linux" | |
img="" | |
iso="" | |
macos_release="" | |
port_forwards=() | |
preallocation="off" | |
ram="" | |
secureboot="off" | |
tpm="off" | |
usb_devices=() | |
viewer="spicy" | |
ssh_port="" | |
spice_port="" | |
public_dir="" | |
monitor="socket" | |
monitor_telnet_port="4440" | |
monitor_telnet_host="localhost" | |
monitor_cmd="" | |
serial="socket" | |
serial_telnet_port="6660" | |
serial_telnet_host="localhost" | |
# options: ehci(USB2.0), xhci(USB3.0) | |
usb_controller="ehci" | |
# options: ps2, usb, virtio | |
keyboard="usb" | |
keyboard_layout="en-us" | |
# options: ps2, usb, tablet, virtio | |
mouse="tablet" | |
# options: ehci, xhci | |
usb_controller="ehci" | |
BRAILLE="" | |
DELETE_DISK=0 | |
DELETE_VM=0 | |
FULLSCREEN="" | |
FULLSPICY="" | |
OUTPUT="" | |
PUBLIC="" | |
PUBLIC_PERMS="" | |
PUBLIC_TAG="" | |
SCREEN="" | |
SHORTCUT=0 | |
SNAPSHOT_ACTION="" | |
SNAPSHOT_TAG="" | |
STATUS_QUO=() | |
USB_PASSTHROUGH="" | |
VM="" | |
VMDIR="" | |
VMNAME="" | |
VMPATH="" | |
VIEWER="" | |
SSH_PORT="" | |
SPICE_PORT="" | |
MONITOR="" | |
MONITOR_TELNET_PORT="" | |
MONITOR_TELNET_HOST="" | |
MONITOR_CMD="" | |
VM_MONITOR_SOCKETPATH="" | |
VM_SERIAL_SOCKETPATH="" | |
SERIAL="" | |
SERIAL_TELNET_PORT="" | |
SERIAL_TELNET_HOST="" | |
KEYBOARD="" | |
KEYBOARD_LAYOUT="" | |
MOUSE="" | |
USB_CONTROLLER="" | |
EXTRA_ARGS="" | |
# shellcheck disable=SC2155 | |
readonly LAUNCHER=$(basename "${0}") | |
readonly DISK_MIN_SIZE=$((197632 * 8)) | |
readonly VERSION="3.16" | |
# PUBLICSHARE is the only directory exposed to guest VMs for file | |
# sharing via 9P, spice-webdavd and Samba. This path is not configurable. | |
if command -v xdg-user-dir &>/dev/null; then | |
PUBLIC=$(xdg-user-dir PUBLICSHARE) | |
if [ "${PUBLIC%/}" != "${HOME}" ]; then | |
if [ ! -d "${PUBLIC}" ]; then | |
mkdir -p "${PUBLIC}" | |
fi | |
PUBLIC_TAG="Public-${USER,,}" | |
# shellcheck disable=SC2012 | |
PUBLIC_PERMS=$(ls -ld "${PUBLIC}" | cut -d' ' -f1) | |
else | |
PUBLIC="" | |
fi | |
fi | |
# 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 | |
exit 0 | |
else | |
while [ $# -gt 0 ]; do | |
case "${1}" in | |
-braille|--braille) | |
BRAILLE="on" | |
shift;; | |
-delete|--delete|-delete-disk|--delete-disk) | |
DELETE_DISK=1 | |
shift;; | |
-delete-vm|--delete-vm) | |
DELETE_VM=1 | |
shift;; | |
-display|--display) | |
OUTPUT="${2}" | |
display_param_check | |
shift | |
shift;; | |
-fullscreen|--fullscreen|-full-screen|--full-screen) | |
FULLSCREEN="-full-screen" | |
FULLSPICY="--full-screen" | |
shift;; | |
-ignore-msrs-always|--ignore-msrs-always) | |
ignore_msrs_always | |
exit;; | |
-screen|--screen) | |
SCREEN="${2}" | |
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;; | |
-shortcut|--shortcut) | |
SHORTCUT=1 | |
shift;; | |
-vm|--vm) | |
VM="${2}" | |
shift | |
shift;; | |
-viewer|--viewer) | |
VIEWER="${2}" | |
shift | |
shift;; | |
-ssh-port|--ssh-port) | |
SSH_PORT="${2}" | |
shift; | |
shift;; | |
-spice-port|--spice-port) | |
SPICE_PORT="${2}" | |
shift; | |
shift;; | |
-public-dir|--public-dir) | |
PUBLIC="${2}" | |
shift; | |
shift;; | |
-monitor|--monitor) | |
MONITOR="${2}" | |
shift; | |
shift;; | |
-monitor-cmd|--monitor-cmd) | |
MONITOR_CMD="${2}" | |
shift; | |
shift;; | |
-monitor-telnet-host|--monitor-telnet-host) | |
MONITOR_TELNET_HOST="${2}" | |
shift; | |
shift;; | |
-monitor-telnet-port|--monitor-telnet-port) | |
MONITOR_TELNET_PORT="${2}" | |
shift; | |
shift;; | |
-serial|--serial) | |
SERIAL="${2}" | |
shift; | |
shift;; | |
-serial-telnet-host|--serial-telnet-host) | |
SERIAL_TELNET_HOST="${2}" | |
shift; | |
shift;; | |
-serial-telnet-port|--serial-telnet-port) | |
SERIAL_TELNET_PORT="${2}" | |
shift; | |
shift;; | |
-keyboard|--keyboard) | |
KEYBOARD="${2}" | |
shift; | |
shift;; | |
-mouse|--mouse) | |
MOUSE="${2}" | |
shift; | |
shift;; | |
-usb-controller|--usb-controller) | |
USB_CONTROLLER="${2}" | |
shift; | |
shift;; | |
-extra_args|--extra_args) | |
EXTRA_ARGS="${2}" | |
shift; | |
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 | |
VMDIR=$(dirname "${disk_img}") | |
VMNAME=$(basename "${VM}" .conf) | |
VMPATH=$(realpath "$(dirname "${VM}")") | |
VM_MONITOR_SOCKETPATH="${VMDIR}/${VMNAME}-monitor.socket" | |
VM_SERIAL_SOCKETPATH="${VMDIR}/${VMNAME}-serial.socket" | |
# 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 | |
if [ -z "${OUTPUT}" ]; then | |
# Braille support requires SDL. Override OUTPUT if braille was requested. | |
if [ -n "${BRAILLE}" ]; then | |
OUTPUT="sdl" | |
elif [ -z "${display}" ]; then | |
OUTPUT="sdl" | |
else | |
OUTPUT="${display}" | |
display_param_check | |
fi | |
fi | |
if [ -z "${VIEWER}" ]; then | |
VIEWER="${viewer}" | |
fi | |
viewer_param_check | |
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 [ -z "${SSH_PORT}" ]; then | |
SSH_PORT=${ssh_port} | |
fi | |
if [ -n "${SSH_PORT}" ] && ! is_numeric "${SSH_PORT}"; then | |
echo "ERROR: ssh-port must be a number!" | |
exit 1 | |
fi | |
if [ -z "${SPICE_PORT}" ]; then | |
SPICE_PORT=${spice_port} | |
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=0 | |
VM_UP=0 | |
if [ -r "${VMDIR}/${VMNAME}.pid" ]; then | |
VM_PID=$(head -c50 "${VMDIR}/${VMNAME}.pid") | |
kill -0 ${VM_PID} 2>&1 >/dev/null | |
if [ $? -eq 0 ]; then | |
echo "VM already started!" | |
VM_UP=1 | |
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 [ ${DELETE_DISK} -eq 1 ]; then | |
delete_disk | |
exit | |
fi | |
if [ ${DELETE_VM} -eq 1 ]; then | |
delete_vm | |
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 | |
if [ $VM_UP -eq 0 ]; then | |
vm_boot | |
start_viewer | |
else | |
parse_ports_from_file | |
start_viewer | |
fi | |
[ -n "${MONITOR_CMD}" ] && monitor_send_cmd "${MONITOR_CMD}" | |
# vim:tabstop=2:shiftwidth=2:expandtab |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment