diff options
Diffstat (limited to 'bin/debefivm-run')
-rwxr-xr-x | bin/debefivm-run | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/bin/debefivm-run b/bin/debefivm-run new file mode 100755 index 0000000..c93d7d2 --- /dev/null +++ b/bin/debefivm-run @@ -0,0 +1,401 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2024-2025 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT + +: <<'POD2MAN' +=head1 NAME + +debefivm-run - Run a virtual machine from an EFI-bootable raw disk image + +=head1 SYNOPSIS + +B<-debefivm-run> [B<-a>] I<architecture>] [B<-i> F<image>] [B<-s> I<sshport>] [B<--> I<qemu options>] + +=head1 DESCRIPTION + +B<debefivm-run> is essentially a thing wrapper around B<qemu> for running a virtual machine from an UEFI bootable raw disk image. +Such an image may be created using B<debefivm-create> or with another image creator, but its use is limited to architectures supporting EFI booting. + +=head1 OPTIONS + +=over 8 + +=item B<-a> I<architecture>, B<--architecture>=I<architecture> + +Override the Debian architecture of the provided image. +If the image uses architecture-specific type UUIDs for the root partition, the architecture can be detected. +Otherwise, the host's architecture is assumed. +The images created by B<debefivm-create> employ these UUIDs. +The value is used to determine the correct emulator binary as well as suitable EFI firmware. + +=item B<--efi-vars>=F<variablefile> + +EFI variables can be changed and retained across reboots of a virtual machine if a separate variable file is supplied. +The passed file is created from a template if absent. +If absent, a read-only variable store will be supplied to the virtual machine. + +=item B<-i> F<image>, B<--image>=F<image> + +This option specifies the location of the virtual machine image file. +By default F<vm.img> in the working directory is used. + +=item B<--netopt>=I<option> + +B<debefivm-run> sets up a user mode network by default. +It therefore passes a B<-netdev> option to B<qemu>. +Using this option, you can customize the value of that B<-netdev> option. +For instance, you can set up additional port forwards by passing e.g. C<--netopt hostfwd=:127.0.0.1:8080-:80>. +It can be used multiple times. + +=item B<--skip>=I<task> + +Skip a particular task or feature. +The option may be specified multiple times or list multiple tasks to be skipped by separating them with a comma. +By default, no tasks are skipped. +The following tasks may be skipped. + +=over 4 + +=item B<network> + +Do not configure a network card. +Use this if you want to configure network on your own. +This should also be passed in addition to passing C<-nic none> when you want to disable networking. + +=item B<rngdev> + +Do not pass a random number generator device. + +=back + +=item B<--transport>=I<transport> + +When B<debefivm-run> adds devices to B<qemu>, it has to select a transport and it most often guesses B<pci>. +When specifying a different machine such as B<-machine microvm>, a different transport such as B<device> may be needed. + +=item B<-s> I<sshport>, B<--sshport>=I<sshport> + +If given, B<qemu> is configured to pass connections to I<127.0.0.1:sshport> to port 22 of the virtual machine. +You can connect to your virtual machine without updating your known hosts like this: + + ssh -o NoHostAuthenticationForLocalhost=yes -p $sshport root@127.0.0.1 + +The option is a shorthand for C<--netopt hostfwd=tcp:127.0.0.1:sshport-:22>. + +=item B<--> I<qemu options> + +All options beyond a double dash are passed to B<qemu>. +This can be used to configure additional hardware components. +One possible use of this method is passing B<-snapshot> to avoid modifying the virtual machine image. + +=back + +=head1 SEE ALSO + + debefivm-create(1) debvm-run(1) qemu(1) + +=cut +POD2MAN + +set -u + +EFI_VARS= +GRAPHICAL= +IMAGE=vm.img +IMAGE_ARCH= +NATIVE_ARCH=$(dpkg --print-architecture) +NETOPTS= +SKIP=, +SSHPORT= +TRANSPORT= + +nth_arg() { + shift "$1" + printf "%s" "$1" +} + +die() { + echo "error: $*" >&2 + exit 1 +} +usage() { + die "usage: $0 [-a architecture] [-g] [-i image] [-s sshport] [-- qemu options]" +} +usage_error() { + echo "error: $*" 1>&2 + usage +} + +opt_architecture() { + IMAGE_ARCH=$1 +} +opt_efi_vars() { + EFI_VARS=$1 +} +opt_graphical() { + GRAPHICAL=1 +} +opt_help() { + usage +} +opt_image() { + IMAGE=$1 +} +opt_netopt() { + NETOPTS="$NETOPTS,$1" +} +opt_skip() { + SKIP="$SKIP$1," +} +opt_sshport() { + SSHPORT=$1 +} +opt_transport() { + TRANSPORT=$1 +} + +while getopts :a:gi:s:-: OPTCHAR; do + case "$OPTCHAR" in + a) opt_architecture "$OPTARG" ;; + g) opt_graphical ;; + i) opt_image "$OPTARG" ;; + s) opt_sshport "$OPTARG" ;; + -) + case "$OPTARG" in + graphical|help) + "opt_$OPTARG" + ;; + architecture|efi-vars|image|netopt|skip|sshport|transport) + test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG" + "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")" + OPTIND=$((OPTIND+1)) + ;; + architecture=*|efi-vars=*|image=*|netopt=*|skip=*|sshport=*|transport=*) + "opt_${OPTARG%%=*}" "${OPTARG#*=}" + ;; + *) + usage_error "unrecognized option --$OPTARG" + ;; + esac + ;; + :) + usage_error "missing argument for -$OPTARG" + ;; + '?') + usage_error "unrecognized option -$OPTARG" + ;; + *) + die "internal error while parsing command options, please report a bug" + ;; + esac +done +shift "$((OPTIND - 1))" + +if test -n "$SSHPORT"; then + opt_netopt "hostfwd=tcp:127.0.0.1:$SSHPORT-:22" +fi + +test -f "$IMAGE" || die "image '$IMAGE' not found" +test -s "$IMAGE" || die "image '$IMAGE' is empty" + +check_skip() { + case "$SKIP" in + *",$1,"*) return 0 ;; + *) return 1 ;; + esac +} + +if test -z "$IMAGE_ARCH"; then + PARTITIONS=$(partx --show --noheadings --output type "$IMAGE") + + case "$PARTITIONS" in + *4f68bce3-e8cd-4db1-96e7-fbcaf984b709*) + IMAGE_ARCH=amd64 + ;; + *b921b045-1df0-41c3-af44-4c6f280d3fae*) + IMAGE_ARCH=arm64 + ;; + *72ec70a6-cf74-40e6-bd49-4bda08e8f224*) + IMAGE_ARCH=riscv64 + ;; + *69dad710-2ce4-4e3c-b16c-21a1d49abed3*) + IMAGE_ARCH=armhf + ;; + *44479540-f297-41b2-9af7-d131d5f0458a*) + IMAGE_ARCH=i386 + ;; + *) + echo "cannot detect image architecture from gpt, assuming $NATIVE_ARCH" >&2 + IMAGE_ARCH="$NATIVE_ARCH" + ;; + esac +fi + +# Translate IMAGE_ARCH (a Debian architecture) to a Debian CPU name. +# This utilizes the QEMU Debian package symlink mapping that ensures that +# calling qemu-system-${DEB_HOST_ARCH_CPU} will run the QEMU binary providing +# the correct emulator for that CPU. +IMAGEARCHCPU="$(dpkg-architecture --force --host-arch "$IMAGE_ARCH" --query DEB_HOST_ARCH_CPU)" +QEMU="qemu-system-$IMAGEARCHCPU" +CPU= +MACHINE= +MAX_SMP= + +case "$IMAGEARCHCPU" in + amd64) + QEMU=qemu-system-x86_64 + MACHINE="type=q35" + BIOSCODE='/usr/share/OVMF/OVMF_CODE_4M.fd' + BIOSDATA='/usr/share/OVMF/OVMF_VARS_4M.fd' + BIOSPACKAGE=ovmf + ;; + arm) + CPU=max + MACHINE="type=virt,highmem=off" + MAX_SMP=8 + BIOSCODE='/usr/share/AAVMF/AAVMF32_CODE.fd' + BIOSDATA='/usr/share/AAVMF/AAVMF32_VARS.fd' + BIOSPACKAGE=qemu-efi-arm + ;; + arm64) + CPU=max,pauth-impdef=on + MACHINE="type=virt,gic-version=max" + BIOSCODE='/usr/share/AAVMF/AAVMF_CODE.fd' + BIOSDATA='/usr/share/AAVMF/AAVMF_VARS.fd' + BIOSPACKAGE=qemu-efi-aarch64 + ;; + i386) + BIOSCODE='/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd' + BIOSDATA='/usr/share/OVMF/OVMF32_VARS_4M.fd' + BIOSPACKAGE=ovfm-ia32 + ;; + riscv64) + MACHINE="type=virt" + BIOSCODE='/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd' + BIOSDATA='/usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd' + BIOSPACKAGE=qemu-efi-riscv64 + ;; + *) + die "support for $IMAGE_ARCH is not implemented" + ;; +esac + +test -e "$BIOSCODE" || + die "cannot file firmware file $BIOSCODE. Is $BIOSPACKAGE installed?" +test -e "$BIOSDATA" || + die "cannot file firmware file $BIOSDATA. Is $BIOSPACKAGE installed?" + +# Assign the default late to allow both cli and arch-specific overrides. +: "${TRANSPORT:=pci}" + +comma_escape() { + # If a filename contains a comma, then that comma must be escaped by + # prefixing it with another comma or otherwise output filenames are + # able to inject options to qemu (and load the wrong file). + comma_escape_str="$1" + while test "${comma_escape_str%,*}" != "$comma_escape_str"; do + printf "%s,," "${comma_escape_str%%,*}" + comma_escape_str="${comma_escape_str#*,}" + done + printf "%s" "$comma_escape_str" +} + +EFI_VAR_DRIVE="if=pflash,format=raw,unit=1" +if test -n "$EFI_VARS"; then + if ! test -e "$EFI_VARS"; then + cp "$BIOSDATA" "$EFI_VARS" + fi + EFI_VAR_DRIVE="$EFI_VAR_DRIVE,read-only=off,file=$(comma_escape "$EFI_VARS")" +else + EFI_VAR_DRIVE="$EFI_VAR_DRIVE,read-only=on,file=$(comma_escape "$BIOSDATA")" +fi + +ENABLE_KVM=no +if test "$NATIVE_ARCH" = "$IMAGE_ARCH"; then + ENABLE_KVM=yes +fi +if test "$ENABLE_KVM" = yes; then + if ! command -v "$QEMU" >/dev/null 2>&1; then + # Fall back to kvm in case we badly guessed qemu. + QEMU=kvm + fi + MACHINE="${MACHINE:+$MACHINE,}accel=kvm:tcg" + # While kvm will fall back gracefully, only override CPU when we expect + # kvm to work. + if test -w /dev/kvm; then + CPU=max + # kvm: "max" will become "host", intended. + # tcg: "max" will actually work, "host" would not. + fi +fi + +if test -n "$MACHINE"; then + set -- -machine "$MACHINE" "$@" +fi + +set -- \ + -no-user-config \ + -nodefaults \ + -chardev stdio,id=console,mux=on,signal=off \ + -serial chardev:console \ + -name "debefivm-run $IMAGE" \ + -m 1G \ + -drive "if=pflash,format=raw,unit=0,read-only=on,file=$(comma_escape "$BIOSCODE")" \ + -drive "$EFI_VAR_DRIVE" \ + -drive "id=root,media=disk,format=raw,discard=unmap,file=$(comma_escape "$IMAGE"),if=none,cache=unsafe" \ + -device "virtio-blk-$TRANSPORT,drive=root,serial=root" \ + "$@" + +if test -n "$CPU"; then + set -- -cpu "$CPU" "$@" +fi + +NPROC=$(nproc) +if test "$NPROC" -gt 1; then + test -n "$MAX_SMP" && test "$NPROC" -gt "$MAX_SMP" && NPROC=$MAX_SMP + set -- -smp "$NPROC" "$@" +fi +if ! check_skip rngdev; then + set -- \ + -device "virtio-rng-$TRANSPORT,rng=rng0" \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + "$@" +fi + +if test -z "$GRAPHICAL"; then + set -- -nographic "$@" +else + case "$KERNELARCH" in + amd64|i386) + set -- -vga virtio "$@" + ;; + *) + set -- \ + -device "virtio-gpu-gl-$TRANSPORT" \ + -display gtk,gl=on \ + "$@" + ;; + esac + set -- \ + -device "virtio-keyboard-$TRANSPORT" \ + -device "virtio-tablet-$TRANSPORT" \ + "$@" +fi + +DNSSEARCH=$(dnsdomainname) +if test -z "$DNSSEARCH"; then + DNSSEARCH=$(sed -n 's/^\s*search\s*\.\?//p;T;q' /etc/resolv.conf) +fi +if test -n "$DNSSEARCH"; then + NETOPTS=",domainname=$DNSSEARCH$NETOPTS" +fi + +if ! check_skip network; then + set -- \ + -netdev "user,id=net0$NETOPTS" \ + -device "virtio-net-$TRANSPORT,netdev=net0" \ + "$@" +fi + +echo "+ $QEMU $*" 1>&2 +exec "$QEMU" "$@" |