aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.org62
-rwxr-xr-xbuild.sh10
-rw-r--r--custom/40_zfs_snapshots13
-rw-r--r--custom/grub-zfs-snap160
-rwxr-xr-xcustom/install-archzfs21
-rw-r--r--custom/zz-grub-zfs-snap.hook22
6 files changed, 287 insertions, 1 deletions
diff --git a/README.org b/README.org
index 16fb406..6306090 100644
--- a/README.org
+++ b/README.org
@@ -21,6 +21,7 @@ manual module loading or package installation during the install process.
- *Genesis Snapshot* - Automatic pristine-state snapshot after installation
- *Rollback Script* - One-command factory reset via ~/root/rollback-to-genesis~
- *Pre-Pacman Snapshots* - Automatic snapshots before package operations
+- *GRUB Snapshot Boot* - Boot into any ZFS snapshot directly from GRUB menu
- *NetworkManager* - WiFi configuration copied to installed system
- *SSH Ready* - Optional SSH with root login for headless servers
- *LTS Kernel* - Uses linux-lts for stability with ZFS
@@ -91,7 +92,10 @@ archzfs/
├── build.sh # Main ISO build script
├── custom/
│ ├── install-archzfs # Interactive installation script
-│ └── install-archzfs.conf.example # Example config for unattended install
+│ ├── install-archzfs.conf.example # Example config for unattended install
+│ ├── grub-zfs-snap # ZFS snapshot GRUB menu generator
+│ ├── 40_zfs_snapshots # GRUB generator script (for /etc/grub.d/)
+│ └── zz-grub-zfs-snap.hook # Pacman hook for auto-regeneration
├── scripts/
│ └── test-vm.sh # QEMU test VM launcher
├── profile/ # archiso profile (generated during build)
@@ -408,6 +412,62 @@ If you enabled SSH during installation:
* Post-Installation
+** ZFS Snapshot Boot Entries (grub-zfs-snap)
+
+The installer configures GRUB to display bootable ZFS snapshots in the boot menu.
+This allows you to boot directly into a previous system state without manually
+rolling back.
+
+*** How It Works
+
+- A GRUB submenu "ZFS Snapshots" appears in the boot menu
+- Shows up to 10 most recent snapshots of the root dataset
+- Selecting a snapshot boots the system in read-only mode from that snapshot
+- The running system sees the snapshot's files, but changes go to a temporary overlay
+
+*** When Snapshots Appear in GRUB
+
+The GRUB menu is automatically updated:
+- After kernel updates (~linux-lts~)
+- After ZFS package updates (~zfs-dkms~, ~zfs-utils~)
+- After GRUB updates
+- After any kernel module installation
+
+*** Manual Snapshot Management
+
+#+BEGIN_SRC bash
+# Create a snapshot before making changes
+zfs snapshot zroot/ROOT/default@before-experiment
+
+# Regenerate GRUB menu to include new snapshot
+grub-zfs-snap
+
+# List snapshots that will appear in GRUB
+grub-zfs-snap --list
+#+END_SRC
+
+*** Customization
+
+Environment variables control behavior:
+
+#+BEGIN_SRC bash
+# Change pool name (default: zroot)
+POOL_NAME=mypool grub-zfs-snap
+
+# Change root dataset (default: ROOT/default)
+ROOT_DATASET=ROOT/arch grub-zfs-snap
+
+# Show more snapshots in menu (default: 10)
+MAX_SNAPSHOTS=20 grub-zfs-snap
+#+END_SRC
+
+*** Booting from a Snapshot
+
+1. Reboot and select "ZFS Snapshots" from GRUB menu
+2. Choose the desired snapshot by date/name
+3. System boots with that snapshot's root filesystem
+4. Files are read-only - to make permanent, rollback from normal boot
+
** Genesis Snapshot
The installer creates a recursive snapshot of the entire pool named ~genesis~.
diff --git a/build.sh b/build.sh
index dd334d3..cf5c086 100755
--- a/build.sh
+++ b/build.sh
@@ -180,6 +180,13 @@ cp "$CUSTOM_DIR/install-archzfs" "$PROFILE_DIR/airootfs/usr/local/bin/"
cp "$CUSTOM_DIR/install-claude" "$PROFILE_DIR/airootfs/usr/local/bin/"
cp "$CUSTOM_DIR/archsetup-zfs" "$PROFILE_DIR/airootfs/usr/local/bin/"
+# Copy grub-zfs-snap for ZFS snapshot boot entries
+info "Copying grub-zfs-snap..."
+cp "$CUSTOM_DIR/grub-zfs-snap" "$PROFILE_DIR/airootfs/usr/local/bin/"
+mkdir -p "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap"
+cp "$CUSTOM_DIR/40_zfs_snapshots" "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap/"
+cp "$CUSTOM_DIR/zz-grub-zfs-snap.hook" "$PROFILE_DIR/airootfs/usr/local/share/grub-zfs-snap/"
+
# Copy example config for unattended installs
mkdir -p "$PROFILE_DIR/airootfs/root"
cp "$CUSTOM_DIR/install-archzfs.conf.example" "$PROFILE_DIR/airootfs/root/"
@@ -197,6 +204,9 @@ if grep -q "file_permissions=" "$PROFILE_DIR/profiledef.sh"; then
/)/ i\ ["/usr/local/bin/archsetup-zfs"]="0:0:755"
}' "$PROFILE_DIR/profiledef.sh"
sed -i '/^file_permissions=(/,/)/ {
+ /)/ i\ ["/usr/local/bin/grub-zfs-snap"]="0:0:755"
+ }' "$PROFILE_DIR/profiledef.sh"
+ sed -i '/^file_permissions=(/,/)/ {
/)/ i\ ["/etc/shadow"]="0:0:400"
}' "$PROFILE_DIR/profiledef.sh"
fi
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