mirror of
https://github.com/oSoWoSo/DistroHopper.git
synced 2024-08-14 22:46:53 +00:00
e97b57352c
The QEMU manpage states: "This backend is available on most systems, but you should use your platform’s native backend if possible."
1812 lines
60 KiB
Bash
Executable file
1812 lines
60 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 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 [ -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 OSK=""
|
|
local SMM="${SMM:-off}"
|
|
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 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=$(lscpu | grep '^Model name:' | cut -d':' -f2 | sed 's/ //g')
|
|
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
|
|
|
|
# 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=""
|
|
RAM_HOST=$(free --mega -h | grep Mem | cut -d':' -f2 | cut -d'G' -f1 | sed 's/ //g')
|
|
#Round up - https://github.com/wimpysworld/quickemu/issues/11
|
|
RAM_HOST=$(printf '%.*f\n' 0 "${RAM_HOST}")
|
|
if [ "${RAM_HOST}" -ge 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,,}
|
|
|
|
if [ "${guest_os}" == "macos" ] || [ "${guest_os}" == "windows" ]; 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"
|
|
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)
|
|
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"
|
|
)
|
|
;;
|
|
*)
|
|
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
|
|
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 [ ! -z "${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)
|
|
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"
|
|
fi
|
|
|
|
if [ -z "${disk_size}" ]; then
|
|
disk_size="16G"
|
|
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)
|
|
#https://www.nicksherlock.com/2020/06/installing-macos-big-sur-on-proxmox/
|
|
# A CPU with SSE4.1 support is required for >= macOS Sierra
|
|
# A CPU with AVX2 support is required for >= macOS Ventura
|
|
case ${macos_release} in
|
|
ventura)
|
|
if check_cpu_flag sse4_1 && check_cpu_flag avx2; then
|
|
CPU="-cpu Haswell,kvm=on,vendor=GenuineIntel,+hypervisor,+invtsc,+kvm_pv_eoi,+kvm_pv_unhalt"
|
|
else
|
|
echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.1 and AVX2 support."
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
if check_cpu_flag sse4_1; then
|
|
# Used in past versions: +movbe,+smep,+xgetbv1,+xsavec,+avx2
|
|
# Warn on AMD: +fma4,+pcid
|
|
CPU="-cpu Penryn,kvm=on,vendor=GenuineIntel,+aes,+avx,+bmi1,+bmi2,+fma,+hypervisor,+invtsc,+kvm_pv_eoi,+kvm_pv_unhalt,+popcnt,+ssse3,+sse4.2,vmware-cpuid-freq=on,+xsave,+xsaveopt,check"
|
|
else
|
|
echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.1 support."
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
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 -global kvm-pit.lost_tick_policy=discard -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|ventura)
|
|
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)
|
|
CPU="-cpu host,kvm=on,+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_frequencies,kvm_pv_unhalt,hv_reenlightenment,hv_relaxed,hv_spinlocks=8191,hv_stimer,hv_synic,hv_time,hv_vapic,hv_vendor_id=1234567890ab,hv_vpindex"
|
|
if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then
|
|
CPU="${CPU},topoext"
|
|
fi
|
|
GUEST_TWEAKS="-no-hpet -global kvm-pit.lost_tick_policy=discard"
|
|
|
|
# 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,kvm=on"
|
|
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
|
|
|
|
# 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 ${OUTPUT} in
|
|
none|spice|spice-app) AUDIO_DEV="spice,id=audio0";;
|
|
*) AUDIO_DEV="pa,id=audio0";;
|
|
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|spice-app)
|
|
DISPLAY_DEVICE="virtio-gpu";;
|
|
*)
|
|
DISPLAY_DEVICE="virtio-vga";;
|
|
esac
|
|
elif [ "${guest_os}" == "macos" ]; then
|
|
# qxl-vga supports seamless mouse and sane resolutions if only one scanout
|
|
# is used. Which is whay '-vga none' is added to the QEMU command line.
|
|
DISPLAY_DEVICE="qxl-vga"
|
|
elif [ "${guest_os}" == "windows" ]; then
|
|
case ${OUTPUT} in
|
|
# 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
|
|
gtk|none|spice) DISPLAY_DEVICE="qxl-vga";;
|
|
sdl|spice-app) DISPLAY_DEVICE="virtio-vga";;
|
|
esac
|
|
else
|
|
DISPLAY_DEVICE="qxl-vga"
|
|
fi
|
|
|
|
# Map Quickemu OUTPUT to QEMU -display
|
|
case ${OUTPUT} in
|
|
gtk) DISPLAY_RENDER="${OUTPUT},grab-on-hover=on,zoom-to-fit=off,gl=${gl}";;
|
|
none|spice) DISPLAY_RENDER="none";;
|
|
sdl) DISPLAY_RENDER="${OUTPUT},gl=${gl}";;
|
|
spice-app) DISPLAY_RENDER="${OUTPUT},gl=${gl}";;
|
|
*) DISPLAY_RENDER="${OUTPUT}";;
|
|
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 " - Display: ${OUTPUT^^}, ${DISPLAY_DEVICE}, GL (${gl}), VirGL (on)"
|
|
else
|
|
echo " - Display: ${OUTPUT^^}, ${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 [ "${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
|
|
|
|
# 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}"
|
|
|
|
# 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 [ "${OUTPUT}" == "none" ] || [ "${OUTPUT}" == "spice" ] || [ "${OUTPUT}" == "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 [ "${OUTPUT}" == "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
|
|
|
|
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},addr=127.0.0.1"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ -n "${PUBLIC}" ]; then
|
|
case ${guest_os} in
|
|
macos)
|
|
if [ "${OUTPUT}" == "none" ] || [ "${OUTPUT}" == "spice" ] || [ "${OUTPUT}" == "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" ] && [ -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=()
|
|
|
|
# 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}
|
|
-audiodev ${AUDIO_DEV}
|
|
-device intel-hda -device hda-duplex,audiodev=audio0
|
|
-rtc base=localtime,clock=host,driftfix=slew)
|
|
|
|
# Only enable SPICE is using SPICE display
|
|
if [ "${OUTPUT}" == "none" ] || [ "${OUTPUT}" == "spice" ] || [ "${OUTPUT}" == "spice-app" ]; then
|
|
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
|
|
|
|
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
|
|
-chardev spicevmc,id=ccid,name=smartcard
|
|
-device ccid-card-passthru,chardev=ccid
|
|
)
|
|
|
|
# setup usb-controller
|
|
[ -z "${USB_CONTROLLER}" ] && USB_CONTROLLER="$usb_controller"
|
|
if [ "${USB_CONTROLLER}" == "ehci" ]; then
|
|
args+=(-device usb-ehci,id=input)
|
|
elif [ "${USB_CONTROLLER}" == "xhci" ]; then
|
|
args+=(-device qemu-xhci,id=input)
|
|
elif [ -z "${USB_CONTROLLER}" ] || [ "${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
|
|
[ -z "${KEYBOARD}" ] && KEYBOARD="$keyboard"
|
|
if [ "${KEYBOARD}" == "usb" ]; then
|
|
args+=(-device usb-kbd,bus=input.0)
|
|
elif [ "${KEYBOARD}" == "virtio" ]; then
|
|
args+=(-device virtio-keyboard)
|
|
elif [ "${KEYBOARD}" == "ps2" ] || [ -z "${KEYBOARD}" ]; 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.
|
|
[ -z "${KEYBOARD_LAYOUT}" ] && KEYBOARD_LAYOUT="$keyboard_layout"
|
|
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
|
|
[ -z "${MOUSE}" ] && MOUSE="$mouse"
|
|
if [ "${MOUSE}" == "usb" ]; then
|
|
args+=(-device usb-mouse,bus=input.0)
|
|
elif [ "${MOUSE}" == "tablet" ]; then
|
|
args+=(-device usb-tablet,bus=input.0)
|
|
elif [ "${MOUSE}" == "virtio" ]; then
|
|
args+=(-device virtio-mouse)
|
|
elif [ "${MOUSE}" == "ps2" ] || [ -z "${MOUSE}" ]; then
|
|
# add nothing, default is ps/2 mouse
|
|
:
|
|
else
|
|
echo "WARNING! Unknown mouse value: '${MOUSE}; Fallback to ps2'"
|
|
fi
|
|
|
|
# $bridge backwards compatibility for Quickemu <= 4.0
|
|
if [ -n "${bridge}" ]; then
|
|
network="${bridge}"
|
|
fi
|
|
|
|
if [ "${network}" == "none" ]; then
|
|
# Disbale 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}")
|
|
|
|
else
|
|
# shellcheck disable=SC2054,SC2206
|
|
args+=(-device virtio-blk-pci,drive=SystemDisk
|
|
-drive id=SystemDisk,if=none,format=qcow2,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" ] && [ -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
|
|
|
|
# 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 " - 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 " - 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 " - 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 " - 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
|
|
}
|
|
|
|
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', '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 " --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', '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() {
|
|
if [ "${OUTPUT}" != "gtk" ] && [ "${OUTPUT}" != "none" ] && [ "${OUTPUT}" != "sdl" ] && [ "${OUTPUT}" != "spice" ] && [ "${OUTPUT}" != "spice-app" ]; 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
|
|
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 " - Sending: ${MSG}"
|
|
echo -e "${MSG}" | socat -,shut-down unix-connect:"${VM_MONITOR_SOCKETPATH}" 2>&1 > /dev/null
|
|
;;
|
|
telnet)
|
|
echo -e " - Sending: ${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"
|
|
cpu_cores=""
|
|
disk_img=""
|
|
disk_size=""
|
|
display=""
|
|
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="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"
|
|
|
|
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="4.2"
|
|
|
|
# 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 [ -n "${display}" ]; then
|
|
OUTPUT="${display}"
|
|
fi
|
|
|
|
# Set the default OUTPUT if not provided by user
|
|
if [ -z "${OUTPUT}" ]; then
|
|
OUTPUT="sdl"
|
|
fi
|
|
|
|
# Braille support requires SDL. Override OUTPUT if braille was requested.
|
|
if [ -n "${BRAILLE}" ]; then
|
|
OUTPUT="sdl"
|
|
fi
|
|
display_param_check
|
|
|
|
if [ -z "${VIEWER}" ]; then
|
|
VIEWER="${viewer}"
|
|
fi
|
|
viewer_param_check
|
|
|
|
# Set the default 3D acceleration.
|
|
if [ -z "${gl}" ]; then
|
|
gl="on"
|
|
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 [ -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
|
|
# 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
|
|
start_viewer
|
|
else
|
|
parse_ports_from_file
|
|
start_viewer
|
|
fi
|
|
|
|
[ -n "${MONITOR_CMD}" ] && monitor_send_cmd "${MONITOR_CMD}"
|
|
|
|
# vim:tabstop=2:shiftwidth=2:expandtab
|