diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10237ba..25a2349 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -223,12 +223,12 @@ test_acceptance_prebuilt_raspberrypi4: test_acceptance_qemux86_64: <<: *test_acceptance script: - - ./scripts/test/run-tests.sh --config versions_override_config --only qemux86_64 + - ./scripts/test/run-tests.sh --config versions_override_config --only ubuntu-qemux86-64 test_acceptance_debian_qemux86_64: <<: *test_acceptance script: - - ./scripts/test/run-tests.sh --config versions_override_config --only debian-qemux86_64 + - ./scripts/test/run-tests.sh --config versions_override_config --only debian-qemux86-64 test_acceptance_raspberrypi: <<: *test_acceptance diff --git a/Dockerfile b/Dockerfile index b54cc9b..6e77706 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,9 @@ RUN apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ # manipulate binary and hex xxd \ # JSON power tool - jq + jq \ +# GRUB command line tools, primarily grub-probe + grub-common COPY --from=build /root/pxz/pxz /usr/bin/pxz diff --git a/configs/debian-qemux86-64_config b/configs/debian-qemux86-64_config index d26fb19..48c6a40 100644 --- a/configs/debian-qemux86-64_config +++ b/configs/debian-qemux86-64_config @@ -2,8 +2,7 @@ # # The image is generated with the following command: # -# mkosi --root-size=2G --distribution=debian --release=bullseye --format=gpt_ext4 --bootable --checksum -# --password password --package=openssh-server,dhcpcd5 --output=Debian-11-x86-64.img build +# mender-convert/scripts/test/generate-image.sh debian # # Then manually uploaded to Mender's AWS S3 bucket for CI testing # @@ -13,10 +12,10 @@ # # and qemu is executed with the following command: # -# qemu-system-x86_64 -enable-kvm -m 512 -smp 2 -bios /usr/share/OVMF/OVMF_CODE.fd -drive format=raw,file=deploy/Debian-11-x86-64-qemux86_64-mender.img +# qemu-system-x86_64 -enable-kvm -m 512 -smp 2 -bios /usr/share/OVMF/OVMF_CODE.fd -drive format=raw,file=deploy/Debian-11-x86-64-qemux86-64-mender.img MENDER_STORAGE_DEVICE_BASE=/dev/sda -MENDER_DEVICE_TYPE="qemux86_64" +MENDER_DEVICE_TYPE="qemux86-64" # Nothing to copy MENDER_COPY_BOOT_GAP="n" diff --git a/configs/mender_grub_config b/configs/mender_grub_config index 69382ca..c14c513 100644 --- a/configs/mender_grub_config +++ b/configs/mender_grub_config @@ -1,8 +1,17 @@ -# Use U-boot -> GRUB -> EFI boot-loader integration +# Use UEFI -> GRUB boot-loader integration +# +# On ARM, U-boot is used as UEFI (EFI) provider # # Only disable this if you know what you are doing MENDER_GRUB_EFI_INTEGRATION=y +# Integrate with /etc/grub.d boot scripts. +# +# This has no effect unless MENDER_GRUB_EFI_INTEGRATION is enabled. Default is +# auto, in which case it will be detected. This is only supported for x86_64 +# platforms at the moment. +MENDER_GRUB_D_INTEGRATION=auto + # Version of GRUB to use. Note that there needs to be a precompiled version # available at the MENDER_STORAGE_URL download source. GRUB_VERSION=2.04 @@ -17,7 +26,7 @@ GRUB_VERSION=2.04 MENDER_GRUB_KERNEL_BOOT_ARGS="" # grub-mender-grubenv is the Mender integration for the GRUB bootloader -MENDER_GRUBENV_VERSION="1a7db967495bbe8be53b7a69dcb42822f39d9a74" +MENDER_GRUBENV_VERSION="b06a8e2cf13776b5cfc896fa8068006dd9992ebb" MENDER_GRUBENV_URL="${MENDER_GITHUB_ORG}/grub-mender-grubenv/archive/${MENDER_GRUBENV_VERSION}.tar.gz" # Name of the storage device containing root filesystem partitions in GRUB diff --git a/configs/qemux86-64_config b/configs/ubuntu-qemux86-64_config similarity index 71% rename from configs/qemux86-64_config rename to configs/ubuntu-qemux86-64_config index 2f21b14..9d62ef0 100644 --- a/configs/qemux86-64_config +++ b/configs/ubuntu-qemux86-64_config @@ -2,8 +2,7 @@ # # The image is generated with the following command: # -# mkosi --root-size=2G --distribution=ubuntu --release=focal --format=gpt_ext4 --bootable --checksum -# --password password --package=openssh-server,dhcpcd5 --output=Ubuntu-Focal-x86-64.img build +# mender-convert/scripts/test/generate-image.sh ubuntu # # Then manually uploaded to Mender's AWS S3 bucket for CI testing # @@ -13,10 +12,10 @@ # # and qemu is executed with the following command: # -# qemu-system-x86_64 -enable-kvm -m 512 -smp 2 -bios /usr/share/OVMF/OVMF_CODE.fd -drive format=raw,file=deploy/Ubuntu-Focal-x86-64-qemux86_64-mender.img +# qemu-system-x86_64 -enable-kvm -m 512 -smp 2 -bios /usr/share/OVMF/OVMF_CODE.fd -drive format=raw,file=deploy/Ubuntu-Focal-x86-64-qemux86-64-mender.img MENDER_STORAGE_DEVICE_BASE=/dev/sda -MENDER_DEVICE_TYPE="qemux86_64" +MENDER_DEVICE_TYPE="qemux86-64" # Nothing to copy MENDER_COPY_BOOT_GAP="n" diff --git a/mender-convert-modify b/mender-convert-modify index 9403e8e..f6ad9b1 100755 --- a/mender-convert-modify +++ b/mender-convert-modify @@ -140,14 +140,26 @@ if [ "${MENDER_GRUB_EFI_INTEGRATION}" == "y" ]; then # Check for known U-Boot problems in all files on the boot partition. check_for_broken_uboot_uefi_support work/boot - if has_secureboot_shim "work/boot"; then + if has_grub_efi "work/boot"; then # No need to install Grub, use the one already present, and only install - # our grub.cfg - grub_install_with_shim_present + # our tools. + log_info "GRUB EFI bootloader already present, not installing one." + grub_install_grub_editenv_binary else + log_info "Installing GRUB EFI bootloader..." grub_install_mender_grub fi + if [ "$MENDER_GRUB_D_INTEGRATION" = y ] || ( [ "$MENDER_GRUB_D_INTEGRATION" = auto ] && supports_grub_d "work/rootfs" ); then + log_info "Generating grub config using update-grub..." + grub_create_grub_config + grub_install_grub_d_config + else + log_info "Generating the mender-grub config..." + grub_create_grub_config + grub_install_standalone_grub_config + fi + fi run_and_log_cmd "sudo mkdir -p work/rootfs/data/mender" diff --git a/modules/grub.sh b/modules/grub.sh index 73bbb60..904f96f 100644 --- a/modules/grub.sh +++ b/modules/grub.sh @@ -45,7 +45,12 @@ EOF mender_kernel_root_base=${MENDER_STORAGE_DEVICE_BASE} EOF fi +} +# grub_install_standalone_grub_config +# +# +function grub_install_standalone_grub_config() { if [ -n "${MENDER_GRUB_KERNEL_BOOT_ARGS}" ]; then cat <<- EOF > work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION}/11_bootargs_grub.cfg set bootargs="${MENDER_GRUB_KERNEL_BOOT_ARGS}" @@ -55,10 +60,83 @@ EOF ( cd work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION} run_and_log_cmd "make 2>&1" - run_and_log_cmd "sudo make DESTDIR=../ BOOT_DIR=boot install-boot-files" - run_and_log_cmd "sudo make DESTDIR=../rootfs install-tools" + run_and_log_cmd "sudo make DESTDIR=$PWD/../ BOOT_DIR=boot install-standalone-boot-files" + run_and_log_cmd "sudo make DESTDIR=$PWD/../rootfs install-tools" + ) + +} + +# grub_install_grub_d_config +# +# +function grub_install_grub_d_config() { + if [ -n "${MENDER_GRUB_KERNEL_BOOT_ARGS}" ]; then + log_warn "MENDER_GRUB_KERNEL_BOOT_ARGS is ignored when MENDER_GRUB_D_INTEGRATION is enabled. Set it in the GRUB configuration instead." + fi + + # When using grub.d integration, /boot/efi must point to the boot partition, + # and /boot/grub must point to grub-mender-grubenv on the boot partition. + if [ ! -d work/rootfs/boot/efi ]; then + run_and_log_cmd "sudo mkdir work/rootfs/boot/efi" + fi + run_and_log_cmd "sudo mkdir work/boot/grub-mender-grubenv" + run_and_log_cmd "sudo mv work/rootfs/boot/grub/* work/boot/grub-mender-grubenv/" + run_and_log_cmd "sudo rmdir work/rootfs/boot/grub" + run_and_log_cmd "sudo ln -s efi/grub-mender-grubenv work/rootfs/boot/grub" + + ( + cd work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION} + run_and_log_cmd "make 2>&1" + run_and_log_cmd "sudo make DESTDIR=$PWD/../ BOOT_DIR=boot install-boot-env" + run_and_log_cmd "sudo make DESTDIR=$PWD/../rootfs install-grub.d-boot-scripts" + run_and_log_cmd "sudo make DESTDIR=$PWD/../rootfs install-tools" + # We need this for running the scripts once. + run_and_log_cmd "sudo make DESTDIR=$PWD/../rootfs install-offline-files" ) + # Mender-convert usually runs in a container. It's difficult to launch + # additional containers from within an existing one, but we need to run + # `update-grub` on a simulated device using some sort of container. Use good + # old `chroot`, which doesn't provide perfect containment, but it is good + # enough for our purposes, and doesn't require special containment + # capabilities. This will not work for foreign architectures, but we could + # probably use something like qemu-aarch64-static to get around that. + run_and_log_cmd "sudo mount work/boot work/rootfs/boot/efi -o bind" + run_and_log_cmd "sudo mount /dev work/rootfs/dev -o bind,ro" + run_and_log_cmd "sudo mount /proc work/rootfs/proc -o bind,ro" + run_and_log_cmd "sudo mount /sys work/rootfs/sys -o bind,ro" + + local ret=0 + + # Use `--no-nvram`, since we cannot update firmware memory in an offline + # build. Instead, use `--removable`, which creates entries that automate + # booting if you put the image into a new device, which you almost certainly + # will after using mender-convert. + run_and_log_cmd_noexit "sudo chroot work/rootfs grub-install --removable --no-nvram" || ret=$? + if [ $ret -eq 0 ]; then + run_and_log_cmd_noexit "sudo chroot work/rootfs grub-install --no-nvram" || ret=$? + fi + + if [ $ret -eq 0 ]; then + run_and_log_cmd_noexit "sudo chroot work/rootfs update-grub" || ret=$? + fi + + # Very important that these are unmounted, otherwise Docker may start to + # remove files inside them while tearing down the container. You can guess + # how I found that out... We run without the logger because otherwise the + # message from the previous command, which is the important one, is lost. + sudo umount -l work/rootfs/boot/efi || true + sudo umount -l work/rootfs/dev || true + sudo umount -l work/rootfs/proc || true + sudo umount -l work/rootfs/sys || true + + [ $ret -ne 0 ] && exit $ret + + ( + cd work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION} + # Should be removed after running. + run_and_log_cmd "sudo make DESTDIR=$PWD/../rootfs uninstall-offline-files" + ) } # grub_install_grub_editenv_binary @@ -74,18 +152,6 @@ function grub_install_grub_editenv_binary() { } -# grub_install_with_shim_present -# -# Keep the existing boot shim, and bootloader, and only install the mender-grub -# config -function grub_install_with_shim_present() { - - grub_create_grub_config - - grub_install_grub_editenv_binary - -} - # grub_install_mender_grub # # Install mender-grub on the converted boot partition @@ -98,9 +164,6 @@ function grub_install_mender_grub() { run_and_log_cmd "sudo ln -s ${initrd_imagetype} work/rootfs/boot/initrd" fi - log_info "Generating the mender-grub config..." - grub_create_grub_config - # Remove conflicting boot files. These files do not necessarily effect the # functionality, but lets get rid of them to avoid confusion. # @@ -110,7 +173,7 @@ function grub_install_mender_grub() { sudo rm -rf work/boot/EFI/systemd sudo rm -rf work/boot/NvVars for empty_dir in $( - cd work/boot && find . -maxdepth 1 -type d -empty + cd work/boot && find . -maxdepth 1 -type d -empty -not -name . ); do sudo rmdir work/boot/$empty_dir done diff --git a/modules/probe.sh b/modules/probe.sh index 1885fc5..0491fad 100644 --- a/modules/probe.sh +++ b/modules/probe.sh @@ -38,7 +38,7 @@ probe_arch() { target_arch="unknown" if grep -q x86-64 <<< "${file_info}"; then - target_arch="x86-64" + target_arch="x86_64" elif grep -Eq "ELF 32-bit.*ARM" <<< "${file_info}"; then target_arch="arm" elif grep -Eq "ELF 64-bit.*aarch64" <<< "${file_info}"; then @@ -57,7 +57,7 @@ probe_grub_efi_name() { local efi_name="" local -r arch=$(probe_arch) case "${arch}" in - "x86-64") + "x86_64") efi_name="grub-efi-bootx64.efi" ;; "arm") @@ -81,7 +81,7 @@ probe_debian_arch_name() { deb_arch="" arch=$(probe_arch) case "${arch}" in - "x86-64") + "x86_64") deb_arch="amd64" ;; "arm") @@ -130,7 +130,7 @@ probe_grub_efi_target_name() { local efi_target_name="" local -r arch=$(probe_arch) case "$arch" in - "x86-64") + "x86_64") efi_target_name="bootx64.efi" ;; "arm") @@ -345,11 +345,22 @@ is_efi_compatible_kernel() { return 0 } -# has_secureboot_shim +# has_grub_efi # -# $1 - the boot partition to search for a secureboot shim +# $1 - the boot partition to search for a grub*.efi # -# Checks the EFI/* filesystem for the presence of a signed boot shim -has_secureboot_shim() { - find "${1}" -type f -name 'shim*.efi' -print0 | grep -qz shim +# Checks the EFI/* filesystem for the presence of a GRUB bootloader +has_grub_efi() { + find "${1}" -type f -name 'grub*.efi' -print0 | grep -qz grub +} + +supports_grub_d() { + test -d "$1"/etc/grub.d || return 1 + + # Because we are executing programs inside a chroot in the image, we cannot + # currently convert non-native architectures to use grub.d integration. See + # relevant section on chroot inside grub_install_grub_d_config. + [ "$(probe_arch)" = "$(uname -m)" ] || return 1 + + return 0 } diff --git a/scripts/test/generate-image.sh b/scripts/test/generate-image.sh new file mode 100755 index 0000000..13d4e56 --- /dev/null +++ b/scripts/test/generate-image.sh @@ -0,0 +1,169 @@ +#!/bin/bash + +set -e + +usage() { + cat 1>&2 <&2 + exec sudo "$0" "$@" +fi + +while [ -n "$1" ]; do + case "$1" in + "ubuntu") + GENERATE_VARIANT=generate_ubuntu + ;; + "debian") + GENERATE_VARIANT=generate_debian + ;; + *) + usage + exit 1 + ;; + esac + shift +done + +cleanup_losetup() { + set +e + for dev in ${LO_DEVICE}p*; do + umount $dev + done + losetup -d $LO_DEVICE + rmdir tmp-p1 + rmdir tmp-p2 +} + +generate_debian() { + local -r image="Debian-11-x86-64.img" + + mkosi --root-size=2G --distribution=debian --release=bullseye --format=gpt_ext4 --bootable --checksum \ + --password password --package=openssh-server,dhcpcd5 --package grub-efi-amd64-signed \ + --package shim-signed --package lsb-release --output="$image" build + + post_process_image "$image" + + echo "Image successfully generated!" 1>&2 +} + +generate_ubuntu() { + local -r image="Ubuntu-Focal-x86-64.img" + + mkosi --root-size=2G --distribution=ubuntu --release=focal --format=gpt_ext4 --bootable --checksum \ + --password password --package=openssh-server,dhcpcd5 --package grub-efi-amd64-signed \ + --package shim-signed --package lsb-release --output="$image" build + + post_process_image "$image" + + echo "Image successfully generated!" 1>&2 +} + +post_process_image() { + local -r image="$1" + + mkdir -p tmp-p1 + mkdir -p tmp-p2 + + LO_DEVICE=$(losetup --find --show --partscan "$image") + trap cleanup_losetup EXIT + mount ${LO_DEVICE}p1 tmp-p1 + mount ${LO_DEVICE}p2 tmp-p2 + + pre_tweaks tmp-p1 tmp-p2 + + create_grub_regeneration_service tmp-p1 tmp-p2 + umount tmp-p1 + umount tmp-p2 + regenerate_grub_live "$image" + mount ${LO_DEVICE}p1 tmp-p1 + mount ${LO_DEVICE}p2 tmp-p2 + + post_tweaks tmp-p1 tmp-p2 +} + +pre_tweaks() { + local -r boot="$1" + local -r root="$2" + + # Fstab is missing for some reason. I'm not exactly sure why systemd-boot + # works without this, and GRUB doesn't. + cat > "$root/etc/fstab" < /dev/null`' > $root/etc/default/grub + fi + + sed -E -i -e 's/^#? *PermitRootLogin .*/PermitRootLogin yes/' $root/etc/ssh/sshd_config +} + +post_tweaks() { + local -r boot="$1" + local -r root="$2" + + # Delete systemd-boot, which isn't normally present in images that were + # installed with OS installers, at least not at the time of writing. + rm -rf "$boot/EFI/systemd" + + # Also replace bootx64.efi, which is the default bootloader. Mkosi installs + # systemd-bootx86.efi, but we want the shim. + rm -f "$boot/EFI/BOOT/*" + mkdir -p "$boot/EFI/BOOT" + cp "$root/usr/lib/shim/shimx64.efi.signed" "$boot/EFI/BOOT/BOOTX64.EFI" +} + +# Unfortunately installing grub scripts is something which is not really +# possible when offline. This is something which is easier with systemd-boot, so +# longterm GRUB will probably follow, or systemd-boot will take over. Anyway, +# let's do it by using a systemd service to perform the job, and then shut down. +create_grub_regeneration_service() { + local -r boot="$1" + local -r root="$2" + + cat > "$root/etc/systemd/system/mender-regenerate-grub-and-shutdown.service" <&2 + qemu-system-x86_64 \ + $maybe_kvm \ + -drive file="$image",if=ide,format=raw \ + -drive file=/usr/share/OVMF/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on \ + -drive file="$nvvars",if=pflash,format=raw,unit=1 \ + -display vnc=:23 \ + -m 512 \ + || ret=$? + [ $ret -eq 0 ] && break + done + + return $ret +} + +$GENERATE_VARIANT diff --git a/scripts/test/mender-convert-qemu b/scripts/test/mender-convert-qemu index 80a9b08..e9956fb 100755 --- a/scripts/test/mender-convert-qemu +++ b/scripts/test/mender-convert-qemu @@ -17,10 +17,11 @@ fi qemu-system-x86_64 \ -enable-kvm \ -nographic \ - -m 256 \ + -m 512 \ -net user,hostfwd=tcp::8822-:22 \ -net nic,macaddr=52:54:00$(od -txC -An -N3 /dev/urandom|tr \ :) \ - -bios ${ovmf_file} \ + -drive file=${ovmf_file},if=pflash,format=raw,unit=0,readonly=on \ + -drive file=./uefi-nvram/OVMF_VARS.fd,if=pflash,format=raw,unit=1,readonly=on \ -drive format=raw,file=${DISK_IMG} & qemu_pid=$! diff --git a/scripts/test/run-tests.sh b/scripts/test/run-tests.sh index b178fce..058e593 100755 --- a/scripts/test/run-tests.sh +++ b/scripts/test/run-tests.sh @@ -83,18 +83,17 @@ test_result=0 if [ -n "$PREBUILT_IMAGE" ]; then run_tests $PREBUILT_IMAGE \ - "-k" "'not test_update'" \ || test_result=$? exit $test_result else - if [ "$TEST_ALL" == "1" -o "$TEST_PLATFORM" == "qemux86_64" ]; then + if [ "$TEST_ALL" == "1" -o "$TEST_PLATFORM" == "ubuntu-qemux86-64" ]; then wget --progress=dot:giga -N ${UBUNTU_IMAGE_URL} -P input/ - convert_and_test "qemux86_64" \ + convert_and_test "qemux86-64" \ "release-1" \ "input/Ubuntu-Focal-x86-64.img.gz" \ "--overlay tests/ssh-public-key-overlay" \ - "--config configs/qemux86-64_config $EXTRA_CONFIG" \ + "--config configs/ubuntu-qemux86-64_config $EXTRA_CONFIG" \ || test_result=$? echo >&2 "----------------------------------------" @@ -104,10 +103,10 @@ else gunzip --force "input/Ubuntu-Focal-x86-64.img.gz" run_convert "release-2" \ "input/Ubuntu-Focal-x86-64.img" \ - "--config configs/qemux86-64_config $EXTRA_CONFIG" || test_result=$? + "--config configs/ubuntu-qemux86-64_config $EXTRA_CONFIG" || test_result=$? ret=0 - test -f deploy/Ubuntu-Focal-x86-64-qemux86_64-mender.img || ret=$? - assert "${ret}" "0" "Expected uncompressed file deploy/Ubuntu-Focal-x86-64-qemux86_64-mender.img" + test -f deploy/Ubuntu-Focal-x86-64-qemux86-64-mender.img || ret=$? + assert "${ret}" "0" "Expected uncompressed file deploy/Ubuntu-Focal-x86-64-qemux86-64-mender.img" fi if [ "$TEST_ALL" == "1" -o "$TEST_PLATFORM" == "raspberrypi3" ]; then @@ -117,8 +116,6 @@ else "release-1" \ "input/${RASPBIAN_IMAGE}" \ "--config configs/raspberrypi3_config $EXTRA_CONFIG" \ - -- \ - "-k" "'not test_update'" \ || test_result=$? fi @@ -132,8 +129,6 @@ else "release-1" \ "input/${BBB_DEBIAN_SDCARD_IMAGE_UNCOMPRESSED}" \ "--config configs/beaglebone_black_debian_sdcard_config $EXTRA_CONFIG" \ - -- \ - "-k" "'not test_update'" \ || test_result=$? rm -rf deploy @@ -145,8 +140,6 @@ else "release-1" \ "input/${BBB_DEBIAN_EMMC_IMAGE_UNCOMPRESSED}" \ "--config configs/beaglebone_black_debian_emmc_config $EXTRA_CONFIG" \ - -- \ - "-k" "'not test_update'" \ || test_result=$? fi @@ -157,14 +150,12 @@ else "release-1" \ "input/${UBUNTU_SERVER_RPI_IMAGE_COMPRESSED}" \ "--config configs/raspberrypi3_config $EXTRA_CONFIG" \ - -- \ - "-k" "'not test_update'" \ || test_result=$? fi - if [ "$TEST_ALL" == "1" -o "$TEST_PLATFORM" == "debian-qemux86_64" ]; then + if [ "$TEST_ALL" == "1" -o "$TEST_PLATFORM" == "debian-qemux86-64" ]; then wget --progress=dot:giga -N ${DEBIAN_IMAGE_URL} -P input/ - convert_and_test "qemux86_64" \ + convert_and_test "qemux86-64" \ "release-1" \ "input/Debian-11-x86-64.img.gz" \ "--overlay tests/ssh-public-key-overlay" \ diff --git a/scripts/test/test-utils.sh b/scripts/test/test-utils.sh index 1ef608b..8e23583 100644 --- a/scripts/test/test-utils.sh +++ b/scripts/test/test-utils.sh @@ -140,7 +140,6 @@ run_tests() { --sdimg-location="${MENDER_CONVERT_DIR}/deploy" \ --ssh-priv-key="./ssh-priv-key/key" \ --qemu-wrapper="../scripts/test/mender-convert-qemu" \ - mender-image-tests \ ${pytest_extra_args} exitcode=$? @@ -163,15 +162,15 @@ prepare_ssh_keys() { sudo chown -R root:root tests/ssh-public-key-overlay/root fi if [ "$(stat -c %a tests/ssh-public-key-overlay/root)" != "755" ]; then - chmod 755 tests/ssh-public-key-overlay/root + sudo chmod 755 tests/ssh-public-key-overlay/root fi if [ "$(stat -c %a tests/ssh-public-key-overlay/root/.ssh)" != "755" ]; then - chmod 700 tests/ssh-public-key-overlay/root/.ssh + sudo chmod 700 tests/ssh-public-key-overlay/root/.ssh fi if [ "$(stat -c %a tests/ssh-public-key-overlay/root/.ssh/authorized_keys)" != "755" ]; then - chmod 600 tests/ssh-public-key-overlay/root/.ssh/authorized_keys + sudo chmod 600 tests/ssh-public-key-overlay/root/.ssh/authorized_keys fi if [ "$(stat -c %a tests/ssh-priv-key/key)" != "600" ]; then - chmod 600 tests/ssh-priv-key/key + sudo chmod 600 tests/ssh-priv-key/key fi } diff --git a/tests/conftest.py b/tests/conftest.py index 5344b09..c00bfff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2021 Northern.tech AS +# Copyright 2022 Northern.tech AS # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,3 +22,5 @@ sys.path.append( # Load the parser for our custom option flags pytest_plugins = "utils.parseropts.parseropts" + +from utils.fixtures import * diff --git a/tests/mender-image-tests b/tests/mender-image-tests index ee1266a..7c081c0 160000 --- a/tests/mender-image-tests +++ b/tests/mender-image-tests @@ -1 +1 @@ -Subproject commit ee1266afe8ec58efce9b4223e2489ac023b0582f +Subproject commit 7c081c042f0024e87e9e15144b18d991fb378bcd diff --git a/tests/test_grub_integration.py b/tests/test_grub_integration.py new file mode 100644 index 0000000..4eaaa56 --- /dev/null +++ b/tests/test_grub_integration.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# Copyright 2022 Northern.tech AS +# +# 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. + +import pytest +import re +import os +import subprocess + +from utils.common import ( + extract_partition, + get_no_sftp, +) + + +@pytest.fixture(scope="function") +def cleanup_boot_scripts(request, connection): + """Take a backup of the various grub.cfg files and restore them after the + test. This is recommended for tests that call `grub-install` and/or + `update-grub`, so that other tests can also run them from a pristine + state.""" + + connection.run( + "cp $(find /boot/efi/EFI/ -name grub.cfg -not -path '*/EFI/BOOT/*') /data/grub-efi.cfg" + ) + connection.run("cp /boot/grub/grub.cfg /data/grub-main.cfg") + connection.run("cp /boot/grub-mender-grubenv.cfg /data/grub-mender-grubenv.cfg") + + def cleanup(): + connection.run( + "mv /data/grub-efi.cfg $(find /boot/efi/EFI/ -name grub.cfg -not -path '*/EFI/BOOT/*')" + ) + connection.run("mv /data/grub-main.cfg /boot/grub/grub.cfg") + connection.run("mv /data/grub-mender-grubenv.cfg /boot/grub-mender-grubenv.cfg") + + request.addfinalizer(cleanup) + + +def check_all_root_occurrences_valid(grub_cfg): + found_expected = False + inside_10_header = False + # One of the functions we define and use. + expected = "mender_check_and_restore_env" + with open(grub_cfg) as fd: + lineno = 0 + for line in fd.readlines(): + lineno += 1 + if re.match(r'^\s*root="\$\{mender_grub_storage_device\}', line): + continue + + if line.strip() == "### BEGIN /etc/grub.d/00_header ###": + # We allow root references inside the 00_header, because they + # are overriden by Mender later. + inside_10_header = True + elif line.strip() == "### END /etc/grub.d/00_header ###": + inside_10_header = False + if not inside_10_header and re.match(r"^\s*(set +)?root=", line): + pytest.fail( + "Found unexpected occurrence of `root=` in grub boot script\n" + "%d:%s" % (lineno, line) + ) + + if line.find(expected) >= 0: + found_expected = True + + assert found_expected, "Expected content (%s) not found" % expected + + +@pytest.mark.usefixtures("setup_board", "cleanup_boot_scripts") +class TestGrubIntegration: + @pytest.mark.min_mender_version("1.0.0") + def test_no_root_occurrences(self, connection, latest_part_image): + """Test that the generated grub scripts do not contain any occurrences of + `root=` except for known instances that we control. This is + important because Mender needs to keep tight control of when this + variable is set, in order to boot from, and mount, the correct root + partition.""" + + # First, check that the offline generated scripts don't have any. + extract_partition(latest_part_image, 1) + try: + subprocess.check_call( + ["mcopy", "-i", "img1.fs", "::/grub-mender-grubenv/grub.cfg", "."] + ) + check_all_root_occurrences_valid("grub.cfg") + finally: + os.remove("img1.fs") + os.remove("grub.cfg") + + extract_partition(latest_part_image, 2) + try: + subprocess.check_call( + [ + "debugfs", + "-R", + "dump -p /boot/grub-mender-grubenv.cfg grub-mender-grubenv.cfg", + "img2.fs", + ] + ) + check_all_root_occurrences_valid("grub-mender-grubenv.cfg") + finally: + os.remove("img2.fs") + os.remove("grub-mender-grubenv.cfg") + + # Then, check that the runtime generated scripts don't have any. + get_no_sftp("/boot/grub/grub.cfg", connection) + try: + check_all_root_occurrences_valid("grub.cfg") + finally: + os.remove("grub.cfg") + + get_no_sftp("/boot/grub-mender-grubenv.cfg", connection) + try: + check_all_root_occurrences_valid("grub-mender-grubenv.cfg") + finally: + os.remove("grub-mender-grubenv.cfg") + + # Check again after running `update-grub`. + connection.run("grub-install && update-grub") + get_no_sftp("/boot/grub/grub.cfg", connection) + try: + check_all_root_occurrences_valid("grub.cfg") + finally: + os.remove("grub.cfg") + + get_no_sftp("/boot/grub-mender-grubenv.cfg", connection) + try: + check_all_root_occurrences_valid("grub-mender-grubenv.cfg") + finally: + os.remove("grub-mender-grubenv.cfg") + + @pytest.mark.min_mender_version("1.0.0") + def test_offline_and_runtime_boot_scripts_identical(self, connection): + # Update scripts at runtime. + connection.run("grub-install && update-grub") + + # Take advantage of the copies already made by cleanup_boot_scripts + # fixture above, and use the copies in /data. + + # Take into account some known, but harmless differences. The "hd0,gpt1" + # style location is missing from the offline generated grub-efi.cfg + # file, but it is harmless because the filesystem UUID is being used + # instead. + connection.run( + r"sed -Ee 's/ *hd[0-9]+,gpt[0-9]+//' " + "$(find /boot/efi/EFI/ -name grub.cfg -not -path '*/EFI/BOOT/*') " + "> /data/new-grub-efi-modified.cfg" + ) + try: + connection.run("diff -u /data/grub-efi.cfg /data/new-grub-efi-modified.cfg") + finally: + connection.run("rm -f /data/new-grub-efi-modified.cfg") + + # Another few differences we work around in the main grub files: + # * `--hint` parameters are not generated in offline copy. + # * `root` variable is not set in offline copy. + # * `fwsetup` is added somewhat randomly depending on availability both + # on build host and device. + try: + connection.run("cp /data/grub-main.cfg /data/old-grub-modified.cfg") + connection.run("cp /boot/grub/grub.cfg /data/new-grub-modified.cfg") + connection.run( + r"sed -i -En -e '/\bsearch\b/{s/ --hint[^ ]*//g;}' " + "-e \"/^set root='hd0,gpt1'$/d\" " + r"-e '\,### BEGIN /etc/grub.d/30_uefi-firmware ###,{p; n; :loop; \,### END /etc/grub.d/30_uefi-firmware ###,b end; n; b loop; :end;}' " + "-e p " + "/data/old-grub-modified.cfg /data/new-grub-modified.cfg" + ) + connection.run("diff -u /data/old-grub-modified.cfg /data/new-grub-modified.cfg") + finally: + connection.run("rm -f /data/old-grub-modified.cfg /data/new-grub-modified.cfg") + + # Same differences as in previous check. + try: + connection.run("cp /data/grub-mender-grubenv.cfg /data/old-grub-mender-grubenv-modified.cfg") + connection.run("cp /boot/grub-mender-grubenv.cfg /data/new-grub-mender-grubenv-modified.cfg") + connection.run( + r"sed -i -En -e '/\bsearch\b/{s/ --hint[^ ]*//g;}' " + "-e \"/^set root='hd0,gpt1'$/d\" " + r"-e '\,### BEGIN /etc/grub.d/30_uefi-firmware ###,{p; n; :loop; \,### END /etc/grub.d/30_uefi-firmware ###,b end; n; b loop; :end;}' " + "-e p " + "/data/old-grub-mender-grubenv-modified.cfg /data/new-grub-mender-grubenv-modified.cfg" + ) + connection.run( + "diff -u /data/old-grub-mender-grubenv-modified.cfg /data/new-grub-mender-grubenv-modified.cfg" + ) + finally: + connection.run("rm -f /data/old-grub-mender-grubenv-modified.cfg /data/new-grub-mender-grubenv-modified.cfg") diff --git a/tests/uefi-nvram/MicCorThiParMarRoo_2010-10-05.crt b/tests/uefi-nvram/MicCorThiParMarRoo_2010-10-05.crt new file mode 100644 index 0000000..d6a50b8 Binary files /dev/null and b/tests/uefi-nvram/MicCorThiParMarRoo_2010-10-05.crt differ diff --git a/tests/uefi-nvram/OVMF_VARS.fd b/tests/uefi-nvram/OVMF_VARS.fd new file mode 100644 index 0000000..fbb72b7 Binary files /dev/null and b/tests/uefi-nvram/OVMF_VARS.fd differ diff --git a/tests/uefi-nvram/README.md b/tests/uefi-nvram/README.md new file mode 100644 index 0000000..3c9cdd4 --- /dev/null +++ b/tests/uefi-nvram/README.md @@ -0,0 +1,71 @@ +UEFI NVRAM +========== + +This directory holds the NVRAM file which is used as the firmware memory of the UEFI software which +runs under QEMU. It's main purpose is to start the UEFI software with certificates pre-loaded into +the firmware memory, and Secure Boot enabled. + +How to recreate the `OVMF_VARS.fd` file +-------------------------------- + +1. Create the `OVMF_VARS.fd` file: + ```bash + cp /usr/share/OVMF/OVMF_VARS.fd OVMF_VARS.fd + ``` + +2. Create a filesystem which contains the UEFI certificates: + ```bash + dd if=/dev/zero of=/tmp/cert-filesystem.fs bs=1M count=10; \ + mkfs.vfat /tmp/cert-filesystem.fs; \ + mkdir cert-filesystem; \ + sudo mount /tmp/cert-filesystem.fs cert-filesystem -o loop,uid=$UID; \ + cp *.crt cert-filesystem; \ + sudo umount cert-filesystem; \ + rmdir cert-filesystem + ``` + + Tip: If you ever need to re-fetch the certificate files, run `mokutil --db` on your own + computer. This lists your installed certificates, and they come with URLs which say where you can + download them. Make sure you download the `.crt`, not the `.crl`. + +3. Launch QEMU with the NVRAM and the filesystem containing the certificates. *Make sure to press F2 + quickly after the window appears to enter the firmware menu*: + ```bash + qemu-system-x86_64 \ + -drive file=/usr/share/OVMF/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on \ + -drive file=./OVMF_VARS.fd,if=pflash,format=raw,unit=1 \ + -drive file=/tmp/cert-filesystem.fs,if=ide,format=raw + ``` + +4. After having entered the firmware menu, perform the following steps: + + 1. Enter "Device Manager". + + 2. Enter "Secure Boot Configuration". + + 3. Switch "Secure Boot Mode" to "Custom Mode". + + 4. Enter "Custom Secure Boot Options". + + 5. Enter "PK Options". + + 6. Enter "Enroll OK". + + 7. Enter "Enroll PK Using File". + + 8. Locate the certificate file in the filesystem that you created in main step 2, and add it. + + 9. Make sure to select "Commit Changes". + + 10. Repeat the same process starting from sub step 5, except for "DB Options" instead. + + 11. Go back to the "Secure Boot Configuration" screen. "Attempt Secure Boot" should now have + been auto-selected. If it's not, enable it and save the change. + + 12. Go back to the main manu and select "Reset". After the setup has been exited, you can kill + QEMU. + +5. Clean up: + ```bash + rm -f /tmp/cert-filesystem.fs + ```