#!/bin/sh set -eu usage() { cat <<-'EOF' Usage: vm [-G] [-n] [-S] [-v] ACTION [OS [-- QEMU_OPTIONS...]] vm -h EOF } help() { cat <<-'EOF' Options: -G use graphics -n dry-run: don't execute any code, assumes -v -S don't write to VM image (snapshot on) -v verbose mode -h, --help show this message ACTION one of: - up - down - status OS the name of the OS to be acted upon QEMU_OPTIONS command line options to be given verbatim to QEMU Manage the state of known virtual machines. The VM QCOW2 images are stored under $XDG_STATE_HOME/vm/qemu/, as "$OS.qcow2" files. The PIDs of the running images are stored in individual files under $XDG_RUNTIME_DIR/vm/pids/, as "$OS.pid" files. It also generates an SSH configuration file under $XDG_DATA_HOME/vm/ssh.conf to be `Included` by the main ~/.ssh/config file, which contains one alias entry for each VM, so that one can do a combination of `vm up alpine && ssh alpine`. Examples: Start the VM for Alpine: $ vm up alpine Stop the VM for Slackware, which was already down $ vm down slackware The VM for "slackware" is not running, already. List the available VMs, and their current state: $ vm status alpine up slackware down freebsd:up EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done DRY_RUN=false GRAPHICS=false SNAPSHOT=false VERBOSE=false while getopts 'GnSvh' flag; do case "$flag" in G) GRAPHICS=true ;; n) DRY_RUN=true VERBOSE=true ;; S) SNAPSHOT=true ;; v) VERBOSE=true ;; h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) QCOW_DIR="${XDG_STATE_HOME:-$HOME/.local/state}"/vm/qemu RUNDIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/vm" LOGS="$RUNDIR"/logs mkdir -p "$RUNDIR" "$QCOW_DIR" "$XDG_DATA_HOME"/vm guess_name() { PREFIX="$1" IMAGES="$(find "$QCOW_DIR" -type f -name "${PREFIX}*")" COUNT="$(echo "$IMAGES" | wc -l)" if [ "$COUNT" != 1 ]; then printf 'Cannot guess name with the given prefix: "%s".\n' "$PREFIX" >&2 printf '\nThe possibilities are:\n' >&2 printf '%s\n' "$IMAGES" | xargs -I% basename % .qcow2 | sed 's/^/- /' >&2 exit 2 fi printf '%s\n' "$(basename "$IMAGES" .qcow2)" } write_ssh_config() { for port in "$RUNDIR"/*.port; do if [ ! -e "$port" ]; then break fi NAME="$(basename "$port" .port)" PORT="$(cat "$port")" cat <<-EOF Host $NAME HostName localhost Port $PORT EOF done > "$XDG_DATA_HOME"/vm/ssh.conf } write_ssh_config ACTION="${1:-}" OS="${2:-}" eval "$(assert-arg "$ACTION" 'ACTION')" shift FLAGS='' if [ "$GRAPHICS" = false ]; then FLAGS="$FLAGS -nographic" else FLAGS="$FLAGS -display sdl" fi if [ "$SNAPSHOT" = true ]; then FLAGS="$FLAGS -snapshot" fi case "$ACTION" in status) for img in "$QCOW_DIR"/*.qcow2; do if [ ! -e "$img" ]; then break fi NAME="$(basename "$img" .qcow2)" if [ -e "$RUNDIR/$NAME.pid" ]; then STATUS=up else STATUS=down fi printf '%s\t%s\n' "$NAME" "$STATUS" done ;; up) eval "$(assert-arg "$OS" 'OS')" shift if [ "${1:-}" = '--' ]; then shift fi OS="$(guess_name "$OS")" PID_F="$RUNDIR/$OS.pid" PORT_F="$RUNDIR/$OS.port" if [ -e "$PID_F" ]; then printf 'The VM for "%s" is already running with PID %s.\n' \ "$OS" "$(cat "$PID_F")" >&2 exit 1 fi PORT="$(free-port)" QCOW="$QCOW_DIR"/"$OS".qcow2 # shellcheck disable=2086 set -- qemu-system-x86_64 \ -m 1G \ -nic user,model=virtio,hostfwd=tcp::"$PORT"-:22 \ -drive file="$QCOW",media=disk,if=virtio \ -enable-kvm \ $FLAGS "$@" if [ "$DRY_RUN" = true ]; then echo "$@" exit fi if [ "$VERBOSE" = true ]; then set -x fi "$@" 1>>"$LOGS" 2>&1 & PID=$! set +x printf '%s' "$PID" > "$PID_F" printf '%s' "$PORT" > "$PORT_F" write_ssh_config ;; down) eval "$(assert-arg "$OS" 'OS')" shift if [ "${1:-}" = '--' ]; then shift fi OS="$(guess_name "$OS")" PID_F="$RUNDIR/$OS.pid" PORT_F="$RUNDIR/$OS.port" if [ ! -e "$PID_F" ]; then printf 'The VM for "%s" is not running, already.\n' "$OS" >&2 exit 1 fi PID="$(cat "$PID_F")" if [ "$DRY_RUN" = true ]; then echo rm -f "$PID_F" "$PORT_F" echo kill "$PID" else rm -f "$PID_F" "$PORT_F" kill "$PID" fi write_ssh_config ;; *) printf 'Unrecognized action: "%s".\n\n' "$ACTION" >&2 usage >&2 exit 2 esac