#!/bin/bash # SPDX-License-Identifier: MIT # Virtual machine control script for QEMU/KVM VMs. # Tested on Gentoo (~amd64/hardened) with qemu-9.0 # # You can find the newest version probably on public git repo: # https://git.holgersson.xyz/nfr/control-vm # If that link does not work, feel free to drop me an email. # For version see variable below comment section. # Author: Nils Freydank # License: MIT # === Setup === # On Gentoo/Linux the following packages are necessary: # app-emulation/qemu net-misc/socat app-emulation/virt-viewer # -- the latter one with USE="vnc spice" -- # plus optionally # sys-firmware/edk2-ovmf via USE="binary" for qemu if EFI is used. # Obviously, bash is also necessary ;) # === How to use this script === # Create a directory to store your VM images and metadata. # Let it be readable and writeable for the user who executes QEMU. # Copy this script into that directory. # For each Guest do the following: # Create a directory named exactly as the guest (case sensitive). # Create an qcow2 image named virtual-machine.img inside # the newly created directory. # Create a configuration file named exactly as the guest plus a suffix '.bash'. # Fill in the data from the example below into the configuration file. # Control the vm by calling: ./ , # e.g. ./control-vm.sh Gentoo-guest start # or ./control-vm.sh Gentoo-guest connect # or ./control-vm.sh Gentoo-guest shudown # # You should now have a setup like: # $(pwd)/ # $(pwd)/control-vm.sh # $(pwd)/Gentoo-guest-1/virtual-machine.img # $(pwd)/Gentoo-guest-1/virtual-machine.bash # === Configuration of the guest === # Create a file with the guest's parameters and name it same as the "GUEST_NAME" # plus a ".bash" suffix, so we can easily assume the name later. # # cat << _EOF > "${GUEST_NAME}/virtual-machine.bash" # # Provide the GUEST_NAME which is used in further configuration. # GUEST_NAME="Guest-OS" # # Configure the number of CPUs. Simplified and based on # # `qemu-system- -smp help`. # CPU_CORES="8" # MEMORY="16384" # NET="True" # SOUND="True" # SSH_PORT="" # # Configure any of the arches QEMU supports in QEMU's notation. # # Leave it empty or omit it for x86_64. # ARCH="" # # Disable EFI and enable BIOS fallback mode with unsetting EFI_MACHINE. # #EFI="" # # Enable the SPICE socket. # UI="True" # CLIPBOARD="True" # _EOF __VERSION__="1.0.0" function set_params(){ # === Define default values === CLIPBOARD="${CLIPBOARD:-}" CPU_CORES="${CPU_CORES:-8}" EFI="${EFI:-True}" GUEST_NAME="${1}" LIVE_ONLY="${LIVE_ONLY:-}" # Keep low default aligned with oldest tested version. # Feel free to bump to latest version that you QEMU supports. MACHINE="${MACHINE:-pc-q35-6.0}" MEMORY="${MEMORY:-16384}" NET="${NET:-}" NETDEV_NAME="vmnic-${GUEST_NAME}" SOUND="${SOUND:-}" SSH_PORT="${SSH_PORT:-}" UI="${UI:-}" # Define paths. # Note: Sort in alphabetical order as long as inheritance is respected. BASE_PATH="$(pwd)/${GUEST_NAME}" GUEST_CONFIG_FILE="${GUEST_CONFIG_FILE:-${BASE_PATH}/virtual-machine.bash}" IMAGE_PATH="${IMAGE_PATH:-${BASE_PATH}/virtual-machine.img}" ISO_PATH="${ISO_PATH:-${BASE_PATH}/../installer/${GUEST_NAME}.iso}" MONITOR_SOCKET="${MONITOR_SOCKET:-${BASE_PATH}/monitor.socket}" PID_FILE="${PID_FILE:-${BASE_PATH}/qemu.pid}" QMP_SOCKET="${QMP_SOCKET:-${BASE_PATH}/qmp.socket}" SNAPSHOT_INFO_FILE="${SNAPSHOT_INFO_FILE:-${BASE_PATH}/last-snapshot-info}" SPICE_SOCKET="${SPICE_SOCKET:-${BASE_PATH}/spice.socket}" # === Source the guest configuration to overwrite defaults if necessary. === if [[ -f "${GUEST_CONFIG_FILE}" ]]; then source "${GUEST_CONFIG_FILE}" else echo " error: No configuration found. Aborting." exit 3 fi # === Define a base and glue everything together. === QEMU_COMMON_ARGS=( # Provide a pretty name for the guest. -name "${GUEST_NAME}" # Disable some less document user configuration loading. -no-user-config # Configure some seccomp mode 2 filters. -sandbox on,obsolete=deny,elevateprivileges=allow,spawn=allow,resourcecontrol=allow # Enable KVM and hardware acceleration. -enable-kvm # Use the same CPU as the host has for maximum performoance and configure # the amount of memory and CPU cores as configured per-host. -cpu host -smp cores="${CPU_CORES}" -m "${MEMORY}" # Detach from the current shell and monitor by pid file and a unix socket. -daemonize -pidfile "${PID_FILE}" -monitor unix:"${MONITOR_SOCKET}",server,nowait -qmp-pretty unix:"${QMP_SOCKET}",server,nowait ) QEMU_DISK_ARGS=( -drive id=hd0,media=disk,if=virtio,file="${IMAGE_PATH}",aio=io_uring,cache=none,cache-size=16M,discard=on ) QEMU_DISPLAY_NONE_ARGS=( -vga none -display none ) QEMU_DISPLAY_VIRTIO_ARGS=( -vga virtio ) QEMU_DISPLAY_STD_ARGS=( -vga std ) QEMU_DISPLAY_SPICE_ARGS=( -spice unix=on,addr="${SPICE_SOCKET}",disable-ticketing=on,gl=off ) QEMU_CLIPBOARD_SPICE_ARGS=( -device virtio-serial-pci -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0 -chardev spicevmc,id=spicechannel0,name=vdagent ) QEMU_EFI_ARGS=( -M "${MACHINE}",accel=kvm -drive file=/usr/share/edk2-ovmf/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on ) QEMU_BIOS_ARGS=( -M "${MACHINE}",accel=kvm ) QEMU_ISO_ARGS=( -cdrom "${ISO_PATH}" ) QEMU_NET_ARGS=( -device virtio-net,netdev="${NETDEV_NAME}" -netdev user,id="${NETDEV_NAME}" ) QEMU_SOUND_ARGS=( -device ich9-intel-hda -device hda-duplex ) QEMU_SSH_ARGS=( -device virtio-net,netdev="${NETDEV_NAME}" -netdev user,id="${NETDEV_NAME}",hostfwd=tcp:127.0.0.1:"${SSH_PORT}"-:22 ) # === Start the final QEMU parameter array. === QEMU_ARGS=( "${QEMU_COMMON_ARGS[@]}" ) if [[ "${EFI}" ]]; then QEMU_ARGS+=( "${QEMU_EFI_ARGS[@]}" ) else QEMU_ARGS+=( "${QEMU_BIOS_ARGS[@]}" ) fi # The following block checks only agains live vs non-live. if [[ "${LIVE_ONLY}" ]]; then QEMU_ARGS+=( "${QEMU_ISO_ARGS[@]}" # d: CD-ROM -boot d ) else QEMU_ARGS+=( "${QEMU_DISK_ARGS[@]}" # c: hard disk -boot c ) fi # [[ -n "${NET}" && -z "${SSH_PORT}" ]] && QEMU_ARGS+=( "${QEMU_NET_ARGS[@]}" ) [[ -n "${SOUND}" ]] && QEMU_ARGS+=( "${QEMU_SOUND_ARGS[@]}" ) [[ -n "${SSH_PORT}" ]] && QEMU_ARGS+=( "${QEMU_SSH_ARGS[@]}" ) if [[ -n "${UI}" ]]; then case "${UI}" in std|stdvga ) QEMU_ARGS+=( "${QEMU_DISPLAY_STD_ARGS[@]}" );; *) QEMU_ARGS+=( "${QEMU_DISPLAY_VIRTIO_ARGS[@]}" );; esac QEMU_ARGS+=( "${QEMU_DISPLAY_SPICE_ARGS[@]}" ) # Only check for clipboard support here as clipoard w/o UI doesn't make # sense in this context. if [[ -n "${CLIPBOARD}" ]]; then QEMU_ARGS+=( "${QEMU_CLIPBOARD_SPICE_ARGS[@]}" ) fi else QEMU_ARGS+=( "${QEMU_DISPLAY_NONE_ARGS[@]}" ) fi } # === Define functions for the actual QEMU controlling. === function start_qemu(){ if [[ ! -f "${PID_FILE}" ]]; then echo "Starting VM ${GUEST_NAME}." # Define the default architecture. [[ -z "${ARCH}" ]] && ARCH="x86_64" # Check the existance of the QEMU binary. if [[ -x "/usr/bin/qemu-system-${ARCH}" ]]; then /usr/bin/qemu-system-"${ARCH}" "${QEMU_ARGS[@]}" else echo "QEMU has target ${ARCH} not installed." echo "Aborting." exit 2 fi else echo "VM ${GUEST_NAME} seems to be running." echo "See ${PID_FILE} for the process ID." fi } function shutdown_qemu(){ if [[ -f "${PID_FILE}" ]]; then echo "Requesting shutdown for VM ${GUEST_NAME}." echo "system_powerdown" | socat - unix-connect:"${MONITOR_SOCKET}" else echo "Looks as VM ${GUEST_NAME} is already stopped." fi } function stop_qemu(){ if [[ -f "${PID_FILE}" ]]; then echo "Forcing shutdown of VM ${GUEST_NAME}." echo "quit" | socat - unix-connect:"${MONITOR_SOCKET}" else echo "Looks as VM ${GUEST_NAME} is already stopped." fi } function connect_to_vm(){ if [[ -f "${PID_FILE}" ]]; then echo "Opening a GUI for ${GUEST_NAME}." local REMOTE_VIEWER_OPTS=( --title "${GUEST_NAME}" --auto-resize=always "spice+unix://${SPICE_SOCKET}" ) # Send the viewer into background and drop # all annoying GTK error messages to /dev/null. ( remote-viewer "${REMOTE_VIEWER_OPTS[@]}" &> /dev/null ) & # Alternativly run spicy which is part of spice-gtk # e.g. as 'spicy --uri="spice+unix://${SPICE_SOCKET}"' # or any other client like remmina. Note that _that_ # one doesn't support unix sockets yet (as of 2021-09-30): # https://gitlab.com/Remmina/Remmina/-/issues/1677 else echo "Looks as VM ${GUEST_NAME} is not running." fi } function save_qemu(){ if [[ -f "${PID_FILE}" ]]; then SNAPSHOT_NAME="snap$(date --utc +%s)" echo "Saving state of VM ${GUEST_NAME}." echo "savevm ${SNAPSHOT_NAME}" | socat - unix-connect:"${MONITOR_SOCKET}" echo "${SNAPSHOT_NAME}" > "${SNAPSHOT_INFO_FILE}" else echo "Guest ${GUEST_NAME} is not running." fi } function pause_qemu(){ if [[ -f "${PID_FILE}" ]]; then echo "Pausing the VM ${GUEST_NAME}." echo "stop" | socat - unix-connect:"${MONITOR_SOCKET}" else echo "Guest ${GUEST_NAME} not running, nothing to pause." fi } function restore_qemu(){ if [[ -f "${PID_FILE}" ]]; then SNAPSHOT_NAME="$(cat "${SNAPSHOT_INFO_FILE}")" echo "Restore state of VM ${GUEST_NAME}." pause_qemu echo "loadvm ${SNAPSHOT_NAME}" | socat - unix-connect:"${MONITOR_SOCKET}" resume_qemu else echo "Guest ${GUEST_NAME} is not running, nothing to restore." fi } function resume_qemu(){ if [[ -f "${PID_FILE}" ]]; then echo "Resuming the VM ${GUEST_NAME}." echo "cont" | socat - unix-connect:"${MONITOR_SOCKET}" else echo "Guest ${GUEST_NAME} is not running, nothing to resume." fi } function print_help(){ echo " =======================================================================" echo " control-vm.sh - simple QEMU/KVM vm manager" echo " version: ${__VERSION__}" echo " author Nils Freydank " echo " license: MIT" echo " url: https://git.holgersson.xyz/nfr/control-vm" echo " =======================================================================" echo "" echo " Use this tool in the following way:" echo " ./control-vm.sh " echo "" echo " with operation as one of:" echo " start | shutdown | stop | connect | save | restore | pause | resume" echo " or as one of the combinations:" echo " start+connect | stop+restore | restore+restart" echo "" echo " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo " ! Note that stop means a forced stop - which can lead to data loss. !" echo " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" } # === Parse the user input. === # Provide a sanity check first. if [[ -z "${1}" ]]; then echo " error: Please provide a guest name!" print_help exit 1 else set_params "${1}" case "${2}" in start+connect) start_qemu && connect_to_vm;; stop+restore) stop_qemu && restore_qemu;; restore+restart) stop_qemu && restore_qemu && start_qemu && connect_to_vm;; start) start_qemu;; shutdown) shutdown_qemu;; stop) stop_qemu;; connect) connect_to_vm;; save) save_qemu;; restore) restore_qemu;; pause) pause_qemu;; resume) resume_qemu;; *) echo " error: Unknown Operation!" print_help exit 2 ;; esac fi # vim:fileencoding=utf-8:ts=4:syntax=bash:expandtab