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

Jan Kiszka
2025-01-30T09:39:28Z

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"
--
2.43.0