Serie: Arch - bootbare Btrfs Snapshots mit UKI und Rollbackfunktion (Teil 3)

  Seid   Lesezeit: 6 Minuten

Eine Tutorialreihe zur Einrichtung einer Rollbackfunktion in Arch Linux unter Nutzung von Unified Kernel Images und systemd-boot.

serie: arch - bootbare btrfs snapshots mit uki und rollbackfunktion (teil 3)

Inhaltsverzeichnis

  • Einleitung
  • Teil 1: Unified Kernel Image & systemd-boot einrichten in Arch Linux
  • Teil 2: Automatische Btrfs Snapshots mit `snapper` und Co.
  • Teil 3: Arch Linux Bootbare Btrfs Snapshots mit UKIs (das ist dieser Artikel)

Teil 3

Die vorherigen Teile beschreiben nur die Voraussetzungen, in diesem letzten Teil geht es ums Eingemachte. Wie in der Einleitung bereits angedeutet, booten wir spezifische Snapshots indem wir dem Kernel über den Parameter rootflags=subvolid=x die Mountoptionen für die Root Subvolume mit readonly flag übergeben. Damit Snapshot UKIs ein Root Subvolume als readonly mounten, dürfen die Mountoptionen für / nicht später durch den fstab Eintrag mit read/write überschrieben werden. Deshalb wird der Root Eintrag aus /etc/fstab entfernt und vollständig über die Kernel Kommandozeile definiert.

Hinweis: Die rw Mountoption in der fstab Datei hat an sich keinen Einfluss auf die Btrfs Subvolume, da die Schreibrechte von Btrfs selbst verwaltet werden. Theoretisch kann also die rw Mountoption in der fstab Datei beibehalten werden. Allerdings habe ich nicht getestet, wie das System darauf reagiert, und es ist einfach sauberer, ein ro Subvolume mit ro Option zu mounten.

An dieser Stelle ist es wichtig, die Grösse der UKIs zu optimieren. Als Referenz: Eine UKI ist bei mir 34MB Gross, meine ESP hat 500 MB Platz – es passen also locker 10 Snapshot UKIs rein mit genügend Platz nach oben. Das initramfs macht den Grossteil der endgültigen UKI Grösse aus.

Hier musst du etwas Eigeninitiative leisten, da die optimalen Hooks für das initramfs in /etc/mkinitcpio.conf auf deine Installation angewiesen sind. Nvidia Nutzern gebe ich einen Tipp: Der Parameter nvidia_drm.modeset=1 ist bei aktuellen Nvidia Treibern in der Regel standardmäßig aktiv, sodass kein zusätzlicher kms Hook erforderlich ist. Alleine damit habe ich ca. 100MB an initramfs Grösse gespart. Du kannst theoretisch auch eine separate Konfiguration für Snapshot UKIs nutzen und sie im Script unten an mkinitcpio mit --config [pfad] übergeben.

Warum genau 10 Snapshot UKIs? 10 UKIs passen problemlos auf meine ESP und bieten ausreichend Auswahl. Du kannst gerne mehr oder weniger nutzen. Da ich meinen Laptop nur selten neu starte, können mehrere Updates zwischen zwei Reboots stattfinden. Der Snapshot des letzten Updates ist daher nicht zwangsläufig der letzte bootbare Zustand. Eine grössere Anzahl an Snapshot UKIs erhöht die Wahrscheinlichkeit, schnell einen stabilen Zustand zu finden.

Root Subvolume über die Kernel Kommandozeile definieren

Ein Eintrag für das Root Subvolume in /etc/fstab kann so aussehen, wir kommentieren ihn aus:

#/dev/mapper/cryptroot LABEL=ARCH
UUID=b18fb5be-fff4-4ee3-99e1-efb175e6c630     /               btrfs           rw,noatime,compress=zstd:1,ssd,space_cache=v2,subvol=/@ 0 0

Als Nächstes definieren wir die zu mountende Root Subvolume und die Mountoptionen in zwei separaten Dateien im Verzeichnis /etc/cmdline.d/. Wir trennen die Mountoptionen von der Definition der Root Disk, damit die Optionen später beim Bau der Snapshot UKIs ignoriert bzw. leicht bearbeitet werden können. In den Beispielen unten nutze ich einen Oneliner, um es kurz zu halten. Erstelle die Dateien und schreibe den Inhalt, wie es dir am geläufigsten ist.

# Inhalt einer simplen Installation (Beispiel)
# haben wir bereits in Teil 1 erledigt
echo "root=UUID=b18fb5be-fff4-4ee3-99e1-efb175e6c630" | sudo tee /etc/cmdline.d/root.conf
# Inhalt (Die Mountoptionen aus der fstab Datei. Kann bei dir anders aussehen.)
echo "rootflags=rw,noatime,compress=zstd:1,ssd,space_cache=v2,subvol=/@" | sudo tee /etc/cmdline.d/root-options.conf

An dieser Stelle würde ich zur Sicherheit die Vordefinierten UKI presets neu bauen, da sonst das System nicht mehr bootet.

sudo mkinitcpio -P

Pacman hook und script

Wir möchten, dass unser pacman Hook vor einem Upgrade und erst nach dem snap-pac-pre Hook ausgeführt wird. Der snap-pac-pre Hook wird in der Datei /usr/share/libalpm/hooks/05-snap-pac-pre.hook gespeichert, das Verzeichnis für paketverwaltete System-Hooks. Unseren Hook speichern wir im Verzeichnis für benutzerdefinierte Hooks, /etc/pacman.d/hooks/. libalpm, die Bibliothek hinter pacman, fasst beide Verzeichnisse zusammen und führt die Dateien in alphanumerischer Reihenfolge aus. Wir nennen unsere Datei 10-create-uki-snapshot-pre.hook und fügen folgenden Inhalt ein:

