diff options
| author | Craig Jennings <c@cjennings.net> | 2026-01-22 23:21:18 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-01-22 23:21:18 -0600 |
| commit | 0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1 (patch) | |
| tree | ccd6c610630cce9eef268ab692999cdfe3bb5a1b /custom | |
| parent | 197a8036af21232276cfbd9624d9eeeebe722df6 (diff) | |
| download | archangel-0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1.tar.gz archangel-0ffe7a85a1b024b88e4ddc3305c5f805edd6e8e1.zip | |
Replace GRUB with ZFSBootMenu bootloader
This is a major change that replaces the GRUB bootloader with ZFSBootMenu,
providing native ZFS boot environment support.
Key changes:
- EFI partition reduced from 1GB to 512MB (only holds ZFSBootMenu)
- EFI now mounts at /efi instead of /boot
- Kernel and initramfs live on ZFS root (enables snapshot boot with matching kernel)
- Downloads pre-built ZFSBootMenu EFI binary from get.zfsbootmenu.org
- Creates EFI boot entries for all disks in multi-disk configurations
- Syncs ZFSBootMenu to all EFI partitions for redundancy
- Sets org.zfsbootmenu:commandline on zroot/ROOT for kernel cmdline inheritance
- Sets bootfs pool property for default boot environment
- AMD GPU workarounds (pg_mask, cwsr_enable) added to kernel cmdline when AMD detected
Deleted GRUB snapshot tooling (no longer needed):
- custom/grub-zfs-snap
- custom/40_zfs_snapshots
- custom/zz-grub-zfs-snap.hook
- custom/zfs-snap-prune
Updated helper scripts:
- zfssnapshot: removed grub-zfs-snap call, shows ZFSBootMenu tip
- zfsrollback: removed grub-zfs-snap call, notes auto-detection
Tested configurations:
- Single disk installation
- 2-disk mirror (mirror-0)
- 3-disk RAIDZ1 (raidz1-0)
- All boot correctly with ZFSBootMenu
Diffstat (limited to 'custom')
| -rw-r--r-- | custom/40_zfs_snapshots | 13 | ||||
| -rw-r--r-- | custom/grub-zfs-snap | 160 | ||||
| -rwxr-xr-x | custom/install-archzfs | 227 | ||||
| -rwxr-xr-x | custom/zfs-snap-prune | 208 | ||||
| -rwxr-xr-x | custom/zfsrollback | 10 | ||||
| -rwxr-xr-x | custom/zfssnapshot | 9 | ||||
| -rw-r--r-- | custom/zz-grub-zfs-snap.hook | 22 |
7 files changed, 102 insertions, 547 deletions
diff --git a/custom/40_zfs_snapshots b/custom/40_zfs_snapshots deleted file mode 100644 index 5215289..0000000 --- a/custom/40_zfs_snapshots +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# /etc/grub.d/40_zfs_snapshots -# GRUB configuration generator for ZFS snapshot boot entries -# -# This script is called by grub-mkconfig to generate menu entries -# for booting into ZFS snapshots. - -set -e - -# Only run if grub-zfs-snap is installed -if [[ -x /usr/local/bin/grub-zfs-snap ]]; then - /usr/local/bin/grub-zfs-snap --generate -fi diff --git a/custom/grub-zfs-snap b/custom/grub-zfs-snap deleted file mode 100644 index 27f2c1f..0000000 --- a/custom/grub-zfs-snap +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash -# grub-zfs-snap - Generate GRUB menu entries for ZFS snapshots -# -# This script scans ZFS snapshots and generates GRUB submenu entries -# allowing boot into previous system states. -# -# Usage: grub-zfs-snap [--generate] -# --generate Output GRUB menu entries (called by /etc/grub.d/40_zfs_snapshots) -# (no args) Regenerate GRUB config -# -# Installation: -# 1. Copy this script to /usr/local/bin/grub-zfs-snap -# 2. Copy 40_zfs_snapshots to /etc/grub.d/ -# 3. Run: grub-mkconfig -o /boot/grub/grub.cfg - -set -e - -# Configuration -POOL_NAME="${POOL_NAME:-zroot}" -ROOT_DATASET="${ROOT_DATASET:-ROOT/default}" -MAX_SNAPSHOTS="${MAX_SNAPSHOTS:-10}" - -# Get full dataset path -FULL_DATASET="${POOL_NAME}/${ROOT_DATASET}" - -# Colors for terminal output -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; } -error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } - -# Check if running as root -check_root() { - if [[ $EUID -ne 0 ]]; then - error "This script must be run as root" - fi -} - -# Get kernel and initramfs paths -get_kernel_info() { - # Find the LTS kernel - KERNEL=$(ls /boot/vmlinuz-linux-lts 2>/dev/null || ls /boot/vmlinuz-linux 2>/dev/null | head -1) - INITRD=$(ls /boot/initramfs-linux-lts.img 2>/dev/null || ls /boot/initramfs-linux.img 2>/dev/null | head -1) - - if [[ -z "$KERNEL" ]] || [[ -z "$INITRD" ]]; then - error "Could not find kernel or initramfs" - fi - - # Get just the filenames - KERNEL_FILE=$(basename "$KERNEL") - INITRD_FILE=$(basename "$INITRD") -} - -# Get GRUB root device hint -get_grub_root() { - # Get the EFI partition device - local efi_dev=$(findmnt -n -o SOURCE /boot 2>/dev/null | head -1) - if [[ -z "$efi_dev" ]]; then - # Fallback - assume first partition of first disk - echo "hd0,gpt1" - return - fi - - # Convert to GRUB device notation - # This is simplified - may need adjustment for complex setups - local disk=$(echo "$efi_dev" | sed 's/[0-9]*$//') - local part=$(echo "$efi_dev" | grep -o '[0-9]*$') - - # Get disk index (simplified - assumes /dev/sda or /dev/vda style) - echo "hd0,gpt${part}" -} - -# Generate GRUB menu entries for snapshots -generate_entries() { - get_kernel_info - - local grub_root=$(get_grub_root) - local hostid=$(hostid) - - # Get list of snapshots, sorted by creation time (newest first) - local snapshots=$(zfs list -H -t snapshot -o name,creation -s creation -r "$FULL_DATASET" 2>/dev/null | \ - grep "^${FULL_DATASET}@" | \ - tac | \ - head -n "$MAX_SNAPSHOTS") - - if [[ -z "$snapshots" ]]; then - return - fi - - # Generate submenu - cat << 'SUBMENU_START' -submenu 'ZFS Snapshots' --class zfs { -SUBMENU_START - - while IFS=$'\t' read -r snapshot creation; do - local snap_name="${snapshot##*@}" - local snap_date=$(echo "$creation" | awk '{print $1, $2, $3, $4, $5}') - - cat << ENTRY - menuentry '${snap_name} (${snap_date})' --class zfs { - insmod part_gpt - insmod fat - insmod zfs - search --no-floppy --fs-uuid --set=root $(grub-probe --target=fs_uuid /boot) - echo 'Loading Linux kernel...' - linux /${KERNEL_FILE} root=ZFS=${snapshot} ro spl.spl_hostid=0x${hostid} - echo 'Loading initial ramdisk...' - initrd /${INITRD_FILE} - } -ENTRY - done <<< "$snapshots" - - echo "}" -} - -# Regenerate GRUB configuration -regenerate_grub() { - check_root - info "Regenerating GRUB configuration..." - grub-mkconfig -o /boot/grub/grub.cfg - info "Done. ZFS snapshots added to GRUB menu." -} - -# List current snapshots -list_snapshots() { - echo "ZFS Snapshots for ${FULL_DATASET}:" - echo "" - zfs list -H -t snapshot -o name,creation,used -s creation -r "$FULL_DATASET" 2>/dev/null | \ - grep "^${FULL_DATASET}@" | \ - tac | \ - head -n "$MAX_SNAPSHOTS" | \ - while IFS=$'\t' read -r name creation used; do - local snap_name="${name##*@}" - printf " %-30s %s (used: %s)\n" "$snap_name" "$creation" "$used" - done -} - -# Main -case "${1:-}" in - --generate) - generate_entries - ;; - --list) - list_snapshots - ;; - "") - regenerate_grub - ;; - *) - echo "Usage: $0 [--generate|--list]" - echo "" - echo "Options:" - echo " --generate Output GRUB menu entries (for /etc/grub.d/)" - echo " --list List available snapshots" - echo " (no args) Regenerate GRUB configuration" - exit 1 - ;; -esac diff --git a/custom/install-archzfs b/custom/install-archzfs index 567a213..a17aad5 100755 --- a/custom/install-archzfs +++ b/custom/install-archzfs @@ -693,7 +693,7 @@ show_summary() { else echo " ZFS Pool: $POOL_NAME (encrypted)" fi - echo " Boot: EFI on all disks (redundant)" + echo " Boot: ZFSBootMenu on all disks (redundant)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -717,8 +717,9 @@ partition_disks() { wipefs -af "$disk" sgdisk --zap-all "$disk" - # Create partitions: 1G EFI + rest for ZFS - sgdisk -n 1:0:+1G -t 1:ef00 -c 1:"EFI" "$disk" + # Create partitions: 512M EFI + rest for ZFS + # EFI only needs to hold ZFSBootMenu binary (~64MB) - 512MB is plenty + sgdisk -n 1:0:+512M -t 1:ef00 -c 1:"EFI" "$disk" sgdisk -n 2:0:0 -t 2:bf00 -c 2:"ZFS" "$disk" # Determine partition names (handle nvme/mmcblk naming) @@ -860,10 +861,11 @@ create_datasets() { mount_efi() { step "Mounting EFI Partition" - mkdir -p /mnt/boot - # Mount primary (first) EFI partition - mount "${EFI_PARTS[0]}" /mnt/boot - info "Primary EFI partition ${EFI_PARTS[0]} mounted at /mnt/boot" + # EFI partition mounts at /efi - only holds ZFSBootMenu binary + # /boot is a directory on ZFS root (kernels live on ZFS for snapshot safety) + mkdir -p /mnt/efi + mount "${EFI_PARTS[0]}" /mnt/efi + info "EFI partition ${EFI_PARTS[0]} mounted at /mnt/efi" } install_base() { @@ -898,8 +900,6 @@ EOF linux-firmware \ zfs-dkms \ zfs-utils \ - grub \ - freetype2 \ efibootmgr \ networkmanager \ avahi \ @@ -922,10 +922,10 @@ EOF configure_system() { step "Configuring System" - # fstab (only for EFI) + # fstab (only for EFI - /boot is on ZFS root) info "Generating fstab..." - echo "# /boot - EFI System Partition" > /mnt/etc/fstab - echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /boot vfat defaults,noatime 0 2" >> /mnt/etc/fstab + echo "# /efi - EFI System Partition (ZFSBootMenu binary)" > /mnt/etc/fstab + echo "UUID=$(blkid -s UUID -o value "${EFI_PARTS[0]}") /efi vfat defaults,noatime 0 2" >> /mnt/etc/fstab # Timezone info "Setting timezone to $TIMEZONE..." @@ -1097,8 +1097,8 @@ EOF arch-chroot /mnt mkinitcpio -P } -configure_bootloader() { - step "Configuring GRUB Bootloader" +configure_zfsbootmenu() { + step "Configuring ZFSBootMenu" # Ensure hostid exists BEFORE reading it # This is critical: hostid command returns a value even without /etc/hostid, @@ -1111,76 +1111,80 @@ configure_bootloader() { local host_id host_id=$(hostid) - # Configure GRUB defaults - cat > /mnt/etc/default/grub << EOF -GRUB_DEFAULT=0 -GRUB_TIMEOUT=5 -GRUB_DISTRIBUTOR="Arch Linux (ZFS)" -GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3" -GRUB_CMDLINE_LINUX="spl.spl_hostid=0x$host_id" -GRUB_PRELOAD_MODULES="part_gpt part_msdos zfs" -GRUB_TERMINAL_OUTPUT="console" -GRUB_DISABLE_OS_PROBER=true -GRUB_GFXMODE=auto -GRUB_GFXPAYLOAD_LINUX=keep -GRUB_FONT=/boot/grub/fonts/DejaVuSansMono32.pf2 -EOF - - # Install GRUB to each EFI partition for boot redundancy - info "Installing GRUB to ${#EFI_PARTS[@]} EFI partition(s)..." - - for i in "${!EFI_PARTS[@]}"; do - local efi_part="${EFI_PARTS[$i]}" - local bootloader_id="GRUB" - if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then - bootloader_id="GRUB-disk$((i+1))" - fi - - # Unmount current boot if mounted, mount this EFI partition - umount /mnt/boot 2>/dev/null || true - mount "$efi_part" /mnt/boot - - # Create directories and font - mkdir -p /mnt/boot/grub/fonts - arch-chroot /mnt grub-mkfont -s 32 -o /boot/grub/fonts/DejaVuSansMono32.pf2 \ - /usr/share/fonts/TTF/DejaVuSansMono.ttf 2>/dev/null || true + # Copy hostid to installed system (ZFS uses this for pool ownership) + cp /etc/hostid /mnt/etc/hostid - # Install GRUB - info "Installing GRUB to $efi_part (bootloader-id: $bootloader_id)..." - arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot \ - --bootloader-id="$bootloader_id" --recheck + # Create ZFSBootMenu directory on EFI + mkdir -p /mnt/efi/EFI/ZBM - # Generate configuration - arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg - done + # Download ZFSBootMenu release EFI binary + # Using the bundled release which includes everything needed + info "Downloading ZFSBootMenu..." + local zbm_url="https://get.zfsbootmenu.org/efi" + if ! curl -fsSL -o /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$zbm_url"; then + error "Failed to download ZFSBootMenu" + fi + info "ZFSBootMenu binary installed." - # Remount primary EFI for rest of installation - umount /mnt/boot 2>/dev/null || true - mount "${EFI_PARTS[0]}" /mnt/boot + # Set kernel command line on the ROOT PARENT dataset + # This allows inheritance to all boot environments (future-proofing) + # ZFSBootMenu reads org.zfsbootmenu:commandline property + local cmdline="rw loglevel=3" - if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then - info "GRUB installed to all ${#EFI_PARTS[@]} disks for boot redundancy." + # Add any AMD GPU workarounds if needed (detect Strix Halo etc) + if lspci | grep -qi "amd.*display\|amd.*vga"; then + info "AMD GPU detected - adding workaround parameters" + cmdline="$cmdline amdgpu.pg_mask=0 amdgpu.cwsr_enable=0" fi -} -configure_grub_zfs_snap() { - step "Configuring ZFS Snapshot Boot Entries" + # Set on ROOT parent so all boot environments inherit it + zfs set org.zfsbootmenu:commandline="$cmdline" "$POOL_NAME/ROOT" + info "Kernel command line set on $POOL_NAME/ROOT (inherited by children)" - # Install grub-zfs-snap script - info "Installing grub-zfs-snap..." - cp /usr/local/bin/grub-zfs-snap /mnt/usr/local/bin/grub-zfs-snap - chmod +x /mnt/usr/local/bin/grub-zfs-snap + # Set bootfs property - tells ZFSBootMenu which dataset to boot by default + zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" + info "Default boot filesystem set to $POOL_NAME/ROOT/default" + + # Create EFI boot entries for each disk + # ZFSBootMenu EFI parameters (passed via --unicode): + # spl_hostid=0x... - Required for pool import + # zbm.timeout=3 - Seconds before auto-boot (-1 = always show menu) + # zbm.prefer=POOLNAME - Preferred pool to boot from + # zbm.import_policy=hostid - How to handle pool imports + local zbm_cmdline="spl_hostid=0x${host_id} zbm.timeout=3 zbm.prefer=${POOL_NAME} zbm.import_policy=hostid" + + for i in "${!SELECTED_DISKS[@]}"; do + local disk="${SELECTED_DISKS[$i]}" + local label="ZFSBootMenu" + if [[ ${#SELECTED_DISKS[@]} -gt 1 ]]; then + label="ZFSBootMenu-disk$((i+1))" + fi - # Install GRUB generator - cp /usr/local/share/grub-zfs-snap/40_zfs_snapshots /mnt/etc/grub.d/40_zfs_snapshots - chmod +x /mnt/etc/grub.d/40_zfs_snapshots + # Determine partition number (always 1 - first partition is EFI) + local part_num=1 + + info "Creating EFI boot entry: $label on $disk" + efibootmgr --create \ + --disk "$disk" \ + --part "$part_num" \ + --label "$label" \ + --loader '\EFI\ZBM\zfsbootmenu.efi' \ + --unicode "$zbm_cmdline" \ + --quiet + done - # Install pacman hook for auto-regeneration - mkdir -p /mnt/etc/pacman.d/hooks - cp /usr/local/share/grub-zfs-snap/zz-grub-zfs-snap.hook /mnt/etc/pacman.d/hooks/ + # Get the boot entry number and set as first in boot order + local bootnum + bootnum=$(efibootmgr | grep "ZFSBootMenu" | head -1 | grep -oP 'Boot\K[0-9A-F]+') + if [[ -n "$bootnum" ]]; then + # Get current boot order, prepend our entry + local current_order + current_order=$(efibootmgr | grep "BootOrder" | cut -d: -f2 | tr -d ' ') + efibootmgr --bootorder "$bootnum,$current_order" --quiet + info "ZFSBootMenu set as primary boot option" + fi - info "ZFS snapshots will appear in GRUB boot menu." - info "Run 'grub-zfs-snap' to manually regenerate after creating snapshots." + info "ZFSBootMenu configuration complete." } configure_zfs_services() { @@ -1198,9 +1202,7 @@ configure_zfs_services() { arch-chroot /mnt systemctl enable zfs-mount.service arch-chroot /mnt systemctl enable zfs-import.target - # Copy hostid to installed system (ZFS uses this for pool ownership) - # Note: hostid is generated in configure_bootloader, so it always exists here - cp /etc/hostid /mnt/etc/hostid + # Note: hostid and bootfs are already set by configure_zfsbootmenu() # Disable cachefile - we use zfs-import-scan which doesn't need it # Also remove any existing cachefile since zfs-import-scan has a condition @@ -1208,9 +1210,6 @@ configure_zfs_services() { zpool set cachefile=none "$POOL_NAME" rm -f /mnt/etc/zfs/zpool.cache - # Set bootfs - zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" - # Enable other services arch-chroot /mnt systemctl enable NetworkManager arch-chroot /mnt systemctl enable avahi-daemon @@ -1251,10 +1250,6 @@ else echo "Warning: Failed to create snapshot" >&2 fi -# Prune old snapshots (runs quietly, non-blocking) -if [[ -x /usr/local/bin/zfs-snap-prune ]]; then - /usr/local/bin/zfs-snap-prune --quiet & -fi EOF chmod +x /mnt/usr/local/bin/zfs-pre-snapshot @@ -1262,49 +1257,17 @@ EOF info "Pacman hook configured." } -configure_snapshot_retention() { - step "Configuring Snapshot Retention" +configure_zfs_tools() { + step "Installing ZFS Management Tools" # Copy ZFS management scripts - cp /usr/local/bin/zfs-snap-prune /mnt/usr/local/bin/zfs-snap-prune cp /usr/local/bin/zfssnapshot /mnt/usr/local/bin/zfssnapshot cp /usr/local/bin/zfsrollback /mnt/usr/local/bin/zfsrollback - chmod +x /mnt/usr/local/bin/zfs-snap-prune chmod +x /mnt/usr/local/bin/zfssnapshot chmod +x /mnt/usr/local/bin/zfsrollback - # Create systemd service for pruning - cat > /mnt/etc/systemd/system/zfs-snap-prune.service << 'EOF' -[Unit] -Description=Prune old ZFS snapshots -After=zfs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/zfs-snap-prune --quiet -EOF - - # Create systemd timer for daily pruning - cat > /mnt/etc/systemd/system/zfs-snap-prune.timer << 'EOF' -[Unit] -Description=Daily ZFS snapshot pruning - -[Timer] -OnCalendar=daily -Persistent=true -RandomizedDelaySec=1h - -[Install] -WantedBy=timers.target -EOF - - # Enable the timer - arch-chroot /mnt systemctl enable zfs-snap-prune.timer - - info "ZFS management scripts installed: zfssnapshot, zfsrollback, zfs-snap-prune" - info "Snapshot retention configured (daily pruning enabled)." - info "Policy: Keep 20 recent, delete if older than 180 days" - info "Genesis snapshot is always preserved." + info "ZFS management scripts installed: zfssnapshot, zfsrollback" + info "Note: Install sanoid via archsetup for automated snapshot retention." } copy_archsetup() { @@ -1326,7 +1289,6 @@ sync_efi_partitions() { step "Syncing EFI Partitions for Redundancy" - local primary_efi="${EFI_PARTS[0]}" local temp_mount="/mnt/efi_sync" for i in "${!EFI_PARTS[@]}"; do @@ -1335,13 +1297,14 @@ sync_efi_partitions() { fi local efi_part="${EFI_PARTS[$i]}" - info "Syncing to EFI partition $((i+1)): $efi_part" + info "Syncing ZFSBootMenu to EFI partition $((i+1)): $efi_part" mkdir -p "$temp_mount" mount "$efi_part" "$temp_mount" - # Sync all content from primary EFI (mounted at /mnt/boot) to secondary - rsync -a --delete /mnt/boot/ "$temp_mount/" + # Copy ZFSBootMenu binary to secondary EFI partitions + mkdir -p "$temp_mount/EFI/ZBM" + cp /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$temp_mount/EFI/ZBM/" umount "$temp_mount" done @@ -1420,7 +1383,7 @@ cleanup() { ZFS_PASSPHRASE="" info "Unmounting filesystems..." - umount /mnt/boot 2>/dev/null || true + umount /mnt/efi 2>/dev/null || true info "Exporting ZFS pool..." zpool export "$POOL_NAME" @@ -1443,11 +1406,18 @@ print_summary() { echo " ZFS Pool: $POOL_NAME (encrypted)" fi echo "" - echo "ZFS Features:" + echo "ZFSBootMenu Features:" + echo " - Boot from any snapshot (Ctrl+D at boot menu)" echo " - Genesis snapshot: pristine post-install state" echo " - Pre-pacman snapshots for safe upgrades" echo " - Sanoid/syncoid configured by archsetup" echo "" + echo "Boot Menu Keys (at ZFSBootMenu):" + echo " Enter - Boot selected environment" + echo " e - Edit kernel command line" + echo " Ctrl+D - Show snapshot selector" + echo " Ctrl+R - Recovery shell" + echo "" echo "Useful Commands:" echo " List snapshots: zfs list -t snapshot" echo " Manual snapshot: zfs snapshot zroot/home@my-backup" @@ -1485,11 +1455,10 @@ main() { configure_wifi configure_ssh configure_initramfs - configure_bootloader - configure_grub_zfs_snap + configure_zfsbootmenu configure_zfs_services configure_pacman_hook - configure_snapshot_retention + configure_zfs_tools copy_archsetup sync_efi_partitions create_genesis_snapshot diff --git a/custom/zfs-snap-prune b/custom/zfs-snap-prune deleted file mode 100755 index 762ff99..0000000 --- a/custom/zfs-snap-prune +++ /dev/null @@ -1,208 +0,0 @@ -#!/bin/bash -# zfs-snap-prune - Prune old ZFS snapshots with hybrid retention policy -# -# Retention Policy: -# - Always keep the N most recent snapshots (default: 20) -# - Delete snapshots beyond N only if older than MAX_AGE (default: 180 days) -# - Never delete genesis snapshot -# -# Usage: -# zfs-snap-prune [OPTIONS] -# -# Options: -# --dry-run Show what would be deleted without deleting -# --verbose Show decision for every snapshot -# --quiet Suppress non-error output -# --test Use mock data from stdin instead of real ZFS -# --help Show this help message -# -# Environment variables: -# POOL_NAME - ZFS pool name (default: zroot) -# ROOT_DATASET - Root dataset path (default: ROOT/default) -# KEEP_COUNT - Number of recent snapshots to always keep (default: 20) -# MAX_AGE_DAYS - Delete older snapshots beyond KEEP_COUNT (default: 180) -# NOW_OVERRIDE - Override current timestamp for testing (epoch seconds) - -set -e - -# Configuration (can be overridden by environment) -POOL_NAME="${POOL_NAME:-zroot}" -ROOT_DATASET="${ROOT_DATASET:-ROOT/default}" -KEEP_COUNT="${KEEP_COUNT:-20}" -MAX_AGE_DAYS="${MAX_AGE_DAYS:-180}" - -FULL_DATASET="${POOL_NAME}/${ROOT_DATASET}" - -# Flags -DRY_RUN=false -VERBOSE=false -QUIET=false -TEST_MODE=false - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -usage() { - sed -n '2,/^$/p' "$0" | sed 's/^# \?//' - exit 0 -} - -info() { - [[ "$QUIET" == "true" ]] && return - echo -e "${GREEN}[INFO]${NC} $1" -} - -verbose() { - [[ "$VERBOSE" != "true" ]] && return - echo -e "${BLUE}[VERBOSE]${NC} $1" -} - -warn() { - [[ "$QUIET" == "true" ]] && return - echo -e "${YELLOW}[WARN]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 - exit 1 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) - DRY_RUN=true - shift - ;; - --verbose) - VERBOSE=true - shift - ;; - --quiet) - QUIET=true - shift - ;; - --test) - TEST_MODE=true - shift - ;; - --help|-h) - usage - ;; - *) - error "Unknown option: $1" - ;; - esac -done - -# Check if running as root (skip in test mode) -if [[ "$TEST_MODE" != "true" ]] && [[ $EUID -ne 0 ]]; then - error "This script must be run as root" -fi - -# Get current timestamp (can be overridden for testing) -NOW="${NOW_OVERRIDE:-$(date +%s)}" -MAX_AGE_SECONDS=$((MAX_AGE_DAYS * 24 * 60 * 60)) -CUTOFF_TIME=$((NOW - MAX_AGE_SECONDS)) - -info "Pruning snapshots for ${FULL_DATASET}" -info "Policy: Keep ${KEEP_COUNT} recent, delete if older than ${MAX_AGE_DAYS} days" -[[ "$DRY_RUN" == "true" ]] && info "DRY RUN - no changes will be made" - -# Get snapshots - either from ZFS or stdin (test mode) -# Expected format: snapshot_name<TAB>creation_date_string -# Example: zroot/ROOT/default@pre-pacman_2025-01-15 Wed Jan 15 10:30 2025 -if [[ "$TEST_MODE" == "true" ]]; then - # Read mock data from stdin - SNAPSHOTS=$(cat | tac) -else - # Query real ZFS - sorted by creation (oldest first), then reversed for newest first - SNAPSHOTS=$(zfs list -H -t snapshot -o name,creation -s creation -r "$FULL_DATASET" 2>/dev/null | \ - grep "^${FULL_DATASET}@" | \ - tac) || true -fi - -if [[ -z "$SNAPSHOTS" ]]; then - info "No snapshots found" - exit 0 -fi - -# Count snapshots -TOTAL=$(echo "$SNAPSHOTS" | wc -l) -info "Found ${TOTAL} snapshots" - -# Track results -DELETED=0 -KEPT=0 -POSITION=0 - -# Process each snapshot -while IFS=$'\t' read -r snapshot creation_str; do - [[ -z "$snapshot" ]] && continue - - POSITION=$((POSITION + 1)) - SNAP_NAME="${snapshot##*@}" - - # Parse creation time - if [[ "$TEST_MODE" == "true" ]]; then - # In test mode, creation_str is epoch seconds - SNAP_TIME="$creation_str" - else - # In real mode, parse date string - SNAP_TIME=$(date -d "$creation_str" +%s 2>/dev/null || echo "0") - fi - - AGE_DAYS=$(( (NOW - SNAP_TIME) / 86400 )) - - # Decision logic - if [[ $POSITION -le $KEEP_COUNT ]]; then - # Always keep the first KEEP_COUNT snapshots (most recent) - verbose "KEEP: ${SNAP_NAME} (position ${POSITION}/${KEEP_COUNT}, ${AGE_DAYS} days old) - within keep count" - KEPT=$((KEPT + 1)) - elif [[ "$SNAP_NAME" == "genesis" ]]; then - # Never delete genesis - verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - genesis protected" - KEPT=$((KEPT + 1)) - elif [[ $SNAP_TIME -ge $CUTOFF_TIME ]]; then - # Not old enough to delete - verbose "KEEP: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old) - younger than ${MAX_AGE_DAYS} days" - KEPT=$((KEPT + 1)) - else - # Delete: beyond keep count AND older than max age - if [[ "$DRY_RUN" == "true" ]]; then - info "WOULD DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)" - DELETED=$((DELETED + 1)) - elif [[ "$TEST_MODE" == "true" ]]; then - # Test mode: simulate deletion (don't actually call zfs) - verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)" - DELETED=$((DELETED + 1)) - else - verbose "DELETE: ${SNAP_NAME} (position ${POSITION}, ${AGE_DAYS} days old)" - if zfs destroy "$snapshot" 2>/dev/null; then - DELETED=$((DELETED + 1)) - else - warn "Failed to delete ${snapshot}" - fi - fi - fi -done <<< "$SNAPSHOTS" - -# Summary -info "Summary: ${KEPT} kept, ${DELETED} deleted" - -# Regenerate GRUB menu if we deleted anything (skip in dry-run and test modes) -if [[ $DELETED -gt 0 ]] && [[ "$DRY_RUN" != "true" ]] && [[ "$TEST_MODE" != "true" ]]; then - if [[ -x /usr/local/bin/grub-zfs-snap ]]; then - info "Regenerating GRUB menu..." - /usr/local/bin/grub-zfs-snap - fi -fi - -# Exit with special code for testing (number of deleted) -if [[ "$TEST_MODE" == "true" ]]; then - echo "RESULT:kept=${KEPT},deleted=${DELETED}" -fi diff --git a/custom/zfsrollback b/custom/zfsrollback index ee858f6..d73e0e8 100755 --- a/custom/zfsrollback +++ b/custom/zfsrollback @@ -171,14 +171,8 @@ done echo "" if [ $failed -eq 0 ]; then echo "Rollback complete." - - # Update GRUB boot menu if grub-zfs-snap is available - # (destroyed snapshots need to be removed from menu) - if command -v grub-zfs-snap &> /dev/null; then - echo "" - echo "Updating GRUB boot menu..." - grub-zfs-snap - fi + echo "" + echo "Note: ZFSBootMenu auto-detects snapshots - no menu regeneration needed." else echo "Rollback completed with $failed failure(s)" exit 1 diff --git a/custom/zfssnapshot b/custom/zfssnapshot index 1fa7e3b..749ea5a 100755 --- a/custom/zfssnapshot +++ b/custom/zfssnapshot @@ -101,10 +101,5 @@ done echo "" echo "Snapshot complete. Verify with: zfs list -t snapshot | grep $snapshot_name" - -# Update GRUB boot menu if grub-zfs-snap is available -if command -v grub-zfs-snap &> /dev/null; then - echo "" - echo "Updating GRUB boot menu..." - grub-zfs-snap -fi +echo "" +echo "To boot from this snapshot: reboot and press Ctrl+D at ZFSBootMenu" diff --git a/custom/zz-grub-zfs-snap.hook b/custom/zz-grub-zfs-snap.hook deleted file mode 100644 index 8153b84..0000000 --- a/custom/zz-grub-zfs-snap.hook +++ /dev/null @@ -1,22 +0,0 @@ -[Trigger] -Type = Package -Operation = Upgrade -Operation = Install -Operation = Remove -Target = linux-lts -Target = linux-lts-headers -Target = zfs-dkms -Target = zfs-utils -Target = grub - -[Trigger] -Type = Path -Operation = Install -Operation = Upgrade -Operation = Remove -Target = usr/lib/modules/*/vmlinuz - -[Action] -Description = Updating GRUB with ZFS snapshots... -When = PostTransaction -Exec = /usr/local/bin/grub-zfs-snap |
