diff options
-rw-r--r-- | .gitlab-ci.yml | 12 | ||||
-rw-r--r-- | README.md | 14 | ||||
-rwxr-xr-x | bin/debvm-create (renamed from debvm-create) | 240 | ||||
-rwxr-xr-x | bin/debvm-run (renamed from debvm-run) | 99 | ||||
-rwxr-xr-x | bin/debvm-waitssh | 178 | ||||
-rwxr-xr-x | share/customize-autologin.sh | 23 | ||||
-rwxr-xr-x | share/customize-dpkgavailable.sh | 12 | ||||
-rwxr-xr-x | share/customize-kernel.sh | 57 | ||||
-rwxr-xr-x | share/customize-networkd.sh | 37 | ||||
-rwxr-xr-x | share/customize-resolved.sh | 26 | ||||
-rwxr-xr-x | tests/create-and-run.sh | 39 | ||||
-rwxr-xr-x | tests/dist-upgrades.sh | 55 | ||||
-rw-r--r-- | tests/test_common.sh | 7 | ||||
-rwxr-xr-x | useraddhook/customize.sh | 34 |
14 files changed, 641 insertions, 192 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6427330..4604045 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,14 +5,14 @@ shellcheck: - apt-get update - apt-get dist-upgrade --yes - apt-get --no-install-recommends --yes install shellcheck - - shellcheck debvm-* + - shellcheck -P tests bin/* share/*.sh tests/*.sh codespell: script: - apt-get update - apt-get dist-upgrade --yes - apt-get --no-install-recommends --yes install codespell - - codespell debvm-* + - codespell bin/* share/*.sh tests/*.sh README.md release_test: parallel: @@ -27,8 +27,8 @@ release_test: script: - apt-get update - apt-get dist-upgrade --yes - - apt-get --no-install-recommends --yes install e2fsprogs genext2fs mmdebstrap openssh-client sleepenh qemu-kvm - - PATH=.:$PATH ./tests/create-and-run.sh $(dpkg --print-architecture) "$RELEASE" + - apt-get --no-install-recommends --yes install e2fsprogs genext2fs mmdebstrap openssh-client qemu-kvm + - PATH=$(pwd)/bin:$PATH ./tests/create-and-run.sh $(dpkg --print-architecture) "$RELEASE" arch_test: parallel: @@ -45,5 +45,5 @@ arch_test: - test -e /proc/sys/fs/binfmt_misc/status || mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc - apt-get update - apt-get dist-upgrade --yes - - apt-get --no-install-recommends --yes install e2fsprogs genext2fs mmdebstrap openssh-client sleepenh qemu-system binfmt-support arch-test qemu-user-static - - PATH=.:$PATH ./tests/create-and-run.sh "$ARCHITECTURE" sid + - apt-get --no-install-recommends --yes install e2fsprogs genext2fs mmdebstrap openssh-client qemu-system binfmt-support arch-test qemu-user-static + - PATH=$(pwd)/bin:$PATH ./tests/create-and-run.sh "$ARCHITECTURE" sid @@ -23,7 +23,7 @@ invocation. The following two invocations will give you a shell inside a qemu virtual machine of the native architecture, leaving all the settings at their defaults: - ./debvm-create && ./debvm-run + ./bin/debvm-create && ./bin/debvm-run What do I need? =============== @@ -45,10 +45,10 @@ contains the root filesystem of a (Debian) installation including an init system and a kernel. There is no partition table or bootloader. The following paths are assumed inside: * `/bin/true` is used to detect the architecture of an image - * `/vmlinuz` or `/vmlinux` (depending on the architecture) must be a symbolic - link pointing to a regular file containing the kernel. - * `/initrd.img` must be a symbolic link pointing to a regular file containing - the initrd image. + * `(|/boot)/vmlinu[xz]` must be a symbolic link pointing to a regular file + containing the kernel. + * `initrd.img` must be a symbolic link in the same directory as the kernel + image pointing to a regular file containing the initrd image. Why? ==== @@ -62,6 +62,10 @@ using the one included in qemu. The other aspect is restricting to Debian-based systems. This allows for a lot of simplification of the problem space. +While most similar tools require root privileges at some point, this one works +with either fakeroot or a subuid allocation for user namespaces, which is often +available. + The implementation is so short that it still is feasible to read and understand it. Let's see how long that lasts. diff --git a/debvm-create b/bin/debvm-create index e8bcf92..c554165 100755 --- a/debvm-create +++ b/bin/debvm-create @@ -11,7 +11,7 @@ debvm-create - Create a VM image for various Debian releases and architectures =head1 SYNOPSIS -B<debvm-create> [B<-a> I<architecture>] [B<-h> I<hostname>] [B<-k> I<sshkey>] [B<-m> I<mirror>] [B<-o> I<output>] [B<-p> I<package>] [B<-r> I<release>] [B<-z> I<size_in_GB>] [B<--> I<mmdebstrap options>] +B<debvm-create> [B<-a> I<architecture>] [B<-h> I<hostname>] [B<-k> F<sshkey>] [B<-o> F<output>] [B<-r> I<release>] [B<-s> <task>] [B<-z> I<size>] [B<--> I<mmdebstrap options>] =head1 DESCRIPTION @@ -37,47 +37,84 @@ A suitable kernel image is automatically selected and installed into the image. Set the hostname of the virtual machine. By default, the hostname is B<testvm>. -=item B<-k> I<sshkey>, B<--sshkey>=I<sshkey> +=item B<-k> F<sshkey>, B<--sshkey>=F<sshkey> Install the given ssh public key file into the virtual machine image for the root user. This option also causes the ssh server to be installed. By default, no key or server is installed. +To connect to the vm, pass a port number to B<debvm-run> with the B<-s> option. -=item B<-m> I<mirror>, B<--mirror>=I<mirror> - -Specify the Debian mirror to be used for downloading packages and to be configured inside the virtual machine image. -By default, B<http://deb.debian.org/debian> is being used. - -=item B<-o> I<output>, B<--output>=I<output> +=item B<-o> F<output>, B<--output>=F<output> Specify the file name of the resulting virtual machine image. -By default, it is written to B<rootfs.ext4>. - -=item B<-p> I<package>, B<--package>=I<package> - -Request additional packages to be installed into the virtual machine image. -This option can be specified multiple times and packages can be separated by a comma. -Package recommendations are not honoured. -If a linux-image is passed here, it will replace the one selected by default. +By default, it is written to F<rootfs.ext4>. =item B<-r> I<release>, B<--release>=I<release> Use the given Debian release. By default, B<unstable> is being used. -=item B<-z> I<size_in_GB>, B<--size>=I<size_in_GB> +=item B<-s> I<task>, 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<autologin> + +Skips adding a the customize-autologin.sh to B<mmdebstrap> that configures +automatic root login on a serial console and also parses the C<TERM> kernel +cmdline and passes it as C<TERM> to B<agetty>. + +=item B<kernel> + +skips installing a linux kernel image. +This can be useful to install a kernel without a package. +If a kernel is installed via B<mmdebstrap> option C<--include>, automtatic kernel installation is automatically skipped. + +=item B<packagelists> -Specify the minimum image size in giga bytes. +reduces the package lists inside the image. +The B<available> database for B<dpkg> is not created. +The package lists used by B<apt> are deleted. +This generally produces a smaller image, but you need to run B<apt update> before installing packages and B<dpkg --set-selections> does not work. + +=item B<systemdnetwork> + +skips installing B<libnss-resolve> as well as automatic network configuration via B<systemd-networkd>. + +=item B<usrmerge> + +By default B<debvm> adds a hook to enable merged-/usr without the B<usrmerge> package given a sufficiently recent Debian release. +Without the hook, dependencies will pull the B<usrmerge> package as needed, which may result in a larger installation. + +=back + +=item B<-z> I<size>, B<--size>=I<size> + +Specify the minimum image size as an integer and optional unit (example: 10K is 10*1024). +Units are K,M,G,T,P,E,Z,Y (powers of 1024) or KB,MB,... (powers of 1000). The resulting image will be grown as a sparse file to this size if necessary. The default is 1 GB. =item B<--> I<mmdebstrap options> -All options beyond a double dash are passed to B<mmdebstrap> before the suite, target and mirror specification. +All options beyond a double dash are passed to B<mmdebstrap> after the suite and target specification. This can be used to provide additional hooks for image customization. +You can also request additional packages to be installed into the image using B<mmdebstrap>'s B<--include> option. +Any positional arguments passed here will be treated as mirror specifications by B<mmdebstrap>. =back +=head1 EXAMPLES + +In order to create images for Debian ports architectures, you can pass two options to mmdebstrap: + + debvm-create ... -- http://deb.debian.org/debian-ports --keyring=/usr/share/keyrings/debian-ports-archive-keyring.gpg + =head1 SEE ALSO debvm-run(1) mmdebstrap(1) @@ -89,13 +126,14 @@ set -u ARCHITECTURE=$(dpkg --print-architecture) IMAGE=rootfs.ext4 -INCLUDE_PACKAGES=init -MIRROR="http://deb.debian.org/debian" -SIZE=$((1024*1024*1024)) +SIZE=1G +SKIP=, SSHKEY= SUITE=unstable VMNAME=testvm +SHARE_DIR="${0%/*}/../share" + nth_arg() { shift "$1" printf "%s" "$1" @@ -106,7 +144,7 @@ die() { exit 1 } usage() { - die "usage: $0 [-a architecture] [-h hostname] [-k sshkey] [-m mirror] [-o output] [-p packages] [-r release] [-z size_in_GB] [-- mmdebstrap options]" + die "usage: $0 [-a architecture] [-h hostname] [-k sshkey] [-o output] [-r release] [-s task] [-z size] [-- mmdebstrap options]" } usage_error() { echo "error: $*" 1>&2 @@ -119,46 +157,43 @@ opt_architecture() { opt_hostname() { VMNAME=$1 } -opt_mirror() { - MIRROR=$1 +opt_skip() { + SKIP="$SKIP$1," } opt_sshkey() { SSHKEY=$1 } opt_output() { IMAGE=$1 -} -opt_package() { - INCLUDE_PACKAGES="$INCLUDE_PACKAGES,$1" + test "${IMAGE#-}" = "$IMAGE" || IMAGE="./$IMAGE" } opt_release() { SUITE=$1 } opt_size() { - SIZE=$(($1*1024*1024*1024)) + SIZE=$1 } -while getopts :a:h:k:m:o:p:r:z:-: OPTCHAR; do +while getopts :a:h:k:o:r:s:z:-: OPTCHAR; do case "$OPTCHAR" in a) opt_architecture "$OPTARG" ;; h) opt_hostname "$OPTARG" ;; k) opt_sshkey "$OPTARG" ;; - m) opt_mirror "$OPTARG" ;; o) opt_output "$OPTARG" ;; - p) opt_package "$OPTARG" ;; r) opt_release "$OPTARG" ;; + s) opt_skip "$OPTARG" ;; z) opt_size "$OPTARG" ;; -) case "$OPTARG" in help) usage ;; - architecture|hostname|mirror|output|package|release|size|sshkey) + architecture|hostname|output|release|size|skip|sshkey) test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG" "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")" OPTIND=$((OPTIND+1)) ;; - architecture=*|hostname=*|mirror=*|output=*|package=*|release=*|size=*|sshkey=*) + architecture=*|hostname=*|output=*|release=*|size=*|skip=*|sshkey=*) "opt_${OPTARG%%=*}" "${OPTARG#*=}" ;; *) @@ -183,84 +218,15 @@ if test -n "$SSHKEY" && ! test -f "$SSHKEY"; then die "error: ssh keyfile '$SSHKEY' not found" fi -case "$SUITE" in - jessie) - DEBVER=8 - ;; - stretch) - DEBVER=9 - ;; - buster) - DEBVER=10 - ;; - bullseye|stable) - DEBVER=11 - ;; - bookworm|testing) - DEBVER=12 - ;; - trixie) - DEBVER=13 - ;; - forky) - DEBVER=14 - ;; - sid|unstable) - DEBVER=999 - ;; - *) - die "unrecognized Debian release: $SUITE" - ;; -esac - -KERNEL_SUFFIX=-$ARCHITECTURE -case "$ARCHITECTURE" in - amd64|arm64) - KERNEL_SUFFIX="-cloud-$ARCHITECTURE" - if test "$DEBVER" -le 9; then - KERNEL_SUFFIX="-$ARCHITECTURE" - fi - ;; - armhf) - KERNEL_SUFFIX=-armmp - ;; - i386) - KERNEL_SUFFIX=-686-pae - ;; - mips64el) - KERNEL_SUFFIX=-5kc-malta - ;; - mipsel) - KERNEL_SUFFIX=-4kc-malta - ;; - ppc64el) - KERNEL_SUFFIX=-powerpc64le - ;; -esac - -case ",$INCLUDE_PACKAGES," in - *,linux-image-*) - ;; - *) - INCLUDE_PACKAGES="$INCLUDE_PACKAGES,linux-image$KERNEL_SUFFIX" - ;; -esac - -if test -n "$SSHKEY"; then - INCLUDE_PACKAGES="$INCLUDE_PACKAGES,openssh-server" -fi +check_skip() { + case "$SKIP" in + *",$1,"*) return 0 ;; + *) return 1 ;; + esac +} -# add a DNS resolver -if test "$DEBVER" -ge 9; then - INCLUDE_PACKAGES="$INCLUDE_PACKAGES,libnss-resolve" -fi -if test "$DEBVER" -le 11; then - set -- '--customize-hook=chroot "$1" systemctl enable systemd-resolved.service' "$@" -fi -if test "$DEBVER" -le 9; then - set -- '--customize-hook=ln -fs ../run/systemd/resolve/resolv.conf "$1/etc/resolv.conf"' "$@" -elif test "$DEBVER" -le 11; then - set -- '--customize-hook=ln -fs ../run/systemd/resolve/stub-resolv.conf "$1/etc/resolv.conf"' "$@" +if ! check_skip kernel; then + set -- "--customize-hook=$SHARE_DIR/customize-kernel.sh" "$@" fi # construct mmdebstrap options as $@: @@ -268,12 +234,11 @@ set -- \ --verbose \ --variant=apt \ --format=ext2 \ + --include=init \ "--architecture=$ARCHITECTURE" \ - "--include=$INCLUDE_PACKAGES" \ '--customize-hook=echo "LABEL=debvm / ext4 defaults 0 1" >"$1/etc/fstab"' \ "$@" - # set up a hostname set -- \ "--customize-hook=echo $VMNAME >"'"$1/etc/hostname"' \ @@ -283,54 +248,53 @@ set -- \ # allow password-less root login set -- '--customize-hook=chroot "$1" passwd --delete root' "$@" -# dhcp on all network interfaces -SYSD_NET_MATCH='Name=en*\n' -test "$DEBVER" -le 8 && SYSD_NET_MATCH="${SYSD_NET_MATCH}Name=eth*\\n" -SYSD_NET_NET='DHCP=yes\n' -# This anchor is included by default since bullseye. Fails DNSSEC validation when missing. -test "$DEBVER" -le 11 && SYSD_NET_NET="${SYSD_NET_NET}DNSSECNegativeTrustAnchors=home.arpa\\n" -set -- \ - '--customize-hook=chroot "$1" systemctl enable systemd-networkd.service' \ - "--customize-hook=printf \"[Match]\\n$SYSD_NET_MATCH\\n[Network]\\n$SYSD_NET_NET"'\n[DHCP]\nUseDomains=yes\n" > "$1/etc/systemd/network/20-wired.network"' \ - "$@" +if ! check_skip systemdnetwork; then + # dhcp on all network interfaces, and add a dns resolver + set -- \ + "--customize-hook=$SHARE_DIR/customize-networkd.sh" \ + '--include=?not(?virtual)?exact-name(libnss-resolve)' \ + "--customize-hook=$SHARE_DIR/customize-resolved.sh" \ + "$@" +fi # add ssh key for root if test -n "$SSHKEY"; then set -- \ - '--customize-hook=mkdir -p "$1/root/.ssh"' \ + --include=openssh-server \ + '--customize-hook=mkdir -m700 -p "$1/root/.ssh"' \ "--customize-hook=upload $SSHKEY /root/.ssh/authorized_keys" \ "$@" fi -set -- --skip=cleanup/apt/lists "$@" - -# Make dpkg --set-selections to work. -set -- '--customize-hook=chroot "$1" apt-cache dumpavail | chroot "$1" dpkg --update-avail' "$@" +if ! check_skip packagelists; then + set -- --skip=cleanup/apt/lists "$@" + set -- "--customize-hook=$SHARE_DIR/customize-dpkgavailable.sh" "$@" +fi -if test "$DEBVER" -le 8; then +if test "$SUITE" = jessie; then # Use obsolete and expired keys. set -- '--keyring=/usr/share/keyrings/debian-archive-removed-keys.gpg' "$@" set -- --aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' "$@" set -- --hook-dir=/usr/share/mmdebstrap/hooks/jessie-or-older "$@" fi -if test "$DEBVER" -ge 12; then +if ! check_skip usrmerge; then # Avoid the usrmerge package - set -- --hook-dir=/usr/share/mmdebstrap/hooks/merged-usr "$@" + set -- --hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr "$@" fi -# suite target mirror -set -- "$@" "$SUITE" "$IMAGE" "deb $MIRROR $SUITE main" +if ! check_skip autologin; then + set -- "--customize-hook=$SHARE_DIR/customize-autologin.sh" "$@" +fi + +set -- "$SUITE" "$IMAGE" "$@" set -ex mmdebstrap "$@" -IMAGESIZE=$(stat -c %s "$IMAGE") -if test "$IMAGESIZE" -lt "$SIZE"; then - truncate -s "$SIZE" "$IMAGE" - /sbin/resize2fs "$IMAGE" -fi +truncate -s ">$SIZE" "$IMAGE" +/sbin/resize2fs "$IMAGE" /sbin/tune2fs -L debvm -i 0 -O extents,uninit_bg,dir_index,has_journal "$IMAGE" # Must fsck after tune2fs: https://ext4.wiki.kernel.org/index.php/UpgradeToExt4 /sbin/fsck.ext4 -fDp "$IMAGE" diff --git a/debvm-run b/bin/debvm-run index d6a5422..0ef8740 100755 --- a/debvm-run +++ b/bin/debvm-run @@ -9,14 +9,14 @@ debvm-run - Run a VM image created by debvm-create =head1 SYNOPSIS -B<debvm-run> [B<-g>] [B<-i> I<image>] [B<-s> I<sshport>] [B<--> I<qemu options>] +B<debvm-run> [B<-g>] [B<-i> F<image>] [B<-s> I<sshport>] [B<--> I<qemu options>] =head1 DESCRIPTION B<debvm-run> is essentially a thin wrapper around B<qemu> for running a virtual machine image created by B<debvm-create> or something compatible. The virtual machine image is expected to be a raw ext4 image with file system label B<debvm>. -The architecture of the machine is detected from the contained B</bin/true>. -It must contain a symbolic link pointing to a kernel image at B</vmlinuz> or B</vmlinux> depending on the architecture and a symbolic link pointing to an initrd image at B</initrd.img>. +The architecture of the machine is detected from the contained F</bin/true>. +It must contain a symbolic link pointing to a kernel image at one of F<(|/boot)/vmlinu[xz]> a symbolic link pointing to an initrd image at F<initrd.img> in the same directory as the kernel image. Both are extracted and passed to B<qemu>. A net interface configured for user mode is added automatically. @@ -29,14 +29,17 @@ A net interface configured for user mode is added automatically. By default, the option B<-nographic> is passed to B<qemu> and one interacts with the serial console of the machine. This configuration is skipped in the presence of this option. -=item B<-i> I<image>, B<--image>=I<image> +=item B<-i> F<image>, B<--image>=F<image> This option specifies the location of the virtual machine image file. -By default B<rootfs.ext4> in the working directory is used. +By default F<rootfs.ext4> in the working directory is used. =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 StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $sshport root@127.0.0.1 =item B<--> I<qemu options> @@ -46,6 +49,32 @@ One possible use of this method is passing B<-snapshot> to avoid modifying the v =back +=head1 EXAMPLES + +Run a virtual machine stored in the image F<rootfs.ext4> (the default) with +local port 8022 routed to port 22 of the virtual machine. The B<-snapshot> +argument is passed to QEMU and prevents any permanent changes to +F<rootfs.ext4>, resulting in an ephemeral run. + + debvm-run -s 8022 -i rootfs.ext4 -- -snapshot + +=head1 FAQ + +=over 8 + +=item The debvm-run console renders wrong. + +Make sure C<$TERM> is set to a value known inside the VM. +You may need to install B<ncurses-term> for more definitions. +It also helps to run C<setterm --resize> after boot and when resizing the terminal emulator. + +=item How can I kill debvm-run? + +The wrapped B<qemu> can be terminated by pressing Ctrl-a x. +Refer to the B<qemu> manual page for more escape sequences. + +=back + =head1 LIMITATIONS Due to the way kernel and bootloader are being extracted before running B<qemu>, one cannot upgrade a kernel and then just reboot. @@ -120,7 +149,7 @@ while getopts :gi:s:-: OPTCHAR; do usage_error "missing argument for -$OPTARG" ;; '?') - usage_erro "unrecognized option -$OPTARG" + usage_error "unrecognized option -$OPTARG" ;; *) die "internal error while parsing command options, please report a bug" @@ -157,20 +186,22 @@ if command -v elf-arch >/dev/null 2>&1; then /sbin/debugfs "$IMAGE" -R "cat /bin/true" > "$KERNELTMP" VMARCH=$(elf-arch "$KERNELTMP") fi -case "$VMARCH" in - mips*|ppc64el) - KERNELLINK=vmlinux - ;; - *) - KERNELLINK=vmlinuz - ;; -esac - -KERNELNAME=$(/sbin/debugfs "$IMAGE" -R "stat $KERNELLINK" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d') -INITRDNAME=$(/sbin/debugfs "$IMAGE" -R "stat initrd.img" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d') +for KERNELLINK in vmlinuz vmlinux boot/vmlinuz boot/vmlinux; do + KERNELNAME=$(/sbin/debugfs "$IMAGE" -R "stat $KERNELLINK" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d') + test -n "$KERNELNAME" && break +done +if test "${KERNELLINK%/*}" = "$KERNELLINK"; then + BOOTDIR= +else + BOOTDIR="${KERNELLINK%/*}/" +fi test -n "$KERNELNAME" || die "failed to discover kernel image" +test "${KERNELNAME#/}" = "$KERNELNAME" && KERNELNAME="$BOOTDIR$KERNELNAME" + +INITRDNAME=$(/sbin/debugfs "$IMAGE" -R "stat ${BOOTDIR}initrd.img" | sed 's/Fast link dest: "\(.*\)"/\1/;t;d') test -n "$INITRDNAME" || die "failed to discover initrd image" +test "${INITRDNAME#/}" = "$INITRDNAME" && INITRDNAME="$BOOTDIR$INITRDNAME" KERNEL_CMDLINE="root=LABEL=debvm rw" NETDEV="user,id=net0" @@ -200,7 +231,11 @@ if test "$ARCHITECTURE" = "$VMARCH"; then ;; esac else + QEMU="qemu-system-$VMARCH" case "$VMARCH" in + amd64) + QEMU=qemu-system-x86_64 + ;; arm64) QEMU=qemu-system-aarch64 set -- -machine virt -cpu max "$@" @@ -209,23 +244,36 @@ else QEMU=qemu-system-arm set -- -machine virt -cpu max "$@" ;; + powerpc) + QEMU=qemu-system-ppc + MAX_SMP=1 + ;; ppc64el) QEMU=qemu-system-ppc64 ;; + m68k) + MAX_SMP=1 + ;; mips64el) - QEMU="qemu-system-$VMARCH" MAX_SMP=1 set -- -cpu 5KEc "$@" ;; mipsel) - QEMU="qemu-system-$VMARCH" MAX_SMP=1 ;; - *) - QEMU="qemu-system-$VMARCH" + riscv64) + set -- -machine virt "$@" + ;; + sparc64) + MAX_SMP=1 ;; esac fi +case "$VMARCH" in + amd64) + set -- -machine q35 "$@" + ;; +esac if test -z "$MAX_SMP" || test "$MAX_SMP" -gt 1; then NPROC=$(nproc) test -n "$MAX_SMP" && test "$NPROC" -gt "$MAX_SMP" && NPROC=$MAX_SMP @@ -239,6 +287,15 @@ if test -z "$GRAPHICAL"; then KERNEL_CMDLINE="$KERNEL_CMDLINE console=ttyS0" ;; esac + if test -t 0 && test -t 1 && test -n "$TERM"; then + KERNEL_CMDLINE="$KERNEL_CMDLINE TERM=$TERM" + fi +else + case "$VMARCH" in + amd64|i386) + set -- -vga virtio "$@" + ;; + esac fi if test -n "$SSHPORT"; then diff --git a/bin/debvm-waitssh b/bin/debvm-waitssh new file mode 100755 index 0000000..82eb14d --- /dev/null +++ b/bin/debvm-waitssh @@ -0,0 +1,178 @@ +#!/bin/sh +# Copyright 2023 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT + +: <<'POD2MAN' +=head1 NAME + +debvm-waitssh - Wait for a ssh server to be reachable + +=head1 SYNOPSIS + +B<debvm-waitssh> [B<-q>] [B<-t> I<timeout>] [I<hostname>:]I<port> + +=head1 DESCRIPTION + +B<debvm-waitssh> can be used to wait for a virtual machine with exposed ssh port to be reachable on that port. +If no hostname is given, B<127.0.0.1> is assumed. No authentication is attempted by B<debvm-waitssh>, so neither +a username nor a key have to be supplied. + +=head1 OPTIONS + +=over 8 + +=item B<-t> I<timeout>, B<--timeout>=I<timeout> + +Set the maximum duration for waiting in seconds. +Defaults to one minute. + +=item B<-q>, B<--quiet> + +Be quiet. +Do not output a message when the timeout has been reached without success. + +=back + +=head1 EXIT VALUES + +=over 8 + +=item B<0> + +The server is reachable. + +=item B<1> + +A timeout was reached before the server answered. + +=item B<2> + +Usage error. + +=back + +=head1 SEE ALSO + + debvm-run(1) + +=cut +POD2MAN + +set -u + +TOTALTIMEOUT=60 +SCANTIMEOUT=10 +SCANDELAY=1 +VERBOSITY=1 + +nth_arg() { + shift "$1" + printf "%s" "$1" +} + +die() { + echo "$*" >&2 + exit 2 +} +usage() { + die "usage: $0 [-q] [-t <timeout>] [<host>:]<port>" +} +usage_error() { + echo "error: $*" >&2 + usage +} + +opt_help() { + # shellcheck disable=SC2317 # not dead, called as "opt_$OPTARG" + usage +} +opt_quiet() { + VERBOSITY=0 +} +opt_timeout() { + TOTALTIMEOUT=$1 +} + +while getopts :qt:-: OPTCHAR; do + case "$OPTCHAR" in + q) opt_quiet ;; + t) opt_timeout "$OPTARG" ;; + -) + case "$OPTARG" in + help|quiet) + "opt_$OPTARG" + ;; + timeout) + test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG" + "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")" + OPTIND=$((OPTIND+1)) + ;; + timeout=) + "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))" + +test "$#" = 1 || usage + +case "$1" in + "") + usage + ;; + *:*) + HOST=${1%:*} + PORT=${1##*:} + ;; + *) + HOST=127.0.0.1 + PORT=$1 + ;; +esac + +case "$HOST" in *@*) + die "$0: hostname '$HOST' must not contain the '@' character. No username is required." +;; esac + +# Guard against strings containing anything but digits, strings starting with +# zero and empty strings as the port number. +# +# We cannot use [!0-9] because that matches on any character (or possibly +# multi-character collation element) that sorts in between 0 and 9. +case "$PORT" in *[!0123456789]*|0?*|""|??????*) + die "$0: port '$PORT' is not an integer between 1 and 65535" +;; esac +if test "$PORT" -lt 1 -o "$PORT" -gt 65535; then + die "$0: port '$PORT' is not an integer between 1 and 65535" +fi + +now=$(date +%s) +deadline=$((now + TOTALTIMEOUT)) +while test "$now" -lt "$deadline"; do + start=$now + ssh-keyscan -t rsa -T "$SCANTIMEOUT" -p "$PORT" "$HOST" >/dev/null 2>&1 && exit 0 + now=$(date +%s) + if test "$((now - start))" -lt "$SCANTIMEOUT"; then + sleep "$SCANDELAY" + now=$(date +%s) + fi +done +if [ "$VERBOSITY" -ge 1 ]; then + echo "$0: timeout reached trying to contact $HOST:$PORT after waiting $TOTALTIMEOUT seconds." >&2 +fi +exit 1 + diff --git a/share/customize-autologin.sh b/share/customize-autologin.sh new file mode 100755 index 0000000..4340650 --- /dev/null +++ b/share/customize-autologin.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Copyright 2022 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# +# This is a mmdebstrap customize hook that configures automatic root login on a +# serial console. It also parses the TERM kernel cmdline and passes it as +# TERM to agetty. + +set -eu + +TARGET=$1 + +UNIT=serial-getty@.service + +mkdir "$TARGET/etc/systemd/system/$UNIT.d" + +( + echo '[Service]' + printf '%s\n' 'ExecStartPre=/bin/sed -n -e "s/^\\(.* \\)\\?\\(TERM=[^ ]*\\).*/\\2/w/run/debvmterm" /proc/cmdline' + echo 'EnvironmentFile=-/run/debvmterm' + echo 'ExecStart=' + sed -n 's,^ExecStart=-/sbin/agetty ,&-a root ,p' "$TARGET/lib/systemd/system/$UNIT" +) > "$TARGET/etc/systemd/system/$UNIT.d/autologin.conf" diff --git a/share/customize-dpkgavailable.sh b/share/customize-dpkgavailable.sh new file mode 100755 index 0000000..f35b81f --- /dev/null +++ b/share/customize-dpkgavailable.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Copyright 2022 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# +# This is a mmdebstrap customize hook that initializes dpkg's available +# database from an updated apt package list cache. +# +# Without the available database, dpkg --set-selections won't work. + +set -eu + +APT_CONFIG=$MMDEBSTRAP_APT_CONFIG apt-cache dumpavail | dpkg --root "$1" --update-avail diff --git a/share/customize-kernel.sh b/share/customize-kernel.sh new file mode 100755 index 0000000..79a7449 --- /dev/null +++ b/share/customize-kernel.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# +# Copyright 2022 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# +# This is a mmdebstrap customize hook that installs a kernel image. The name +# of the kernel image depends on the architecture, derivative and release. + +set -eu + +TARGET="$1" + +if dpkg-query --root="$TARGET" --showformat='${db:Status-Status}\n' --show 'linux-image-*' 2>/dev/null | grep -q '^installed$'; then + exit 0 +fi + +ARCHITECTURE=$(cat "$TARGET/var/lib/dpkg/arch") + +KERNEL_ARCH="$ARCHITECTURE" +case "$ARCHITECTURE" in + armhf) + KERNEL_ARCH=armmp + ;; + hppa) + KERNEL_ARCH=parisc + ;; + i386) + KERNEL_ARCH=686-pae + ;; + mips64el) + KERNEL_ARCH=5kc-malta + ;; + mipsel) + KERNEL_ARCH=4kc-malta + ;; + ppc64) + KERNEL_ARCH=powerpc64 + ;; + ppc64el) + KERNEL_ARCH=powerpc64le + ;; +esac + +export APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" + +if test "${MMDEBSTRAP_MODE:-}" = chrootless; then + set -- \ + -oDPkg::Options::=--force-not-root \ + -oDPkg::Options::=--force-script-chrootless \ + -oDPkg::Options::=--root="$TARGET" \ + -oDPkg::Options::=--log="$TARGET/var/log/dpkg.log" +else + set -- -oDPkg::Chroot-Directory="$TARGET" +fi + +# On some derivatives such as Ubuntu, linux image does not depend on an initramfs. +apt-get --yes satisfy "$@" "linux-image-cloud-$KERNEL_ARCH | linux-image-$KERNEL_ARCH | linux-image-generic" "initramfs-tools | linux-initramfs-tool" diff --git a/share/customize-networkd.sh b/share/customize-networkd.sh new file mode 100755 index 0000000..c89aae2 --- /dev/null +++ b/share/customize-networkd.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Copyright 2022 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# +# This is a mmdebstrap customize hook that enables and configures +# systemd-networkd on various Debian releases. + +set -eu + +TARGET=$1 + +SYSTEMD_VERSION=$(dpkg-query --root "$TARGET" -f '${Version}' -W systemd) + +if test "${MMDEBSTRAP_MODE:-}" = chrootless; then + systemctl --root "$TARGET" enable systemd-networkd.service +else + chroot "$TARGET" systemctl enable systemd-networkd.service +fi + +{ + echo '[Match]' + echo 'Name=en*' + if dpkg --compare-versions "$SYSTEMD_VERSION" lt 220-7~; then + echo 'Name=eth*' + fi + + echo '[Network]' + echo 'DHCP=yes' + + if dpkg --compare-versions "$SYSTEMD_VERSION" lt 249; then + # This anchor is included by default since bullseye. Fails DNSSEC + # validation when missing. + echo 'DNSSECNegativeTrustAnchors=home.arpa' + fi + echo '[DHCP]' + echo 'UseDomains=yes' +} >"$TARGET/etc/systemd/network/20-wired.network" diff --git a/share/customize-resolved.sh b/share/customize-resolved.sh new file mode 100755 index 0000000..e8fe248 --- /dev/null +++ b/share/customize-resolved.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Copyright 2022 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# +# This is a mmdebstrap customize hook that enables systemd-resolved on various +# Debian releases. + +set -eu + +TARGET=$1 + +LIBNSS_RESOLVE_VERSION=$(dpkg-query --root "$TARGET" -f '${Version}' -W libnss-resolve 2>/dev/null) || : + +if dpkg --compare-versions "$LIBNSS_RESOLVE_VERSION" lt 251.3-2~exp1; then + if test "${MMDEBSTRAP_MODE:-}" = chrootless; then + systemctl --root "$TARGET" enable systemd-resolved.service + else + chroot "$TARGET" systemctl enable systemd-resolved.service + fi + + if test -z "$LIBNSS_RESOLVE_VERSION"; then + ln -fs ../run/systemd/resolve/resolv.conf "$TARGET/etc/resolv.conf" + else + ln -fs ../run/systemd/resolve/stub-resolv.conf "$TARGET/etc/resolv.conf" + fi +fi diff --git a/tests/create-and-run.sh b/tests/create-and-run.sh index aafd41d..f978052 100755 --- a/tests/create-and-run.sh +++ b/tests/create-and-run.sh @@ -1,36 +1,31 @@ #!/bin/sh -# shellcheck disable=SC2086 - if test "$#" -ne 2; then - echo "$(basename $0) takes two positional arguments. architecture and release" 1>&2 + echo "$(basename "$0") takes two positional arguments: architecture and release" 1>&2 exit 1 fi +ARCHITECTURE=$1 +RELEASE=$2 +SSH_KEYPATH=ssh_id +IMAGE=test.ext4 + +set -eux -set -ex +. "$(dirname "$0")/test_common.sh" cleanup() { - rm -f ssh_id ssh_id.pub test.ext4 + rm -f "$SSH_KEYPATH" "$SSH_KEYPATH.pub" "$IMAGE" } trap cleanup EXIT INT TERM QUIT -ssh-keygen -f ssh_id -N '' -debvm-create -k ssh_id.pub -o test.ext4 -a "$1" -r "$2" +ssh-keygen -f "$SSH_KEYPATH" -N '' +debvm-create -k "$SSH_KEYPATH.pub" -o "$IMAGE" -a "$ARCHITECTURE" -r "$RELEASE" -timeout 240s debvm-run -s 2222 -i test.ext4 & -timeout=5 -sshopt="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $(if test "$2" = jessie; then echo -o PubkeyAcceptedKeyTypes=+ssh-rsa; fi)" -ts=$(sleepenh 0 || [ $? -eq 1 ]) -for i in $(seq 30); do - rv=0 - ssh $sshopt -o ConnectTimeout="$timeout" -i ssh_id -p 2222 root@localhost echo success || rv=$? - test $rv -eq 0 && break - ts=$(sleepenh "$ts" "$timeout" || [ $? -eq 1 ]); - if test "$i" -eq 30; then - echo "timeout reached" >&2 - exit 1 - fi -done -ssh $sshopt -i ssh_id -p 2222 root@localhost poweroff +SSH_PORT=2222 +timeout 240s debvm-run -s "$SSH_PORT" -i "$IMAGE" & +set -- localhost +test "$RELEASE" = jessie && set -- -o PubkeyAcceptedKeyTypes=+ssh-rsa "$@" +debvm-waitssh -t 150 "$SSH_PORT" +run_ssh "$@" poweroff wait diff --git a/tests/dist-upgrades.sh b/tests/dist-upgrades.sh new file mode 100755 index 0000000..b90f17b --- /dev/null +++ b/tests/dist-upgrades.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Copyright 2022 Jochen Sprickerhof <debvm@jochen.sprickerhof.de> +# SPDX-License-Identifier: MIT +# +# apt install e2fsprogs genext2fs mmdebstrap openssh-client qemu-kvm + +set -x + +. "$(dirname "$0")/test_common.sh" + +SSH_KEYPATH=ssh_id + +cleanup() { + rm -f "$SSH_KEYPATH" "$SSH_KEYPATH.pub" upgrade +} + +trap cleanup EXIT INT TERM QUIT + +cat > upgrade << "EOF" +#!/bin/sh + +set -ex + +export DEBIAN_FRONTEND=noninteractive + +sed -i "s/\([^ ]*\) \([^ ]*\) [^ ]* \(.*\)/\1 \2 $1 \3/" /etc/apt/sources.list +apt update +apt dist-upgrade -y + +test "$1" = stretch && apt install libnss-resolve + +apt autoremove --purge -y +apt clean +poweroff +EOF + +chmod +x upgrade +ssh-keygen -f "$SSH_KEYPATH" -N '' + +debvm-create --sshkey="$SSH_KEYPATH.pub" -r jessie --size=3G -- --customize-hook="copy-in upgrade /usr/local/bin" + +SSH_PORT=2222 +for RELEASE in stretch buster bullseye bookworm sid; do + timeout 15m debvm-run -s "$SSH_PORT" & + set -- localhost + test "$RELEASE" = stretch && set -- -o PubkeyAcceptedKeyTypes=+ssh-rsa "$@" + debvm-waitssh -t 150 "$SSH_PORT" + run_ssh "$@" "upgrade $RELEASE" + wait +done + +timeout 5m debvm-run -s "$SSH_PORT" & +debvm-waitssh -t 150 "$SSH_PORT" +run_ssh localhost poweroff +wait diff --git a/tests/test_common.sh b/tests/test_common.sh new file mode 100644 index 0000000..cba9693 --- /dev/null +++ b/tests/test_common.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +run_ssh() { + test -n "${SSH_KEYPATH:-}" && set -- -i "$SSH_KEYPATH" "$@" + test -n "${SSH_PORT:-}" && set -- -p "$SSH_PORT" "$@" + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -l root "$@" +} diff --git a/useraddhook/customize.sh b/useraddhook/customize.sh new file mode 100755 index 0000000..3bba263 --- /dev/null +++ b/useraddhook/customize.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Copyright 2023 Johannes Schauer Marin Rodrigues <josch@debian.org> +# SPDX-License-Identifier: MIT +# +# Add a non-root user, add them to the sudo group and use the same authorized +# ssh keys as the root user. +# +# - the new user is called "user" +# - no password required for login +# - requires the passwd and coreutils packages installed inside the chroot +# - adds the new user to the sudo group if it exists +# - ~/.ssh/authorized_keys files is copied from root user if it exists +# +# Example usage: +# +# $ debvm-create -k ~/.ssh/id_rsa.pub -- --hook-dir=.../useraddhook --include sudo +# $ debvm-run -s 8022 +# $ ssh -l user -p 8022 127.0.0.1 whoami +# user +# $ ssh -l user -p 8022 127.0.0.1 sudo whoami +# root +# + +set -eu + +chroot "$1" useradd --home-dir /home/user --create-home --shell /bin/bash user +chroot "$1" passwd --delete user +if chroot "$1" getent group sudo >/dev/null; then + chroot "$1" usermod --append --groups sudo user +fi +if [ -e "$1"/root/.ssh/authorized_keys ]; then + chroot "$1" install -o user -g user -m 700 -d /home/user/.ssh + chroot "$1" install -o user -g user -t /home/user/.ssh /root/.ssh/authorized_keys +fi |