#!/usr/bin/env bash # set -euo pipefail IFS=$'\n\t' DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Share a host directory to the guest via 9pfs. # # Inside the VM run: # mount -t virtfs -o trans=virtio sharename /some/vm/path # mount -t 9p -o cache=mmap -o msize=512000 sharename /mnt/9p # mount -t 9p -o trans=virtio,cache=mmap,msize=512000 bind9p /path/to/mountpoint # Example usage: # # doas qemurc create-disk mint 10 # doas env CD=/vm/iso/linuxmint-22.2-cinnamon-64bit.iso qemurc start mint # doas qemurc start mint # doas env WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" XDG_RUNTIME_DIR=/run/user/0 qemurc start mint : ${VERBOSE:="NO"} # or YES if [ "$VERBOSE" = "YES" ]; then set -x fi : ${CPU_CORES:="1"} : ${MEMORY:="1G"} : ${GTK_ENABLE:="NO"} # Only enable one, either GTK or VNC : ${VNC_ENABLE:="NO"} # Only enable one, either GTK or VNC : ${VNC_LISTEN:="127.0.0.1:0"} : ${VNC_WIDTH:="1920"} : ${VNC_HEIGHT:="1080"} : ${AUDIO_ENABLE:="NO"} : ${TPM_ENABLE:="NO"} : ${BIND9P:=""} : "${CD:=}" : ${SHUTDOWN_TIMEOUT:="600"} : ${MOUNT_ROOT:="@mount_root@"} : ${ZFS_ROOT:="@zfs_root@"} ############## Setup ######################### function cleanup { sync for p in "${pids[@]}"; do log "Killing $p" kill "$p" log "Killed $p" done for vm in "${vms[@]}"; do log "Stopping $vm" stop_one "$vm" log "Stopped $vm" done } pids=() vms=() trap "set +e; cleanup" EXIT function die { local status_code="$1" shift (>&2 echo "${@}") exit "$status_code" } function log { (>&2 echo "${@}") } ############## Program ######################### function main { local cmd cmd=$1 shift if [ "$cmd" = "start" ]; then init start "${@}" elif [ "$cmd" = "stop" ]; then init stop "${@}" elif [ "$cmd" = "status" ]; then init status "${@}" elif [ "$cmd" = "console" ]; then init console "${@}" elif [ "$cmd" = "_start_body" ]; then init start_body "${@}" elif [ "$cmd" = "create-disk" ]; then create_disk "${@}" else (>&2 echo "Unknown command: $cmd") exit 1 fi } function start { local num_vms="$#" if [ "$num_vms" -eq 0 ]; then log "No VMs specified." return 0 fi while [ "$#" -gt 0 ]; do local name="$1" shift 1 log "Starting VM $name." start_one "$name" [ "$#" -eq 0 ] || sleep 5 done } function start_one { local name="$1" local tmux_name="$name" tmux new-session -d -s "$tmux_name" "$0" "_start_body" "$name" } function launch_pidfile { local pidfile="$1" shift 1 mkdir -p "$(dirname "$pidfile")" cat > "${pidfile}" <<< "$$" set -x exec "${@}" } export -f launch_pidfile function stop { local num_vms="$#" if [ "$num_vms" -eq 0 ]; then log "No VMs specified." return 0 fi while [ "$#" -gt 0 ]; do local name="$1" shift 1 log "Stopping VM $name." stop_one "$name" [ "$#" -eq 0 ] || sleep 5 done } function stop_one { local name="$1" local pidfile="/run/qemurc/${name}/pid" if [ ! -e "$pidfile" ]; then log "Pid file $pidfile does not exist." return 0 fi local qemu_pid qemu_pid=$(cat "$pidfile") if ps -p "$qemu_pid" >/dev/null; then # We cannot send a graceful shutdown command externally to qemu: https://gitlab.com/qemu-project/qemu/-/issues/148 log "Killing ${name}:${qemu_pid}." kill -SIGTERM "$qemu_pid" fi local timeout_start timeout_end timeout_start=$(date +%s) while ps -p "$qemu_pid" >/dev/null; do timeout_end=$(date +%s) if [ $((timeout_end-timeout_start)) -ge "$SHUTDOWN_TIMEOUT" ]; then log "${name}:${qemu_pid} took more than $SHUTDOWN_TIMEOUT seconds to shut down. Hard powering down." break fi log "Waiting for ${name}:${qemu_pid} to exit." sleep 2 done kill -9 "$qemu_pid" local timeout_start timeout_end timeout_start=$(date +%s) while ps -p "$qemu_pid" >/dev/null; do timeout_end=$(date +%s) if [ $((timeout_end-timeout_start)) -ge "$SHUTDOWN_TIMEOUT" ]; then log "${name}:${qemu_pid} took more than $SHUTDOWN_TIMEOUT seconds to hard power down. Giving up." break fi log "Waiting for ${name}:${qemu_pid} to hard power down." sleep 2 done rm -f "$pidfile" log "Finished stopping $name." } function status { local num_vms="$#" if [ "$num_vms" -gt 0 ]; then for name in "$@"; do status_one "$name" done else log "No VMs specified." fi } function status_one { local name="$1" local pidfile="/run/qemurc/${name}/pid" if [ ! -e "$pidfile" ]; then log "$name is not running." return 0 fi local qemu_pid qemu_pid=$(cat "$pidfile") if ! ps -p "$qemu_pid" >/dev/null; then log "$name is not running." return 0 fi log "$name is running as pid $qemu_pid." } function console { local num_vms="$#" if [ "$num_vms" -gt 0 ]; then for name in "$@"; do log "Attaching to console of VM $name." console_one "$name" done else log "No VMs specified." fi } function console_one { local name="$1" local tmux_name="$name" exec tmux a -t "$tmux_name" } function init { mkdir -p /run/qemurc } ############## qemu ############################ function create_disk { local name="$1" local gigabytes="$2" local zfs_path="${ZFS_ROOT}/${name}" local mount_path="${MOUNT_ROOT}/${name}" zfs create -o mountpoint=none -o canmount=off "$zfs_path" zfs create -o "mountpoint=$mount_path" -o canmount=on "$zfs_path/settings" zfs create -s "-V${gigabytes}G" -o volmode=dev -o primarycache=metadata -o secondarycache=none "$zfs_path/disk0" zfs snapshot -r "$zfs_path@empty" install -m0600 "@OVMFfd@/FV/OVMF_VARS.fd" "${mount_path}/" tee "${mount_path}/settings" <