From 37abf5827120895326a98870cf4d412b51c14c6c Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 16 Sep 2025 18:26:01 -0400 Subject: [PATCH] Add a qemu port of my bhyverc script for running virtual machines on Linux. --- nix/configuration/roles/qemu/default.nix | 36 ++ .../roles/qemu/files/qemurc.bash | 375 ++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 nix/configuration/roles/qemu/files/qemurc.bash diff --git a/nix/configuration/roles/qemu/default.nix b/nix/configuration/roles/qemu/default.nix index c59e907..be098aa 100644 --- a/nix/configuration/roles/qemu/default.nix +++ b/nix/configuration/roles/qemu/default.nix @@ -5,6 +5,41 @@ ... }: +let + qemurc = + (pkgs.writeScriptBin "qemurc" ( + builtins.readFile ( + pkgs.replaceVars ./files/qemurc.bash { + "OVMFfd" = "${pkgs.OVMF.fd}"; + mount_root = "/vm"; + zfs_root = "zroot/linux/nix/vm"; + } + ) + )).overrideAttrs + (old: { + buildCommand = '' + ${old.buildCommand} + patchShebangs $out + ''; + }); + qemurc_wrapped = + (pkgs.writeScriptBin "qemurc" '' + #!/usr/bin/env bash + export "PATH=${ + lib.makeBinPath [ + pkgs.swtpm + pkgs.tmux + ] + }:''${PATH}" + exec ${qemurc}/bin/qemurc "''${@}" + '').overrideAttrs + (old: { + buildCommand = '' + ${old.buildCommand} + patchShebangs $out + ''; + }); +in { imports = [ ]; @@ -22,6 +57,7 @@ { environment.systemPackages = with pkgs; [ qemu + qemurc_wrapped ]; } ] diff --git a/nix/configuration/roles/qemu/files/qemurc.bash b/nix/configuration/roles/qemu/files/qemurc.bash new file mode 100644 index 0000000..ca131d5 --- /dev/null +++ b/nix/configuration/roles/qemu/files/qemurc.bash @@ -0,0 +1,375 @@ +#!/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" <