From 6505511f2e6b43a37570fc840f6d2851c7cc170c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 18 Jan 2026 10:55:18 -0600 Subject: Add grub-zfs-snap for ZFS snapshot boot entries Add ability to boot into ZFS snapshots directly from GRUB menu: - grub-zfs-snap: generates GRUB submenu entries for recent snapshots - 40_zfs_snapshots: GRUB generator script installed to /etc/grub.d/ - zz-grub-zfs-snap.hook: pacman hook for automatic GRUB regeneration The GRUB menu automatically updates after kernel/ZFS package changes. Up to 10 most recent snapshots appear in a "ZFS Snapshots" submenu. --- custom/40_zfs_snapshots | 13 ++++ custom/grub-zfs-snap | 160 +++++++++++++++++++++++++++++++++++++++++++ custom/install-archzfs | 21 ++++++ custom/zz-grub-zfs-snap.hook | 22 ++++++ 4 files changed, 216 insertions(+) create mode 100644 custom/40_zfs_snapshots create mode 100644 custom/grub-zfs-snap create mode 100644 custom/zz-grub-zfs-snap.hook (limited to 'custom') diff --git a/custom/40_zfs_snapshots b/custom/40_zfs_snapshots new file mode 100644 index 0000000..5215289 --- /dev/null +++ b/custom/40_zfs_snapshots @@ -0,0 +1,13 @@ +#!/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 new file mode 100644 index 0000000..f1eacb4 --- /dev/null +++ b/custom/grub-zfs-snap @@ -0,0 +1,160 @@ +#!/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 dfba090..660cf34 100755 --- a/custom/install-archzfs +++ b/custom/install-archzfs @@ -1031,6 +1031,26 @@ EOF fi } +configure_grub_zfs_snap() { + step "Configuring ZFS Snapshot Boot Entries" + + # 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 + + # 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 + + # 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/ + + info "ZFS snapshots will appear in GRUB boot menu." + info "Run 'grub-zfs-snap' to manually regenerate after creating snapshots." +} + configure_zfs_services() { step "Configuring ZFS Services" @@ -1276,6 +1296,7 @@ main() { configure_ssh configure_initramfs configure_bootloader + configure_grub_zfs_snap configure_zfs_services configure_pacman_hook copy_archsetup diff --git a/custom/zz-grub-zfs-snap.hook b/custom/zz-grub-zfs-snap.hook new file mode 100644 index 0000000..8153b84 --- /dev/null +++ b/custom/zz-grub-zfs-snap.hook @@ -0,0 +1,22 @@ +[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 -- cgit v1.2.3