Browse Source

MEN-5219: Integrate with `grub.d` framework.

This means that `grub-install` and `update-grub` no longer risk
bricking the device, but will produce boot scripts with Mender support
integrated. It also means that the standard GRUB menu will be
available.

It is supported on x86_64 platforms where `grub.d` is available, and
can be turned on and off with `MENDER_GRUB_D_INTEGRATION`. The default
is to use it if available.

Devices that did not previously use `grub.d` integration won't be
upgraded correctly with it turned on, so it is advised to set
`MENDER_GRUB_D_INTEGRATION=n` if you are upgrading existing devices.

Changelog: Commit

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>
change-dependabot-prefix
Kristian Amlie 3 years ago
parent
commit
245930608b
No known key found for this signature in database GPG Key ID: F464407C996AF03F
  1. 4
      Dockerfile
  2. 13
      configs/mender_grub_config
  3. 16
      mender-convert-modify
  4. 97
      modules/grub.sh
  5. 11
      modules/probe.sh
  6. 2
      scripts/test/mender-convert-qemu
  7. 1
      scripts/test/test-utils.sh
  8. 4
      tests/conftest.py
  9. 2
      tests/mender-image-tests
  10. 199
      tests/test_grub_integration.py

4
Dockerfile

@ -53,7 +53,9 @@ RUN apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y \
# manipulate binary and hex # manipulate binary and hex
xxd \ xxd \
# JSON power tool # JSON power tool
jq jq \
# GRUB command line tools, primarily grub-probe
grub-common
COPY --from=build /root/pxz/pxz /usr/bin/pxz COPY --from=build /root/pxz/pxz /usr/bin/pxz

13
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 # Only disable this if you know what you are doing
MENDER_GRUB_EFI_INTEGRATION=y 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 # Version of GRUB to use. Note that there needs to be a precompiled version
# available at the MENDER_STORAGE_URL download source. # available at the MENDER_STORAGE_URL download source.
GRUB_VERSION=2.04 GRUB_VERSION=2.04
@ -17,7 +26,7 @@ GRUB_VERSION=2.04
MENDER_GRUB_KERNEL_BOOT_ARGS="" MENDER_GRUB_KERNEL_BOOT_ARGS=""
# grub-mender-grubenv is the Mender integration for the GRUB bootloader # 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" 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 # Name of the storage device containing root filesystem partitions in GRUB

16
mender-convert-modify

@ -142,12 +142,24 @@ if [ "${MENDER_GRUB_EFI_INTEGRATION}" == "y" ]; then
if has_grub_efi "work/boot"; then if has_grub_efi "work/boot"; then
# No need to install Grub, use the one already present, and only install # No need to install Grub, use the one already present, and only install
# our grub.cfg # our tools.
grub_install_with_grub_efi_present log_info "GRUB EFI bootloader already present, not installing one."
grub_install_grub_editenv_binary
else else
log_info "Installing GRUB EFI bootloader..."
grub_install_mender_grub grub_install_mender_grub
fi 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 fi
run_and_log_cmd "sudo mkdir -p work/rootfs/data/mender" run_and_log_cmd "sudo mkdir -p work/rootfs/data/mender"

97
modules/grub.sh

@ -45,7 +45,12 @@ EOF
mender_kernel_root_base=${MENDER_STORAGE_DEVICE_BASE} mender_kernel_root_base=${MENDER_STORAGE_DEVICE_BASE}
EOF EOF
fi fi
}
# grub_install_standalone_grub_config
#
#
function grub_install_standalone_grub_config() {
if [ -n "${MENDER_GRUB_KERNEL_BOOT_ARGS}" ]; then if [ -n "${MENDER_GRUB_KERNEL_BOOT_ARGS}" ]; then
cat <<- EOF > work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION}/11_bootargs_grub.cfg cat <<- EOF > work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION}/11_bootargs_grub.cfg
set bootargs="${MENDER_GRUB_KERNEL_BOOT_ARGS}" set bootargs="${MENDER_GRUB_KERNEL_BOOT_ARGS}"
@ -55,10 +60,83 @@ EOF
( (
cd work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION} cd work/grub-mender-grubenv-${MENDER_GRUBENV_VERSION}
run_and_log_cmd "make 2>&1" 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=$PWD/../ BOOT_DIR=boot install-standalone-boot-files"
run_and_log_cmd "sudo make DESTDIR=../rootfs install-tools" 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 # 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_grub_efi_present() {
grub_create_grub_config
grub_install_grub_editenv_binary
}
# grub_install_mender_grub # grub_install_mender_grub
# #
# Install mender-grub on the converted boot partition # 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" run_and_log_cmd "sudo ln -s ${initrd_imagetype} work/rootfs/boot/initrd"
fi fi
log_info "Generating the mender-grub config..."
grub_create_grub_config
# Remove conflicting boot files. These files do not necessarily effect the # Remove conflicting boot files. These files do not necessarily effect the
# functionality, but lets get rid of them to avoid confusion. # functionality, but lets get rid of them to avoid confusion.
# #

11
modules/probe.sh

@ -353,3 +353,14 @@ is_efi_compatible_kernel() {
has_grub_efi() { has_grub_efi() {
find "${1}" -type f -name 'grub*.efi' -print0 | grep -qz grub 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
}

2
scripts/test/mender-convert-qemu

@ -17,7 +17,7 @@ fi
qemu-system-x86_64 \ qemu-system-x86_64 \
-enable-kvm \ -enable-kvm \
-nographic \ -nographic \
-m 256 \ -m 512 \
-net user,hostfwd=tcp::8822-:22 \ -net user,hostfwd=tcp::8822-:22 \
-net nic,macaddr=52:54:00$(od -txC -An -N3 /dev/urandom|tr \ :) \ -net nic,macaddr=52:54:00$(od -txC -An -N3 /dev/urandom|tr \ :) \
-bios ${ovmf_file} \ -bios ${ovmf_file} \

1
scripts/test/test-utils.sh

@ -140,7 +140,6 @@ run_tests() {
--sdimg-location="${MENDER_CONVERT_DIR}/deploy" \ --sdimg-location="${MENDER_CONVERT_DIR}/deploy" \
--ssh-priv-key="./ssh-priv-key/key" \ --ssh-priv-key="./ssh-priv-key/key" \
--qemu-wrapper="../scripts/test/mender-convert-qemu" \ --qemu-wrapper="../scripts/test/mender-convert-qemu" \
mender-image-tests \
${pytest_extra_args} ${pytest_extra_args}
exitcode=$? exitcode=$?

4
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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # Load the parser for our custom option flags
pytest_plugins = "utils.parseropts.parseropts" pytest_plugins = "utils.parseropts.parseropts"
from utils.fixtures import *

2
tests/mender-image-tests

@ -1 +1 @@
Subproject commit ee1266afe8ec58efce9b4223e2489ac023b0582f Subproject commit 8af131b9eface1555d2141d513c0dd6860c7815f

199
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=<something>` 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")
Loading…
Cancel
Save