Inspired from Android A/B system updates mechanism.

Series Index

  1. Linux Bootstrap Installation
  2. Linux A/B System Updates via BTRFS Snapshot
  3. Linux Post Installation: Desktop Preparation
  4. Linux Desktop: Sway, Labwc, GUI Apps

Preface

This guide is distro independent, could work on any distros.

Background

Things started when I was intrigued by some discussions about booting from BTRFS snapshots directly. And before this A/B solution coming to my mind, I was just using following commands to create and remove snapshots manually.

(root)# btrfs subvolume snapshot / <destination>
(root)# btrfs subvolume delete <destination>

Principle

The core idea of this A/B solutions is there are two subvolumes for root partition, @a and @b, they are mutually to be the snapshot of each other. And we maintain two bootloader entries for them respectively. The tricky part is you need to alter the fstab to make these two subvolumes point to the right ones after generating the new snapshot everytime.

Subvolumes

Let’s begin with subvolume overview. You need entrering a live system environment to create subvolumes.

(root)# mount /dev/disk/by-partlabel/ROOTPART /mnt
(root)# btrfs subvolume create /mnt/@a
(root)# btrfs subvolume create /mnt/@a/@
(root)# btrfs subvolume create /mnt/@b
(root)# btrfs subvolume create /mnt/@b/@
(root)# btrfs subvolume create /mnt/@home

If you feel strange about this ROOTPART label, read the previous guide for basic system installation first. For simplicity, the root partition in this guide is not encrypted, which means LUKS is not involved.

The nested subvolumes @ are the real root partition, which will be mounted as /, and they also are the snapshots for each other (subvolume and snapshot are basically same in BTRFS). The reason of subvolume stucture be like this is when we creating BTRFS snapshot, the destination path must not exist, which means we need to delete old @ subvolume first to create new one.

For example, if we remove this nested layer, just mount @a as /, @b as /b, we could not remove /b or @b since it’s a mount point in use. If we add this nested layer, mount @a/@ as / and still @b as /b, we could delete and recreate /b/@ from @a to @b and vice versa.

Fstab

Fstab for @a/@:

# <file system> <dir> <type> <options> <dump> <pass>
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /     btrfs compress=zstd,subvol=/@a/@ 0 0
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /b    btrfs compress=zstd,subvol=/@b   0 0
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /home btrfs compress=zstd,subvol=/@home 0 0
UUID=3A19-XXXX /efi vfat defaults 0 2

Fstab for @b/@:

# <file system> <dir> <type> <options> <dump> <pass>
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /     btrfs compress=zstd,subvol=/@b/@ 0 0
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /a    btrfs compress=zstd,subvol=/@a   0 0
UUID=40379083-xxxx-xxxx-xxxx-848931c8d87c /home btrfs compress=zstd,subvol=/@home 0 0
UUID=3A19-XXXX /efi vfat defaults 0 2

Be careful with the minor differences.

Bootloader Entries

This A/B solution can work with any bootloader, I use systemd-boot for now.

Systemd-boot entry for @a in /efi/loader/entries/a.conf:

title Boot A
linux /a/vmlinuz
initrd /a/initrd
options rootflags=subvol=@a/@ quiet
sort-key A

Systemd-boot entry for @b in /efi/loader/entries/b.conf:

title Boot B
linux /b/vmlinuz
initrd /b/initrd
options rootflags=subvol=@b/@ quiet
sort-key B

Bash Script

All the processes can be automated into a bash script:

#!/usr/bin/bash
set -e

errf() { printf "${@}" >&2 && exit 1; }

[[ ${EUID} == 0 ]] || errf "need root priviledge\n"

case ${1} in
    ab)
        srcname=a
        dstname=b
        ;;
    ba)
        srcname=b
        dstname=a
        ;;
    *)
        errf "Usage: $(basename ${0}) <ab|ba>\n"
        ;;
esac

dstvol_alert="Abort: you are running under '@${dstname}' subvolume now\n"
findmnt /${srcname} &>/dev/null && errf "${dstvol_alert}"
findmnt /${dstname} &>/dev/null || errf "${dstvol_alert}"

printf "==> Copy vmlinuz and initrd from '@${srcname}' to '@${dstname}'\n"
stubsrc=/efi/${srcname}
stubdst=/efi/${dstname}
stubtmp=/efi/t
[[ -d ${stubdst} ]] && mv ${stubdst} ${stubtmp}
[[ -d ${stubsrc} ]] && cp -r ${stubsrc} ${stubdst}
[[ -d ${stubtmp} ]] && rm -rf ${stubtmp}

dstvol=/${dstname}/@

# remove the read-only protection just in case
[[ -d ${dstvol} ]] && btrfs prop set -f -ts ${dstvol} ro false

printf "==> "
[[ -d ${dstvol} ]] && btrfs subvolume delete ${dstvol}

printf "==> "
btrfs subvolume snapshot / ${dstvol}

printf "==> Modify fstab in '/${dstname}/@'\n"
sed -i -r \
    -e "s#/${dstname}#/${srcname}#" \
    -e "s#@${dstname}\s+0#@${srcname}   0#" \
    -e "s#@${srcname}/@#@${dstname}/@#" \
    ${dstvol}/etc/fstab

time=$(date +%Y%m%d.%H%M%S)
timetxt=/${dstname}/timestamp.${time}.txt
rm /${dstname}/*.txt
printf "${time}\n" > ${timetxt}
printf "==> Create ${timetxt}\n"

Cache Clean

You may want to clean cache files before creating snapshot to save storage.

Limit systemd journal size in /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=120M

Clean package cache:

pacman: paccache -r && paccache -ruk0

dnf-clean(8): dnf clean packages

apt(8): apt autoremove && apt autoclean