0
0

control-vm.sh: Import the control script

This commit is contained in:
Nils Freydank 2021-09-30 11:49:57 +02:00
parent a13daf6fc5
commit bc83dd4987
Signed by: nfr
GPG Key ID: 0F1DEAB2D36AD112

303
control-vm.sh Executable file
View File

@ -0,0 +1,303 @@
#!/bin/bash
# SPDX-License-Identifier: MIT
# Virtual machine control script for QEMU/KVM VMs.
# Tested on Gentoo (~amd64/hardened) with qemu-6.0.
# Version: 2021-09-28
# Author: Nils Freydank <holgersson@posteo.de>
# License: MIT
# TODO (not in specific order):
# 1. Fix different ARCHes (-cpu host won't work for non-x86)
# 2. qemu-img commit
# 3. clipboard support via spice
# 4. Support boot from CD for first installations.
# 5. query status
# 6. save and stop in a single command?
# same for restore: stop, restore, cont(inue)
# 7. shared dir without smb, but via FUSE
# 8. bash and zsh-completion
# 9. set up a git repo
# === Installation ===
# The following packages are necessary on Gentoo/Linux:
# app-emulation/qemu net-misc/socat app-emulation/virt-viewer
# To use an EFI for the clients you will need also sys-firmware/edk2-ovmf
# which is pulled into the depgraph by USE="binary" on qemu.
# === 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 '.sh'.
# Fill in the data from the example below into the configuration file.
# Control the vm by calling: ./<name-of-this-script.sh> <guest> <operation>,
# 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:
# ${HOME}/vm/
# ${HOME}/vm/control-vm.sh
# ${HOME}/vm/Gentoo-guest/virtual-machine.img
# ${HOME}/vm/Gentoo-guest.sh
# === Configuration of the guest ===
# Create a file with the guest's parameters and name it same as the "GUEST_NAME"
# plus a "sh" suffix, so we can easily assume the name later.
#
# cat << _EOF > "${GUEST_NAME}.sh"
# # Provide the GUEST_NAME which is used in further configuration.
# GUEST_NAME="Guest-OS"
# # Configure the number of CPUs.
# CPUS="2"
# 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
# Provide a sanity check first.
[[ -z "${1}" ]] && echo "Please provide a guest name!" && exit 1
# === Define default values ===
CLIPBOARD="${CLIPBOARD:-}"
CPUS="${CPUS:-16}"
CPU_CORES="${CPU_CORES:-1}"
EFI="${EFI:-True}"
GUEST_NAME="${1}"
LIVE_ONLY="${LIVE_ONLY:-}"
MACHINE="${MACHINE:-pc-q35-6.1}"
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="${BASE_PATH:-${HOME}/vm/${GUEST_NAME}}"
GUEST_CONFIG_FILE="${HOME}/vm/${GUEST_NAME}.sh"
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 "No configuration found. Aborting."
exit 2
fi
# === Define a base and glue everything together. ===
QEMU_COMMON_ARGS=(
# Provide a pretty name for the guest.
-name "${GUEST_NAME}"
# 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 cpus="${CPUS}",maxcpus="${CPUS}",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}",discard=on,aio=io_uring,cache=none )
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=on )
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. ===
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
}
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
}
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
}
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.
# ( spicy --uri="spice+unix:///${SPICE_SOCKET}" &> /dev/null ) &
else
echo "Looks as VM ${GUEST_NAME} is not running."
fi
}
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
}
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
}
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
}
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
}
# === Parse the user input. ===
# "$1 != nothing" is already checked earlier.
case "${2}" in
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 "Unknown Operation!"
echo "Use one of: start | shutdown | stop | connect | save | restore | pause | resume"
echo "Note that stop means a forced stop.";;
esac
# vim:fileencoding=utf-8:ts=4:syntax=bash:expandtab