From 5889acd1e36ecfb82c1aceba48002c8a083bc201 Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Sun, 6 Sep 2026 14:44:24 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20Automatische=20Btrfs-Root-Erkennung=20u?= =?UTF-8?q?nd=20Subvolume-Migration=20(@,=20@home)=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Junie --- AGENTS.md | 7 ++ README.md | 1 + lib/btrfs.sh | 263 +++++++++++++++++++++++++++++++++++++++++ lib/utils.sh | 15 ++- setup.sh | 1 + stages/00-preflight.sh | 5 + 6 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 lib/btrfs.sh diff --git a/AGENTS.md b/AGENTS.md index 099106f..74e53b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ Dieses Repository ist ein modulares, automatisiertes Setup- und Konfigurations-F ├── lib/ # Wiederverwendbare Funktionsbibliotheken │ ├── utils.sh # Logging, Error-Traps, Benutzer- und Pfadermittlung, Backups │ ├── apt.sh # Deb822-Quellen, Keyrings (/etc/apt/keyrings), APT-Pinning, Paketinstallation +│ ├── btrfs.sh # Btrfs Root- & Subvolume-Erkennung, Migration (@, @home), fstab-Verwaltung │ └── tui.sh # Whiptail-TUI-Menüs und Dialoge ├── config/ # Konfigurationsdateien & Paketlisten │ ├── setup.conf # Globale Umgebungsvariablen und Standardwerte @@ -132,6 +133,12 @@ Dieses Repository ist ein modulares, automatisiertes Setup- und Konfigurations-F - `install_apt_keyring "name" "url"`: Lädt einen GPG-Schlüssel herunter, de-armort ihn sauber und speichert ihn in `/etc/apt/keyrings/.gpg`. - `install_packages_from_file "filepath"`: Liest eine `.list`-Datei ein (ignoriert Kommentare `#` und Leerzeilen) und installiert alle Pakete via `apt-get install -y`. +### `lib/btrfs.sh` +- `is_root_btrfs`: Prüft, ob das Root-Dateisystem auf Btrfs liegt. +- `is_root_subvolume_configured`: Prüft, ob Root bereits in einem Subvolume (`@`) läuft. +- `update_btrfs_fstab "fstab_file" "root_id"`: Aktualisiert Mount-Optionen in `/etc/fstab` für `@` und `@home`. +- `check_and_setup_btrfs_subvolumes`: Erkennt flache Btrfs-Root-Volumes und migriert `/` und `/home` transparent in Subvolumes `@` und `@home`. + ### `lib/tui.sh` - `tui_show_stage_selection`: Zeigt ein `whiptail` Checklist-Menü zur interaktiven Auswahl der auszuführenden Phasen. diff --git a/README.md b/README.md index cb1b1da..cd06f37 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ sudo ./setup.sh --stages 04,05 ├── lib/ # Wiederverwendbare Hilfsbibliotheken │ ├── utils.sh # Logging, Error-Traps (set -euo pipefail), Root/User-Erkennung & Abfrage │ ├── apt.sh # APT-Keyring-Management (/etc/apt/keyrings/), Deb822, Pinning +│ ├── btrfs.sh # Btrfs Root- & Subvolume-Erkennung, Migration (@, @home) │ └── tui.sh # Whiptail-Menüs und Dialoge ├── config/ # Konfigurationsdateien & Paketlisten │ ├── setup.conf # Globale Variablen (Kernel, GRUB-Theme, Plymouth) diff --git a/lib/btrfs.sh b/lib/btrfs.sh new file mode 100644 index 0000000..1f75d33 --- /dev/null +++ b/lib/btrfs.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# ============================================================================== +# lib/btrfs.sh - Btrfs root volume detection, subvolume management & migration +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=lib/utils.sh +source "$SCRIPT_DIR/lib/utils.sh" + +# Check if the root filesystem is Btrfs +is_root_btrfs() { + local fstype + fstype="$(findmnt -n -o FSTYPE / 2>/dev/null || true)" + [[ "$fstype" == "btrfs" ]] +} + +# Resolve the root block device (stripping any subvolume bracket suffix) +get_root_btrfs_device() { + local dev + dev="$(findmnt -n -o SOURCE --canonical / 2>/dev/null || findmnt -n -o SOURCE / 2>/dev/null || true)" + dev="${dev%%\[*}" + echo "$dev" +} + +# Resolve UUID of root filesystem +get_root_btrfs_uuid() { + local uuid dev + uuid="$(findmnt -n -o UUID / 2>/dev/null || true)" + if [[ -z "$uuid" ]]; then + dev="$(get_root_btrfs_device)" + if [[ -n "$dev" && -b "$dev" ]]; then + uuid="$(blkid -s UUID -o value "$dev" 2>/dev/null || true)" + fi + fi + echo "$uuid" +} + +# Check if /home is mounted on a separate filesystem / partition from / +is_home_separate_filesystem() { + local root_dev home_dev + root_dev="$(get_root_btrfs_device)" + home_dev="$(findmnt -n -o SOURCE --canonical /home 2>/dev/null || findmnt -n -o SOURCE /home 2>/dev/null || true)" + home_dev="${home_dev%%\[*}" + if [[ -n "$home_dev" && -n "$root_dev" && "$home_dev" != "$root_dev" ]]; then + return 0 + fi + return 1 +} + +# Check if root is already running from a dedicated subvolume (@ or @rootfs) +is_root_subvolume_configured() { + local fsroot opts + fsroot="$(findmnt -n -o FSROOT / 2>/dev/null || true)" + opts="$(findmnt -n -o OPTIONS / 2>/dev/null || true)" + if [[ "$fsroot" == "/@" || "$fsroot" == "/@rootfs" ]] || \ + [[ "$opts" =~ subvol=@ || "$opts" =~ subvol=/@ ]]; then + return 0 + fi + return 1 +} + +# Update /etc/fstab to mount / with subvol=@ and /home with subvol=@home +update_btrfs_fstab() { + local fstab_file="$1" + local root_id="$2" + + [[ -f "$fstab_file" ]] || return 0 + + backup_file_or_dir "$fstab_file" + + local tmp_fstab + tmp_fstab="$(mktemp)" + + local found_root=0 + local found_home=0 + local root_spec="${root_id:-}" + local root_opts="defaults,subvol=@" + + while IFS= read -r line || [[ -n "$line" ]]; do + # Retain comments and blank lines + if [[ "$line" =~ ^[[:space:]]*# || -z "${line// /}" ]]; then + echo "$line" >> "$tmp_fstab" + continue + fi + + # Parse fields + local spec mnt type opts freq pass + read -r spec mnt type opts freq pass <<< "$line" + + if [[ "$mnt" == "/" && "$type" == "btrfs" ]]; then + found_root=1 + root_spec="${root_id:-$spec}" + local new_opts="$opts" + if [[ "$new_opts" =~ subvol= ]] || [[ "$new_opts" =~ subvolid= ]]; then + new_opts="$(echo "$new_opts" | sed -E 's/(subvol|subvolid)=[^,]*/subvol=@/g')" + else + new_opts="${new_opts},subvol=@" + fi + new_opts="$(echo "$new_opts" | sed -E -e 's/,,+/,/g' -e 's/^,//' -e 's/,$//')" + root_opts="$new_opts" + spec="$root_spec" + printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$spec" "$mnt" "$type" "$new_opts" "${freq:-0}" "${pass:-0}" >> "$tmp_fstab" + elif [[ "$mnt" == "/home" && "$type" == "btrfs" ]]; then + found_home=1 + local new_opts="$opts" + if [[ "$new_opts" =~ subvol= ]] || [[ "$new_opts" =~ subvolid= ]]; then + new_opts="$(echo "$new_opts" | sed -E 's/(subvol|subvolid)=[^,]*/subvol=@home/g')" + else + new_opts="${new_opts},subvol=@home" + fi + new_opts="$(echo "$new_opts" | sed -E -e 's/,,+/,/g' -e 's/^,//' -e 's/,$//')" + spec="${root_id:-$spec}" + printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$spec" "$mnt" "$type" "$new_opts" "${freq:-0}" "${pass:-0}" >> "$tmp_fstab" + else + echo "$line" >> "$tmp_fstab" + fi + done < "$fstab_file" + + # If home was not present in fstab and root was found/specified, add /home entry + if [[ "$found_home" -eq 0 && ( "$found_root" -eq 1 || -n "$root_spec" ) ]]; then + local home_opts="${root_opts/subvol=@/subvol=@home}" + if [[ "$home_opts" == "$root_opts" ]]; then + home_opts="defaults,subvol=@home" + fi + printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$root_spec" "/home" "btrfs" "$home_opts" "0" "0" >> "$tmp_fstab" + fi + + if ! cat "$tmp_fstab" > "$fstab_file" 2>/dev/null; then + log_warn "Could not write to $fstab_file (permission denied)." + fi + rm -f "$tmp_fstab" +} + +# Main function to check and configure Btrfs subvolumes for root and home +check_and_setup_btrfs_subvolumes() { + if ! is_root_btrfs; then + local fstype + fstype="$(findmnt -n -o FSTYPE / 2>/dev/null || echo "unknown")" + log_info "Root filesystem is not Btrfs ($fstype). Skipping Btrfs subvolume migration." + return 0 + fi + + log_info "Btrfs root filesystem detected." + + if is_root_subvolume_configured; then + local fsroot + fsroot="$(findmnt -n -o FSROOT / 2>/dev/null || true)" + log_success "Root filesystem is already running inside subvolume '$fsroot'." + return 0 + fi + + # Ensure btrfs utility is available + if ! command_exists btrfs; then + log_info "Installing btrfs-progs..." + DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1 || true + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends btrfs-progs || { + log_error "btrfs-progs could not be installed." + return 1 + } + fi + + local root_dev root_uuid + root_dev="$(get_root_btrfs_device)" + root_uuid="$(get_root_btrfs_uuid)" + + if [[ -z "$root_dev" ]]; then + log_warn "Could not determine root block device for Btrfs. Skipping migration." + return 0 + fi + + local root_fstab_id + if [[ -n "$root_uuid" ]]; then + root_fstab_id="UUID=$root_uuid" + else + root_fstab_id="$root_dev" + fi + + log_step "Btrfs Migration: Creating subvolumes '@' and '@home'..." + + local tmp_mnt + tmp_mnt="$(mktemp -d /tmp/btrfs-toplevel.XXXXXX)" + + # Trap to safely unmount and remove temporary mountpoint + cleanup_tmp_mnt() { + if [[ -d "$tmp_mnt" ]]; then + if mountpoint -q "$tmp_mnt"; then + umount "$tmp_mnt" 2>/dev/null || true + fi + rm -rf "$tmp_mnt" + fi + } + + # Mount top-level btrfs root (subvolid=5) + if ! mount -t btrfs -o subvolid=5 "$root_dev" "$tmp_mnt"; then + log_error "Failed to mount top-level Btrfs volume on $tmp_mnt." + rm -rf "$tmp_mnt" + return 1 + fi + + # 1. Create or snapshot '@' subvolume for root + if [[ ! -d "$tmp_mnt/@" ]]; then + log_info "Creating snapshot of top-level filesystem into '@' subvolume..." + btrfs subvolume snapshot "$tmp_mnt" "$tmp_mnt/@" + log_success "Created '@' subvolume snapshot." + else + log_info "'@' subvolume already exists in top-level Btrfs tree." + fi + + # 2. Create '@home' subvolume if /home is on the same volume + if ! is_home_separate_filesystem; then + if [[ ! -d "$tmp_mnt/@home" ]]; then + log_info "Creating '@home' subvolume..." + btrfs subvolume create "$tmp_mnt/@home" + log_success "Created '@home' subvolume." + + # Move existing home data into @home + if [[ -d "$tmp_mnt/@/home" && "$(ls -A "$tmp_mnt/@/home" 2>/dev/null)" ]]; then + log_info "Moving existing /home data to '@home' subvolume..." + cp -a --reflink=auto "$tmp_mnt/@/home/." "$tmp_mnt/@home/" + # Clean up the directory inside @/home so it acts as an empty mountpoint + rm -rf "$tmp_mnt/@/home"/* "$tmp_mnt/@/home"/.[!.]* "$tmp_mnt/@/home"/..?* 2>/dev/null || true + chmod 755 "$tmp_mnt/@/home" 2>/dev/null || true + chown root:root "$tmp_mnt/@/home" 2>/dev/null || true + log_success "Moved /home data to '@home' subvolume." + fi + else + log_info "'@home' subvolume already exists in top-level Btrfs tree." + fi + else + log_info "/home is mounted on a separate filesystem. Skipping '@home' subvolume creation." + fi + + # 3. Set default subvolume to '@' + local subvol_at_id + subvol_at_id="$(btrfs subvolume list "$tmp_mnt" 2>/dev/null | awk '$NF == "@" {print $2}' | tail -n1)" + if [[ -n "$subvol_at_id" ]]; then + btrfs subvolume set-default "$subvol_at_id" "$tmp_mnt" + log_success "Set default Btrfs subvolume to '@' (ID: $subvol_at_id)." + fi + + # 4. Update /etc/fstab (and the snapshotted @/etc/fstab) + log_info "Updating /etc/fstab entries for '@' and '@home'..." + update_btrfs_fstab "/etc/fstab" "$root_fstab_id" + if [[ -f "$tmp_mnt/@/etc/fstab" ]]; then + update_btrfs_fstab "$tmp_mnt/@/etc/fstab" "$root_fstab_id" + fi + log_success "Updated /etc/fstab with subvolume mount options." + + # 5. Mount '@home' at /home for the current live session if not already mounted + if ! is_home_separate_filesystem; then + if ! findmnt /home >/dev/null 2>&1; then + log_info "Mounting '@home' subvolume at /home for current setup session..." + mount -t btrfs -o subvol=@home "$root_dev" /home || log_warn "Could not mount @home at /home live." + fi + fi + + # Clean up temporary top-level mount + cleanup_tmp_mnt + + log_success "Btrfs subvolume setup and migration completed successfully." +} diff --git a/lib/utils.sh b/lib/utils.sh index d8cdb9f..266f3e0 100755 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -250,6 +250,10 @@ run_as_target_user() { fi } +run_as_user() { + run_as_target_user "$@" +} + # Create a safe temporary directory with auto-cleanup create_temp_dir() { local prefix="${1:-setup-temp}" @@ -265,7 +269,14 @@ backup_file_or_dir() { timestamp="$(date +%Y%m%d_%H%M%S)" if [[ -e "$target" ]]; then local backup_path="${target}.backup_${timestamp}" - cp -r "$target" "$backup_path" - log_info "Created backup: $backup_path" + if cp -r "$target" "$backup_path" 2>/dev/null; then + log_info "Created backup: $backup_path" + else + log_warn "Could not create backup of $target (permission denied)." + fi fi } + +backup_file() { + backup_file_or_dir "$@" +} diff --git a/setup.sh b/setup.sh index f830649..d5c8fe9 100755 --- a/setup.sh +++ b/setup.sh @@ -10,6 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source Libraries & Config source "$SCRIPT_DIR/lib/utils.sh" source "$SCRIPT_DIR/lib/apt.sh" +source "$SCRIPT_DIR/lib/btrfs.sh" source "$SCRIPT_DIR/lib/tui.sh" if [[ -f "$SCRIPT_DIR/config/setup.conf" ]]; then diff --git a/stages/00-preflight.sh b/stages/00-preflight.sh index 8a8445d..1a96cca 100755 --- a/stages/00-preflight.sh +++ b/stages/00-preflight.sh @@ -7,6 +7,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$SCRIPT_DIR/lib/utils.sh" +source "$SCRIPT_DIR/lib/btrfs.sh" setup_err_trap @@ -67,4 +68,8 @@ else log_success "Sufficient disk space available ($(( AVAILABLE_KB / 1024 / 1024 )) GB free)." fi +# 6. Check & Setup Btrfs Subvolumes (@, @home) +log_substep "Checking Btrfs root volume & subvolume layout..." +check_and_setup_btrfs_subvolumes + log_success "Stage 00: Pre-flight checks passed successfully."