control-vm.sh: Import the control script
This commit is contained in:
parent
a13daf6fc5
commit
bc83dd4987
303
control-vm.sh
Executable file
303
control-vm.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user