[Trigger]
Operation = Upgrade
Type = Package
# Wird bei einem Upgrade beliebiger Pakete ausgelöst
Target = *

[Action]
Description = Creating UKI with current snapshot as root Subvolume...
When = PreTransaction
# Unser Script, welches die eigentliche Logik beinhaltet
Exec = /usr/local/bin/pre-update-snapshot-uki.sh

Script

Speichere das Script unter demselben Verzeichnis, das in der Exec Angabe des pacman Hooks steht, und vergiss nicht, die Datei ausführbar zu machen.

sudo touch /usr/local/bin/pre-update-snapshot-uki.sh
sudo chmod +x !$

Copy Paste das Script. Bitte basht (hehe) meine Scripting Skills nicht allzu streng in den Kommentaren, ihr könnt das gerne nach Lust und Laune euren eigenen Anforderungen entsprechend bearbeiten. Die Kommentare beschreiben, was es genau tut.

#!/usr/bin/env bash
# # -- Abhängigkeiten --
# - snapper
# - btrfs-progs
# - mkinitcpio
# - sort
# - grep
# - sed
# - coreutils
# - findutils

# maximale Anzahl an gespeicherten UKIs
max_ukis=10

# usage Funktion
usage() {
  echo "Usage: $0" >&2
  exit 1
}
[[ $1 ]] && usage

# bricht das Script bei gewissen Fehlern frühzeitig ab
set -euo pipefail
# extended globbing, benötigt für exclude !(root-options.conf)
shopt -s extglob

# sortiert snapshot UKis und löscht die älteste, sodass am Ende des Scripts maximal max_ukis vorhanden sind
find /efi/EFI/Linux -type f -name "arch-snap-*" | sort | head -n -$((max_ukis - 1)) | xargs -r rm --

# snapshot id vom snapper snapshot, der letzte "pre" Snapshot der "important" ist
snapshotid_snapper=$(snapper --machine-readable csv list | grep "important=yes" | grep ",pre," | tail -n 1 | cut -d "," -f 3)
# btrfs subvolume id des snapper Snapshots, diesen wollen wir booten
subvolid_btrfs=$(btrfs subvolume list -t / | grep "@snapshots/$snapshotid_snapper/" | cut -f 1)
now_date=$(date +"%Y-%m-%d_%H-%M-%S")

# check ob eine UKI mit dieser snapshot ID bereits existiert
if ! find /efi/EFI/Linux -type f -name "*-id-$snapshotid_snapper.efi" -print -quit | grep -q .; then
  # temporäre Datei für cmdline
  # suffix damit sie einfacher zu finden ist, falls überprüft werden sollte
  tmpfile_cmdline=$(mktemp --suffix="-arch-snap-$snapshotid_snapper")

  # schreibe alle cmdline Dateien ohne die root-options.conf in die temp Datei
  cat /etc/cmdline.d/!(root-options.conf) >"$tmpfile_cmdline"
  # füge die root-options.conf hinzu, aber ersetze "rw" mit "ro" und "subvol=/@" mit "subvolid=[ID]"
  sed \
    -e 's/\brw\b/ro/g' \
    -e "s/subvol=\/@/subvolid=$subvolid_btrfs/g" \
    /etc/cmdline.d/root-options.conf >>"$tmpfile_cmdline"

  # OS release Informationen (neu, Dateiname zuvor)
  tmpfile_osrelease=$(mktemp --suffix="-os-release")
  # /etc/os-release übernehmen und PRETTY_NAME ersetzen
  sed \
    -e "s/^PRETTY_NAME=\"Arch Linux\"/PRETTY_NAME=\"Arch snapshot $snapshotid_snapper ($now_date)\"/g" \
    /etc/os-release >>"$tmpfile_osrelease"

  # Erstellen der UKI, mit aktuellem Kernel
  mkinitcpio \
    --kernel /boot/vmlinuz-linux \
    --cmdline "$tmpfile_cmdline" \
    --osrelease "$tmpfile_osrelease" \
    --uki /efi/EFI/Linux/arch-snap-"$now_date-id-$snapshotid_snapper".efi

else
  echo "skipping UKI with Snapshot ID $snapshotid_snapper, exists already"
fi

# resetten
shopt -u extglob

Ein Rollback sieht wie folgt aus: Boote einen Snapshot UKI und merke dir die ID aus dem Bootmenü, führe das snapper-rollback Script mit der gebooteten ID aus und baue die "standard" UKIs (arch-linux.efi) neu, um den Kernel und das initramfs zu synchronisieren. Nach dem Reboot musst du allenfalls die db lock Datei von Pacman löschen. Bitte sei dir des Warnhinweises aus dem Teil 2 der Tutorialreihe bewusst, bevor du wahllos das Rollback Script nutzt.

sudo snapper-rollback [ID]
# wichtig: Danach "standard" UKIs neu bauen
sudo mkinitcpio -P
reboot
# Nach neustart pacman db lock datei löschen
sudo rm /var/lib/pacman/db.lck

Das war's. Falls etwas unklar oder schlecht beschrieben ist, könnt ihr eure Fragen gerne in den Kommentaren stellen. Ich versuche sie so gut wie es geht zu beantworten.

Quellen:
https://wiki.archlinux.org/title/Main_page

Tags

Arch, Btrfs, UKI, rollback, snapshot

Es wurden noch keine Kommentare verfasst, sei der erste!