diff options
author | Alistair Delva <adelva@google.com> | 2021-03-29 17:52:41 -0700 |
---|---|---|
committer | Alistair Delva <adelva@google.com> | 2021-06-29 18:19:38 +0000 |
commit | 4323b9f4e5d4cfad7178da8039bf63d495ef4670 (patch) | |
tree | ed392500a2856ae05dcca0295f3134cee9a577da | |
parent | 17eec518e354dc478a4db7aa2175f7e252304ddf (diff) | |
download | tests-4323b9f4e5d4cfad7178da8039bf63d495ef4670.tar.gz |
Replace chroot based debootstrap with QEMU
To better support foreign debootstraps, and to debootstrap hermetically,
move away from using chroots. The chroot feature for foreign archs needs
qemu-user, and some versions of these tools crash randomly, which has
caused problems generating new filesystems.
This new method is quite a bit more complicated, but it also provides a
lot more validation. The user must specify a prebuilt kernel and
initramfs, and the initramfs is combined with one produced by the stage2
script, which allows the suite script to run under the kernel it will be
used with, which guarantees the combination will work.
Since we require the user to specify a kernel, we can also support
embedding the kernel, ramdisk (and for arm64 dtb) into the rootfs at
build time, because `uname -r` expands to the true kernel that will be
used.
The script supports building filesystems for non-virtual devices, but
the kernel provided must have at least virtio pci, blk, net and rng to
complete successfully.
These changes also finally enable a long requested capability to boot
the net tests root filesystem with a modular kernel, so that the GKI
kernel prebuilts can be used directly against it, without having to
build a custom "built-in" kernel equivalent. This in turn will enable
changes to run_net_test.sh to skip the kernel build and use cuttlefish
to host the test, which lets us integrate the testing with ATP.
Finally, these changes have been used to build Other OS root filesystems
for cuttlefish, and the root filesystem for the RockPi devices we are
using for ARM64 lab testing.
The bootstrapping process is conducted in three stages:
1) The debootstrap tool is run in --foreign mode offline to download
the needed packages for installation. However, no installation is
done and the binaries are simply unpacked, which works without
needing qemu-user to be installed.
2) QEMU is used to boot the user-provided kernel and a basic root
filesystem from debootstrap with a stage1.sh control script. The root
filesystem (512M max) is passed as a ramdisk, so no special drivers
are required. The stage1.sh script loads some virtual machine modules
and sets up the psuedo-filesystems, like the old debootstrap second
stage chroot did. Then, chains to stage2.sh to complete the
debootstrap second stage on *another* copy of the root filesystem
from virtio_blk. This process generates an initrd fragment which can
be combined with the user's initramfs to boot the final system.
3) Finally, the "installed" root filesystem is booted with the Debian
initial ramdisk and user-provided kernel modules. Once the root
filesystem is mounted, the suite script runs ("stage3"). This script
makes the usual customizations from our original chroot method.
If the -e option is specified, the kernel and "baked" initramfs are
stored in /boot. This process works for cuttlefish and rockpi but for
the normal net tests setup, embedding is not preferred (as the kernel
will always be changing). The grub2-common package is also installed to
create a grub.cfg for bootloader config chaining on cuttlefish.
Change-Id: I53f68031103e95b4b3c26da286195260ccd12f88
-rwxr-xr-x | net/test/build_rootfs.sh | 257 | ||||
-rwxr-xr-x | net/test/rootfs/bullseye-cuttlefish.sh | 2 | ||||
-rw-r--r-- | net/test/rootfs/common.sh | 33 | ||||
-rwxr-xr-x | net/test/rootfs/stage1.sh | 51 | ||||
-rwxr-xr-x | net/test/rootfs/stage2.sh | 18 |
5 files changed, 305 insertions, 56 deletions
diff --git a/net/test/build_rootfs.sh b/net/test/build_rootfs.sh index d6a2d91..4034f4e 100755 --- a/net/test/build_rootfs.sh +++ b/net/test/build_rootfs.sh @@ -20,17 +20,11 @@ set -u SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) -# Make sure we're in C locale so build inside chroot does not complain -# about missing files -unset LANG LANGUAGE \ - LC_ADDRESS LC_ALL LC_COLLATE LC_CTYPE LC_IDENTIFICATION LC_MEASUREMENT \ - LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME -export LC_ALL=C - usage() { echo -n "usage: $0 [-h] [-s bullseye|bullseye-cuttlefish] " - echo -n "[-a i386|amd64|armhf|arm64] " - echo -n "[-m http://mirror/debian] [-n rootfs] [-r initrd]" + echo -n "[-a i386|amd64|armhf|arm64] -k /path/to/kernel " + echo -n "-i /path/to/initramfs.gz [-d /path/to/dtb:subdir] " + echo "[-m http://mirror/debian] [-n rootfs] [-r initrd] [-e]" exit 1 } @@ -38,10 +32,13 @@ mirror=http://ftp.debian.org/debian suite=bullseye arch=amd64 +embed_kernel_initrd_dtb= +dtb_subdir= ramdisk= rootfs= +dtb= -while getopts ":hs:a:m:n:r:" opt; do +while getopts ":hs:a:m:n:r:k:i:d:e" opt; do case "${opt}" in h) usage @@ -54,15 +51,7 @@ while getopts ":hs:a:m:n:r:" opt; do suite="${OPTARG}" ;; a) - case "${OPTARG}" in - i386|amd64|armhf|arm64) - arch="${OPTARG}" - ;; - *) - echo "Invalid arch: ${OPTARG}" >&2 - usage - ;; - esac + arch="${OPTARG}" ;; m) mirror="${OPTARG}" @@ -73,6 +62,21 @@ while getopts ":hs:a:m:n:r:" opt; do r) ramdisk="${OPTARG}" ;; + k) + kernel="${OPTARG}" + ;; + i) + initramfs="${OPTARG}" + ;; + d) + dtb="${OPTARG%:*}" + if [ "${OPTARG#*:}" != "${dtb}" ]; then + dtb_subdir="${OPTARG#*:}/" + fi + ;; + e) + embed_kernel_initrd_dtb=1 + ;; \?) echo "Invalid option: ${OPTARG}" >&2 usage @@ -84,6 +88,45 @@ while getopts ":hs:a:m:n:r:" opt; do esac done +# Disable Debian's "persistent" network device renaming +cmdline="net.ifnames=0 rw PATH=/usr/sbin:/usr/bin" + +# Pass down embedding option, if specified +if [ -n "${embed_kernel_initrd_dtb}" ]; then + cmdline="${cmdline} embed_kernel_initrd_dtb=${embed_kernel_initrd_dtb}" +fi + +case "${arch}" in + i386) + cmdline="${cmdline} exitcode=/dev/ttyS1" + machine="pc-i440fx-2.8,accel=kvm" + qemu="qemu-system-i386" + cpu="max" + ;; + amd64) + cmdline="${cmdline} exitcode=/dev/ttyS1" + machine="pc-i440fx-2.8,accel=kvm" + qemu="qemu-system-x86_64" + cpu="max" + ;; + armhf) + cmdline="${cmdline} exitcode=/dev/ttyS0" + machine="virt,gic-version=2" + qemu="qemu-system-arm" + cpu="cortex-a15" + ;; + arm64) + cmdline="${cmdline} exitcode=/dev/ttyS0" + machine="virt,gic-version=2" + qemu="qemu-system-aarch64" + cpu="cortex-a53" # "max" is too slow + ;; + *) + echo "Invalid arch: ${OPTARG}" >&2 + usage + ;; +esac + if [[ -z "${rootfs}" ]]; then rootfs="rootfs.${arch}.${suite}.$(date +%Y%m%d)" fi @@ -94,6 +137,22 @@ if [[ -z "${ramdisk}" ]]; then fi ramdisk=$(realpath "${ramdisk}") +if [[ -z "${kernel}" ]]; then + echo "$0: Path to kernel image must be specified (with '-k')" + usage +elif [[ ! -e "${kernel}" ]]; then + echo "$0: Kernel image not found at '${kernel}'" + exit 2 +fi + +if [[ -z "${initramfs}" ]]; then + echo "Path to initial ramdisk image must be specified (with '-i')" + usage +elif [[ ! -e "${initramfs}" ]]; then + echo "Initial ramdisk image not found at '${initramfs}'" + exit 3 +fi + # Sometimes it isn't obvious when the script fails failure() { echo "Filesystem generation process failed." >&2 @@ -125,23 +184,90 @@ sudo debootstrap --arch="${arch}" --variant=minbase --include="${packages}" \ # Copy some bootstrapping scripts into the rootfs sudo cp -a "${SCRIPT_DIR}"/rootfs/*.sh root/ sudo cp -a "${SCRIPT_DIR}"/rootfs/net_test.sh sbin/net_test.sh -sudo chown -R root:root root/ sbin/net_test.sh +sudo chown root:root sbin/net_test.sh + +# Extract the ramdisk to bootstrap with to / +lz4 -lcd "${initramfs}" | sudo cpio -idum lib/modules/* # Create /host, for the pivot_root and 9p mount use cases sudo mkdir host -sudo chroot . root/stage2.sh -sudo chroot . root/${suite}.sh -raw_initrd="${PWD}"/boot/initrd.img +# Leave the workdir, to build the filesystem +cd - -# Workarounds for bugs in the debootstrap suite scripts -for mount in $(cat /proc/mounts | cut -d' ' -f2 | grep -e "^${workdir}"); do - echo "Unmounting mountpoint ${mount}.." >&2 +# For the initial ramdisk, and later for the final rootfs +mount=$(mktemp -d) +mount_remove() { + rmdir "${mount}" + tmpdir_remove +} +trap mount_remove EXIT + +# The initial ramdisk filesystem must be <=512M, or QEMU's -initrd +# option won't touch it +initrd=$(mktemp) +initrd_remove() { + rm -f "${initrd}" + mount_remove +} +trap initrd_remove EXIT +truncate -s 512M "${initrd}" +mke2fs -F -t ext3 -L ROOT "${initrd}" + +# Mount the new filesystem locally +sudo mount -o loop -t ext3 "${initrd}" "${mount}" +image_unmount() { sudo umount "${mount}" -done + initrd_remove +} +trap image_unmount EXIT -# Leave the workdir, to process the initrd -cd - +# Copy the patched debootstrap results into the new filesystem +sudo cp -a "${workdir}"/* "${mount}" +sudo rm -rf "${workdir}" + +# Unmount the initial ramdisk +sudo umount "${mount}" +trap initrd_remove EXIT + +# Copy the initial ramdisk to the final rootfs name and extend it +sudo cp -a "${initrd}" "${rootfs}" +truncate -s 2G "${rootfs}" +e2fsck -p -f "${rootfs}" || true +resize2fs "${rootfs}" + +# Create another fake block device for initrd.img writeout +raw_initrd=$(mktemp) +raw_initrd_remove() { + rm -f "${raw_initrd}" + initrd_remove +} +trap raw_initrd_remove EXIT +truncate -s 64M "${raw_initrd}" + +# Complete the bootstrap process using QEMU and the specified kernel +${qemu} -machine "${machine}" -cpu "${cpu}" -m 2048 >&2 \ + -kernel "${kernel}" -initrd "${initrd}" -no-user-config -nodefaults \ + -no-reboot -display none -nographic -serial stdio -parallel none \ + -smp 8,sockets=8,cores=1,threads=1 \ + -object rng-random,id=objrng0,filename=/dev/urandom \ + -device virtio-rng-pci-non-transitional,rng=objrng0,id=rng0,max-bytes=1024,period=2000 \ + -drive file="${rootfs}",format=raw,if=none,aio=threads,id=drive-virtio-disk0 \ + -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk0 \ + -drive file="${raw_initrd}",format=raw,if=none,aio=threads,id=drive-virtio-disk1 \ + -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk1 \ + -chardev file,id=exitcode,path=exitcode \ + -device pci-serial,chardev=exitcode \ + -append "root=/dev/ram0 ramdisk_size=524288 init=/root/stage1.sh ${cmdline}" +[[ -s exitcode ]] && exitcode=$(cat exitcode | tr -d '\r') || exitcode=2 +rm -f exitcode +if [ "${exitcode}" != "0" ]; then + echo "Second stage debootstrap failed (err=${exitcode})" + exit "${exitcode}" +fi + +# Fix up any issues from the unclean shutdown +e2fsck -p -f "${rootfs}" || true # New workdir for the initrd extraction workdir="${tmpdir}/initrd" @@ -153,8 +279,7 @@ sudo chown root:root "${workdir}" cd "${workdir}" # Process the initrd to remove kernel-specific metadata -lz4 -lcd "${raw_initrd}" | sudo cpio -idum -sudo rm -f "${raw_initrd}" +kernel_version=$(basename $(lz4 -lcd "${raw_initrd}" | sudo cpio -idumv 2>&1 | grep usr/lib/modules/ - | head -n1)) sudo rm -rf usr/lib/modules sudo mkdir -p usr/lib/modules @@ -168,32 +293,66 @@ sudo ln -s /lib usr/lib # Repack the ramdisk to the final output find * | sudo cpio -H newc -o --quiet | lz4 -lc9 >"${ramdisk}" -# Leave the workdir, to build the filesystem -workdir="${tmpdir}/_" +# Pack another ramdisk with the combined artifacts, for boot testing +cat "${ramdisk}" "${initramfs}" >"${initrd}" + +# Leave workdir to boot-test combined initrd cd - -# For the final image mount -mount=$(mktemp -d) -mount_remove() { - rmdir "${mount}" - tmpdir_remove +# Mount the new filesystem locally +sudo mount -o loop -t ext3 "${rootfs}" "${mount}" +image_unmount2() { + sudo umount "${mount}" + raw_initrd_remove } -trap mount_remove EXIT +trap image_unmount2 EXIT -# Create a 1G empty ext3 filesystem -truncate -s 1G "${rootfs}" -mke2fs -F -t ext3 -L ROOT "${rootfs}" +# Embed the kernel and dtb images now, if requested +if [ -n "${embed_kernel_initrd_dtb}" ]; then + if [ -n "${dtb}" ]; then + sudo mkdir -p "${mount}/boot/dtb/${dtb_subdir}" + sudo cp -a "${dtb}" "${mount}/boot/dtb/${dtb_subdir}" + sudo chown -R root:root "${mount}/boot/dtb/${dtb_subdir}" + fi + sudo cp -a "${kernel}" "${mount}/boot/vmlinuz-${kernel_version}" + sudo chown root:root "${mount}/boot/vmlinuz-${kernel_version}" +fi -# Mount the new filesystem locally +# Unmount the initial ramdisk +sudo umount "${mount}" +trap raw_initrd_remove EXIT + +# Boot test the new system and run stage 3 +${qemu} -machine "${machine}" -cpu "${cpu}" -m 2048 >&2 \ + -kernel "${kernel}" -initrd "${initrd}" -no-user-config -nodefaults \ + -no-reboot -display none -nographic -serial stdio -parallel none \ + -smp 8,sockets=8,cores=1,threads=1 \ + -object rng-random,id=objrng0,filename=/dev/urandom \ + -device virtio-rng-pci-non-transitional,rng=objrng0,id=rng0,max-bytes=1024,period=2000 \ + -drive file="${rootfs}",format=raw,if=none,aio=threads,id=drive-virtio-disk0 \ + -device virtio-blk-pci-non-transitional,scsi=off,drive=drive-virtio-disk0 \ + -chardev file,id=exitcode,path=exitcode \ + -device pci-serial,chardev=exitcode \ + -netdev user,id=usernet0,ipv6=off \ + -device virtio-net-pci-non-transitional,netdev=usernet0,id=net0 \ + -append "root=LABEL=ROOT init=/root/${suite}.sh ${cmdline}" +[[ -s exitcode ]] && exitcode=$(cat exitcode | tr -d '\r') || exitcode=2 +rm -f exitcode +if [ "${exitcode}" != "0" ]; then + echo "Root filesystem finalization failed (err=${exitcode})" + exit "${exitcode}" +fi + +# Fix up any issues from the unclean shutdown +e2fsck -p -f "${rootfs}" || true + +# Mount the final rootfs locally sudo mount -o loop -t ext3 "${rootfs}" "${mount}" -image_unmount() { +image_unmount3() { sudo umount "${mount}" - mount_remove + raw_initrd_remove } -trap image_unmount EXIT - -# Copy the patched debootstrap results into the new filesystem -sudo cp -a "${workdir}"/* "${mount}" +trap image_unmount3 EXIT # Fill the rest of the space with zeroes, to optimize compression sudo dd if=/dev/zero of="${mount}/sparse" bs=1M 2>/dev/null || true diff --git a/net/test/rootfs/bullseye-cuttlefish.sh b/net/test/rootfs/bullseye-cuttlefish.sh index a4a909b..b61f8e5 100755 --- a/net/test/rootfs/bullseye-cuttlefish.sh +++ b/net/test/rootfs/bullseye-cuttlefish.sh @@ -44,5 +44,7 @@ install_and_cleanup_iptables create_systemd_getty_symlinks ttyS0 hvc1 +setup_grub "net.ifnames=0" + apt-get purge -y vim-tiny bullseye_cleanup diff --git a/net/test/rootfs/common.sh b/net/test/rootfs/common.sh index 3c9d8d7..c86c39c 100644 --- a/net/test/rootfs/common.sh +++ b/net/test/rootfs/common.sh @@ -15,6 +15,8 @@ # limitations under the License. # +trap "echo 3 >${exitcode}" ERR + # $1 - Suite name for apt sources update_apt_sources() { # Add the needed debian sources @@ -49,6 +51,11 @@ remove_installed_packages() { } setup_static_networking() { + # Temporarily bring up static QEMU SLIRP networking (no DHCP) + ip link set dev eth0 up + ip addr add 10.0.2.15/24 broadcast 10.0.2.255 dev eth0 + ip route add default via 10.0.2.2 dev eth0 + # Permanently update the resolv.conf with the Google DNS servers echo "nameserver 8.8.8.8" >/etc/resolv.conf echo "nameserver 8.8.4.4" >>/etc/resolv.conf @@ -106,6 +113,23 @@ create_systemd_getty_symlinks() { done } +# $1 - Additional default command line +setup_grub() { + if [ -n "${embed_kernel_initrd_dtb}" ]; then + # For testing the image with a virtual device + apt-get install -y grub2-common + cat >/etc/default/grub <<EOF +GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=Debian +GRUB_CMDLINE_LINUX_DEFAULT="quiet" +GRUB_CMDLINE_LINUX="\\\$cmdline $1" +EOF + mkdir /boot/grub + update-grub + fi +} + cleanup() { # Prevents systemd boot issues with read-only rootfs mkdir -p /var/lib/systemd/{coredump,linger,rfkill,timesync} @@ -114,10 +138,17 @@ cleanup() { # If embedding isn't enabled, remove the embedded modules and initrd and # uninstall the tools to regenerate the initrd, as they're unlikely to # ever be used - apt-get purge -y initramfs-tools initramfs-tools-core klibc-utils kmod + if [ -z "${embed_kernel_initrd_dtb}" ]; then + apt-get purge -y initramfs-tools initramfs-tools-core klibc-utils kmod + rm -f "/boot/initrd.img-$(uname -r)" + rm -rf "/lib/modules/$(uname -r)" + fi # Miscellaneous cleanup rm -rf /var/lib/apt/lists/* || true rm -f /root/* || true apt-get clean + + echo 0 >"${exitcode}" + sync && poweroff -f } diff --git a/net/test/rootfs/stage1.sh b/net/test/rootfs/stage1.sh new file mode 100755 index 0000000..ccf54f1 --- /dev/null +++ b/net/test/rootfs/stage1.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e +set -u + +trap "echo 1 >${exitcode}" ERR + +# So we have a rw location to extract kmod +mount -t tmpfs tmpfs /tmp + +# Extract kmod utility to /tmp +dpkg-deb -x /var/cache/apt/archives/kmod*.deb /tmp +ln -s /tmp/bin/kmod /tmp/insmod + +# Load just enough to get the rootfs from virtio_blk +module_dir=/lib/modules/$(uname -r)/kernel +# virtio_pci_modern_dev was split out in 5.12 +/tmp/insmod ${module_dir}/drivers/virtio/virtio_pci_modern_dev.ko || true +/tmp/insmod ${module_dir}/drivers/virtio/virtio_pci.ko +/tmp/insmod ${module_dir}/drivers/block/virtio_blk.ko +/tmp/insmod ${module_dir}/drivers/char/hw_random/virtio-rng.ko + +# Mount devtmpfs so we can see /dev/vda +mount -t devtmpfs devtmpfs /dev + +# Mount /dev/vda over the top of /root +mount /dev/vda /root + +# Switch to the new root and start stage 2 +mount -n --move /dev /root/dev +mount -n --move /tmp /root/tmp +mount -n -t proc none /root/proc +mount -n -t sysfs none /root/sys +mount -n -t tmpfs tmpfs /root/run +pivot_root /root /root/host +exec chroot / /root/stage2.sh ${exitcode} </dev/console >/dev/console 2>&1 diff --git a/net/test/rootfs/stage2.sh b/net/test/rootfs/stage2.sh index 26be79e..7f2e76f 100755 --- a/net/test/rootfs/stage2.sh +++ b/net/test/rootfs/stage2.sh @@ -18,6 +18,11 @@ set -e set -u +trap "echo 2 >${exitcode}" ERR + +# Remove the old ramdisk root; we don't need it any more +umount -l /host + # Complete the debootstrap process /debootstrap/debootstrap --second-stage @@ -50,14 +55,15 @@ EOF mkdir -p /var/empty # Clean up any other junk created by the imaging process -rm -rf /root/stage2.sh /tmp/* +rm -rf /root/stage1.sh /root/stage2.sh /root/lib /tmp/* find /var/log -type f -exec rm -f '{}' ';' find /var/tmp -type f -exec rm -f '{}' ';' # Create an empty initramfs to be combined with modules later sed -i 's,^COMPRESS=gzip,COMPRESS=lz4,' /etc/initramfs-tools/initramfs.conf -mkdir -p /lib/modules/0.0 -depmod -a 0.0 -update-initramfs -c -k 0.0 -mv /boot/initrd.img-0.0 /boot/initrd.img -rm -rf /lib/modules/0.0 +depmod -a $(uname -r) +update-initramfs -c -k $(uname -r) +dd if=/boot/initrd.img-$(uname -r) of=/dev/vdb conv=fsync + +echo 0 >"${exitcode}" +sync && poweroff -f |