Re: [isar-cip-core][RFC][PATCH 03/10] initramfs: Add hook for managing /var in A/B fashion

Jan Kiszka
2025-01-30T16:10:41Z

On 30.01.25 10:39, Jan Kiszka wrote:
> From: Jan Kiszka <jan.kiszka@siemens.com>
>
> This hook manages snapshots of the filesystem backing /var in an A/B
> way, aligned with rootfs instances. That means when an new version is
> first booted in testing mode (EFI Boot Guard ustate=2), a snapshot of
> the current version is taken before that snapshot is mounted as /var.
> On rollback, the previous version will still be available and will be
> used again. After committing a new version, garbage collection of the
> previous snapshot is happening on next boot.
>
> This first version of the hook is written around EFI Boot Guard as A/B
> boot manager and btrfs as mechanism (here in form of a filesystem) to
> create and manage snapshots with low overhead. Other implementations
> are imaginable and would only require small refactorings to enable them
> as configurable alternatives.
>
> Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com>
> ---
> .../files/local-bottom.tmpl | 86 +++++++++++++++++++
> .../initramfs-abvar-hook_0.1.bb | 31 +++++++
> 2 files changed, 117 insertions(+)
> create mode 100644 recipes-initramfs/initramfs-abvar-hook/files/local-bottom.tmpl
> create mode 100644 recipes-initramfs/initramfs-abvar-hook/initramfs-abvar-hook_0.1.bb
>
> diff --git a/recipes-initramfs/initramfs-abvar-hook/files/local-bottom.tmpl b/recipes-initramfs/initramfs-abvar-hook/files/local-bottom.tmpl
> new file mode 100644
> index 00000000..91b8d017
> --- /dev/null
> +++ b/recipes-initramfs/initramfs-abvar-hook/files/local-bottom.tmpl
> @@ -0,0 +1,86 @@
> +#
> +# CIP Core, generic profile
> +#
> +# Copyright (c) Siemens, 2025
> +#
> +# Authors:
> +# Jan Kiszka <jan.kiszka@siemens.com>
> +#
> +
> +var_device="${INITRAMFS_VAR_DEVICE}"
> +var_opts="${INITRAMFS_VAR_MOUNT_OPTIONS}"
> +btrfs_mnt="/.var-btrfs"
> +snap_prefix="abvar-"
> +
> +. "${rootmnt}/etc/os-release"
> +
> +get_default_subvolume()
> +{
> + subvol=$(btrfs subvolume get-default "$1" | grep path || true)
> + echo "${subvol##*path }"
> +}
> +
> +ustate_val=$(bg_printenv -c -r -o ustate)
> +
> +# when in testing state or on first boot, create new snapshot
> +if [ "$ustate_val" = "USTATE=2" ] ||
> + ! mount -t btrfs -o "$var_opts,subvol=/$snap_prefix$IMAGE_UUID" \
> + "$var_device" "${rootmnt}/var" 2>/dev/null; then
> + log_begin_msg "Creating new /var snapshot for image $IMAGE_UUID"
> +
> + mkdir "$btrfs_mnt"
> + mount -t btrfs -o subvol=/ "$var_device" "$btrfs_mnt"
> +
> + # delete any dangling previous snapshot
> + btrfs subvolume delete "$btrfs_mnt/$snap_prefix$IMAGE_UUID" \
> + 2>/dev/null || true
> +
> + default_subvol=$(get_default_subvolume "$btrfs_mnt")
> + btrfs subvolume snapshot "$btrfs_mnt/$default_subvol" \
> + "$btrfs_mnt/$snap_prefix$IMAGE_UUID"
> +
> + umount "$btrfs_mnt"
> + rmdir "$btrfs_mnt"
> +
> + log_end_msg
> +
> + mount -t btrfs -o "$var_opts,subvol=/$snap_prefix$IMAGE_UUID" \
> + "$var_device" "${rootmnt}/var"
> +else
> + default_subvol=$(get_default_subvolume "${rootmnt}/var")
> +fi
> +
> +active_entry=$(btrfs subvolume list -a "${rootmnt}/var" |
> + grep "<FS_TREE>/$snap_prefix$IMAGE_UUID")
> +active_id="${active_entry#ID }"
> +active_id="${active_id%% *}"
> +
> +# adjust default subvolume to the active one
> +if [ "$default_subvol" != "$snap_prefix$IMAGE_UUID" ]; then
> + log_begin_msg "Adjusting /var subvolume default to $IMAGE_UUID"
> + btrfs subvolume set-default "$active_id" "${rootmnt}/var"
> + log_end_msg
> +fi
> +
> +if [ "$(bg_printenv -c -r -o ustate)" != "USTATE=0" ]; then
> + # still testing the current version, skip cleanup
> + exit 0
> +fi
> +
> +# get rid of obsolete snapshots, ie. everything but the active one
> +log_begin_msg "Performing /var snapshot housekeeping"
> +
> +IFS="
> +"
> +for entry in $(btrfs subvolume list -aqu "${rootmnt}/var" |
> + grep "<FS_TREE>/$snap_prefix"); do
> + entry_id="${entry#ID }"
> + entry_id="${entry_id%% *}"
> +
> + if [ "$entry_id" != "$active_id" ]; then
> + btrfs subvolume delete -i "$entry_id" "${rootmnt}/var"
> + fi
> +done
> +unset IFS
> +
> +log_end_msg
> diff --git a/recipes-initramfs/initramfs-abvar-hook/initramfs-abvar-hook_0.1.bb b/recipes-initramfs/initramfs-abvar-hook/initramfs-abvar-hook_0.1.bb
> new file mode 100644
> index 00000000..c7b58243
> --- /dev/null
> +++ b/recipes-initramfs/initramfs-abvar-hook/initramfs-abvar-hook_0.1.bb
> @@ -0,0 +1,31 @@
> +#
> +# CIP Core, generic profile
> +#
> +# Copyright (c) Siemens, 2025
> +#
> +# Authors:
> +# Jan Kiszka <jan.kiszka@siemens.com>
> +#
> +# SPDX-License-Identifier: MIT
> +
> +require recipes-initramfs/initramfs-hook/hook.inc
> +
> +SRC_URI += " \
> + file://local-bottom.tmpl"
> +
> +# override this to switch to UUID or PARTUUID based mounts
> +INITRAMFS_VAR_DEVICE ??= "/dev/disk/by-label/var"
> +
> +INITRAMFS_VAR_MOUNT_OPTIONS ??= "defaults,nodev,nosuid,noexec"
> +
> +TEMPLATE_FILES += "local-bottom.tmpl"
> +TEMPLATE_VARS += "\
> + INITRAMFS_VAR_DEVICE \
> + INITRAMFS_VAR_MOUNT_OPTIONS"
> +
> +HOOK_ADD_MODULES = "btrfs"
> +HOOK_COPY_EXECS = "btrfs grep rmdir bg_printenv"
> +
> +DEBIAN_DEPENDS .= ", btrfs-progs, efibootguard"
> +
> +SCRIPT_PREREQ = "crypt"

This prereq is actually wrong because crypt tries to mount the partition
as well, but that would happen without the logic of this hook. Happens
to work because crypt pushes itself to the end of the list and wins so far.

We generally do not need to wait on crypt bottom because also the top
script unlocks the partition AND updates by-xxx links as well. We just
need to use only those for the abvar hook. Next version should therefore
enforce the usage of labels or fs UUIDs.

Jan

--
Siemens AG, Foundational Technologies
Linux Expert Center