Message ID | 37b9560989414bfd128bfe7e1d5afec53a148179.1738229957.git.jan.kiszka@siemens.com (mailing list archive) |
---|---|
State | New |
Headers | show |
Series | Provide A/B snapshot support for persistent /var | expand |
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
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"