aboutsummaryrefslogtreecommitdiff
path: root/installer
diff options
context:
space:
mode:
Diffstat (limited to 'installer')
-rw-r--r--installer/RESCUE-GUIDE.txt2618
-rwxr-xr-xinstaller/archangel1688
-rw-r--r--installer/archangel.conf.example96
-rwxr-xr-xinstaller/install-claude24
-rw-r--r--installer/lib/btrfs.sh900
-rw-r--r--installer/lib/common.sh173
-rw-r--r--installer/lib/config.sh131
-rw-r--r--installer/lib/disk.sh204
-rw-r--r--installer/lib/zfs.sh359
-rwxr-xr-xinstaller/zfsrollback179
-rwxr-xr-xinstaller/zfssnapshot105
11 files changed, 6477 insertions, 0 deletions
diff --git a/installer/RESCUE-GUIDE.txt b/installer/RESCUE-GUIDE.txt
new file mode 100644
index 0000000..e241125
--- /dev/null
+++ b/installer/RESCUE-GUIDE.txt
@@ -0,0 +1,2618 @@
+================================================================================
+ ARCHZFS RESCUE GUIDE
+================================================================================
+
+This guide covers common rescue and recovery scenarios. For quick command
+reference, use: tldr <command>
+
+Table of Contents:
+ 1. ZFS Recovery
+ 2. Data Recovery
+ 3. Boot Repair
+ 4. Windows Recovery
+ 5. Hardware Diagnostics
+ 6. Disk Operations
+ 7. Network Troubleshooting
+ 8. Encryption & GPG
+ 9. System Tracing (eBPF/bpftrace)
+ 10. Terminal Web Browsing
+
+================================================================================
+1. ZFS RECOVERY
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr zfs # ZFS filesystem commands
+ tldr zpool # ZFS pool commands
+ man zfs # Full ZFS manual
+ man zpool # Full zpool manual
+
+SCENARIO: Import a pool from another system
+-------------------------------------------
+List pools available for import:
+
+ zpool import
+
+Import a specific pool:
+
+ zpool import poolname
+
+If the pool was not cleanly exported (e.g., system crash):
+
+ zpool import -f poolname
+
+Import with a different name (to avoid conflicts):
+
+ zpool import oldname newname
+
+
+SCENARIO: Pool won't import - "pool may be in use"
+--------------------------------------------------
+Force import (use when you know it's safe):
+
+ zpool import -f poolname
+
+If that fails, try recovery mode:
+
+ zpool import -F poolname
+
+Last resort - import read-only to recover data:
+
+ zpool import -o readonly=on poolname
+
+
+SCENARIO: Check pool health and repair
+--------------------------------------
+Check pool status:
+
+ zpool status poolname
+
+Start a scrub (checks all data, can take hours):
+
+ zpool scrub poolname
+
+Check scrub progress:
+
+ zpool status poolname
+
+Clear transient errors after fixing hardware:
+
+ zpool clear poolname
+
+
+SCENARIO: Recover from snapshot / Rollback
+------------------------------------------
+List all snapshots:
+
+ zfs list -t snapshot
+
+Rollback to a snapshot (destroys changes since snapshot):
+
+ zfs rollback poolname/dataset@snapshot
+
+For snapshots with intermediate snapshots, use -r:
+
+ zfs rollback -r poolname/dataset@snapshot
+
+
+SCENARIO: Copy data from ZFS pool
+---------------------------------
+Mount datasets if not auto-mounted:
+
+ zfs mount -a
+
+Or mount specific dataset:
+
+ zfs set mountpoint=/mnt/recovery poolname/dataset
+ zfs mount poolname/dataset
+
+Copy with rsync (preserves permissions, shows progress):
+
+ rsync -avP --progress /mnt/recovery/ /destination/
+
+
+SCENARIO: Send/Receive snapshots (backup/migrate)
+-------------------------------------------------
+Create a snapshot first:
+
+ zfs snapshot poolname/dataset@backup
+
+Send to a file (local backup):
+
+ zfs send poolname/dataset@backup > /path/to/backup.zfs
+
+Send with progress indicator:
+
+ zfs send poolname/dataset@backup | pv > /path/to/backup.zfs
+
+Send to another pool locally:
+
+ zfs send poolname/dataset@backup | zfs recv newpool/dataset
+
+Send to remote system over SSH:
+
+ zfs send poolname/dataset@backup | ssh user@remote zfs recv pool/dataset
+
+With progress and buffering for network transfers:
+
+ zfs send poolname/dataset@backup | pv | mbuffer -s 128k -m 1G | \
+ ssh user@remote "mbuffer -s 128k -m 1G | zfs recv pool/dataset"
+
+
+SCENARIO: Encrypted pool - unlock and mount
+-------------------------------------------
+Load the encryption key (will prompt for passphrase):
+
+ zfs load-key poolname
+
+Or for all encrypted datasets:
+
+ zfs load-key -a
+
+Then mount:
+
+ zfs mount -a
+
+
+SCENARIO: Replace failed drive in mirror/raidz
+----------------------------------------------
+Check which drive failed:
+
+ zpool status poolname
+
+Replace the drive (assuming /dev/sdc is new drive):
+
+ zpool replace poolname /dev/old-drive /dev/sdc
+
+Monitor resilver progress:
+
+ zpool status poolname
+
+
+SCENARIO: See what's using a dataset (before unmount)
+-----------------------------------------------------
+Check what processes have files open:
+
+ lsof /mountpoint
+
+Or for all ZFS mounts:
+
+ lsof | grep poolname
+
+
+USEFUL ZFS COMMANDS
+-------------------
+ zpool status # Pool health overview
+ zpool list # Pool capacity
+ zpool history poolname # Command history
+ zfs list # All datasets
+ zfs list -t snapshot # All snapshots
+ zfs get all poolname # All properties
+ zdb -l /dev/sdX # Low-level pool label info
+
+
+================================================================================
+2. DATA RECOVERY
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr ddrescue # Clone failing drives
+ tldr testdisk # Partition/file recovery
+ tldr photorec # Recover deleted files by type
+ tldr smartctl # Check drive health
+
+FIRST: Assess drive health before recovery
+------------------------------------------
+Check if drive is failing (SMART data):
+
+ smartctl -H /dev/sdX # Quick health check
+ smartctl -a /dev/sdX # Full SMART report
+
+Key things to look for:
+ - "PASSED" vs "FAILED" health status
+ - Reallocated_Sector_Ct - bad sectors remapped (increasing = dying)
+ - Current_Pending_Sector - sectors waiting to be remapped
+ - Offline_Uncorrectable - sectors that couldn't be read
+
+If SMART shows problems, STOP and use ddrescue immediately.
+Do not run fsck or other tools that write to a failing drive.
+
+
+SCENARIO: Clone a failing drive (CRITICAL - do this first!)
+------------------------------------------------------------
+Golden rule: NEVER work directly on a failing drive.
+Clone it first, then recover from the clone.
+
+Clone to an image file (safest):
+
+ ddrescue -d -r3 /dev/sdX /path/to/image.img /path/to/logfile.log
+
+ -d = direct I/O, bypass cache
+ -r3 = retry bad sectors 3 times
+ logfile = allows resuming if interrupted
+
+Clone to another drive:
+
+ ddrescue -d -r3 /dev/sdX /dev/sdY /path/to/logfile.log
+
+Monitor progress (ddrescue shows its own progress, but for pipes):
+
+ ddrescue -d /dev/sdX - 2>/dev/null | pv > /path/to/image.img
+
+Resume an interrupted clone:
+
+ ddrescue -d -r3 /dev/sdX /path/to/image.img /path/to/logfile.log
+
+The log file tracks what's been copied. Same command resumes.
+
+If drive is very bad, do a quick pass first, then retry bad sectors:
+
+ ddrescue -d -n /dev/sdX image.img logfile.log # Fast pass, skip errors
+ ddrescue -d -r3 /dev/sdX image.img logfile.log # Retry bad sectors
+
+
+SCENARIO: Recover deleted files (PhotoRec)
+------------------------------------------
+PhotoRec recovers files by their content signatures, not filesystem.
+Works even if filesystem is damaged or reformatted.
+
+Run PhotoRec (included with testdisk):
+
+ photorec /dev/sdX # From device
+ photorec image.img # From disk image
+
+Interactive steps:
+ 1. Select the disk/partition
+ 2. Choose filesystem type (usually "Other" for FAT/NTFS/exFAT)
+ 3. Choose "Free" (unallocated) or "Whole" (entire partition)
+ 4. Select destination folder for recovered files
+ 5. Wait (can take hours for large drives)
+
+Recovered files are named by type (e.g., f0001234.jpg) in recup_dir.*/
+
+
+SCENARIO: Recover lost partition / Fix partition table
+------------------------------------------------------
+TestDisk can find and recover lost partitions.
+
+Run TestDisk:
+
+ testdisk /dev/sdX # From device
+ testdisk image.img # From disk image
+
+Interactive steps:
+ 1. Select disk
+ 2. Select partition table type (usually Intel/PC for MBR, EFI GPT)
+ 3. Choose "Analyse" to scan for partitions
+ 4. "Quick Search" finds most partitions
+ 5. "Deeper Search" if quick search misses any
+ 6. Review found partitions, select ones to recover
+ 7. "Write" to save new partition table (or just note the info)
+
+TestDisk can also:
+ - Recover deleted files from FAT/NTFS/ext filesystems
+ - Repair FAT/NTFS boot sectors
+ - Rebuild NTFS MFT
+
+
+SCENARIO: Recover specific file types (Foremost)
+------------------------------------------------
+Foremost carves files based on headers/footers.
+Useful when PhotoRec doesn't find what you need.
+
+Basic usage:
+
+ foremost -t all -i /dev/sdX -o /output/dir
+ foremost -t all -i image.img -o /output/dir
+
+Specific file types:
+
+ foremost -t jpg,png,gif -i image.img -o /output/dir
+ foremost -t pdf,doc,xls -i image.img -o /output/dir
+
+Supported types: jpg, gif, png, bmp, avi, exe, mpg, wav, riff,
+wmv, mov, pdf, ole (doc/xls/ppt), doc, zip, rar, htm, cpp, all
+
+
+SCENARIO: Can't mount filesystem - try repair
+----------------------------------------------
+WARNING: Only run fsck on a COPY, not the original failing drive!
+
+For ext2/ext3/ext4:
+
+ fsck.ext4 -n /dev/sdX # Check only, no changes (safe)
+ fsck.ext4 -p /dev/sdX # Auto-repair safe problems
+ fsck.ext4 -y /dev/sdX # Say yes to all repairs (risky)
+
+For NTFS:
+
+ ntfsfix /dev/sdX # Fix common NTFS issues
+
+For XFS:
+
+ xfs_repair -n /dev/sdX # Check only
+ xfs_repair /dev/sdX # Repair
+
+For FAT32:
+
+ fsck.fat -n /dev/sdX # Check only
+ fsck.fat -a /dev/sdX # Auto-repair
+
+
+SCENARIO: Mount a disk image for file access
+---------------------------------------------
+Mount a full disk image (find partitions first):
+
+ fdisk -l image.img # List partitions and offsets
+
+Note the "Start" sector of the partition you want, multiply by 512:
+
+ mount -o loop,offset=$((START*512)) image.img /mnt/recovery
+
+Or use losetup to set up loop devices for all partitions:
+
+ losetup -P /dev/loop0 image.img
+ mount /dev/loop0p1 /mnt/recovery
+
+For NTFS images:
+
+ mount -t ntfs-3g -o loop,offset=$((START*512)) image.img /mnt/recovery
+
+
+SCENARIO: Low-level recovery from very bad drives (safecopy)
+------------------------------------------------------------
+Safecopy is more aggressive than ddrescue for very damaged media.
+Use when ddrescue can't make progress.
+
+ safecopy /dev/sdX image.img
+
+With multiple passes (increasingly aggressive):
+
+ safecopy --stage1 /dev/sdX image.img # Quick pass
+ safecopy --stage2 /dev/sdX image.img # Retry errors
+ safecopy --stage3 /dev/sdX image.img # Maximum recovery
+
+
+DATA RECOVERY TIPS
+------------------
+1. STOP using a failing drive immediately - every access risks more damage
+2. Clone first, recover from clone - never work on original
+3. Keep the log file from ddrescue - allows resuming
+4. Recover to a DIFFERENT drive - never same drive
+5. For deleted files on working drive, unmount immediately to prevent
+ overwriting the deleted data
+6. If drive makes clicking/grinding noises, consider professional recovery
+7. For SSDs, TRIM may have already zeroed deleted blocks - recovery harder
+
+================================================================================
+3. BOOT REPAIR
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr grub-install # Install GRUB bootloader
+ tldr efibootmgr # Manage UEFI boot entries
+ tldr arch-chroot # Chroot into installed system
+ man mkinitcpio # Rebuild initramfs
+
+FIRST: Identify your boot mode
+------------------------------
+Check if system is UEFI or Legacy BIOS:
+
+ ls /sys/firmware/efi # If exists, you're in UEFI mode
+
+If booting from this rescue USB in UEFI mode, you need to fix UEFI.
+If booting in Legacy mode, you need to fix MBR/Legacy boot.
+
+
+SCENARIO: Chroot into broken system (preparation for most repairs)
+------------------------------------------------------------------
+This is the foundation for most boot repairs.
+
+1. Find your partitions:
+
+ lsblk -f # Shows filesystems and labels
+
+2. Mount the root filesystem:
+
+ mount /dev/sdX2 /mnt # Replace with your root partition
+
+ For ZFS root:
+
+ zpool import -R /mnt zroot
+ zfs mount -a
+
+3. Mount required system directories:
+
+ mount /dev/sdX1 /mnt/boot # EFI partition (if separate)
+ mount --bind /dev /mnt/dev
+ mount --bind /proc /mnt/proc
+ mount --bind /sys /mnt/sys
+ mount --bind /sys/firmware/efi/efivars /mnt/sys/firmware/efi/efivars
+
+ Or use arch-chroot (handles mounts automatically):
+
+ arch-chroot /mnt
+
+4. Now you can run commands as if booted into the system.
+
+
+SCENARIO: Reinstall GRUB (UEFI)
+-------------------------------
+After chrooting into the system:
+
+ grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
+
+If EFI partition is mounted elsewhere:
+
+ grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB
+
+Regenerate GRUB config:
+
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+
+SCENARIO: Reinstall GRUB (Legacy BIOS/MBR)
+------------------------------------------
+After chrooting into the system:
+
+ grub-install --target=i386-pc /dev/sdX # Note: device, not partition
+
+Regenerate GRUB config:
+
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+
+SCENARIO: Fix UEFI boot entries
+-------------------------------
+List current boot entries:
+
+ efibootmgr -v
+
+Delete a broken entry (replace XXXX with boot number):
+
+ efibootmgr -b XXXX -B
+
+Create a new boot entry:
+
+ efibootmgr --create --disk /dev/sdX --part 1 --label "Arch Linux" \
+ --loader /EFI/GRUB/grubx64.efi
+
+Change boot order (comma-separated boot numbers):
+
+ efibootmgr -o 0001,0002,0003
+
+Set next boot only:
+
+ efibootmgr -n 0001
+
+
+SCENARIO: Rebuild initramfs (kernel panic, missing modules)
+-----------------------------------------------------------
+After chrooting into the system:
+
+List available presets:
+
+ ls /etc/mkinitcpio.d/
+
+Rebuild for specific kernel:
+
+ mkinitcpio -p linux # Standard kernel
+ mkinitcpio -p linux-lts # LTS kernel
+
+Rebuild all:
+
+ mkinitcpio -P
+
+Check mkinitcpio.conf for ZFS:
+
+ grep "^HOOKS" /etc/mkinitcpio.conf
+
+For ZFS, HOOKS should include 'zfs':
+ HOOKS=(base udev autodetect modconf block zfs filesystems keyboard fsck)
+
+
+SCENARIO: GRUB not detecting Windows (dual-boot)
+------------------------------------------------
+After chrooting into the system:
+
+Enable os-prober in GRUB config:
+
+ echo 'GRUB_DISABLE_OS_PROBER=false' >> /etc/default/grub
+
+Mount the Windows EFI partition if not already mounted.
+
+Regenerate GRUB config:
+
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+os-prober should find Windows and add it to the menu.
+
+
+SCENARIO: Restore Windows MBR (remove GRUB, restore Windows boot)
+-----------------------------------------------------------------
+If you need to remove Linux and restore Windows-only MBR:
+
+ ms-sys -w /dev/sdX # Write Windows 7+ MBR
+
+Other options:
+ ms-sys -7 /dev/sdX # Windows 7 MBR specifically
+ ms-sys -i /dev/sdX # Show current MBR type
+
+
+SCENARIO: Install syslinux (lightweight alternative to GRUB)
+------------------------------------------------------------
+For Legacy BIOS:
+
+ syslinux-install_update -i -a -m
+
+For UEFI, copy the EFI binary:
+
+ cp /usr/lib/syslinux/efi64/* /boot/EFI/syslinux/
+
+Create /boot/syslinux/syslinux.cfg with boot entries.
+
+
+SCENARIO: Can't boot - kernel panic with ZFS
+--------------------------------------------
+Common causes:
+1. ZFS module not in initramfs - rebuild with mkinitcpio
+2. Pool name changed - check zpool.cache
+3. hostid mismatch - regenerate hostid
+
+After chrooting:
+
+Check if ZFS hook is present:
+
+ grep zfs /etc/mkinitcpio.conf
+
+Regenerate hostid if needed:
+
+ zgenhostid $(hostid)
+
+Rebuild initramfs:
+
+ mkinitcpio -P
+
+
+SCENARIO: Emergency boot from GRUB command line
+-----------------------------------------------
+If GRUB loads but config is broken, press 'c' for command line:
+
+For Linux (non-ZFS):
+
+ set root=(hd0,gpt2)
+ linux /boot/vmlinuz-linux root=/dev/sda2
+ initrd /boot/initramfs-linux.img
+ boot
+
+For Linux with ZFS root:
+
+ set root=(hd0,gpt1)
+ linux /vmlinuz-linux-lts root=ZFS=zroot/ROOT/default
+ initrd /initramfs-linux-lts.img
+ boot
+
+Tab completion works in GRUB command line!
+
+
+BOOT REPAIR TIPS
+----------------
+1. Always backup your current EFI partition before making changes
+2. Use 'efibootmgr -v' to see full paths and verify entries
+3. Some UEFI firmwares are picky about the bootloader path -
+ try /EFI/BOOT/BOOTX64.EFI as a fallback
+4. If all else fails, most UEFI has a boot menu (F12, F8, Esc at POST)
+5. GRUB reinstall usually fixes most boot issues
+6. For ZFS, the initramfs must include the zfs hook
+
+================================================================================
+4. WINDOWS RECOVERY
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr chntpw # Reset Windows passwords
+ tldr ntfs-3g # Mount NTFS filesystems
+ man dislocker # Access BitLocker drives
+ man hivexregedit # Edit Windows registry
+
+FIRST: Identify and mount the Windows partition
+-----------------------------------------------
+Find Windows partition:
+
+ lsblk -f # Look for "ntfs" filesystem
+ fdisk -l # Look for "Microsoft basic data" type
+
+Check if BitLocker encrypted:
+
+ lsblk -f # Will show "BitLocker" instead of "ntfs"
+
+Mount NTFS partition (read-write):
+
+ mkdir -p /mnt/windows
+ mount -t ntfs-3g /dev/sdX1 /mnt/windows
+
+If Windows wasn't shut down cleanly (hibernation/fast startup):
+
+ mount -t ntfs-3g -o remove_hiberfile /dev/sdX1 /mnt/windows
+
+Read-only mount (safer):
+
+ mount -t ntfs-3g -o ro /dev/sdX1 /mnt/windows
+
+
+SCENARIO: Reset forgotten Windows password
+------------------------------------------
+Mount the Windows partition first (see above).
+
+Navigate to the SAM database:
+
+ cd /mnt/windows/Windows/System32/config
+
+List all users:
+
+ chntpw -l SAM
+
+Reset password for a specific user (interactive):
+
+ chntpw -u "Username" SAM
+
+In the interactive menu:
+ 1. Clear (blank) user password <-- Recommended
+ 2. Unlock and enable user account
+ 3. Promote user to administrator
+ q. Quit
+
+After making changes, type 'q' to quit, then 'y' to save.
+
+Alternative - blank ALL passwords:
+
+ chntpw -i SAM # Interactive mode, select options
+
+
+SCENARIO: Unlock disabled/locked Windows account
+------------------------------------------------
+ cd /mnt/windows/Windows/System32/config
+ chntpw -u "Username" SAM
+
+Select option 2: "Unlock and enable user account"
+
+
+SCENARIO: Promote user to Administrator
+---------------------------------------
+ cd /mnt/windows/Windows/System32/config
+ chntpw -u "Username" SAM
+
+Select option 3: "Promote user (make user an administrator)"
+
+
+SCENARIO: Access BitLocker encrypted drive
+------------------------------------------
+You MUST have either:
+ - The BitLocker password, OR
+ - The 48-digit recovery key
+
+Find your recovery key:
+ - Microsoft account: account.microsoft.com/devices/recoverykey
+ - Printed/saved during BitLocker setup
+ - Active Directory (for domain-joined PCs)
+
+Decrypt with password:
+
+ mkdir -p /mnt/bitlocker-decrypted /mnt/windows
+ dislocker -V /dev/sdX1 -u -- /mnt/bitlocker-decrypted
+ # Enter password when prompted
+
+Decrypt with recovery key:
+
+ dislocker -V /dev/sdX1 -p123456-789012-345678-901234-567890-123456-789012-345678 -- /mnt/bitlocker-decrypted
+
+Now mount the decrypted volume:
+
+ mount -t ntfs-3g /mnt/bitlocker-decrypted/dislocker-file /mnt/windows
+
+When done:
+
+ umount /mnt/windows
+ umount /mnt/bitlocker-decrypted
+
+
+SCENARIO: Copy files from Windows that won't boot
+-------------------------------------------------
+Mount the Windows partition (see above), then:
+
+Copy specific files/folders:
+
+ cp -r "/mnt/windows/Users/Username/Documents" /destination/
+
+Copy with rsync (shows progress, preserves attributes):
+
+ rsync -avP "/mnt/windows/Users/Username/" /destination/
+
+Common locations for user data:
+ /mnt/windows/Users/Username/Desktop/
+ /mnt/windows/Users/Username/Documents/
+ /mnt/windows/Users/Username/Downloads/
+ /mnt/windows/Users/Username/Pictures/
+ /mnt/windows/Users/Username/AppData/ (hidden app data)
+
+
+SCENARIO: Edit Windows Registry
+-------------------------------
+The registry is stored in several hive files:
+
+ SYSTEM - Hardware, services, boot config
+ SOFTWARE - Installed programs, system settings
+ SAM - User accounts (password hashes)
+ SECURITY - Security policies
+ DEFAULT - Default user profile
+ NTUSER.DAT - Per-user settings (in each user's profile)
+
+View registry contents:
+
+ hivexregedit --export /mnt/windows/Windows/System32/config/SYSTEM '\' > system.reg
+
+Merge changes from a .reg file:
+
+ hivexregedit --merge /mnt/windows/Windows/System32/config/SOFTWARE changes.reg
+
+Interactive registry shell:
+
+ hivexsh /mnt/windows/Windows/System32/config/SYSTEM
+ # Commands: cd, ls, lsval, cat, exit
+
+
+SCENARIO: Fix Windows boot (from Linux)
+---------------------------------------
+Sometimes you can fix Windows boot issues from Linux:
+
+Rebuild BCD (Windows Boot Configuration Data):
+ - This usually requires Windows Recovery Environment
+ - From Linux, you can backup/restore the BCD file:
+
+ cp /mnt/windows/Boot/BCD /mnt/windows/Boot/BCD.backup
+
+Restore Windows bootloader to MBR (if GRUB overwrote it):
+
+ ms-sys -w /dev/sdX # Write Windows 7+ compatible MBR
+
+For UEFI systems, Windows boot files are in:
+ /mnt/efi/EFI/Microsoft/Boot/
+
+
+SCENARIO: Scan Windows for malware (offline scan)
+-------------------------------------------------
+Update ClamAV definitions first (requires internet):
+
+ freshclam
+
+Scan the Windows partition:
+
+ clamscan -r /mnt/windows # Basic scan
+ clamscan -r -i /mnt/windows # Only show infected files
+ clamscan -r --move=/quarantine /mnt/windows # Quarantine infected
+
+Scan common malware locations:
+
+ clamscan -r "/mnt/windows/Users/*/AppData"
+ clamscan -r "/mnt/windows/Windows/Temp"
+ clamscan -r "/mnt/windows/ProgramData"
+
+Note: ClamAV detection isn't as comprehensive as commercial AV.
+Best for known malware; may miss new/sophisticated threats.
+
+
+SCENARIO: Disable Windows Fast Startup (to mount NTFS read-write)
+-----------------------------------------------------------------
+Windows 8+ uses "Fast Startup" (hybrid shutdown) by default.
+This leaves NTFS in a "dirty" state, preventing safe writes from Linux.
+
+Option 1: Force mount (may cause issues):
+
+ mount -t ntfs-3g -o remove_hiberfile /dev/sdX1 /mnt/windows
+
+Option 2: Boot Windows and disable Fast Startup:
+ - Control Panel > Power Options > "Choose what the power buttons do"
+ - Click "Change settings that are currently unavailable"
+ - Uncheck "Turn on fast startup"
+ - Shutdown (not restart) Windows
+
+Option 3: Via registry from Linux:
+
+ hivexregedit --merge /mnt/windows/Windows/System32/config/SYSTEM << 'EOF'
+ Windows Registry Editor Version 5.00
+
+ [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Power]
+ "HiberbootEnabled"=dword:00000000
+ EOF
+
+
+WINDOWS RECOVERY TIPS
+---------------------
+1. Always try mounting read-only first to assess the situation
+2. Windows Fast Startup/hibernation prevents safe NTFS writes
+3. BitLocker recovery key is essential - no key = no access
+4. chntpw blanks passwords; it cannot recover/show old passwords
+5. Back up registry hives before editing them
+6. If Windows is bootable but locked out, just reset the password
+7. For serious Windows issues, Windows Recovery Environment may be needed
+8. Some antivirus/security software may re-lock accounts on next boot
+
+================================================================================
+5. HARDWARE DIAGNOSTICS
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr smartctl # Check drive health
+ tldr lshw # List hardware
+ tldr hdparm # Disk info and benchmarks
+ man memtester # Memory testing
+ man stress-ng # Stress testing
+ man iotop # Disk I/O monitor by process
+
+SCENARIO: Check if a drive is failing (SMART)
+---------------------------------------------
+Quick health check:
+
+ smartctl -H /dev/sdX
+
+Full SMART report:
+
+ smartctl -a /dev/sdX
+
+For NVMe drives:
+
+ smartctl -a /dev/nvme0n1
+ nvme smart-log /dev/nvme0n1
+
+Key SMART attributes to watch:
+ - Reallocated_Sector_Ct: Bad sectors remapped (increasing = dying)
+ - Current_Pending_Sector: Sectors waiting to be remapped
+ - Offline_Uncorrectable: Unreadable sectors
+ - UDMA_CRC_Error_Count: Cable/connection issues
+ - Wear_Leveling_Count: SSD wear (lower = more worn)
+
+Run a self-test:
+
+ smartctl -t short /dev/sdX # Quick test (~2 min)
+ smartctl -t long /dev/sdX # Thorough test (~hours)
+
+Check test results:
+
+ smartctl -l selftest /dev/sdX
+
+
+SCENARIO: Test RAM for errors
+-----------------------------
+Option 1: Memtest86+ (from boot menu)
+ - Restart and select "Memtest86+" from the boot menu
+ - Most thorough test, runs before OS loads
+ - Let it run for at least 1-2 passes (can take hours)
+
+Option 2: memtester (from running system)
+ - Tests available RAM while system is running
+ - Can't test RAM used by kernel/programs
+
+Test 1GB of RAM (adjust based on free memory):
+
+ free -h # Check available memory
+ memtester 1G 1 # Test 1GB, 1 iteration
+ memtester 2G 5 # Test 2GB, 5 iterations
+
+Note: memtester can only test free RAM. For thorough testing,
+use Memtest86+ from the boot menu.
+
+
+SCENARIO: Monitor temperatures, fans, voltages
+----------------------------------------------
+First, detect and load sensor modules:
+
+ sensors-detect --auto # Auto-detect sensors
+
+Then view readings:
+
+ sensors # Show all sensor data
+
+Continuous monitoring:
+
+ watch -n 1 sensors # Update every second
+
+If sensors shows nothing, modules may need loading:
+
+ modprobe coretemp # Intel CPU temps
+ modprobe k10temp # AMD CPU temps
+ modprobe nct6775 # Common motherboard chip
+
+
+SCENARIO: Stress test hardware (verify stability)
+-------------------------------------------------
+Useful for:
+ - Testing used/refurbished hardware
+ - Verifying overclocking stability
+ - Burn-in testing before deployment
+ - Reproducing intermittent issues
+
+CPU stress test:
+
+ stress-ng --cpu $(nproc) --timeout 300s # All cores, 5 min
+
+Memory stress test:
+
+ stress-ng --vm 2 --vm-bytes 1G --timeout 300s
+
+Combined CPU + memory:
+
+ stress-ng --cpu $(nproc) --vm 2 --vm-bytes 1G --timeout 600s
+
+Disk I/O stress:
+
+ stress-ng --hdd 2 --timeout 300s
+
+Monitor during stress test (in another terminal):
+
+ watch -n 1 sensors # Watch temperatures
+ htop # Watch CPU/memory usage
+
+
+SCENARIO: Get detailed hardware information
+-------------------------------------------
+Full hardware report:
+
+ lshw # All hardware (verbose)
+ lshw -short # Summary view
+ lshw -html > hardware.html # HTML report
+
+Specific components:
+
+ lshw -class processor # CPU info
+ lshw -class memory # RAM info
+ lshw -class disk # Disk info
+ lshw -class network # Network adapters
+
+BIOS/motherboard info:
+
+ dmidecode # All DMI tables
+ dmidecode -t bios # BIOS info
+ dmidecode -t system # System/motherboard
+ dmidecode -t memory # Memory slots and modules
+ dmidecode -t processor # CPU socket info
+
+Quick system overview:
+
+ inxi -Fxz # If inxi is installed
+ cat /proc/cpuinfo # CPU details
+ cat /proc/meminfo # Memory details
+
+
+SCENARIO: Test disk speed / benchmark
+-------------------------------------
+Basic read speed test:
+
+ hdparm -t /dev/sdX # Buffered read speed
+ hdparm -T /dev/sdX # Cached read speed
+
+More accurate test (run 3 times, average):
+
+ hdparm -tT /dev/sdX
+ hdparm -tT /dev/sdX
+ hdparm -tT /dev/sdX
+
+Get drive information:
+
+ hdparm -I /dev/sdX # Detailed drive info
+
+For NVMe drives:
+
+ nvme list # List NVMe drives
+ nvme id-ctrl /dev/nvme0n1 # Controller info
+ nvme smart-log /dev/nvme0n1 # SMART/health data
+
+
+SCENARIO: Check for bad blocks (surface scan)
+---------------------------------------------
+WARNING: This is read-only but takes a long time on large drives.
+
+ badblocks -sv /dev/sdX
+
+For faster progress indication:
+
+ badblocks -sv -b 4096 /dev/sdX
+
+Note: For modern drives, SMART is usually more informative.
+badblocks is useful for older drives without good SMART support.
+
+
+SCENARIO: Identify unknown hardware / find drivers
+--------------------------------------------------
+List PCI devices:
+
+ lspci # All PCI devices
+ lspci -v # Verbose (with drivers)
+ lspci -k # Show kernel drivers
+
+List USB devices:
+
+ lsusb # All USB devices
+ lsusb -v # Verbose
+
+Find what driver a device is using:
+
+ lspci -k | grep -A3 "Network" # Network adapter driver
+ lspci -k | grep -A3 "VGA" # Graphics driver
+
+
+SCENARIO: Find what's doing disk I/O (iotop)
+--------------------------------------------
+iotop shows disk read/write by process - like top for disk I/O.
+Useful when disk is thrashing and you need to find the cause.
+
+Basic usage (requires root):
+
+ iotop
+
+Only show processes doing I/O:
+
+ iotop -o
+
+Batch mode (non-interactive, for logging):
+
+ iotop -b -n 5 # 5 iterations then exit
+
+Show accumulated I/O instead of bandwidth:
+
+ iotop -a
+
+Key columns:
+ - DISK READ: current read bandwidth
+ - DISK WRITE: current write bandwidth
+ - IO>: percentage of time spent waiting on I/O
+
+Interactive commands:
+ - o: toggle showing only active processes
+ - a: toggle accumulated vs bandwidth
+ - r: reverse sort
+ - q: quit
+
+Common culprits for high I/O:
+ - jbd2: journaling (normal on ext4)
+ - kswapd: swapping (need more RAM)
+ - Large file copies or database operations
+
+
+HARDWARE DIAGNOSTICS TIPS
+-------------------------
+1. Run SMART checks regularly - drives often show warning signs
+2. Memtest86+ (from boot menu) is more thorough than memtester
+3. Stress test new/used hardware before trusting it with data
+4. High temperatures during stress test = cooling problem
+5. Random crashes/errors often indicate RAM or power issues
+6. SMART "Reallocated Sector Count" increasing = drive dying
+7. Back up immediately if SMART shows any warnings
+8. SSDs have limited write cycles - check Wear_Leveling_Count
+9. iotop -o filters to only processes actively doing I/O
+
+================================================================================
+6. DISK OPERATIONS
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr partclone # Filesystem-aware partition cloning
+ tldr fsarchiver # Backup/restore filesystems to archive
+ man nwipe # Secure disk wiping (DBAN replacement)
+ tldr parted # Partition management
+ tldr mkfs # Create filesystems
+ tldr ncdu # Interactive disk usage analyzer
+ tldr tree # Directory tree viewer
+
+FIRST: Understand your options for disk copying
+-----------------------------------------------
+Different tools for different situations:
+
+ dd / ddrescue - Byte-for-byte copy (use for failing drives)
+ partclone - Filesystem-aware, only copies used blocks (faster)
+ fsarchiver - Creates compressed archive (smallest, most flexible)
+ partimage - Legacy imaging (for restoring old partimage backups)
+
+Rule of thumb:
+ - Failing drive? Use ddrescue (section 2)
+ - Clone partition quickly? Use partclone
+ - Backup for long-term storage? Use fsarchiver
+ - Restore old .img.gz from partimage? Use partimage
+
+
+SCENARIO: Clone a partition (partclone - faster than dd)
+--------------------------------------------------------
+Partclone only copies used blocks. A 500GB partition with 50GB used
+takes ~50GB to clone instead of 500GB.
+
+Clone ext4 partition to image:
+
+ partclone.ext4 -c -s /dev/sdX1 -o partition.img
+
+Clone with compression (recommended):
+
+ partclone.ext4 -c -s /dev/sdX1 | gzip -c > partition.img.gz
+
+ -c = clone mode
+ -s = source
+ -o = output
+
+Restore from image:
+
+ partclone.ext4 -r -s partition.img -o /dev/sdX1
+
+Restore from compressed image:
+
+ gunzip -c partition.img.gz | partclone.ext4 -r -s - -o /dev/sdX1
+
+Supported filesystems:
+
+ partclone.ext4 partclone.ext3 partclone.ext2
+ partclone.ntfs partclone.fat32 partclone.fat16
+ partclone.xfs partclone.btrfs partclone.exfat
+ partclone.f2fs partclone.dd (dd mode for any fs)
+
+
+SCENARIO: Create a full system backup (fsarchiver)
+--------------------------------------------------
+Fsarchiver creates compressed, portable archives. Archives can be
+restored to different-sized partitions.
+
+Backup a filesystem:
+
+ fsarchiver savefs backup.fsa /dev/sdX1
+
+Backup with compression level and progress:
+
+ fsarchiver savefs -v -z7 backup.fsa /dev/sdX1
+
+ -v = verbose
+ -z7 = compression level (1-9, higher = smaller but slower)
+
+Backup multiple filesystems to one archive:
+
+ fsarchiver savefs backup.fsa /dev/sdX1 /dev/sdX2 /dev/sdX3
+
+List contents of archive:
+
+ fsarchiver archinfo backup.fsa
+
+Restore to a partition:
+
+ fsarchiver restfs backup.fsa id=0,dest=/dev/sdX1
+
+ id=0 = first filesystem in archive (0, 1, 2...)
+
+Restore to different-sized partition (will resize):
+
+ fsarchiver restfs backup.fsa id=0,dest=/dev/sdY1
+
+
+SCENARIO: Restore a legacy partimage backup
+-------------------------------------------
+Partimage is legacy software but you may have old backups to restore.
+
+Restore partimage backup:
+
+ partimage restore /dev/sdX1 backup.img.gz
+
+Interactive mode:
+
+ partimage
+
+Note: partimage cannot create images of ext4, GPT, or modern filesystems.
+Use fsarchiver for new backups.
+
+
+SCENARIO: Securely wipe a drive (nwipe)
+---------------------------------------
+DANGER: This PERMANENTLY DESTROYS all data. Triple-check the device!
+
+Interactive mode (recommended - shows all drives, select with space):
+
+ nwipe
+
+Wipe specific drive with single zero pass (usually sufficient):
+
+ nwipe --method=zero /dev/sdX
+
+Wipe with DoD 3-pass method:
+
+ nwipe --method=dod /dev/sdX
+
+Wipe with verification:
+
+ nwipe --verify=last /dev/sdX
+
+Available wipe methods:
+
+ zero - Single pass of zeros (fastest, usually sufficient)
+ one - Single pass of ones
+ random - Random data
+ dod - DoD 5220.22-M (3 passes)
+ dodshort - DoD short (3 passes)
+ gutmann - Gutmann 35-pass (overkill for modern drives)
+
+For SSDs, use the drive's built-in secure erase instead:
+
+ # Set a temporary password
+ hdparm --user-master u --security-set-pass Erase /dev/sdX
+ # Trigger secure erase (password is cleared after)
+ hdparm --user-master u --security-erase Erase /dev/sdX
+
+For NVMe SSDs:
+
+ nvme format /dev/nvme0n1 --ses=1 # Cryptographic erase
+
+
+SCENARIO: Work with XFS filesystems
+-----------------------------------
+Create XFS filesystem:
+
+ mkfs.xfs /dev/sdX1
+ mkfs.xfs -L "mylabel" /dev/sdX1 # With label
+
+Repair XFS (must be unmounted):
+
+ xfs_repair /dev/sdX1
+ xfs_repair -n /dev/sdX1 # Check only, no changes
+
+Grow XFS filesystem (while mounted):
+
+ xfs_growfs /mountpoint
+
+Note: XFS cannot be shrunk, only grown.
+
+Show XFS info:
+
+ xfs_info /mountpoint
+
+
+SCENARIO: Work with Btrfs filesystems
+-------------------------------------
+Create Btrfs filesystem:
+
+ mkfs.btrfs /dev/sdX1
+ mkfs.btrfs -L "mylabel" /dev/sdX1 # With label
+
+Check Btrfs (must be unmounted):
+
+ btrfs check /dev/sdX1
+ btrfs check --repair /dev/sdX1 # Repair (use with caution!)
+
+Scrub (online integrity check - safe):
+
+ btrfs scrub start /mountpoint
+ btrfs scrub status /mountpoint
+
+Show filesystem info:
+
+ btrfs filesystem show
+ btrfs filesystem df /mountpoint
+ btrfs filesystem usage /mountpoint
+
+List/manage subvolumes:
+
+ btrfs subvolume list /mountpoint
+ btrfs subvolume create /mountpoint/newsubvol
+ btrfs subvolume delete /mountpoint/subvol
+
+
+SCENARIO: Work with F2FS filesystems (Flash-Friendly)
+-----------------------------------------------------
+F2FS is optimized for flash storage (SSDs, SD cards, USB drives).
+Common on Android devices.
+
+Create F2FS filesystem:
+
+ mkfs.f2fs /dev/sdX1
+ mkfs.f2fs -l "mylabel" /dev/sdX1 # With label
+
+Check/repair F2FS:
+
+ fsck.f2fs /dev/sdX1
+ fsck.f2fs -a /dev/sdX1 # Auto-repair
+
+
+SCENARIO: Work with exFAT filesystems
+-------------------------------------
+exFAT is common on USB drives and SD cards (>32GB).
+Cross-platform compatible (Windows, Mac, Linux).
+
+Create exFAT filesystem:
+
+ mkfs.exfat /dev/sdX1
+ mkfs.exfat -L "LABEL" /dev/sdX1 # With label (uppercase recommended)
+
+Check/repair exFAT:
+
+ fsck.exfat /dev/sdX1
+ fsck.exfat -a /dev/sdX1 # Auto-repair
+
+
+SCENARIO: Partition a disk
+--------------------------
+Interactive partition editors:
+
+ parted /dev/sdX # Works with GPT and MBR
+ gdisk /dev/sdX # GPT-specific (recommended for UEFI)
+ fdisk /dev/sdX # Traditional (MBR or GPT)
+
+Create GPT partition table:
+
+ parted /dev/sdX mklabel gpt
+
+Create partitions (example: 512MB EFI + rest for Linux):
+
+ parted /dev/sdX mkpart primary fat32 1MiB 513MiB
+ parted /dev/sdX set 1 esp on
+ parted /dev/sdX mkpart primary ext4 513MiB 100%
+
+View partition layout:
+
+ parted /dev/sdX print
+ lsblk -f /dev/sdX
+ fdisk -l /dev/sdX
+
+
+SCENARIO: Find what's using disk space (ncdu)
+---------------------------------------------
+ncdu is an interactive disk usage analyzer - much faster than
+repeatedly running du.
+
+Analyze current directory:
+
+ ncdu
+
+Analyze specific path:
+
+ ncdu /home
+ ncdu /var
+
+Analyze root filesystem:
+
+ ncdu /
+
+Exclude mounted filesystems (just local disk):
+
+ ncdu -x /
+
+Navigation:
+ - Arrow keys or j/k to move
+ - Enter to drill into directory
+ - d to delete file/folder (confirms first)
+ - q to quit
+ - g to show percentage/graph
+ - n to sort by name
+ - s to sort by size
+
+Export scan to file (for slow disks, scan once):
+
+ ncdu -o scan.json /
+ ncdu -f scan.json # Load later
+
+
+SCENARIO: Visualize directory structure (tree)
+----------------------------------------------
+tree shows directories as an indented tree.
+
+Show current directory:
+
+ tree
+
+Show specific path:
+
+ tree /etc/systemd
+
+Limit depth:
+
+ tree -L 2 # Only 2 levels deep
+ tree -L 3 /home # 3 levels under /home
+
+Show hidden files:
+
+ tree -a
+
+Show only directories:
+
+ tree -d
+
+With file sizes:
+
+ tree -h # Human-readable sizes
+ tree -sh # Include size for files
+
+Filter by pattern:
+
+ tree -P "*.conf" # Only .conf files
+ tree -I "node_modules|.git" # Exclude patterns
+
+
+DISK OPERATIONS TIPS
+--------------------
+1. partclone is 5-10x faster than dd for partially-filled partitions
+2. fsarchiver archives can restore to different-sized partitions
+3. For SSDs, nwipe is less effective than ATA/NVMe secure erase
+4. Always verify backups can be restored before wiping originals
+5. XFS cannot be shrunk, only grown - plan partition sizes carefully
+6. Btrfs check --repair is risky; try without --repair first
+7. Keep partition tables aligned to 1MiB boundaries for SSD performance
+8. exFAT is best for cross-platform USB drives >32GB
+9. F2FS is optimized for flash but less portable than ext4
+10. ncdu -x avoids crossing filesystem boundaries (stays on one disk)
+11. tree -L 2 gives quick overview without overwhelming detail
+
+================================================================================
+7. NETWORK TROUBLESHOOTING
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr ip # Network interface configuration
+ tldr nmcli # NetworkManager CLI
+ tldr ping # Test connectivity
+ tldr ss # Socket statistics (netstat replacement)
+ tldr curl # Transfer data from URLs
+ tldr mtr # Combined ping + traceroute
+ tldr iperf3 # Network bandwidth testing
+ tldr tcpdump # Packet capture and analysis
+ tldr nmap # Network scanner
+ man iftop # Live bandwidth monitor
+ man nethogs # Per-process bandwidth
+ man tshark # Wireshark CLI (packet analysis)
+ tldr speedtest-cli # Internet speed test
+ tldr mosh # Mobile shell (survives disconnects)
+ tldr aria2c # Multi-protocol downloader
+ tldr tmate # Terminal sharing
+ tldr sshuttle # VPN over SSH
+
+FIRST: Check basic network connectivity
+---------------------------------------
+Is the interface up?
+
+ ip link show
+ ip a # Show all addresses
+
+Is there an IP address?
+
+ ip addr show dev eth0 # Replace eth0 with your interface
+ ip addr show dev wlan0 # For WiFi
+
+Can you reach the gateway?
+
+ ip route # Show default gateway
+ ping -c 3 $(ip route | grep default | awk '{print $3}')
+
+Can you reach the internet?
+
+ ping -c 3 1.1.1.1 # Test IP connectivity
+ ping -c 3 google.com # Test DNS resolution
+
+
+SCENARIO: Configure network with NetworkManager
+-----------------------------------------------
+List connections:
+
+ nmcli connection show
+
+Show WiFi networks:
+
+ nmcli device wifi list
+
+Connect to WiFi:
+
+ nmcli device wifi connect "SSID" password "password"
+
+Show current connection details:
+
+ nmcli device show
+
+Restart networking:
+
+ systemctl restart NetworkManager
+
+
+SCENARIO: Configure network manually (no NetworkManager)
+--------------------------------------------------------
+Bring up interface:
+
+ ip link set eth0 up
+
+Get IP via DHCP:
+
+ dhclient eth0
+ # or
+ dhcpcd eth0
+
+Set static IP:
+
+ ip addr add 192.168.1.100/24 dev eth0
+ ip route add default via 192.168.1.1
+
+Set DNS:
+
+ echo "nameserver 1.1.1.1" > /etc/resolv.conf
+
+
+SCENARIO: Mount remote filesystem over SSH (sshfs)
+--------------------------------------------------
+Access files on a remote system as if they were local.
+Useful for copying data to/from a working machine during recovery.
+
+Mount remote directory:
+
+ mkdir -p /mnt/remote
+ sshfs user@hostname:/path/to/dir /mnt/remote
+
+Mount with password prompt (if no SSH keys):
+
+ sshfs user@hostname:/home/user /mnt/remote -o password_stdin
+
+Mount remote root filesystem:
+
+ sshfs root@192.168.1.100:/ /mnt/remote
+
+Common options:
+
+ sshfs user@host:/path /mnt/remote -o reconnect # Auto-reconnect
+ sshfs user@host:/path /mnt/remote -o port=2222 # Custom SSH port
+ sshfs user@host:/path /mnt/remote -o IdentityFile=~/.ssh/key # SSH key
+
+Copy files to/from mounted remote:
+
+ cp /mnt/remote/important-file.txt /local/backup/
+ rsync -avP /local/data/ /mnt/remote/backup/
+
+Unmount when done:
+
+ fusermount -u /mnt/remote
+ # or
+ umount /mnt/remote
+
+Why use sshfs instead of scp/rsync?
+ - Browse remote files interactively before deciding what to copy
+ - Run local tools on remote files (grep, diff, etc.)
+ - Easier than remembering rsync syntax for quick operations
+
+
+SCENARIO: Transfer files over SSH
+---------------------------------
+Copy file to remote:
+
+ scp localfile.txt user@host:/path/to/destination/
+
+Copy file from remote:
+
+ scp user@host:/path/to/file.txt /local/destination/
+
+Copy directory recursively:
+
+ scp -r /local/dir user@host:/remote/path/
+
+With progress and compression:
+
+ rsync -avzP /local/path/ user@host:/remote/path/
+
+
+SCENARIO: Test network path and latency (mtr)
+---------------------------------------------
+mtr combines ping and traceroute into one tool. Shows packet loss and
+latency at each hop in real-time.
+
+Interactive mode (updates continuously):
+
+ mtr google.com
+
+Report mode (runs 10 cycles and exits):
+
+ mtr -r -c 10 google.com
+
+With IP addresses only (faster, no DNS lookups):
+
+ mtr -n google.com
+
+Show both hostnames and IPs:
+
+ mtr -b google.com
+
+Reading mtr output:
+ - Loss% = packet loss at that hop (>0% = problem)
+ - Snt = packets sent
+ - Last/Avg/Best/Wrst = latency in ms
+ - StDev = latency variation (high = inconsistent)
+
+Common patterns:
+ - High loss at one hop, normal after = that router deprioritizes ICMP (OK)
+ - Loss increasing at each hop = real network problem
+ - Sudden latency jump = congested link or long physical distance
+
+
+SCENARIO: Test bandwidth between two machines (iperf3)
+------------------------------------------------------
+iperf3 measures actual throughput between two endpoints.
+Requires iperf3 running on both ends.
+
+On the server (machine to test TO):
+
+ iperf3 -s # Listen on default port 5201
+
+On the client (machine to test FROM):
+
+ iperf3 -c server-ip # Basic test (10 seconds)
+ iperf3 -c server-ip -t 30 # Test for 30 seconds
+ iperf3 -c server-ip -R # Reverse (test download instead of upload)
+
+Test both directions:
+
+ iperf3 -c server-ip # Upload speed
+ iperf3 -c server-ip -R # Download speed
+
+With parallel streams (better for high-latency links):
+
+ iperf3 -c server-ip -P 4 # 4 parallel streams
+
+Test UDP (for VoIP/streaming quality):
+
+ iperf3 -c server-ip -u -b 100M # UDP at 100 Mbps
+
+Interpreting results:
+ - Bitrate = actual throughput achieved
+ - Retr = TCP retransmissions (high = packet loss)
+ - Cwnd = TCP congestion window
+
+
+SCENARIO: Monitor live bandwidth usage (iftop)
+----------------------------------------------
+iftop shows bandwidth usage per connection in real-time.
+Like top, but for network traffic.
+
+Monitor all interfaces:
+
+ iftop
+
+Monitor specific interface:
+
+ iftop -i eth0
+ iftop -i wlan0
+
+Without DNS lookups (faster):
+
+ iftop -n
+
+Show port numbers:
+
+ iftop -P
+
+Filter to specific host:
+
+ iftop -f "host 192.168.1.100"
+
+Interactive commands while running:
+ h = help
+ n = toggle DNS resolution
+ s = toggle source display
+ d = toggle destination display
+ p = toggle port display
+ P = pause display
+ q = quit
+
+
+SCENARIO: Find which process is using bandwidth (nethogs)
+---------------------------------------------------------
+nethogs shows bandwidth usage per process, not per connection.
+Essential for finding what's eating your bandwidth.
+
+Monitor all interfaces:
+
+ nethogs
+
+Monitor specific interface:
+
+ nethogs eth0
+
+Refresh faster (every 0.5 seconds):
+
+ nethogs -d 0.5
+
+Interactive commands:
+ m = cycle through display modes (KB/s, KB, B, MB)
+ r = sort by received
+ s = sort by sent
+ q = quit
+
+
+SCENARIO: Check network interface details (ethtool)
+---------------------------------------------------
+ethtool shows and configures network interface settings.
+
+Show interface status:
+
+ ethtool eth0
+
+Key information:
+ - Speed: 1000Mb/s (link speed)
+ - Duplex: Full (full or half duplex)
+ - Link detected: yes (cable connected)
+
+Show driver information:
+
+ ethtool -i eth0
+
+Show interface statistics:
+
+ ethtool -S eth0
+
+Check for errors (look for non-zero values):
+
+ ethtool -S eth0 | grep -i error
+ ethtool -S eth0 | grep -i drop
+
+Wake-on-LAN settings:
+
+ ethtool eth0 | grep Wake-on
+
+Enable Wake-on-LAN:
+
+ ethtool -s eth0 wol g
+
+
+SCENARIO: Capture and analyze packets (tcpdump)
+-----------------------------------------------
+tcpdump captures network traffic for analysis.
+Essential for debugging network issues at the packet level.
+
+Capture all traffic on an interface:
+
+ tcpdump -i eth0
+
+Capture with more detail:
+
+ tcpdump -i eth0 -v # Verbose
+ tcpdump -i eth0 -vv # More verbose
+ tcpdump -i eth0 -X # Show packet contents in hex + ASCII
+
+Capture to a file (for later analysis):
+
+ tcpdump -i eth0 -w capture.pcap
+
+Read a capture file:
+
+ tcpdump -r capture.pcap
+
+Common filters:
+
+ tcpdump -i eth0 host 192.168.1.100 # Traffic to/from host
+ tcpdump -i eth0 port 80 # HTTP traffic
+ tcpdump -i eth0 port 443 # HTTPS traffic
+ tcpdump -i eth0 tcp # TCP only
+ tcpdump -i eth0 udp # UDP only
+ tcpdump -i eth0 icmp # Ping traffic
+ tcpdump -i eth0 'port 22 and host 10.0.0.1' # SSH to specific host
+
+Capture only N packets:
+
+ tcpdump -i eth0 -c 100 # Stop after 100 packets
+
+Show only packet summaries (no payload):
+
+ tcpdump -i eth0 -q
+
+Useful for debugging:
+
+ # See DNS queries
+ tcpdump -i eth0 port 53
+
+ # See all SYN packets (connection attempts)
+ tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0'
+
+ # See HTTP requests
+ tcpdump -i eth0 -A port 80 | grep -E '^(GET|POST|HEAD)'
+
+
+SCENARIO: Scan network and discover hosts (nmap)
+------------------------------------------------
+nmap is a powerful network scanner for discovery and security auditing.
+
+Discover hosts on local network:
+
+ nmap -sn 192.168.1.0/24 # Ping scan (no port scan)
+
+Quick scan of common ports:
+
+ nmap 192.168.1.100 # Top 1000 ports
+
+Scan specific ports:
+
+ nmap -p 22,80,443 192.168.1.100
+ nmap -p 1-1000 192.168.1.100 # Port range
+ nmap -p- 192.168.1.100 # All 65535 ports (slow)
+
+Service version detection:
+
+ nmap -sV 192.168.1.100 # Detect service versions
+
+Operating system detection:
+
+ nmap -O 192.168.1.100 # Requires root
+
+Comprehensive scan:
+
+ nmap -A 192.168.1.100 # OS detection, version, scripts, traceroute
+
+Fast scan (fewer ports):
+
+ nmap -F 192.168.1.100 # Top 100 ports only
+
+Scan multiple hosts:
+
+ nmap 192.168.1.1-50 # Range
+ nmap 192.168.1.1 192.168.1.2 # Specific hosts
+ nmap -iL hosts.txt # From file
+
+Output formats:
+
+ nmap -oN scan.txt 192.168.1.100 # Normal output
+ nmap -oX scan.xml 192.168.1.100 # XML output
+ nmap -oG scan.grep 192.168.1.100 # Greppable output
+
+Common use cases:
+
+ # Find all web servers on network
+ nmap -p 80,443 192.168.1.0/24
+
+ # Find SSH servers
+ nmap -p 22 192.168.1.0/24
+
+ # Find all live hosts quickly
+ nmap -sn -T4 192.168.1.0/24
+
+
+SCENARIO: Deep packet analysis (tshark/Wireshark CLI)
+-----------------------------------------------------
+tshark is the command-line version of Wireshark. More powerful than
+tcpdump for protocol analysis.
+
+Capture on interface:
+
+ tshark -i eth0
+
+Capture to file:
+
+ tshark -i eth0 -w capture.pcap
+
+Read and analyze capture file:
+
+ tshark -r capture.pcap
+
+Filter during capture:
+
+ tshark -i eth0 -f "port 80" # Capture filter (BPF syntax)
+
+Filter during display:
+
+ tshark -r capture.pcap -Y "http" # HTTP traffic
+ tshark -r capture.pcap -Y "dns" # DNS traffic
+ tshark -r capture.pcap -Y "tcp.port == 443" # HTTPS
+ tshark -r capture.pcap -Y "ip.addr == 192.168.1.1" # Specific host
+
+Show specific fields:
+
+ tshark -r capture.pcap -T fields -e ip.src -e ip.dst -e tcp.port
+
+Protocol statistics:
+
+ tshark -r capture.pcap -q -z io,stat,1 # I/O statistics
+ tshark -r capture.pcap -q -z conv,tcp # TCP conversations
+ tshark -r capture.pcap -q -z http,tree # HTTP statistics
+
+Follow a TCP stream:
+
+ tshark -r capture.pcap -q -z follow,tcp,ascii,0 # First TCP stream
+
+Extract HTTP objects:
+
+ tshark -r capture.pcap --export-objects http,./extracted/
+
+Useful filters:
+
+ # Failed TCP connections
+ tshark -r capture.pcap -Y "tcp.flags.reset == 1"
+
+ # DNS queries only
+ tshark -r capture.pcap -Y "dns.flags.response == 0"
+
+ # HTTP requests
+ tshark -r capture.pcap -Y "http.request"
+
+ # TLS handshakes
+ tshark -r capture.pcap -Y "tls.handshake"
+
+
+SCENARIO: Debug DNS issues
+--------------------------
+Check current DNS servers:
+
+ cat /etc/resolv.conf
+
+Test DNS resolution:
+
+ host google.com
+ dig google.com
+ nslookup google.com
+
+Test specific DNS server:
+
+ dig @1.1.1.1 google.com
+ dig @8.8.8.8 google.com
+
+Temporarily use different DNS:
+
+ echo "nameserver 1.1.1.1" > /etc/resolv.conf
+
+
+SCENARIO: Check what's listening on ports
+-----------------------------------------
+Show all listening ports:
+
+ ss -tlnp # TCP
+ ss -ulnp # UDP
+ ss -tulnp # Both
+
+Check if specific port is open:
+
+ ss -tlnp | grep :22 # SSH
+ ss -tlnp | grep :80 # HTTP
+
+Check what process is using a port:
+
+ ss -tlnp | grep :8080
+
+
+SCENARIO: Download files
+------------------------
+Download with curl:
+
+ curl -O https://example.com/file.iso
+ curl -L -O https://example.com/file # Follow redirects
+
+Download with wget:
+
+ wget https://example.com/file.iso
+ wget -c https://example.com/file.iso # Resume partial download
+
+Download and verify checksum:
+
+ curl -O https://example.com/file.iso
+ curl -O https://example.com/file.iso.sha256
+ sha256sum -c file.iso.sha256
+
+
+SCENARIO: Test internet connection speed (speedtest-cli)
+--------------------------------------------------------
+Tests download/upload speed using speedtest.net servers.
+
+Basic speed test:
+
+ speedtest-cli
+
+Show simple output (just speeds):
+
+ speedtest-cli --simple
+
+List nearby servers:
+
+ speedtest-cli --list
+
+Test against specific server:
+
+ speedtest-cli --server 1234
+
+No download test (upload only):
+
+ speedtest-cli --no-download
+
+No upload test (download only):
+
+ speedtest-cli --no-upload
+
+Output as JSON (for scripting):
+
+ speedtest-cli --json
+
+Note: Requires working internet and DNS.
+Test basic connectivity first with: ping 1.1.1.1
+
+
+SCENARIO: SSH over unreliable connection (mosh)
+-----------------------------------------------
+mosh is SSH that survives disconnects, IP changes, and high latency.
+Shows local echo immediately - feels responsive even on slow links.
+
+Connect to server:
+
+ mosh user@hostname
+
+With specific SSH port:
+
+ mosh --ssh="ssh -p 2222" user@hostname
+
+With SSH key:
+
+ mosh --ssh="ssh -i ~/.ssh/key" user@hostname
+
+How it works:
+ - Initial connection via SSH (for auth)
+ - Then switches to UDP for the session
+ - Reconnects automatically when network changes
+ - Local echo - typing appears instantly
+
+Requirements:
+ - mosh-server must be installed on the remote
+ - UDP port 60001 (default) must be open
+
+When to use mosh vs SSH:
+ - Flaky WiFi: mosh
+ - Cellular/roaming: mosh
+ - Stable network: SSH is fine
+ - Need port forwarding: SSH (mosh doesn't support it)
+
+
+SCENARIO: Download files reliably (aria2)
+-----------------------------------------
+aria2 is a multi-protocol downloader with resume, parallel
+connections, and BitTorrent support.
+
+Basic download:
+
+ aria2c https://example.com/file.iso
+
+Resume interrupted download:
+
+ aria2c -c https://example.com/file.iso
+
+Multiple connections (faster for large files):
+
+ aria2c -x 8 https://example.com/file.iso # 8 connections
+
+Download multiple files:
+
+ aria2c -i urls.txt # One URL per line
+
+Download with specific filename:
+
+ aria2c -o myfile.iso https://example.com/file.iso
+
+BitTorrent:
+
+ aria2c file.torrent
+ aria2c "magnet:?xt=..."
+
+Metalink (auto-selects mirrors):
+
+ aria2c file.metalink
+
+Limit download speed:
+
+ aria2c --max-download-limit=1M https://example.com/file.iso
+
+Why aria2 over wget/curl:
+ - Multi-connection downloads (significantly faster)
+ - Automatic resume
+ - BitTorrent built-in
+ - Downloads from multiple sources simultaneously
+
+
+SCENARIO: Share terminal for remote assistance (tmate)
+------------------------------------------------------
+tmate lets you share your terminal session via a URL.
+Someone can view or control your terminal from anywhere.
+
+Start a shared session:
+
+ tmate
+
+tmate shows connection strings:
+
+ ssh session: ssh XYZ123@nyc1.tmate.io
+ read-only: ssh ro-XYZ123@nyc1.tmate.io
+ web (rw): https://tmate.io/t/XYZ123
+ web (ro): https://tmate.io/t/ro-XYZ123
+
+Share the appropriate link:
+ - Full access: give them the ssh or web (rw) link
+ - View only: give them the ro- link
+
+Get the links programmatically:
+
+ tmate show-messages
+
+End the session:
+
+ exit # Or Ctrl+D
+
+Security notes:
+ - Anyone with the link has access
+ - Use read-only link unless they need to type
+ - Session ends when you exit
+ - New session = new random URL
+
+
+SCENARIO: VPN over SSH (sshuttle)
+---------------------------------
+sshuttle tunnels all traffic through an SSH connection.
+No server-side setup needed - just SSH access.
+
+Tunnel all traffic through remote server:
+
+ sshuttle -r user@server 0/0
+
+Tunnel only specific subnet:
+
+ sshuttle -r user@server 10.0.0.0/8
+ sshuttle -r user@server 192.168.1.0/24
+
+Exclude local network:
+
+ sshuttle -r user@server 0/0 -x 192.168.1.0/24
+
+With specific SSH port:
+
+ sshuttle -r user@server:2222 0/0
+
+DNS through tunnel too:
+
+ sshuttle --dns -r user@server 0/0
+
+Use cases:
+ - Access office network from rescue environment
+ - Bypass network restrictions
+ - Secure all traffic on untrusted network
+ - Access remote resources without full VPN setup
+
+Requirements:
+ - SSH access to a server on the target network
+ - Python on remote server (most Linux servers have it)
+ - Root locally (uses iptables)
+
+
+NETWORK TROUBLESHOOTING TIPS
+----------------------------
+1. If no IP, check cable/wifi and try dhclient or dhcpcd
+2. If IP but no internet, check gateway with ip route
+3. If gateway reachable but no internet, check DNS
+4. Use ping 1.1.1.1 to test IP connectivity without DNS
+5. sshfs is great for browsing before deciding what to copy
+6. rsync -avzP is better than scp for large transfers (resumable)
+7. Check firewall if services aren't reachable: iptables -L
+8. For WiFi issues, check rfkill: rfkill list
+9. mtr is better than traceroute - shows packet loss at each hop
+10. Use iperf3 to test actual throughput, not just connectivity
+11. nethogs shows bandwidth by process; iftop shows by connection
+12. tcpdump -w saves packets; analyze later with tshark
+13. nmap -sn for quick host discovery without port scanning
+14. ethtool shows link speed and cable status (Link detected: yes/no)
+15. High latency + low packet loss = congestion; high loss = hardware issue
+16. tcpdump and tshark capture files (.pcap) are interchangeable
+17. mosh survives network changes; use for flaky connections
+18. aria2c -x 8 uses 8 connections for faster downloads
+19. tmate for instant terminal sharing - great for getting remote help
+20. sshuttle -r user@server 0/0 tunnels ALL traffic through SSH
+
+================================================================================
+8. ENCRYPTION & GPG
+================================================================================
+
+QUICK REFERENCE
+---------------
+ tldr gpg # GNU Privacy Guard
+ tldr cryptsetup # LUKS disk encryption
+ tldr pass # Password manager
+ man gpg # Full GPG manual
+
+FIRST: Understand encryption types you may encounter
+----------------------------------------------------
+Common encryption scenarios in recovery:
+
+ GPG symmetric - Password-protected files (gpg -c)
+ GPG asymmetric - Public/private key encrypted files
+ LUKS - Full disk/partition encryption (Linux standard)
+ BitLocker - Windows disk encryption (see section 4)
+ ZFS encryption - ZFS native encryption (see section 1)
+
+This section covers GPG and LUKS. For BitLocker, see section 4.
+For ZFS encryption, see section 1.
+
+
+SCENARIO: Decrypt a password-protected file (GPG symmetric)
+-----------------------------------------------------------
+Files encrypted with `gpg -c` use a password only, no keys needed.
+
+Decrypt to original filename:
+
+ gpg -d encrypted-file.gpg > decrypted-file
+
+Decrypt (GPG auto-detects output name if .gpg extension):
+
+ gpg encrypted-file.gpg
+
+You'll be prompted for the password.
+
+Decrypt with password on command line (less secure, visible in history):
+
+ gpg --batch --passphrase "password" -d file.gpg > file
+
+
+SCENARIO: Decrypt a file encrypted to your GPG key
+--------------------------------------------------
+Files encrypted with `gpg -e -r yourname@email.com` require your private key.
+
+If your private key is on this system:
+
+ gpg -d encrypted-file.gpg > decrypted-file
+
+If you need to import your private key first:
+
+ gpg --import /path/to/private-key.asc
+ gpg -d encrypted-file.gpg > decrypted-file
+
+You'll be prompted for your key's passphrase.
+
+
+SCENARIO: Import GPG keys (public or private)
+---------------------------------------------
+Import a public key (to verify signatures or encrypt to someone):
+
+ gpg --import public-key.asc
+
+Import from a keyserver:
+
+ gpg --keyserver keyserver.ubuntu.com --recv-keys KEYID
+
+Import your private key (for decryption):
+
+ gpg --import private-key.asc
+
+List keys on the system:
+
+ gpg --list-keys # Public keys
+ gpg --list-secret-keys # Private keys
+
+
+SCENARIO: Verify a signed file or ISO
+-------------------------------------
+Verify a detached signature (.sig or .asc file):
+
+ gpg --verify file.iso.sig file.iso
+
+If you don't have the signer's public key:
+
+ # Find the key ID in the error message, then:
+ gpg --keyserver keyserver.ubuntu.com --recv-keys KEYID
+ gpg --verify file.iso.sig file.iso
+
+Verify an inline-signed message:
+
+ gpg --verify signed-message.asc
+
+
+SCENARIO: Encrypt a file for safe transfer
+------------------------------------------
+Symmetric encryption (password only - recipient needs password):
+
+ gpg -c sensitive-file.txt
+ # Creates sensitive-file.txt.gpg
+
+With specific cipher and compression:
+
+ gpg -c --cipher-algo AES256 sensitive-file.txt
+
+Asymmetric encryption (to someone's public key):
+
+ gpg -e -r recipient@email.com sensitive-file.txt
+
+Encrypt to multiple recipients:
+
+ gpg -e -r alice@example.com -r bob@example.com file.txt
+
+
+SCENARIO: Unlock a LUKS-encrypted partition
+-------------------------------------------
+LUKS is the standard Linux disk encryption.
+
+Check if a partition is LUKS-encrypted:
+
+ cryptsetup isLuks /dev/sdX1 && echo "LUKS encrypted"
+ lsblk -f # Shows "crypto_LUKS" for encrypted partitions
+
+Open (decrypt) a LUKS partition:
+
+ cryptsetup open /dev/sdX1 decrypted
+ # Enter passphrase when prompted
+ # Creates /dev/mapper/decrypted
+
+Mount the decrypted partition:
+
+ mount /dev/mapper/decrypted /mnt/recovery
+
+When done, unmount and close:
+
+ umount /mnt/recovery
+ cryptsetup close decrypted
+
+
+SCENARIO: Open LUKS with a key file
+-----------------------------------
+If LUKS was set up with a key file instead of (or in addition to) password:
+
+ cryptsetup open /dev/sdX1 decrypted --key-file /path/to/keyfile
+
+Key file might be on a USB drive:
+
+ mount /dev/sdb1 /mnt/usb
+ cryptsetup open /dev/sdX1 decrypted --key-file /mnt/usb/luks-key
+
+
+SCENARIO: Recover data from damaged LUKS header
+-----------------------------------------------
+If LUKS header is damaged, you need a header backup (hopefully you made one).
+
+Restore LUKS header from backup:
+
+ cryptsetup luksHeaderRestore /dev/sdX1 --header-backup-file header-backup.img
+
+If no backup exists and header is damaged, data is likely unrecoverable.
+This is why LUKS header backups are critical:
+
+ # How to create a header backup (do this BEFORE disaster):
+ cryptsetup luksHeaderBackup /dev/sdX1 --header-backup-file header-backup.img
+
+
+SCENARIO: Access eCryptfs encrypted home directory
+--------------------------------------------------
+Ubuntu's legacy home encryption uses eCryptfs.
+
+Mount an eCryptfs-encrypted home:
+
+ # You need the user's login password
+ ecryptfs-recover-private
+
+Or manually:
+
+ mount -t ecryptfs /home/.ecryptfs/username/.Private /mnt/recovery
+
+
+SCENARIO: Access stored passwords (pass)
+----------------------------------------
+pass is the standard Unix password manager. Passwords are GPG-encrypted
+files in ~/.password-store.
+
+If you use pass, your passwords may be recoverable if you have:
+ - Your GPG private key
+ - Your ~/.password-store directory
+
+List all passwords:
+
+ pass
+
+Show a password:
+
+ pass Email/gmail
+ pass -c Email/gmail # Copy to clipboard instead
+
+Search passwords:
+
+ pass grep searchterm
+
+Initialize new password store (if setting up):
+
+ pass init GPG-KEY-ID
+
+Import existing password store:
+ 1. Import your GPG private key: gpg --import key.asc
+ 2. Copy ~/.password-store from backup
+ 3. Use pass commands as normal
+
+Generate new password:
+
+ pass generate -n 20 NewSite/login
+
+Note: Requires your GPG private key to decrypt.
+If you don't use pass, this tool isn't useful for you.
+
+
+ENCRYPTION TIPS
+---------------
+1. GPG symmetric encryption (gpg -c) only needs the password to decrypt
+2. GPG asymmetric encryption requires the private key - no key = no access
+3. Always keep LUKS header backups separate from the encrypted drive
+4. BitLocker recovery keys are often in Microsoft accounts
+5. ZFS encryption keys are derived from passphrase - no separate key file
+6. eCryptfs wrapped passphrase is in ~/.ecryptfs/wrapped-passphrase
+7. If you forget encryption passwords and have no backups, data is gone
+8. Hardware security keys (YubiKey) may be required for some GPG keys
+9. pass stores passwords as GPG-encrypted files - need your GPG key to access
+
+================================================================================
+9. SYSTEM TRACING (eBPF/bpftrace)
+================================================================================
+
+Linux equivalent of DTrace. Uses eBPF (extended Berkeley Packet Filter) for
+safe, dynamic kernel tracing. Essential for diagnosing performance issues,
+kernel problems, and understanding system behavior.
+
+QUICK REFERENCE
+---------------
+ tldr bpftrace # Quick examples
+ man bpftrace # Full manual
+ bpftrace -l # List available probes
+ bpftrace -e 'BEGIN { printf("hello\n"); }' # Test it works
+
+TOOLS AVAILABLE
+---------------
+ bpftrace - High-level tracing language (like DTrace)
+ bcc-tools - 100+ pre-built diagnostic tools
+ perf - Linux kernel profiler
+
+USEFUL BCC TOOLS (run as root)
+------------------------------
+ execsnoop # Trace new process execution
+ opensnoop # Trace file opens
+ biolatency # Block I/O latency histogram
+ tcpconnect # Trace TCP connections
+ tcpaccept # Trace TCP accepts
+ ext4slower # Trace slow ext4 operations
+ zfsslower # Trace slow ZFS operations (if available)
+ runqlat # CPU scheduler latency
+ cpudist # CPU usage distribution
+ cachestat # Page cache hit/miss stats
+ memleak # Memory leak detector
+
+BPFTRACE ONE-LINERS
+-------------------
+Count system calls by process:
+
+ bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
+
+Trace disk I/O latency histogram:
+
+ bpftrace -e 'kprobe:blk_account_io_start { @start[arg0] = nsecs; }
+ kprobe:blk_account_io_done /@start[arg0]/
+ { @usecs = hist((nsecs - @start[arg0]) / 1000); delete(@start[arg0]); }'
+
+Trace file opens:
+
+ bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }'
+
+Trace TCP connections:
+
+ bpftrace -e 'kprobe:tcp_connect { printf("%s connecting\n", comm); }'
+
+Profile kernel stacks at 99Hz:
+
+ bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
+
+ZFS-SPECIFIC TRACING
+--------------------
+Trace ZFS reads:
+
+ bpftrace -e 'kprobe:zfs_read { @[comm] = count(); }'
+
+Trace ZFS writes:
+
+ bpftrace -e 'kprobe:zfs_write { @[comm] = count(); }'
+
+PERF BASICS
+-----------
+Record CPU profile for 10 seconds:
+
+ perf record -g sleep 10
+
+View the report:
+
+ perf report
+
+List available events:
+
+ perf list
+
+Real-time top-like view:
+
+ perf top
+
+LEARN MORE
+----------
+ https://www.brendangregg.com/bpf-performance-tools-book.html
+ https://github.com/iovisor/bcc
+ https://github.com/iovisor/bpftrace
+ https://www.brendangregg.com/ebpf.html
+
+
+================================================================================
+10. TERMINAL WEB BROWSING
+================================================================================
+
+Two terminal web browsers available for documentation and troubleshooting.
+
+BROWSERS AVAILABLE
+------------------
+ lynx - Classic text browser, most compatible, keyboard-driven
+ w3m - Better table rendering, can display images in some terminals
+
+LYNX BASICS
+-----------
+Start browsing:
+
+ lynx https://wiki.archlinux.org
+ lynx file.html
+
+Navigation:
+ Arrow keys - Move around
+ Enter - Follow link
+ Backspace - Go back
+ q - Quit
+ / - Search in page
+ g - Go to URL
+ p - Print/save page
+
+W3M BASICS
+----------
+Start browsing:
+
+ w3m https://wiki.archlinux.org
+ w3m file.html
+
+Navigation:
+ Arrow keys - Scroll
+ Enter - Follow link
+ B - Go back
+ U - Enter URL
+ q - Quit (Q to quit without confirm)
+ / - Search forward
+ Tab - Next link
+ Shift+Tab - Previous link
+
+OFFLINE ARCH WIKI (NO NETWORK NEEDED)
+-------------------------------------
+This ISO includes the full Arch Wiki for offline use - invaluable when
+networking is broken and you need documentation.
+
+arch-wiki-lite (CLI, smaller):
+ wiki-search zfs # Search for articles
+ wiki-search mkinitcpio # Find mkinitcpio docs
+ wiki-search "grub rescue" # Search with spaces
+
+arch-wiki-docs (HTML, complete):
+ Location: /usr/share/doc/arch-wiki/html/
+
+ Browse with w3m:
+ w3m /usr/share/doc/arch-wiki/html/index.html
+
+ Search for topic:
+ find /usr/share/doc/arch-wiki/html -iname "*zfs*"
+ w3m /usr/share/doc/arch-wiki/html/en/ZFS.html
+
+USEFUL URLS FOR RESCUE (WHEN ONLINE)
+------------------------------------
+ https://wiki.archlinux.org
+ https://wiki.archlinux.org/title/ZFS
+ https://wiki.archlinux.org/title/GRUB
+ https://wiki.archlinux.org/title/Mkinitcpio
+ https://bbs.archlinux.org
+ https://openzfs.github.io/openzfs-docs/
+
+SAVE PAGE FOR OFFLINE
+---------------------
+ lynx -dump URL > page.txt # Save as text
+ w3m -dump URL > page.txt # Save as text
+ wget -p -k URL # Download with assets
+ curl URL > page.html # Just the HTML
+
+
+================================================================================
+ END OF GUIDE
+================================================================================
diff --git a/installer/archangel b/installer/archangel
new file mode 100755
index 0000000..023115e
--- /dev/null
+++ b/installer/archangel
@@ -0,0 +1,1688 @@
+#!/usr/bin/env bash
+# archangel - Arch Linux Installer with Snapshot-Based Recovery
+# Craig Jennings (github.com/cjennings)
+#
+# Installs Arch Linux on ZFS or Btrfs root with snapshot support.
+# Choose your filesystem: ZFS (native encryption) or Btrfs (GRUB snapshots).
+#
+# Features:
+# - Filesystem choice: ZFS or Btrfs
+# - All questions asked upfront, then unattended installation
+# - Optional WiFi configuration with connection test
+# - Optional ZFS native encryption (passphrase required at boot)
+# - Pre-pacman snapshots for safe upgrades
+# - Genesis snapshot for factory reset
+#
+# UNATTENDED MODE:
+# Use --config-file /path/to/archangel.conf for automated installs.
+# Config file must be explicitly specified to prevent accidental disk wipes.
+# See /root/archangel.conf.example for a template with all options.
+
+set -e
+
+#############################
+# Source Library Functions
+#############################
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/lib/common.sh"
+source "$SCRIPT_DIR/lib/config.sh"
+source "$SCRIPT_DIR/lib/disk.sh"
+source "$SCRIPT_DIR/lib/zfs.sh"
+source "$SCRIPT_DIR/lib/btrfs.sh"
+
+#############################
+# Configuration
+#############################
+
+# Filesystem selection (zfs or btrfs)
+FILESYSTEM="zfs" # Default to ZFS, can be changed interactively or via config
+
+# These will be set interactively
+HOSTNAME=""
+TIMEZONE=""
+LOCALE="en_US.UTF-8"
+KEYMAP="us"
+ROOT_PASSWORD=""
+ZFS_PASSPHRASE=""
+WIFI_SSID=""
+WIFI_PASSWORD=""
+
+# ZFS Configuration
+POOL_NAME="zroot"
+COMPRESSION="zstd"
+ASHIFT="12" # 4K sectors (use 13 for 8K)
+
+# Multi-disk RAID support
+SELECTED_DISKS=() # Array of selected disk paths (/dev/sda, /dev/sdb, ...)
+ZFS_PARTS=() # Array of ZFS partition paths
+EFI_PARTS=() # Array of EFI partition paths
+RAID_LEVEL="" # "", "mirror", "raidz1", "raidz2", "raidz3"
+ENABLE_SSH="yes" # Enable SSH with root login (default yes for headless)
+NO_ENCRYPT="no" # Skip ZFS encryption (for testing only)
+
+# Logging
+LOGFILE="/tmp/archangel-$(date +'%Y-%m-%d-%H-%M-%S').log"
+exec > >(tee -a "$LOGFILE") 2>&1
+
+# Log header with timestamp
+echo ""
+echo "================================================================================"
+echo "archangel started @ $(date +'%Y-%m-%d %H:%M:%S')"
+echo "================================================================================"
+echo ""
+
+# Output functions now in lib/common.sh
+# Config functions now in lib/config.sh
+
+#############################
+# Pre-flight Checks
+#############################
+
+preflight_checks() {
+ require_root
+}
+
+# Filesystem-specific preflight (called after filesystem is selected)
+filesystem_preflight() {
+ if [[ "$FILESYSTEM" == "zfs" ]]; then
+ zfs_preflight
+ elif [[ "$FILESYSTEM" == "btrfs" ]]; then
+ btrfs_preflight
+ fi
+}
+
+#############################
+# Phase 1: Gather All Input
+#############################
+
+gather_input() {
+ if [[ "$UNATTENDED" == true ]]; then
+ # Validate required config values
+ if [[ -z "$HOSTNAME" ]]; then error "Config missing required: HOSTNAME"; fi
+ if [[ -z "$TIMEZONE" ]]; then error "Config missing required: TIMEZONE"; fi
+ if [[ -z "$ROOT_PASSWORD" ]]; then error "Config missing required: ROOT_PASSWORD"; fi
+ if [[ ${#SELECTED_DISKS[@]} -eq 0 ]]; then error "Config missing required: DISKS"; fi
+
+ # Set defaults for optional values
+ [[ -z "$FILESYSTEM" ]] && FILESYSTEM="zfs" || true
+ [[ -z "$LOCALE" ]] && LOCALE="en_US.UTF-8" || true
+ [[ -z "$KEYMAP" ]] && KEYMAP="us" || true
+ [[ -z "$ENABLE_SSH" ]] && ENABLE_SSH="yes" || true
+
+ # ZFS-specific validation
+ if [[ "$FILESYSTEM" == "zfs" ]]; then
+ if [[ "$NO_ENCRYPT" != "yes" && -z "$ZFS_PASSPHRASE" ]]; then
+ error "Config missing required: ZFS_PASSPHRASE (or set NO_ENCRYPT=yes)"
+ fi
+ fi
+
+ # Btrfs-specific validation
+ if [[ "$FILESYSTEM" == "btrfs" ]]; then
+ if [[ "$NO_ENCRYPT" != "yes" && -z "$LUKS_PASSPHRASE" ]]; then
+ error "Config missing required: LUKS_PASSPHRASE (or set NO_ENCRYPT=yes)"
+ fi
+ fi
+
+ # Validate filesystem choice
+ if [[ "$FILESYSTEM" != "zfs" && "$FILESYSTEM" != "btrfs" ]]; then
+ error "Invalid FILESYSTEM: $FILESYSTEM (must be 'zfs' or 'btrfs')"
+ fi
+
+ # Determine RAID level if not specified
+ if [[ -z "$RAID_LEVEL" && ${#SELECTED_DISKS[@]} -gt 1 ]]; then
+ RAID_LEVEL="mirror"
+ info "Defaulting to mirror for ${#SELECTED_DISKS[@]} disks"
+ fi
+
+ info "Configuration loaded:"
+ info " Filesystem: $FILESYSTEM"
+ info " Hostname: $HOSTNAME"
+ info " Timezone: $TIMEZONE"
+ info " Locale: $LOCALE"
+ info " Keymap: $KEYMAP"
+ info " Disks: ${SELECTED_DISKS[*]}"
+ [[ -n "$RAID_LEVEL" ]] && info " RAID: $RAID_LEVEL"
+ info " SSH: $ENABLE_SSH"
+ [[ "$NO_ENCRYPT" == "yes" ]] && warn " Encryption: DISABLED (testing mode)"
+ [[ -n "$WIFI_SSID" ]] && info " WiFi: $WIFI_SSID"
+ return 0
+ fi
+
+ echo ""
+ echo "╔═══════════════════════════════════════════════════════════════╗"
+ echo "║ Archangel ║"
+ echo "║ Arch Linux with Snapshot-Based Recovery ║"
+ echo "╚═══════════════════════════════════════════════════════════════╝"
+ echo ""
+ info "Answer all questions now. Installation will run unattended afterward."
+ echo ""
+
+ select_filesystem
+ get_hostname
+ get_timezone
+ get_locale
+ get_keymap
+ get_disks
+ get_raid_level
+ get_wifi
+
+ # Encryption handling (filesystem-specific)
+ if [[ "$FILESYSTEM" == "zfs" ]]; then
+ get_encryption_choice
+ [[ "$NO_ENCRYPT" != "yes" ]] && get_zfs_passphrase
+ elif [[ "$FILESYSTEM" == "btrfs" ]]; then
+ get_btrfs_encryption_choice
+ [[ "$NO_ENCRYPT" != "yes" ]] && get_luks_passphrase
+ fi
+
+ get_root_password
+ get_ssh_config
+ show_summary
+}
+
+get_hostname() {
+ step "Hostname"
+ prompt "Enter hostname for this system:"
+ read -p "> " HOSTNAME
+ while [[ -z "$HOSTNAME" || ! "$HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]]; do
+ warn "Invalid hostname. Use letters, numbers, and hyphens (no spaces)."
+ read -p "> " HOSTNAME
+ done
+}
+
+get_timezone() {
+ step "Timezone"
+ echo ""
+ info "Type to search, ENTER to select"
+ echo ""
+
+ TIMEZONE=$(find /usr/share/zoneinfo -type f ! -path '*/posix/*' ! -path '*/right/*' \
+ | sed 's|/usr/share/zoneinfo/||' \
+ | sort \
+ | fzf --height=20 --layout=reverse --border \
+ --header="Select Timezone" \
+ --preview='echo "Timezone: {}"; echo ""; TZ={} date "+Current time: %Y-%m-%d %H:%M:%S %Z"' \
+ --preview-window=right:40%)
+
+ if [[ -z "$TIMEZONE" ]]; then
+ error "No timezone selected!"
+ fi
+ info "Selected: $TIMEZONE"
+}
+
+get_locale() {
+ step "Locale"
+ echo ""
+ info "Type to search, ENTER to select"
+ echo ""
+
+ # Get available locales from locale.gen
+ LOCALE=$(grep -E "^#?[a-z]" /etc/locale.gen \
+ | sed 's/^#//' \
+ | awk '{print $1}' \
+ | sort -u \
+ | fzf --height=20 --layout=reverse --border \
+ --header="Select Locale (type to search, e.g. 'de_DE', 'fr_FR')" \
+ --preview='
+ loc={}
+ echo "Locale: $loc"
+ echo ""
+ lang=${loc%%_*}
+ country=${loc#*_}
+ country=${country%%.*}
+ echo "Language: $lang"
+ echo "Country: $country"
+ echo ""
+ echo "Example formats:"
+ echo " Date: $(LC_ALL={} date "+%x" 2>/dev/null || echo "N/A")"
+ echo " Currency: $(LC_ALL={} locale currency_symbol 2>/dev/null || echo "N/A")"
+ ' \
+ --preview-window=right:45%)
+
+ if [[ -z "$LOCALE" ]]; then
+ error "No locale selected!"
+ fi
+ info "Selected: $LOCALE"
+}
+
+get_keymap() {
+ step "Keyboard Layout"
+ echo ""
+ info "Type to search, ENTER to select"
+ echo ""
+
+ KEYMAP=$(localectl list-keymaps \
+ | fzf --height=20 --layout=reverse --border \
+ --header="Select Keyboard Layout (type to search)" \
+ --preview='
+ echo "Keymap: {}"
+ echo ""
+ echo "This will set your console keyboard layout."
+ echo ""
+ echo "Common layouts:"
+ echo " us - US English (QWERTY)"
+ echo " uk - UK English"
+ echo " de - German (QWERTZ)"
+ echo " fr - French (AZERTY)"
+ echo " dvorak - Dvorak"
+ ' \
+ --preview-window=right:45%)
+
+ if [[ -z "$KEYMAP" ]]; then
+ error "No keymap selected!"
+ fi
+ info "Selected: $KEYMAP"
+}
+
+get_disks() {
+ step "Disk Selection"
+ echo ""
+ info "TAB to select multiple disks, ENTER to confirm"
+ echo ""
+
+ # Get list of available disks with info
+ local disk_list
+ disk_list=$(lsblk -d -n -o NAME,SIZE,TYPE | awk '$3=="disk"{printf "/dev/%-8s %8s\n", $1, $2}')
+
+ if [[ -z "$disk_list" ]]; then
+ error "No disks found!"
+ fi
+
+ # Use fzf for multi-select with disk details preview
+ local selected
+ selected=$(echo "$disk_list" \
+ | fzf --multi --height=20 --layout=reverse --border \
+ --header="Select Disks (TAB to toggle, ENTER to confirm)" \
+ --preview='
+ disk=$(echo {} | awk "{print \$1}")
+ echo "Disk: $disk"
+ echo ""
+ echo "Details:"
+ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT "$disk" 2>/dev/null
+ echo ""
+ echo "Disk info:"
+ udevadm info --query=property "$disk" 2>/dev/null | grep -E "ID_MODEL=|ID_SERIAL=" | sed "s/^/ /"
+ ' \
+ --preview-window=right:50%)
+
+ if [[ -z "$selected" ]]; then
+ error "No disks selected!"
+ fi
+
+ # Parse selected disks
+ SELECTED_DISKS=()
+ while IFS= read -r line; do
+ local disk
+ disk=$(echo "$line" | awk '{print $1}')
+ SELECTED_DISKS+=("$disk")
+ done <<< "$selected"
+
+ echo ""
+ warn "Selected ${#SELECTED_DISKS[@]} disk(s):"
+ for disk in "${SELECTED_DISKS[@]}"; do
+ local size
+ size=$(lsblk -d -n -o SIZE "$disk" | tr -d ' ')
+ echo " - $disk ($size)"
+ done
+ echo ""
+
+ read -p "This will DESTROY all data on these disks. Type 'yes' to continue: " confirm
+ if [[ "$confirm" != "yes" ]]; then
+ error "Aborted by user"
+ fi
+}
+
+get_raid_level() {
+ local disk_count=${#SELECTED_DISKS[@]}
+
+ if [[ $disk_count -eq 1 ]]; then
+ RAID_LEVEL=""
+ info "Single disk selected - no RAID"
+ return
+ fi
+
+ step "RAID Configuration"
+ echo ""
+ info "Select RAID level (ENTER to confirm)"
+ echo ""
+
+ # Calculate total raw size for preview
+ local total_bytes=0
+ local smallest_bytes=0
+ for disk in "${SELECTED_DISKS[@]}"; do
+ local bytes
+ bytes=$(lsblk -b -d -n -o SIZE "$disk")
+ total_bytes=$((total_bytes + bytes))
+ if [[ $smallest_bytes -eq 0 ]] || [[ $bytes -lt $smallest_bytes ]]; then
+ smallest_bytes=$bytes
+ fi
+ done
+ local total_gb=$((total_bytes / 1073741824))
+ local smallest_gb=$((smallest_bytes / 1073741824))
+
+ # Build options based on disk count
+ local options="mirror\nstripe"
+ [[ $disk_count -ge 3 ]] && options+="\nraidz1"
+ [[ $disk_count -ge 4 ]] && options+="\nraidz2"
+ [[ $disk_count -ge 5 ]] && options+="\nraidz3"
+
+ # Export variables for preview subshell
+ export RAID_DISK_COUNT=$disk_count
+ export RAID_TOTAL_GB=$total_gb
+ export RAID_SMALLEST_GB=$smallest_gb
+
+ RAID_LEVEL=$(echo -e "$options" \
+ | fzf --height=20 --layout=reverse --border \
+ --header="Select RAID Level ($disk_count disks, ${total_gb}GB total)" \
+ --preview='
+ n=$RAID_DISK_COUNT
+ total=$RAID_TOTAL_GB
+ small=$RAID_SMALLEST_GB
+
+ case {} in
+ mirror)
+ echo "MIRROR"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "All disks contain identical copies of data."
+ echo "Maximum redundancy - can survive loss of"
+ echo "all disks except one."
+ echo ""
+ echo "Redundancy: Can lose $((n-1)) of $n disks"
+ echo "Usable space: ~${small}GB (smallest disk)"
+ echo "Read speed: Fast (parallel reads)"
+ echo "Write speed: Normal"
+ echo ""
+ echo "Best for:"
+ echo " - Boot drives"
+ echo " - Critical data"
+ echo " - Maximum safety"
+ ;;
+ stripe)
+ echo "STRIPE (RAID0)"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "WARNING: NO REDUNDANCY!"
+ echo "Data is striped across all disks."
+ echo "ANY disk failure = ALL data lost!"
+ echo ""
+ echo "Redundancy: NONE"
+ echo "Usable space: ~${total}GB (all disks)"
+ echo "Read speed: Very fast"
+ echo "Write speed: Very fast"
+ echo ""
+ echo "Best for:"
+ echo " - Scratch/temp space"
+ echo " - Replaceable data"
+ echo " - Maximum performance"
+ ;;
+ raidz1)
+ usable=$(( (n-1) * small ))
+ echo "RAIDZ1 (Single Parity)"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "One disk worth of parity distributed"
+ echo "across all disks."
+ echo ""
+ echo "Redundancy: Can lose 1 disk"
+ echo "Usable space: ~${usable}GB ($((n-1)) of $n disks)"
+ echo "Read speed: Fast"
+ echo "Write speed: Good"
+ echo ""
+ echo "Best for:"
+ echo " - General storage"
+ echo " - Good balance of space/safety"
+ ;;
+ raidz2)
+ usable=$(( (n-2) * small ))
+ echo "RAIDZ2 (Double Parity)"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "Two disks worth of parity distributed"
+ echo "across all disks."
+ echo ""
+ echo "Redundancy: Can lose 2 disks"
+ echo "Usable space: ~${usable}GB ($((n-2)) of $n disks)"
+ echo "Read speed: Fast"
+ echo "Write speed: Good"
+ echo ""
+ echo "Best for:"
+ echo " - Large arrays (5+ disks)"
+ echo " - Important data"
+ ;;
+ raidz3)
+ usable=$(( (n-3) * small ))
+ echo "RAIDZ3 (Triple Parity)"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "Three disks worth of parity distributed"
+ echo "across all disks."
+ echo ""
+ echo "Redundancy: Can lose 3 disks"
+ echo "Usable space: ~${usable}GB ($((n-3)) of $n disks)"
+ echo "Read speed: Fast"
+ echo "Write speed: Moderate"
+ echo ""
+ echo "Best for:"
+ echo " - Very large arrays (8+ disks)"
+ echo " - Archival storage"
+ ;;
+ esac
+ ' \
+ --preview-window=right:50%)
+
+ # Clean up exported variables
+ unset RAID_DISK_COUNT RAID_TOTAL_GB RAID_SMALLEST_GB
+
+ if [[ -z "$RAID_LEVEL" ]]; then
+ error "No RAID level selected!"
+ fi
+ info "Selected: $RAID_LEVEL"
+}
+
+get_wifi() {
+ step "WiFi Configuration (Optional)"
+ echo ""
+ prompt "Do you want to configure WiFi? [Y/n]:"
+ read -p "> " configure_wifi
+
+ if [[ ! "$configure_wifi" =~ ^[Nn]$ ]]; then
+ # Ensure NetworkManager is running
+ systemctl start NetworkManager 2>/dev/null || true
+ sleep 2
+
+ echo ""
+ info "Scanning for networks..."
+ nmcli device wifi rescan 2>/dev/null || true
+ sleep 3
+
+ # Get list of networks for fzf
+ local networks
+ networks=$(nmcli -t -f SSID,SIGNAL,SECURITY device wifi list | grep -v '^$' | sort -t: -k2 -rn | uniq)
+
+ if [[ -z "$networks" ]]; then
+ warn "No WiFi networks found."
+ info "Skipping WiFi configuration."
+ return
+ fi
+
+ echo ""
+ info "Select network (ENTER to confirm, ESC to skip)"
+ echo ""
+
+ # Use fzf to select network
+ WIFI_SSID=$(echo "$networks" \
+ | fzf --height=15 --layout=reverse --border \
+ --header="Select WiFi Network" \
+ --delimiter=':' \
+ --with-nth=1 \
+ --preview='
+ IFS=":" read -r ssid signal security <<< {}
+ echo "Network: $ssid"
+ echo ""
+ echo "Signal: ${signal}%"
+ echo "Security: ${security:-Open}"
+ echo ""
+ if [[ -z "$security" ]]; then
+ echo "WARNING: Open network (no encryption)"
+ fi
+ ' \
+ --preview-window=right:40% \
+ | cut -d: -f1)
+
+ if [[ -z "$WIFI_SSID" ]]; then
+ info "Skipping WiFi configuration."
+ return
+ fi
+
+ prompt "Enter WiFi password for '$WIFI_SSID':"
+ read -s -p "> " WIFI_PASSWORD
+ echo ""
+
+ # Test the connection
+ info "Testing WiFi connection..."
+ if nmcli device wifi connect "$WIFI_SSID" password "$WIFI_PASSWORD" 2>/dev/null; then
+ info "WiFi connection successful!"
+ else
+ warn "WiFi connection failed. You can configure it manually after installation."
+ WIFI_SSID=""
+ WIFI_PASSWORD=""
+ fi
+ else
+ info "Skipping WiFi configuration."
+ fi
+}
+
+get_btrfs_encryption_choice() {
+ step "Btrfs Encryption (LUKS)"
+
+ echo ""
+ echo "LUKS encryption protects your data at rest."
+ echo "You'll need to enter a passphrase at each boot."
+ echo ""
+
+ prompt "Enable LUKS encryption? [Y/n]:"
+ read -p "> " encrypt_choice
+
+ if [[ "$encrypt_choice" =~ ^[Nn]$ ]]; then
+ NO_ENCRYPT="yes"
+ warn "Encryption DISABLED - data will not be encrypted at rest"
+ else
+ NO_ENCRYPT="no"
+ info "LUKS encryption enabled - you'll set a passphrase next"
+ fi
+}
+
+get_luks_passphrase() {
+ step "LUKS Encryption Passphrase"
+
+ echo ""
+ echo "Choose a strong passphrase for disk encryption."
+ echo "You'll need this passphrase every time you boot."
+ echo ""
+ echo "IMPORTANT: If you forget this passphrase, your data is UNRECOVERABLE!"
+ echo ""
+
+ while true; do
+ prompt "Enter LUKS encryption passphrase:"
+ read -rs LUKS_PASSPHRASE
+ echo ""
+
+ prompt "Confirm passphrase:"
+ read -rs confirm
+ echo ""
+
+ if [[ "$LUKS_PASSPHRASE" == "$confirm" ]]; then
+ if [[ ${#LUKS_PASSPHRASE} -lt 8 ]]; then
+ warn "Passphrase should be at least 8 characters. Try again."
+ else
+ info "Passphrase confirmed."
+ break
+ fi
+ else
+ warn "Passphrases don't match. Try again."
+ fi
+ done
+}
+
+get_encryption_choice() {
+ step "ZFS Encryption"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "ZFS native encryption protects your data at rest."
+ echo ""
+ echo " - Passphrase required at every boot"
+ echo " - If forgotten, data is UNRECOVERABLE"
+ echo " - Recommended for laptops and sensitive data"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ prompt "Enable ZFS encryption? [Y/n]:"
+ read -p "> " encrypt_choice
+
+ if [[ "$encrypt_choice" =~ ^[Nn]$ ]]; then
+ NO_ENCRYPT="yes"
+ warn "Encryption DISABLED - data will not be encrypted at rest"
+ else
+ NO_ENCRYPT="no"
+ info "Encryption enabled - you'll set a passphrase next"
+ fi
+}
+
+get_zfs_passphrase() {
+ step "ZFS Encryption Passphrase"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "This passphrase will be required at EVERY boot."
+ echo ""
+ echo "Requirements:"
+ echo " - Use a strong, memorable passphrase"
+ echo " - If forgotten, your data is UNRECOVERABLE"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ while true; do
+ prompt "Enter ZFS encryption passphrase:"
+ read -s -p "> " ZFS_PASSPHRASE
+ echo ""
+
+ prompt "Confirm passphrase:"
+ read -s -p "> " confirm_pass
+ echo ""
+
+ if [[ "$ZFS_PASSPHRASE" == "$confirm_pass" ]]; then
+ if [[ ${#ZFS_PASSPHRASE} -lt 8 ]]; then
+ warn "Passphrase should be at least 8 characters."
+ continue
+ fi
+ break
+ else
+ warn "Passphrases do not match. Try again."
+ fi
+ done
+}
+
+get_root_password() {
+ step "Root Password"
+ echo ""
+
+ while true; do
+ prompt "Enter root password:"
+ read -s -p "> " ROOT_PASSWORD
+ echo ""
+
+ prompt "Confirm root password:"
+ read -s -p "> " confirm_pass
+ echo ""
+
+ if [[ "$ROOT_PASSWORD" == "$confirm_pass" ]]; then
+ break
+ else
+ warn "Passwords do not match. Try again."
+ fi
+ done
+}
+
+get_ssh_config() {
+ step "SSH Configuration"
+ echo ""
+ info "SSH enables remote access after installation."
+ info "Recommended for headless servers. Harden after install (key auth, fail2ban)."
+ echo ""
+ prompt "Enable SSH with root login? [Y/n]:"
+ read -p "> " ssh_choice
+
+ if [[ "$ssh_choice" =~ ^[Nn]$ ]]; then
+ ENABLE_SSH="no"
+ info "SSH will not be enabled."
+ else
+ ENABLE_SSH="yes"
+ info "SSH will be enabled with root password login."
+ warn "Remember to harden SSH after install (key auth, fail2ban)!"
+ fi
+}
+
+show_summary() {
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "Configuration Summary:"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo " Filesystem: $FILESYSTEM"
+ echo " Hostname: $HOSTNAME"
+ echo " Timezone: $TIMEZONE"
+ echo " Locale: $LOCALE"
+ echo " Keymap: $KEYMAP"
+ echo " Disks: ${#SELECTED_DISKS[@]} disk(s)"
+ for disk in "${SELECTED_DISKS[@]}"; do
+ local size
+ size=$(lsblk -d -n -o SIZE "$disk" | tr -d ' ')
+ echo " - $disk ($size)"
+ done
+ echo " RAID Level: ${RAID_LEVEL:-single (no RAID)}"
+ echo " WiFi: ${WIFI_SSID:-Not configured}"
+ echo " SSH: ${ENABLE_SSH:-yes} (root login)"
+ if [[ "$FILESYSTEM" == "zfs" ]]; then
+ if [[ "$NO_ENCRYPT" == "yes" ]]; then
+ echo " ZFS Pool: $POOL_NAME (NOT encrypted)"
+ else
+ echo " ZFS Pool: $POOL_NAME (encrypted)"
+ fi
+ echo " Boot: ZFSBootMenu on all disks (redundant)"
+ else
+ if [[ "$NO_ENCRYPT" == "yes" ]]; then
+ echo " Encryption: None"
+ else
+ echo " Encryption: LUKS2"
+ fi
+ echo " Boot: GRUB + grub-btrfs (snapshot boot)"
+ fi
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ read -p "Press Enter to begin installation, or Ctrl+C to abort..."
+}
+
+#############################
+# Phase 2: Installation
+#############################
+
+partition_disks() {
+ step "Partitioning ${#SELECTED_DISKS[@]} disk(s)"
+
+ EFI_PARTS=()
+ ZFS_PARTS=()
+
+ for disk in "${SELECTED_DISKS[@]}"; do
+ info "Partitioning $disk..."
+
+ # Wipe existing signatures
+ wipefs -af "$disk"
+ sgdisk --zap-all "$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)
+ local efi_part zfs_part
+ if [[ "$disk" == *"nvme"* ]] || [[ "$disk" == *"mmcblk"* ]]; then
+ efi_part="${disk}p1"
+ zfs_part="${disk}p2"
+ else
+ efi_part="${disk}1"
+ zfs_part="${disk}2"
+ fi
+
+ EFI_PARTS+=("$efi_part")
+ ZFS_PARTS+=("$zfs_part")
+
+ sleep 1
+ partprobe "$disk"
+ done
+
+ sleep 2
+
+ # Format all EFI partitions
+ for i in "${!EFI_PARTS[@]}"; do
+ info "Formatting EFI partition ${EFI_PARTS[$i]}..."
+ mkfs.fat -F32 -n "EFI$i" "${EFI_PARTS[$i]}"
+ done
+
+ info "Partitioning complete. Created ${#EFI_PARTS[@]} EFI and ${#ZFS_PARTS[@]} ZFS partitions."
+}
+
+create_zfs_pool() {
+ step "Creating ZFS Pool with Native Encryption"
+
+ if zpool list "$POOL_NAME" &>/dev/null; then
+ warn "Pool $POOL_NAME already exists. Destroying..."
+ zpool destroy -f "$POOL_NAME"
+ fi
+
+ # Build pool configuration based on RAID level
+ local pool_config
+ if [[ "$RAID_LEVEL" == "stripe" ]]; then
+ # Stripe: just list devices without a vdev type (RAID0 equivalent)
+ pool_config="${ZFS_PARTS[*]}"
+ info "Creating striped pool with ${#ZFS_PARTS[@]} disks (NO redundancy)..."
+ warn "Data loss will occur if ANY disk fails!"
+ elif [[ -n "$RAID_LEVEL" ]]; then
+ pool_config="$RAID_LEVEL ${ZFS_PARTS[*]}"
+ info "Creating $RAID_LEVEL pool with ${#ZFS_PARTS[@]} disks..."
+ else
+ pool_config="${ZFS_PARTS[0]}"
+ info "Creating single-disk pool..."
+ fi
+
+ # Create pool (with or without encryption)
+ # Note: We use zfs-import-scan at boot which doesn't require a cachefile
+ if [[ "$NO_ENCRYPT" == "yes" ]]; then
+ warn "Creating pool WITHOUT encryption (testing mode)"
+ zpool create -f \
+ -o ashift="$ASHIFT" \
+ -o autotrim=on \
+ -O acltype=posixacl \
+ -O atime=off \
+ -O canmount=off \
+ -O compression="$COMPRESSION" \
+ -O dnodesize=auto \
+ -O normalization=formD \
+ -O relatime=on \
+ -O xattr=sa \
+ -O mountpoint=none \
+ -R /mnt \
+ "$POOL_NAME" $pool_config
+ else
+ echo "$ZFS_PASSPHRASE" | zpool create -f \
+ -o ashift="$ASHIFT" \
+ -o autotrim=on \
+ -O acltype=posixacl \
+ -O atime=off \
+ -O canmount=off \
+ -O compression="$COMPRESSION" \
+ -O dnodesize=auto \
+ -O normalization=formD \
+ -O relatime=on \
+ -O xattr=sa \
+ -O encryption=aes-256-gcm \
+ -O keyformat=passphrase \
+ -O keylocation=prompt \
+ -O mountpoint=none \
+ -R /mnt \
+ "$POOL_NAME" $pool_config
+ fi
+
+ info "ZFS pool created successfully."
+ zpool status "$POOL_NAME"
+}
+
+create_datasets() {
+ step "Creating ZFS Datasets"
+
+ # Root dataset container
+ zfs create -o mountpoint=none -o canmount=off "$POOL_NAME/ROOT"
+
+ # Main root filesystem
+ # Reserve 20% of pool or 20G max to prevent pool from filling completely
+ local pool_size_bytes
+ pool_size_bytes=$(zpool get -Hp size "$POOL_NAME" | awk '{print $3}')
+ local pool_size_gb=$((pool_size_bytes / 1024 / 1024 / 1024))
+ local reserve_gb=$((pool_size_gb / 5)) # 20%
+ [[ $reserve_gb -gt 20 ]] && reserve_gb=20
+ [[ $reserve_gb -lt 5 ]] && reserve_gb=5
+
+ zfs create -o mountpoint=/ -o canmount=noauto -o reservation=${reserve_gb}G "$POOL_NAME/ROOT/default"
+ zfs mount "$POOL_NAME/ROOT/default"
+
+ # Home (archsetup will create user subdataset)
+ zfs create -o mountpoint=/home "$POOL_NAME/home"
+ zfs create -o mountpoint=/root "$POOL_NAME/home/root"
+
+ # Media - compression off for already-compressed files
+ zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media"
+
+ # VMs - 64K recordsize for VM disk images
+ zfs create -o mountpoint=/vms -o recordsize=64K "$POOL_NAME/vms"
+
+ # Var datasets
+ zfs create -o mountpoint=/var -o canmount=off "$POOL_NAME/var"
+ zfs create -o mountpoint=/var/log "$POOL_NAME/var/log"
+ zfs create -o mountpoint=/var/cache "$POOL_NAME/var/cache"
+ zfs create -o mountpoint=/var/lib -o canmount=off "$POOL_NAME/var/lib"
+ zfs create -o mountpoint=/var/lib/pacman "$POOL_NAME/var/lib/pacman"
+ zfs create -o mountpoint=/var/lib/docker "$POOL_NAME/var/lib/docker"
+
+ # Temp directories - excluded from snapshots
+ zfs create -o mountpoint=/var/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/var/tmp"
+ zfs create -o mountpoint=/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/tmp"
+ chmod 1777 /mnt/tmp /mnt/var/tmp
+
+ info "Datasets created:"
+ zfs list -r "$POOL_NAME" -o name,mountpoint,compression
+}
+
+mount_efi() {
+ step "Mounting EFI Partition"
+ # 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() {
+ step "Installing Base System"
+
+ info "Updating pacman keys..."
+ pacman-key --init
+ pacman-key --populate archlinux
+
+ # Add archzfs repo to pacman.conf for pacstrap
+ # SigLevel=Never: pacstrap -K creates empty keyring where key import fails;
+ # repo is explicitly added and served over HTTPS, GPG adds no real value here
+ if ! grep -q "\[archzfs\]" /etc/pacman.conf; then
+ cat >> /etc/pacman.conf << 'EOF'
+
+[archzfs]
+Server = https://archzfs.com/$repo/$arch
+SigLevel = Never
+EOF
+ fi
+
+ info "Installing base packages (this takes a while)..."
+ info "ZFS will be built from source via DKMS - this ensures kernel compatibility."
+ # Use yes to auto-select defaults for provider prompts
+ yes "" | pacstrap -K /mnt \
+ base \
+ base-devel \
+ linux-lts \
+ linux-lts-headers \
+ linux-firmware \
+ zfs-dkms \
+ zfs-utils \
+ efibootmgr \
+ networkmanager \
+ avahi \
+ nss-mdns \
+ openssh \
+ git \
+ vim \
+ sudo \
+ zsh \
+ nodejs \
+ npm \
+ ttf-dejavu \
+ fzf \
+ wget \
+ wireless-regdb
+
+ info "Base system installed."
+}
+
+install_base_btrfs() {
+ step "Installing Base System (Btrfs)"
+
+ info "Updating pacman keys..."
+ pacman-key --init
+ pacman-key --populate archlinux
+
+ info "Installing base packages (this takes a while)..."
+ yes "" | pacstrap -K /mnt \
+ base \
+ base-devel \
+ linux-lts \
+ linux-lts-headers \
+ linux-firmware \
+ btrfs-progs \
+ grub \
+ grub-btrfs \
+ efibootmgr \
+ snapper \
+ snap-pac \
+ networkmanager \
+ avahi \
+ nss-mdns \
+ openssh \
+ git \
+ vim \
+ sudo \
+ zsh \
+ nodejs \
+ npm \
+ ttf-dejavu \
+ fzf \
+ wget \
+ wireless-regdb
+
+ info "Base system installed."
+}
+
+configure_system() {
+ step "Configuring System"
+
+ # fstab (only for EFI - /boot is on ZFS root)
+ info "Generating 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..."
+ arch-chroot /mnt ln -sf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
+ arch-chroot /mnt hwclock --systohc
+
+ # Locale
+ info "Configuring locale..."
+ echo "$LOCALE UTF-8" >> /mnt/etc/locale.gen
+ arch-chroot /mnt locale-gen
+ echo "LANG=$LOCALE" > /mnt/etc/locale.conf
+
+ # Keymap
+ echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf
+
+ # Hostname
+ info "Setting hostname to $HOSTNAME..."
+ echo "$HOSTNAME" > /mnt/etc/hostname
+ cat > /mnt/etc/hosts << EOF
+127.0.0.1 localhost
+::1 localhost
+127.0.1.1 $HOSTNAME.localdomain $HOSTNAME
+EOF
+
+ # Add archzfs repo (SigLevel=Never — same rationale as install_base)
+ info "Adding archzfs repository..."
+ cat >> /mnt/etc/pacman.conf << 'EOF'
+
+[archzfs]
+Server = https://archzfs.com/$repo/$arch
+SigLevel = Never
+EOF
+
+ # Configure journald for ZFS
+ # Problem: journald starts before ZFS mounts /var/log, so journal files
+ # get created in tmpfs then hidden when ZFS mounts over it.
+ # Solution: Make journal-flush wait for zfs-mount, and enable persistent storage.
+ info "Configuring journald for ZFS..."
+ mkdir -p /mnt/etc/systemd/journald.conf.d
+ cat > /mnt/etc/systemd/journald.conf.d/persistent.conf << 'EOF'
+[Journal]
+Storage=persistent
+EOF
+
+ mkdir -p /mnt/etc/systemd/system/systemd-journal-flush.service.d
+ cat > /mnt/etc/systemd/system/systemd-journal-flush.service.d/zfs.conf << 'EOF'
+[Unit]
+After=zfs-mount.service
+EOF
+
+ # Set root password
+ info "Setting root password..."
+ echo "root:$ROOT_PASSWORD" | arch-chroot /mnt chpasswd
+}
+
+configure_wifi() {
+ if [[ -n "$WIFI_SSID" ]]; then
+ step "Configuring WiFi"
+
+ # Copy NetworkManager connection from live environment
+ if [[ -d /etc/NetworkManager/system-connections ]]; then
+ mkdir -p /mnt/etc/NetworkManager/system-connections
+ cp /etc/NetworkManager/system-connections/* /mnt/etc/NetworkManager/system-connections/ 2>/dev/null || true
+ chmod 600 /mnt/etc/NetworkManager/system-connections/* 2>/dev/null || true
+ fi
+
+ info "WiFi configuration copied to installed system."
+ fi
+}
+
+configure_ssh() {
+ if [[ "$ENABLE_SSH" == "yes" ]]; then
+ step "Configuring SSH"
+
+ # Ensure sshd config allows root login with password
+ sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /mnt/etc/ssh/sshd_config
+ sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /mnt/etc/ssh/sshd_config
+
+ # Enable sshd service
+ arch-chroot /mnt systemctl enable sshd
+
+ info "SSH enabled with root password login."
+ warn "Harden SSH after install (key auth, fail2ban)."
+ else
+ info "SSH not enabled. Enable manually if needed."
+ fi
+}
+
+configure_initramfs() {
+ step "Configuring Initramfs for ZFS"
+
+ cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak
+
+ # CRITICAL: Remove archiso drop-in that overrides mkinitcpio.conf HOOKS
+ # The archiso.conf contains live ISO-specific hooks that are incompatible with ZFS
+ # If not removed, it overrides our HOOKS setting and breaks boot after kernel updates
+ if [[ -f /mnt/etc/mkinitcpio.conf.d/archiso.conf ]]; then
+ info "Removing archiso drop-in config..."
+ rm -f /mnt/etc/mkinitcpio.conf.d/archiso.conf
+ fi
+
+ # CRITICAL: Fix linux-lts preset file
+ # The preset from archiso uses archiso-specific config that breaks mkinitcpio -P
+ info "Creating proper linux-lts preset..."
+ cat > /mnt/etc/mkinitcpio.d/linux-lts.preset << 'PRESET_EOF'
+# mkinitcpio preset file for linux-lts
+
+PRESETS=(default fallback)
+
+ALL_kver="/boot/vmlinuz-linux-lts"
+
+default_image="/boot/initramfs-linux-lts.img"
+
+fallback_image="/boot/initramfs-linux-lts-fallback.img"
+fallback_options="-S autodetect"
+PRESET_EOF
+
+ # Check for AMD ISP (Image Signal Processor) firmware needs
+ # ISP is used for camera processing on AMD APUs (Strix, Strix Halo, etc.)
+ # The firmware must be in initramfs since amdgpu loads before root is mounted
+ if lspci | grep -qi "amd.*display\|amd.*vga\|radeon"; then
+ local isp_firmware
+ isp_firmware=$(ls /mnt/usr/lib/firmware/amdgpu/isp_*.bin.zst 2>/dev/null | head -1)
+ if [[ -n "$isp_firmware" ]]; then
+ # Remove /mnt prefix - config is used inside chroot where root is /
+ local chroot_path="${isp_firmware#/mnt}"
+ info "AMD APU detected with ISP firmware - adding to initramfs"
+ mkdir -p /mnt/etc/mkinitcpio.conf.d
+ cat > /mnt/etc/mkinitcpio.conf.d/amd-isp.conf << EOF
+# AMD ISP (Image Signal Processor) firmware for camera support
+# Loaded early so amdgpu can initialize ISP before root is mounted
+FILES+=($chroot_path)
+EOF
+ fi
+ fi
+
+ # Configure hooks for ZFS
+ # - Use udev (not systemd): ZFS hook is busybox-based and incompatible with systemd init
+ # - Remove autodetect: it filters modules based on live ISO hardware, not target
+ # This ensures NVMe, AHCI, and other storage drivers are always included
+ # - Remove fsck: ZFS doesn't use it, avoids confusing error messages
+ # - Add zfs: required for ZFS root boot
+ sed -i 's/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block zfs filesystems)/' /mnt/etc/mkinitcpio.conf
+
+ # Get the installed kernel version (not the running kernel)
+ local kernel_ver
+ kernel_ver=$(ls /mnt/usr/lib/modules | grep lts | head -1)
+ if [[ -z "$kernel_ver" ]]; then
+ error "Could not find LTS kernel modules"
+ fi
+ info "Installed kernel: $kernel_ver"
+
+ # Ensure kernel module dependencies are up to date after DKMS build
+ # Must specify kernel version since running kernel differs from installed kernel
+ info "Updating module dependencies..."
+ arch-chroot /mnt depmod "$kernel_ver"
+
+ # Verify ZFS module exists
+ if ! [[ -f "/mnt/usr/lib/modules/$kernel_ver/updates/dkms/zfs.ko.zst" ]]; then
+ error "ZFS module not found! DKMS build may have failed."
+ fi
+ info "ZFS module verified for kernel $kernel_ver"
+
+ info "Regenerating initramfs..."
+ arch-chroot /mnt mkinitcpio -P
+}
+
+configure_zfsbootmenu() {
+ step "Configuring ZFSBootMenu"
+
+ # Ensure hostid exists BEFORE reading it
+ # This is critical: hostid command returns a value even without /etc/hostid,
+ # but zgenhostid creates a DIFFERENT value. We must generate first, then read.
+ if [[ ! -f /etc/hostid ]]; then
+ zgenhostid
+ fi
+
+ # Now get the consistent hostid for kernel parameter
+ local host_id
+ host_id=$(hostid)
+
+ # Copy hostid to installed system (ZFS uses this for pool ownership)
+ cp /etc/hostid /mnt/etc/hostid
+
+ # Create ZFSBootMenu directory on EFI
+ mkdir -p /mnt/efi/EFI/ZBM
+
+ # 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."
+
+ # 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"
+
+ # 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
+
+ # 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)"
+
+ # 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
+
+ # 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
+
+ # 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 "ZFSBootMenu configuration complete."
+}
+
+configure_zfs_services() {
+ step "Configuring ZFS Services"
+
+ arch-chroot /mnt systemctl enable zfs.target
+
+ # Use zfs-import-scan instead of zfs-import-cache
+ # This is the recommended method - it uses blkid to scan for pools
+ # and doesn't require a cachefile
+ # Note: ZFS package preset enables zfs-import-cache by default, so we must
+ # explicitly disable it before enabling zfs-import-scan
+ arch-chroot /mnt systemctl disable zfs-import-cache.service
+ arch-chroot /mnt systemctl enable zfs-import-scan.service
+ arch-chroot /mnt systemctl enable zfs-mount.service
+ arch-chroot /mnt systemctl enable zfs-import.target
+
+ # 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
+ # that prevents it from running if /etc/zfs/zpool.cache exists
+ zpool set cachefile=none "$POOL_NAME"
+ rm -f /mnt/etc/zfs/zpool.cache
+
+ # Enable other services
+ arch-chroot /mnt systemctl enable NetworkManager
+ arch-chroot /mnt systemctl enable avahi-daemon
+ arch-chroot /mnt systemctl enable sshd
+
+ info "ZFS services configured."
+}
+
+configure_pacman_hook() {
+ step "Configuring Pacman Snapshot Hook"
+
+ mkdir -p /mnt/etc/pacman.d/hooks
+
+ cat > /mnt/etc/pacman.d/hooks/zfs-snapshot.hook << EOF
+[Trigger]
+Operation = Upgrade
+Operation = Install
+Operation = Remove
+Type = Package
+Target = *
+
+[Action]
+Description = Creating ZFS snapshot before pacman transaction...
+When = PreTransaction
+Exec = /usr/local/bin/zfs-pre-snapshot
+EOF
+
+ cat > /mnt/usr/local/bin/zfs-pre-snapshot << 'EOF'
+#!/bin/bash
+POOL="zroot"
+DATASET="$POOL/ROOT/default"
+TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
+SNAPSHOT_NAME="pre-pacman_$TIMESTAMP"
+
+if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then
+ echo "Created snapshot: $DATASET@$SNAPSHOT_NAME"
+else
+ echo "Warning: Failed to create snapshot" >&2
+fi
+
+EOF
+
+ chmod +x /mnt/usr/local/bin/zfs-pre-snapshot
+
+ info "Pacman hook configured."
+}
+
+configure_zfs_tools() {
+ step "Installing ZFS Management Tools"
+
+ # Copy ZFS management scripts
+ 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/zfssnapshot
+ chmod +x /mnt/usr/local/bin/zfsrollback
+
+ info "ZFS management scripts installed: zfssnapshot, zfsrollback"
+ info "Tip: Install sanoid for automated snapshot retention."
+}
+
+sync_efi_partitions() {
+ # Skip if only one disk
+ if [[ ${#EFI_PARTS[@]} -le 1 ]]; then
+ return
+ fi
+
+ step "Syncing EFI Partitions for Redundancy"
+
+ local temp_mount="/mnt/efi_sync"
+
+ for i in "${!EFI_PARTS[@]}"; do
+ if [[ $i -eq 0 ]]; then
+ continue # Skip primary
+ fi
+
+ local efi_part="${EFI_PARTS[$i]}"
+ info "Syncing ZFSBootMenu to EFI partition $((i+1)): $efi_part"
+
+ mkdir -p "$temp_mount"
+ mount "$efi_part" "$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
+
+ rmdir "$temp_mount" 2>/dev/null || true
+ info "All EFI partitions synchronized."
+}
+
+create_genesis_snapshot() {
+ step "Creating Genesis Snapshot"
+
+ # Create recursive snapshot of entire pool
+ info "Creating snapshot ${POOL_NAME}@genesis..."
+ zfs snapshot -r "${POOL_NAME}@genesis"
+
+ # Create rollback script in /root
+ info "Installing rollback-to-genesis script..."
+ cat > /mnt/root/rollback-to-genesis << 'ROLLBACK_EOF'
+#!/bin/bash
+# rollback-to-genesis - Roll back all datasets to the genesis snapshot
+#
+# This script rolls back the entire ZFS pool to its pristine post-install state.
+# WARNING: This will destroy all changes made since installation!
+
+set -e
+
+POOL_NAME="zroot"
+
+echo "╔═══════════════════════════════════════════════════════════════╗"
+echo "║ WARNING: Full System Rollback ║"
+echo "╚═══════════════════════════════════════════════════════════════╝"
+echo ""
+echo "This will roll back ALL datasets to the genesis snapshot!"
+echo "All changes since installation will be permanently lost."
+echo ""
+
+# Show what will be rolled back
+echo "Datasets to roll back:"
+zfs list -r -t snapshot -o name "${POOL_NAME}" 2>/dev/null | grep "@genesis" | while read snap; do
+ dataset="${snap%@genesis}"
+ echo " - $dataset"
+done
+echo ""
+
+read -p "Type 'ROLLBACK' to confirm: " confirm
+if [[ "$confirm" != "ROLLBACK" ]]; then
+ echo "Aborted."
+ exit 1
+fi
+
+echo ""
+echo "Rolling back to genesis..."
+
+# Roll back each dataset (must do in reverse order for dependencies)
+zfs list -r -H -o name "${POOL_NAME}" | tac | while read dataset; do
+ if zfs list -t snapshot "${dataset}@genesis" &>/dev/null; then
+ echo " Rolling back: $dataset"
+ zfs rollback -r "${dataset}@genesis"
+ fi
+done
+
+echo ""
+echo "Rollback complete!"
+echo "Reboot to complete the process: reboot"
+ROLLBACK_EOF
+
+ chmod +x /mnt/root/rollback-to-genesis
+ info "Genesis snapshot created. Rollback script: /root/rollback-to-genesis"
+}
+
+cleanup() {
+ step "Cleaning Up"
+
+ # Clear sensitive variables
+ ROOT_PASSWORD=""
+ ZFS_PASSPHRASE=""
+
+ info "Unmounting filesystems..."
+ umount /mnt/efi 2>/dev/null || true
+
+ info "Exporting ZFS pool..."
+ zpool export "$POOL_NAME"
+
+ info "Cleanup complete."
+}
+
+print_summary() {
+ echo ""
+ echo "╔═══════════════════════════════════════════════════════════════╗"
+ echo "║ Installation Complete! ║"
+ echo "╚═══════════════════════════════════════════════════════════════╝"
+ echo ""
+ echo "System Configuration:"
+ echo " Hostname: $HOSTNAME"
+ echo " Timezone: $TIMEZONE"
+ if [[ "$NO_ENCRYPT" == "yes" ]]; then
+ echo " ZFS Pool: $POOL_NAME (not encrypted)"
+ else
+ echo " ZFS Pool: $POOL_NAME (encrypted)"
+ fi
+ echo ""
+ 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 " - Install sanoid/syncoid for automated retention"
+ 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"
+ echo " Rollback: zfs rollback zroot/home@my-backup"
+ echo " Factory reset: /root/rollback-to-genesis"
+ echo " Pool status: zpool status"
+ echo ""
+ info "Installation log: $LOGFILE"
+ echo ""
+}
+
+print_btrfs_summary() {
+ echo ""
+ echo "╔═══════════════════════════════════════════════════════════════╗"
+ echo "║ Installation Complete! ║"
+ echo "╚═══════════════════════════════════════════════════════════════╝"
+ echo ""
+ echo "System Configuration:"
+ echo " Hostname: $HOSTNAME"
+ echo " Timezone: $TIMEZONE"
+ echo " Filesystem: Btrfs"
+ if [[ "$NO_ENCRYPT" == "yes" ]]; then
+ echo " Encryption: None"
+ else
+ echo " Encryption: LUKS2"
+ fi
+ echo ""
+ echo "Btrfs Snapshot Features:"
+ echo " - Boot from any snapshot via GRUB menu"
+ echo " - Genesis snapshot: pristine post-install state"
+ echo " - Pre/post pacman snapshots via snap-pac"
+ echo " - Timeline snapshots: 6 hourly, 7 daily, 2 weekly, 1 monthly"
+ echo ""
+ echo "GRUB Boot Menu:"
+ echo " - Select 'Arch Linux snapshots' submenu to boot from snapshots"
+ echo " - Snapshots auto-added when created by snapper"
+ echo ""
+ echo "Useful Commands:"
+ echo " List snapshots: snapper -c root list"
+ echo " Manual snapshot: snapper -c root create -d 'description'"
+ echo " Rollback: snapper -c root rollback <number>"
+ echo " Compare: snapper -c root diff <num1>..<num2>"
+ echo ""
+ info "Installation log: $LOGFILE"
+ echo ""
+}
+
+#############################
+# Main
+#############################
+
+main() {
+ parse_args "$@"
+ preflight_checks
+ check_config
+ gather_input
+ filesystem_preflight
+
+ # Unattended installation begins
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "Beginning unattended installation ($FILESYSTEM)..."
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ if [[ "$FILESYSTEM" == "zfs" ]]; then
+ install_zfs
+ elif [[ "$FILESYSTEM" == "btrfs" ]]; then
+ install_btrfs
+ fi
+}
+
+#############################
+# ZFS Installation Path
+#############################
+
+install_zfs() {
+ partition_disks
+ create_zfs_pool
+ create_datasets
+ mount_efi
+ install_base
+ configure_system
+ configure_wifi
+ configure_ssh
+ configure_initramfs
+ configure_zfsbootmenu
+ configure_zfs_services
+ configure_pacman_hook
+ configure_zfs_tools
+ sync_efi_partitions
+ create_genesis_snapshot
+ cleanup
+ print_summary
+}
+
+#############################
+# Btrfs Installation Path
+#############################
+
+install_btrfs() {
+ local num_disks=${#SELECTED_DISKS[@]}
+ local btrfs_devices=()
+ local efi_parts=()
+ local root_parts=()
+
+ # Collect partition references for all disks
+ for disk in "${SELECTED_DISKS[@]}"; do
+ root_parts+=("$(get_root_partition "$disk")")
+ efi_parts+=("$(get_efi_partition "$disk")")
+ done
+
+ # Partition all disks
+ for disk in "${SELECTED_DISKS[@]}"; do
+ partition_disk "$disk"
+ done
+
+ # Format all EFI partitions
+ format_efi_partitions "${SELECTED_DISKS[@]}"
+
+ # LUKS encryption (if enabled)
+ if [[ "$NO_ENCRYPT" != "yes" ]]; then
+ if [[ $num_disks -eq 1 ]]; then
+ # Single disk LUKS
+ create_luks_container "${root_parts[0]}" "$LUKS_PASSPHRASE"
+ open_luks_container "${root_parts[0]}" "$LUKS_PASSPHRASE"
+ btrfs_devices=("/dev/mapper/$LUKS_MAPPER_NAME")
+ else
+ # Multi-disk LUKS - encrypt each partition
+ create_luks_containers "$LUKS_PASSPHRASE" "${root_parts[@]}"
+ open_luks_containers "$LUKS_PASSPHRASE" "${root_parts[@]}"
+ btrfs_devices=($(get_luks_devices $num_disks))
+ fi
+ else
+ # No encryption - use raw partitions
+ btrfs_devices=("${root_parts[@]}")
+ fi
+
+ # Create btrfs filesystem
+ if [[ $num_disks -eq 1 ]]; then
+ create_btrfs_volume "${btrfs_devices[0]}"
+ else
+ create_btrfs_volume "${btrfs_devices[@]}" --raid-level "$RAID_LEVEL"
+ fi
+
+ # Create and mount subvolumes (use first device for mount)
+ create_btrfs_subvolumes "${btrfs_devices[0]}"
+ mount_btrfs_subvolumes "${btrfs_devices[0]}"
+
+ # Mount primary EFI
+ mkdir -p /mnt/efi
+ mount "${efi_parts[0]}" /mnt/efi
+
+ # Install base system
+ install_base_btrfs
+
+ # Configure system
+ configure_system
+ configure_wifi
+ configure_ssh
+
+ # Configure encryption if enabled
+ if [[ "$NO_ENCRYPT" != "yes" ]]; then
+ setup_luks_testing_keyfile "$LUKS_PASSPHRASE" "${root_parts[@]}"
+ configure_crypttab "${root_parts[@]}"
+ configure_luks_grub "${root_parts[0]}"
+ configure_luks_initramfs
+ fi
+
+ generate_btrfs_fstab "${btrfs_devices[0]}" "${efi_parts[0]}"
+ configure_btrfs_initramfs
+
+ # GRUB installation
+ if [[ $num_disks -eq 1 ]]; then
+ configure_grub "${efi_parts[0]}"
+ else
+ # Multi-disk: install GRUB to all EFI partitions
+ configure_grub "${efi_parts[0]}"
+ install_grub_all_efi "${efi_parts[@]}"
+ create_grub_sync_hook "${efi_parts[@]}"
+ fi
+
+ configure_snapper
+ configure_btrfs_services
+ configure_btrfs_pacman_hook
+
+ # Genesis snapshot
+ create_btrfs_genesis_snapshot
+
+ # Cleanup
+ btrfs_cleanup
+ if [[ "$NO_ENCRYPT" != "yes" ]]; then
+ if [[ $num_disks -eq 1 ]]; then
+ close_luks_container
+ else
+ close_luks_containers $num_disks
+ fi
+ fi
+ print_btrfs_summary
+}
+
+trap 'error "Installation interrupted!"' INT TERM
+
+main "$@"
diff --git a/installer/archangel.conf.example b/installer/archangel.conf.example
new file mode 100644
index 0000000..c3c1877
--- /dev/null
+++ b/installer/archangel.conf.example
@@ -0,0 +1,96 @@
+# archangel.conf - Unattended Installation Configuration
+#
+# Copy this file and edit values.
+# Usage: archangel --config-file /path/to/your-config.conf
+#
+# Required fields: HOSTNAME, TIMEZONE, DISKS, ROOT_PASSWORD
+# For ZFS: also need ZFS_PASSPHRASE or NO_ENCRYPT=yes
+# For Btrfs: also need LUKS_PASSPHRASE or NO_ENCRYPT=yes
+# All other fields have sensible defaults.
+
+#############################
+# Filesystem Selection
+#############################
+
+# Filesystem type (optional, default: zfs)
+# Options: zfs, btrfs
+FILESYSTEM=zfs
+
+#############################
+# System Configuration
+#############################
+
+# Hostname for the installed system (required)
+HOSTNAME=archangel
+
+# Timezone (required) - Use format: Region/City
+# Examples: America/Los_Angeles, Europe/London, Asia/Tokyo
+TIMEZONE=America/Los_Angeles
+
+# Locale (optional, default: en_US.UTF-8)
+LOCALE=en_US.UTF-8
+
+# Console keymap (optional, default: us)
+KEYMAP=us
+
+#############################
+# Disk Configuration
+#############################
+
+# Disks to use for installation (required)
+# Single disk: DISKS=/dev/vda
+# Multiple disks: DISKS=/dev/vda,/dev/vdb,/dev/vdc
+DISKS=/dev/vda
+
+# RAID level for multi-disk setups (optional)
+# Options: mirror, stripe, raidz1, raidz2, raidz3
+# Default: mirror (when multiple disks specified)
+# Leave empty for single disk
+RAID_LEVEL=
+
+#############################
+# Security
+#############################
+
+# ZFS encryption passphrase (required for ZFS unless NO_ENCRYPT=yes)
+# This will be required at every boot to unlock the pool
+ZFS_PASSPHRASE=changeme
+
+# LUKS encryption passphrase (required for Btrfs unless NO_ENCRYPT=yes)
+# This will be required at every boot to unlock the disk
+#LUKS_PASSPHRASE=changeme
+
+# Skip encryption (optional, default: no)
+# Set to "yes" to skip ZFS native encryption or LUKS
+# WARNING: Without encryption, anyone with physical access can read your data
+#NO_ENCRYPT=no
+
+# Root password (required)
+ROOT_PASSWORD=changeme
+
+#############################
+# Network Configuration
+#############################
+
+# Enable SSH with root login (optional, default: yes)
+# Set to "no" to disable SSH
+ENABLE_SSH=yes
+
+# WiFi configuration (optional)
+# Leave empty for ethernet-only or to skip WiFi setup
+WIFI_SSID=
+WIFI_PASSWORD=
+
+#############################
+# Advanced ZFS Options
+#############################
+
+# Pool name (optional, default: zroot)
+#POOL_NAME=zroot
+
+# Compression algorithm (optional, default: zstd)
+#COMPRESSION=zstd
+
+# Sector size shift (optional, default: 12 for 4K sectors)
+# Use 13 for 8K sector drives
+#ASHIFT=12
diff --git a/installer/install-claude b/installer/install-claude
new file mode 100755
index 0000000..f312861
--- /dev/null
+++ b/installer/install-claude
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# install-claude - Install Claude Code CLI
+# Run this if you need AI assistance during installation
+
+set -e
+
+echo "Installing Claude Code..."
+
+# Check if npm is available
+if ! command -v npm &>/dev/null; then
+ echo "npm not found. Installing nodejs and npm..."
+ pacman -Sy --noconfirm nodejs npm
+fi
+
+# Install Claude Code globally
+npm install -g @anthropic-ai/claude-code
+
+echo ""
+echo "Claude Code installed successfully!"
+echo ""
+echo "To start Claude Code, run:"
+echo " claude"
+echo ""
+echo "You'll need to authenticate on first run."
diff --git a/installer/lib/btrfs.sh b/installer/lib/btrfs.sh
new file mode 100644
index 0000000..321c05c
--- /dev/null
+++ b/installer/lib/btrfs.sh
@@ -0,0 +1,900 @@
+#!/usr/bin/env bash
+# btrfs.sh - Btrfs-specific functions for archangel installer
+# Source this file after common.sh, config.sh, disk.sh
+
+#############################
+# Btrfs/LUKS Constants
+#############################
+
+# LUKS settings
+LUKS_MAPPER_NAME="cryptroot"
+
+# Mount options for btrfs subvolumes
+BTRFS_OPTS="noatime,compress=zstd,space_cache=v2,discard=async"
+
+# Subvolume layout (matches ZFS dataset structure)
+# Format: "name:mountpoint:extra_opts"
+BTRFS_SUBVOLS=(
+ "@:/::"
+ "@home:/home::"
+ "@snapshots:/.snapshots::"
+ "@var_log:/var/log::"
+ "@var_cache:/var/cache::"
+ "@tmp:/tmp::nosuid,nodev"
+ "@var_tmp:/var/tmp::nosuid,nodev"
+ "@media:/media::compress=no"
+ "@vms:/vms::nodatacow,compress=no"
+ "@var_lib_docker:/var/lib/docker::"
+)
+
+#############################
+# LUKS Functions
+#############################
+
+create_luks_container() {
+ local partition="$1"
+ local passphrase="$2"
+
+ step "Creating LUKS Encrypted Container"
+
+ info "Setting up LUKS encryption on $partition..."
+
+ # Create LUKS container (-q for batch mode, -d - to read key from stdin)
+ echo -n "$passphrase" | cryptsetup -q luksFormat --type luks2 \
+ --cipher aes-xts-plain64 --key-size 512 --hash sha512 \
+ --iter-time 2000 --pbkdf argon2id \
+ -d - "$partition" \
+ || error "Failed to create LUKS container"
+
+ info "LUKS container created."
+}
+
+open_luks_container() {
+ local partition="$1"
+ local passphrase="$2"
+ local name="${3:-$LUKS_MAPPER_NAME}"
+
+ info "Opening LUKS container..."
+
+ echo -n "$passphrase" | cryptsetup open "$partition" "$name" -d - \
+ || error "Failed to open LUKS container"
+
+ info "LUKS container opened as /dev/mapper/$name"
+}
+
+close_luks_container() {
+ local name="${1:-$LUKS_MAPPER_NAME}"
+
+ cryptsetup close "$name" 2>/dev/null || true
+}
+
+# Testing keyfile for automated LUKS testing
+# When TESTING=yes, we embed a keyfile in initramfs to allow unattended boot
+LUKS_KEYFILE="/etc/cryptroot.key"
+
+setup_luks_testing_keyfile() {
+ local passphrase="$1"
+ shift
+ local partitions=("$@")
+
+ [[ "${TESTING:-}" != "yes" ]] && return 0
+
+ step "Setting Up Testing Keyfile (TESTING MODE)"
+ warn "Adding keyfile to initramfs for automated testing."
+ warn "This reduces security - for testing only!"
+
+ # Generate random keyfile
+ dd if=/dev/urandom of="/mnt${LUKS_KEYFILE}" bs=512 count=4 status=none \
+ || error "Failed to generate keyfile"
+ chmod 000 "/mnt${LUKS_KEYFILE}"
+
+ # Add keyfile to each LUKS partition (slot 1, passphrase stays in slot 0)
+ for partition in "${partitions[@]}"; do
+ info "Adding keyfile to $partition..."
+ echo -n "$passphrase" | cryptsetup luksAddKey "$partition" "/mnt${LUKS_KEYFILE}" -d - \
+ || error "Failed to add keyfile to $partition"
+ done
+
+ info "Testing keyfile configured for ${#partitions[@]} partition(s)."
+}
+
+# Multi-disk LUKS functions
+create_luks_containers() {
+ local passphrase="$1"
+ shift
+ local partitions=("$@")
+
+ step "Creating LUKS Encrypted Containers"
+
+ local i=0
+ for partition in "${partitions[@]}"; do
+ info "Setting up LUKS encryption on $partition..."
+ echo -n "$passphrase" | cryptsetup -q luksFormat --type luks2 \
+ --cipher aes-xts-plain64 --key-size 512 --hash sha512 \
+ --iter-time 2000 --pbkdf argon2id \
+ -d - "$partition" \
+ || error "Failed to create LUKS container on $partition"
+ ((++i))
+ done
+
+ info "Created $i LUKS containers."
+}
+
+open_luks_containers() {
+ local passphrase="$1"
+ shift
+ local partitions=("$@")
+
+ step "Opening LUKS Containers"
+
+ local i=0
+ for partition in "${partitions[@]}"; do
+ local name="${LUKS_MAPPER_NAME}${i}"
+ [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME" # First one has no suffix
+ info "Opening LUKS container: $partition -> /dev/mapper/$name"
+ echo -n "$passphrase" | cryptsetup open "$partition" "$name" -d - \
+ || error "Failed to open LUKS container: $partition"
+ ((++i))
+ done
+
+ info "Opened ${#partitions[@]} LUKS containers."
+}
+
+close_luks_containers() {
+ local count="${1:-1}"
+
+ for ((i=0; i<count; i++)); do
+ local name="${LUKS_MAPPER_NAME}${i}"
+ [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
+ cryptsetup close "$name" 2>/dev/null || true
+ done
+}
+
+# Get list of opened LUKS mapper devices
+get_luks_devices() {
+ local count="$1"
+ local devices=()
+
+ for ((i=0; i<count; i++)); do
+ local name="${LUKS_MAPPER_NAME}${i}"
+ [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
+ devices+=("/dev/mapper/$name")
+ done
+
+ echo "${devices[@]}"
+}
+
+configure_crypttab() {
+ local partitions=("$@")
+
+ step "Configuring crypttab"
+
+ echo "# LUKS encrypted root partitions" > /mnt/etc/crypttab
+
+ # Use keyfile if in testing mode, otherwise prompt for passphrase
+ local key_source="none"
+ if [[ "${TESTING:-}" == "yes" ]]; then
+ key_source="$LUKS_KEYFILE"
+ info "Testing mode: using keyfile for automatic unlock"
+ fi
+
+ local i=0
+ for partition in "${partitions[@]}"; do
+ local uuid
+ uuid=$(blkid -s UUID -o value "$partition")
+ local name="${LUKS_MAPPER_NAME}${i}"
+ [[ $i -eq 0 ]] && name="$LUKS_MAPPER_NAME"
+
+ echo "$name UUID=$uuid $key_source luks,discard" >> /mnt/etc/crypttab
+ info "crypttab: $name -> UUID=$uuid"
+ ((++i))
+ done
+
+ info "crypttab configured for $i partition(s)"
+}
+
+configure_luks_initramfs() {
+ step "Configuring Initramfs for LUKS"
+
+ # Backup original
+ cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak
+
+ # Add encrypt hook before filesystems (configure_btrfs_initramfs overwrites
+ # this with the final hook list, using sd-encrypt for multi-disk setups)
+ sed -i 's/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)/' \
+ /mnt/etc/mkinitcpio.conf
+
+ # Include keyfile in initramfs for testing mode (unattended boot)
+ if [[ "${TESTING:-}" == "yes" ]]; then
+ info "Testing mode: embedding keyfile in initramfs"
+ sed -i "s|^FILES=.*|FILES=($LUKS_KEYFILE)|" /mnt/etc/mkinitcpio.conf
+ # If FILES line doesn't exist, add it
+ if ! grep -q "^FILES=" /mnt/etc/mkinitcpio.conf; then
+ echo "FILES=($LUKS_KEYFILE)" >> /mnt/etc/mkinitcpio.conf
+ fi
+ fi
+
+ # Create crypttab.initramfs for sd-encrypt (used by multi-disk LUKS)
+ # sd-encrypt reads this file to open all LUKS devices during initramfs
+ if [[ -f /mnt/etc/crypttab ]]; then
+ cp /mnt/etc/crypttab /mnt/etc/crypttab.initramfs
+ info "Created crypttab.initramfs for sd-encrypt."
+ fi
+
+ info "Added encrypt hook to initramfs."
+}
+
+configure_luks_grub() {
+ local partition="$1"
+
+ step "Configuring GRUB for LUKS"
+
+ local uuid
+ uuid=$(blkid -s UUID -o value "$partition")
+
+ # Enable GRUB cryptodisk support (required for encrypted /boot)
+ echo "GRUB_ENABLE_CRYPTODISK=y" >> /mnt/etc/default/grub
+
+ # Add cryptdevice to GRUB cmdline
+ # For testing mode, also add cryptkey parameter for automated unlock
+ local cryptkey_param=""
+ if [[ "${TESTING:-}" == "yes" ]]; then
+ # rootfs: prefix tells encrypt hook the keyfile is in the initramfs
+ cryptkey_param="cryptkey=rootfs:$LUKS_KEYFILE "
+ info "Testing mode: adding cryptkey parameter for automated unlock"
+ fi
+
+ sed -i "s|^GRUB_CMDLINE_LINUX=\"|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=$uuid:$LUKS_MAPPER_NAME:allow-discards ${cryptkey_param}|" \
+ /mnt/etc/default/grub
+
+ info "GRUB configured with cryptdevice parameter and cryptodisk enabled."
+}
+
+#############################
+# Btrfs Pre-flight
+#############################
+
+btrfs_preflight() {
+ step "Checking Btrfs Requirements"
+
+ # Check for btrfs-progs
+ if ! command_exists mkfs.btrfs; then
+ error "btrfs-progs not installed. Cannot create btrfs filesystem."
+ fi
+ info "btrfs-progs available."
+
+ # Check for required tools
+ require_command btrfs
+ require_command grub-install
+
+ info "Btrfs preflight checks passed."
+}
+
+#############################
+# Btrfs Volume Creation
+#############################
+
+# Create btrfs filesystem (single or multi-device)
+# Usage: create_btrfs_volume device1 [device2 ...] [--raid-level level]
+create_btrfs_volume() {
+ local devices=()
+ local raid_level=""
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --raid-level)
+ raid_level="$2"
+ shift 2
+ ;;
+ *)
+ devices+=("$1")
+ shift
+ ;;
+ esac
+ done
+
+ step "Creating Btrfs Filesystem"
+
+ local num_devices=${#devices[@]}
+
+ if [[ $num_devices -eq 1 ]]; then
+ # Single device
+ info "Formatting ${devices[0]} as btrfs..."
+ mkfs.btrfs -f -L "archroot" "${devices[0]}" || error "Failed to create btrfs filesystem"
+ info "Btrfs filesystem created on ${devices[0]}"
+ else
+ # Multi-device RAID
+ local data_profile="raid1"
+ local meta_profile="raid1"
+
+ case "$raid_level" in
+ stripe)
+ data_profile="raid0"
+ meta_profile="raid1" # Always mirror metadata for safety
+ info "Creating striped btrfs (RAID0 data, RAID1 metadata) with $num_devices devices..."
+ ;;
+ mirror)
+ data_profile="raid1"
+ meta_profile="raid1"
+ info "Creating mirrored btrfs (RAID1) with $num_devices devices..."
+ ;;
+ *)
+ # Default to mirror for safety
+ data_profile="raid1"
+ meta_profile="raid1"
+ info "Creating mirrored btrfs (RAID1) with $num_devices devices..."
+ ;;
+ esac
+
+ mkfs.btrfs -f -L "archroot" \
+ -d "$data_profile" \
+ -m "$meta_profile" \
+ "${devices[@]}" || error "Failed to create btrfs filesystem"
+
+ info "Btrfs $raid_level filesystem created on ${devices[*]}"
+ fi
+}
+
+#############################
+# Subvolume Creation
+#############################
+
+create_btrfs_subvolumes() {
+ local partition="$1"
+
+ step "Creating Btrfs Subvolumes"
+
+ # Mount the raw btrfs volume temporarily
+ mount "$partition" /mnt || error "Failed to mount btrfs volume"
+
+ # Create each subvolume
+ for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
+ IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
+ info "Creating subvolume: $name -> $mountpoint"
+ btrfs subvolume create "/mnt/$name" || error "Failed to create subvolume $name"
+ done
+
+ # Unmount raw volume
+ umount /mnt
+
+ info "Created ${#BTRFS_SUBVOLS[@]} subvolumes."
+}
+
+#############################
+# Btrfs Mount Functions
+#############################
+
+mount_btrfs_subvolumes() {
+ local partition="$1"
+
+ step "Mounting Btrfs Subvolumes"
+
+ # Mount root subvolume first
+ info "Mounting @ -> /mnt"
+ mount -o "subvol=@,$BTRFS_OPTS" "$partition" /mnt || error "Failed to mount root subvolume"
+
+ # Create mount points and mount remaining subvolumes
+ for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
+ IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
+
+ # Skip root, already mounted
+ [[ "$name" == "@" ]] && continue
+
+ # Build mount options
+ local opts="subvol=$name,$BTRFS_OPTS"
+
+ # Apply extra options (override defaults where specified)
+ if [[ -n "$extra" ]]; then
+ # Handle compress=no by removing compress from opts and not adding it
+ if [[ "$extra" == *"compress=no"* ]]; then
+ opts=$(echo "$opts" | sed 's/,compress=zstd//')
+ fi
+ # Handle nodatacow
+ if [[ "$extra" == *"nodatacow"* ]]; then
+ opts="$opts,nodatacow"
+ opts=$(echo "$opts" | sed 's/,compress=zstd//')
+ fi
+ # Handle nosuid,nodev for tmp
+ if [[ "$extra" == *"nosuid"* ]]; then
+ opts="$opts,nosuid,nodev"
+ fi
+ fi
+
+ info "Mounting $name -> /mnt$mountpoint"
+ mkdir -p "/mnt$mountpoint"
+ mount -o "$opts" "$partition" "/mnt$mountpoint" || error "Failed to mount $name"
+ done
+
+ # Set permissions on tmp directories
+ chmod 1777 /mnt/tmp /mnt/var/tmp
+
+ info "All subvolumes mounted."
+}
+
+#############################
+# Fstab Generation
+#############################
+
+generate_btrfs_fstab() {
+ local partition="$1"
+ local efi_partition="$2"
+
+ step "Generating fstab"
+
+ local uuid
+ uuid=$(blkid -s UUID -o value "$partition")
+
+ # Start with header
+ cat > /mnt/etc/fstab << EOF
+# /etc/fstab - Btrfs subvolume mounts
+# IMPORTANT: Using subvol= NOT subvolid= for snapshot compatibility
+# Generated by archangel installer
+
+EOF
+
+ # Add each subvolume
+ for subvol_spec in "${BTRFS_SUBVOLS[@]}"; do
+ IFS=':' read -r name mountpoint extra <<< "$subvol_spec"
+
+ # Build mount options
+ local opts="subvol=$name,$BTRFS_OPTS"
+
+ # Apply extra options
+ if [[ -n "$extra" ]]; then
+ if [[ "$extra" == *"compress=no"* ]]; then
+ opts=$(echo "$opts" | sed 's/,compress=zstd//')
+ fi
+ if [[ "$extra" == *"nodatacow"* ]]; then
+ opts="$opts,nodatacow"
+ opts=$(echo "$opts" | sed 's/,compress=zstd//')
+ fi
+ if [[ "$extra" == *"nosuid"* ]]; then
+ opts="$opts,nosuid,nodev"
+ fi
+ fi
+
+ echo "UUID=$uuid $mountpoint btrfs $opts 0 0" >> /mnt/etc/fstab
+ done
+
+ # Add EFI partition
+ local efi_uuid
+ efi_uuid=$(blkid -s UUID -o value "$efi_partition")
+ echo "" >> /mnt/etc/fstab
+ echo "# EFI System Partition" >> /mnt/etc/fstab
+ echo "UUID=$efi_uuid /efi vfat defaults,noatime 0 2" >> /mnt/etc/fstab
+
+ info "fstab generated with ${#BTRFS_SUBVOLS[@]} btrfs mounts + EFI"
+}
+
+#############################
+# Snapper Configuration
+#############################
+
+configure_snapper() {
+ step "Configuring Snapper"
+
+ # Snapper needs D-Bus which isn't available in chroot
+ # Create a firstboot service to properly initialize snapper
+
+ info "Creating snapper firstboot configuration..."
+
+ # Create the firstboot script using echo (more reliable than HEREDOC)
+ {
+ echo '#!/bin/bash'
+ echo '# Snapper firstboot configuration'
+ echo 'set -e'
+ echo ''
+ echo '# Check if snapper is already configured'
+ echo 'if snapper list-configs 2>/dev/null | grep -q "^root"; then'
+ echo ' exit 0'
+ echo 'fi'
+ echo ''
+ echo 'echo "Configuring snapper for btrfs root..."'
+ echo ''
+ echo '# Unmount the pre-created @snapshots'
+ echo 'umount /.snapshots 2>/dev/null || true'
+ echo 'rmdir /.snapshots 2>/dev/null || true'
+ echo ''
+ echo '# Let snapper create its config'
+ echo 'snapper -c root create-config /'
+ echo ''
+ echo '# Replace snapper .snapshots with our @snapshots'
+ echo 'btrfs subvolume delete /.snapshots'
+ echo 'mkdir /.snapshots'
+ echo 'ROOT_DEV=$(findmnt -n -o SOURCE / | sed "s/\[.*\]//")'
+ echo 'mount -o subvol=@snapshots "$ROOT_DEV" /.snapshots'
+ echo 'chmod 750 /.snapshots'
+ echo ''
+ echo '# Configure timeline'
+ echo 'snapper -c root set-config "TIMELINE_CREATE=yes"'
+ echo 'snapper -c root set-config "TIMELINE_CLEANUP=yes"'
+ echo 'snapper -c root set-config "TIMELINE_LIMIT_HOURLY=6"'
+ echo 'snapper -c root set-config "TIMELINE_LIMIT_DAILY=7"'
+ echo 'snapper -c root set-config "TIMELINE_LIMIT_WEEKLY=2"'
+ echo 'snapper -c root set-config "TIMELINE_LIMIT_MONTHLY=1"'
+ echo 'snapper -c root set-config "NUMBER_LIMIT=50"'
+ echo ''
+ echo '# Create genesis snapshot'
+ echo 'snapper -c root create -d "genesis"'
+ echo ''
+ echo '# Update GRUB (config on EFI partition)'
+ echo 'grub-mkconfig -o /efi/grub/grub.cfg'
+ echo ''
+ echo 'echo "Snapper configuration complete!"'
+ } > /mnt/usr/local/bin/snapper-firstboot
+ chmod +x /mnt/usr/local/bin/snapper-firstboot
+
+ # Create systemd service for firstboot
+ {
+ echo '[Unit]'
+ echo 'Description=Snapper First Boot Configuration'
+ echo 'After=local-fs.target dbus.service'
+ echo 'Wants=dbus.service'
+ echo 'ConditionPathExists=!/etc/snapper/.firstboot-done'
+ echo ''
+ echo '[Service]'
+ echo 'Type=oneshot'
+ echo 'ExecStart=/usr/local/bin/snapper-firstboot'
+ echo 'ExecStartPost=/usr/bin/touch /etc/snapper/.firstboot-done'
+ echo 'RemainAfterExit=yes'
+ echo ''
+ echo '[Install]'
+ echo 'WantedBy=multi-user.target'
+ } > /mnt/etc/systemd/system/snapper-firstboot.service
+
+ # Enable the firstboot service
+ arch-chroot /mnt systemctl enable snapper-firstboot.service
+
+ # Enable snapper timers
+ arch-chroot /mnt systemctl enable snapper-timeline.timer
+ arch-chroot /mnt systemctl enable snapper-cleanup.timer
+
+ info "Snapper firstboot service configured."
+ info "Snapper will be fully configured on first boot."
+}
+
+#############################
+# GRUB Configuration
+#############################
+
+configure_grub() {
+ local efi_partition="$1"
+
+ step "Configuring GRUB Bootloader"
+
+ # Mount EFI partition
+ mkdir -p /mnt/efi
+ mount "$efi_partition" /mnt/efi
+
+ # Configure GRUB defaults for btrfs
+ info "Setting GRUB configuration..."
+ cat > /mnt/etc/default/grub << 'EOF'
+# GRUB configuration for btrfs root with snapshots
+GRUB_DEFAULT=0
+GRUB_TIMEOUT=5
+GRUB_DISTRIBUTOR="Arch"
+GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
+GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200"
+
+# Serial console support (for headless/VM testing)
+GRUB_TERMINAL="console serial"
+GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
+
+# Disable os-prober (single-boot system)
+GRUB_DISABLE_OS_PROBER=true
+
+# Btrfs: tell GRUB where to find /boot within subvolume
+GRUB_BTRFS_OVERRIDE_BOOT_PARTITION_DETECTION=true
+EOF
+
+ # Add LUKS encryption settings if enabled
+ if [[ "$NO_ENCRYPT" != "yes" && -n "$LUKS_PASSPHRASE" ]]; then
+ echo "" >> /mnt/etc/default/grub
+ echo "# LUKS encryption support" >> /mnt/etc/default/grub
+ echo "GRUB_ENABLE_CRYPTODISK=y" >> /mnt/etc/default/grub
+
+ # For multi-disk LUKS, sd-encrypt reads crypttab.initramfs — no cmdline params needed
+ # For single-disk LUKS, the encrypt hook needs cryptdevice= on the cmdline
+ local num_luks_disks
+ num_luks_disks=$(echo "$DISKS" | tr ',' '\n' | wc -l)
+
+ if [[ $num_luks_disks -eq 1 ]]; then
+ local luks_part
+ luks_part=$(echo "$DISKS" | cut -d',' -f1)2
+ if [[ -b "$luks_part" ]]; then
+ local uuid
+ uuid=$(blkid -s UUID -o value "$luks_part")
+ local cryptkey_param=""
+ if [[ "${TESTING:-}" == "yes" ]]; then
+ cryptkey_param="cryptkey=rootfs:$LUKS_KEYFILE "
+ info "Testing mode: adding cryptkey parameter for automated unlock"
+ fi
+ sed -i "s|^GRUB_CMDLINE_LINUX=\"|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=$uuid:$LUKS_MAPPER_NAME:allow-discards ${cryptkey_param}|" \
+ /mnt/etc/default/grub
+ info "Added cryptdevice parameter for LUKS partition."
+ fi
+ else
+ info "Multi-disk LUKS: sd-encrypt reads crypttab.initramfs (no cryptdevice cmdline needed)"
+ fi
+ fi
+
+ # Create grub directory on EFI partition
+ # GRUB modules on FAT32 EFI partition avoid btrfs subvolume path issues
+ mkdir -p /mnt/efi/grub
+
+ # Install GRUB with boot-directory on EFI partition
+ info "Installing GRUB to EFI partition..."
+ arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/efi \
+ --bootloader-id=GRUB --boot-directory=/efi \
+ || error "GRUB installation failed"
+
+ # Create symlink BEFORE grub-mkconfig (grub-btrfs expects /boot/grub)
+ rm -rf /mnt/boot/grub 2>/dev/null || true
+ arch-chroot /mnt ln -sfn /efi/grub /boot/grub
+
+ # Generate GRUB config (uses /boot/grub symlink -> /efi/grub)
+ info "Generating GRUB configuration..."
+ arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg \
+ || error "Failed to generate GRUB config"
+
+ # Sync to ensure grub.cfg is written to FAT32 EFI partition
+ sync
+
+ # Enable grub-btrfsd for automatic snapshot menu updates
+ info "Enabling grub-btrfs daemon..."
+ arch-chroot /mnt systemctl enable grub-btrfsd
+
+ info "GRUB configured with btrfs snapshot support."
+}
+
+#############################
+# EFI Redundancy (Multi-disk)
+#############################
+
+# Install GRUB to all EFI partitions for redundancy
+install_grub_all_efi() {
+ local efi_partitions=("$@")
+
+ step "Installing GRUB to All EFI Partitions"
+
+ local i=1
+ for efi_part in "${efi_partitions[@]}"; do
+ # First EFI at /efi (already mounted), subsequent at /efi2, /efi3, etc.
+ local chroot_efi_dir="/efi"
+ local mount_point="/mnt/efi"
+ local bootloader_id="GRUB"
+
+ if [[ $i -gt 1 ]]; then
+ chroot_efi_dir="/efi${i}"
+ mount_point="/mnt/efi${i}"
+ bootloader_id="GRUB-disk${i}"
+
+ # Mount secondary EFI partitions
+ if ! mountpoint -q "$mount_point" 2>/dev/null; then
+ mkdir -p "$mount_point"
+ mount "$efi_part" "$mount_point" || { warn "Failed to mount $efi_part"; ((++i)); continue; }
+ # Also create the directory in chroot for grub-install
+ mkdir -p "/mnt${chroot_efi_dir}"
+ mount --bind "$mount_point" "/mnt${chroot_efi_dir}"
+ fi
+ fi
+
+ info "Installing GRUB to $efi_part ($bootloader_id)..."
+ arch-chroot /mnt grub-install --target=x86_64-efi \
+ --efi-directory="$chroot_efi_dir" \
+ --bootloader-id="$bootloader_id" \
+ --boot-directory=/efi \
+ || warn "GRUB install to $efi_part may have failed (continuing)"
+
+ ((++i))
+ done
+
+ info "GRUB installed to ${#efi_partitions[@]} EFI partition(s)."
+}
+
+# Create pacman hook to sync GRUB across all EFI partitions
+create_grub_sync_hook() {
+ local efi_partitions=("$@")
+
+ step "Creating GRUB Sync Hook"
+
+ # Only needed for multi-disk
+ if [[ ${#efi_partitions[@]} -lt 2 ]]; then
+ info "Single disk - no sync hook needed."
+ return
+ fi
+
+ # Create sync script
+ local script_content='#!/bin/bash
+# Sync GRUB to all EFI partitions after grub package update
+# Generated by archangel installer
+
+set -e
+
+EFI_PARTITIONS=('
+ for part in "${efi_partitions[@]}"; do
+ script_content+="\"$part\" "
+ done
+ script_content+=')
+
+PRIMARY_EFI="/efi"
+
+sync_grub() {
+ local i=0
+ for part in "${EFI_PARTITIONS[@]}"; do
+ if [[ $i -eq 0 ]]; then
+ # Primary - just reinstall GRUB
+ grub-install --target=x86_64-efi --efi-directory="$PRIMARY_EFI" \
+ --bootloader-id=GRUB --boot-directory=/efi 2>/dev/null || true
+ else
+ # Secondary - mount, install, unmount
+ local mount_point="/tmp/efi-sync-$i"
+ mkdir -p "$mount_point"
+ mount "$part" "$mount_point" 2>/dev/null || continue
+ grub-install --target=x86_64-efi --efi-directory="$mount_point" \
+ --bootloader-id="GRUB-disk$((i+1))" --boot-directory=/efi 2>/dev/null || true
+ umount "$mount_point" 2>/dev/null || true
+ rmdir "$mount_point" 2>/dev/null || true
+ fi
+ ((++i))
+ done
+}
+
+sync_grub
+'
+ echo "$script_content" > /mnt/usr/local/bin/grub-sync-efi
+ chmod +x /mnt/usr/local/bin/grub-sync-efi
+
+ # Create pacman hook
+ mkdir -p /mnt/etc/pacman.d/hooks
+ cat > /mnt/etc/pacman.d/hooks/99-grub-sync-efi.hook << 'HOOKEOF'
+[Trigger]
+Type = Package
+Operation = Upgrade
+Target = grub
+
+[Action]
+Description = Syncing GRUB to all EFI partitions...
+When = PostTransaction
+Exec = /usr/local/bin/grub-sync-efi
+HOOKEOF
+
+ info "GRUB sync hook created for ${#efi_partitions[@]} EFI partitions."
+}
+
+#############################
+# Pacman Snapshot Hook
+#############################
+
+configure_btrfs_pacman_hook() {
+ step "Configuring Pacman Snapshot Hook"
+
+ # snap-pac handles this automatically when installed
+ # Just verify it's set up
+ info "snap-pac will create pre/post snapshots for pacman transactions."
+ info "Snapshots visible in GRUB menu via grub-btrfs."
+}
+
+#############################
+# Genesis Snapshot
+#############################
+
+create_btrfs_genesis_snapshot() {
+ step "Creating Genesis Snapshot"
+
+ # Genesis snapshot will be created by snapper-firstboot service on first boot
+ # This ensures snapper is properly configured before creating snapshots
+
+ info "Genesis snapshot will be created on first boot."
+ info "The snapper-firstboot service handles this automatically."
+}
+
+#############################
+# Btrfs Services
+#############################
+
+configure_btrfs_services() {
+ step "Configuring System Services"
+
+ # Enable standard services
+ arch-chroot /mnt systemctl enable NetworkManager
+ arch-chroot /mnt systemctl enable avahi-daemon
+
+ # Snapper timers (already enabled in configure_snapper)
+
+ # grub-btrfsd (already enabled in configure_grub)
+
+ info "System services configured."
+}
+
+#############################
+# Btrfs Initramfs
+#############################
+
+configure_btrfs_initramfs() {
+ step "Configuring Initramfs for Btrfs"
+
+ # Backup original
+ cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak
+
+ # Remove archiso drop-in if present
+ if [[ -f /mnt/etc/mkinitcpio.conf.d/archiso.conf ]]; then
+ info "Removing archiso drop-in config..."
+ rm -f /mnt/etc/mkinitcpio.conf.d/archiso.conf
+ fi
+
+ # Create proper linux-lts preset
+ info "Creating linux-lts preset..."
+ cat > /mnt/etc/mkinitcpio.d/linux-lts.preset << 'EOF'
+# mkinitcpio preset file for linux-lts
+
+PRESETS=(default fallback)
+
+ALL_kver="/boot/vmlinuz-linux-lts"
+
+default_image="/boot/initramfs-linux-lts.img"
+
+fallback_image="/boot/initramfs-linux-lts-fallback.img"
+fallback_options="-S autodetect"
+EOF
+
+ # Configure hooks for btrfs
+ # Include encrypt hook if LUKS is enabled, btrfs hook if multi-device
+ local num_disks=${#SELECTED_DISKS[@]}
+ local luks_enabled="no"
+ [[ "$NO_ENCRYPT" != "yes" && -n "$LUKS_PASSPHRASE" ]] && luks_enabled="yes"
+
+ if [[ $num_disks -gt 1 && "$luks_enabled" == "yes" ]]; then
+ # Multi-disk LUKS: use sd-encrypt (reads crypttab.initramfs to open ALL devices)
+ # The traditional encrypt hook only supports a single cryptdevice
+ info "Multi-device LUKS: using sd-encrypt for multi-device LUKS unlock"
+ sed -i "s/^HOOKS=.*/HOOKS=(base systemd microcode modconf kms keyboard sd-vconsole block sd-encrypt btrfs filesystems fsck)/" \
+ /mnt/etc/mkinitcpio.conf
+ elif [[ $num_disks -gt 1 ]]; then
+ info "Multi-device btrfs: adding btrfs hook for device assembly"
+ sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block btrfs filesystems fsck)/" \
+ /mnt/etc/mkinitcpio.conf
+ elif [[ "$luks_enabled" == "yes" ]]; then
+ sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)/" \
+ /mnt/etc/mkinitcpio.conf
+ else
+ sed -i "s/^HOOKS=.*/HOOKS=(base udev microcode modconf kms keyboard keymap consolefont block filesystems fsck)/" \
+ /mnt/etc/mkinitcpio.conf
+ fi
+
+ # Regenerate initramfs
+ info "Regenerating initramfs..."
+ arch-chroot /mnt mkinitcpio -P
+
+ info "Initramfs configured for btrfs."
+}
+
+#############################
+# Btrfs Cleanup
+#############################
+
+btrfs_cleanup() {
+ step "Cleaning Up Btrfs"
+
+ # Unmount in reverse order
+ info "Unmounting subvolumes..."
+
+ # Sync all filesystems before unmounting (important for FAT32 EFI partition)
+ sync
+
+ # Unmount EFI first
+ umount /mnt/efi 2>/dev/null || true
+
+ # Unmount all btrfs subvolumes (reverse order)
+ for ((i=${#BTRFS_SUBVOLS[@]}-1; i>=0; i--)); do
+ IFS=':' read -r name mountpoint extra <<< "${BTRFS_SUBVOLS[$i]}"
+ [[ "$name" == "@" ]] && continue
+ umount "/mnt$mountpoint" 2>/dev/null || true
+ done
+
+ # Unmount root last
+ umount /mnt 2>/dev/null || true
+
+ info "Btrfs cleanup complete."
+}
diff --git a/installer/lib/common.sh b/installer/lib/common.sh
new file mode 100644
index 0000000..0f02e37
--- /dev/null
+++ b/installer/lib/common.sh
@@ -0,0 +1,173 @@
+#!/usr/bin/env bash
+# common.sh - Shared functions for archangel installer
+# Source this file: source "$(dirname "$0")/lib/common.sh"
+
+#############################
+# Output Functions
+#############################
+
+# Colors (optional, gracefully degrade if not supported)
+if [[ -t 1 ]]; then
+ RED='\033[0;31m'
+ GREEN='\033[0;32m'
+ YELLOW='\033[0;33m'
+ BLUE='\033[0;34m'
+ BOLD='\033[1m'
+ NC='\033[0m' # No Color
+else
+ RED=''
+ GREEN=''
+ YELLOW=''
+ BLUE=''
+ BOLD=''
+ NC=''
+fi
+
+info() { echo -e "${GREEN}[INFO]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
+step() { echo ""; echo -e "${BOLD}==> $1${NC}"; }
+prompt() { echo -e "${BLUE}$1${NC}"; }
+
+# Log to file if LOG_FILE is set
+log() {
+ local msg
+ msg="[$(date +'%Y-%m-%d %H:%M:%S')] $1"
+ if [[ -n "$LOG_FILE" ]]; then
+ echo "$msg" >> "$LOG_FILE"
+ fi
+}
+
+#############################
+# Validation Functions
+#############################
+
+require_root() {
+ if [[ $EUID -ne 0 ]]; then
+ error "This script must be run as root"
+ fi
+}
+
+command_exists() {
+ command -v "$1" &>/dev/null
+}
+
+require_command() {
+ command_exists "$1" || error "Required command not found: $1"
+}
+
+#############################
+# FZF Prompts
+#############################
+
+# Check if fzf is available
+has_fzf() {
+ command_exists fzf
+}
+
+# Generic fzf selection
+# Usage: result=$(fzf_select "prompt" "option1" "option2" ...)
+fzf_select() {
+ local prompt="$1"
+ shift
+ local options=("$@")
+
+ if has_fzf; then
+ printf '%s\n' "${options[@]}" | fzf --prompt="$prompt " --height=15 --reverse
+ else
+ # Fallback to simple select
+ PS3="$prompt "
+ select opt in "${options[@]}"; do
+ if [[ -n "$opt" ]]; then
+ echo "$opt"
+ break
+ fi
+ done
+ fi
+}
+
+# Multi-select with fzf
+# Usage: readarray -t results < <(fzf_multi "prompt" "opt1" "opt2" ...)
+fzf_multi() {
+ local prompt="$1"
+ shift
+ local options=("$@")
+
+ if has_fzf; then
+ printf '%s\n' "${options[@]}" | fzf --prompt="$prompt " --height=20 --reverse --multi
+ else
+ # Fallback: just return all options (user must edit)
+ printf '%s\n' "${options[@]}"
+ fi
+}
+
+#############################
+# Filesystem Selection
+#############################
+
+# Select filesystem type (ZFS or Btrfs)
+# Sets global FILESYSTEM variable
+select_filesystem() {
+ step "Select Filesystem"
+
+ local options=(
+ "ZFS - Built-in encryption, best data integrity (recommended)"
+ "Btrfs - Copy-on-write, LUKS encryption, GRUB snapshot boot"
+ )
+
+ local selected
+ selected=$(fzf_select "Filesystem:" "${options[@]}")
+
+ case "$selected" in
+ ZFS*)
+ FILESYSTEM="zfs"
+ info "Selected: ZFS"
+ ;;
+ Btrfs*)
+ FILESYSTEM="btrfs"
+ info "Selected: Btrfs"
+ ;;
+ *)
+ error "No filesystem selected"
+ ;;
+ esac
+}
+
+#############################
+# Disk Utilities
+#############################
+
+# Get disk size in human-readable format
+get_disk_size() {
+ local disk="$1"
+ lsblk -dno SIZE "$disk" 2>/dev/null | tr -d ' '
+}
+
+# Get disk model
+get_disk_model() {
+ local disk="$1"
+ lsblk -dno MODEL "$disk" 2>/dev/null | tr -d ' ' | head -c 20
+}
+
+# Check if disk is in use (mounted or has holders)
+disk_in_use() {
+ local disk="$1"
+ [[ -n "$(lsblk -no MOUNTPOINT "$disk" 2>/dev/null | grep -v '^$')" ]] && return 0
+ [[ -n "$(ls /sys/block/"$(basename "$disk")"/holders/ 2>/dev/null)" ]] && return 0
+ return 1
+}
+
+# List available disks (not in use)
+list_available_disks() {
+ local disks=()
+ for disk in /dev/nvme[0-9]n[0-9] /dev/sd[a-z] /dev/vd[a-z]; do
+ [[ -b "$disk" ]] || continue
+ disk_in_use "$disk" && continue
+ local size
+ size=$(get_disk_size "$disk")
+ local model
+ model=$(get_disk_model "$disk")
+ disks+=("$disk ($size, $model)")
+ done
+ printf '%s\n' "${disks[@]}"
+}
diff --git a/installer/lib/config.sh b/installer/lib/config.sh
new file mode 100644
index 0000000..358a5f4
--- /dev/null
+++ b/installer/lib/config.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+# config.sh - Configuration and argument handling for archangel installer
+# Source this file after common.sh
+
+#############################
+# Global Config Variables
+#############################
+
+CONFIG_FILE=""
+UNATTENDED=false
+
+# These get populated by config file or interactive prompts
+FILESYSTEM="" # "zfs" or "btrfs"
+HOSTNAME=""
+TIMEZONE=""
+LOCALE=""
+KEYMAP=""
+SELECTED_DISKS=()
+RAID_LEVEL=""
+WIFI_SSID=""
+WIFI_PASSWORD=""
+ENCRYPTION_ENABLED=false
+ZFS_PASSPHRASE=""
+LUKS_PASSPHRASE=""
+ROOT_PASSWORD=""
+SSH_ENABLED=false
+SSH_KEY=""
+
+#############################
+# Argument Parsing
+#############################
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --config-file)
+ if [[ -n "$2" && ! "$2" =~ ^- ]]; then
+ CONFIG_FILE="$2"
+ shift 2
+ else
+ error "--config-file requires a path argument"
+ fi
+ ;;
+ --help|-h)
+ show_usage
+ exit 0
+ ;;
+ *)
+ error "Unknown option: $1 (use --help for usage)"
+ ;;
+ esac
+ done
+}
+
+show_usage() {
+ cat <<EOF
+Usage: archangel [OPTIONS]
+
+Arch Linux installer with ZFS/Btrfs support and snapshot management.
+
+Options:
+ --config-file PATH Use config file for unattended installation
+ --help, -h Show this help message
+
+Without --config-file, runs in interactive mode.
+See /root/archangel.conf.example for a config template.
+EOF
+}
+
+#############################
+# Config File Loading
+#############################
+
+load_config() {
+ local config_path="$1"
+
+ if [[ ! -f "$config_path" ]]; then
+ error "Config file not found: $config_path"
+ fi
+
+ info "Loading config from: $config_path"
+
+ # Source the config file (it's just key=value pairs)
+ # shellcheck disable=SC1090
+ source "$config_path"
+
+ # Convert DISKS from comma-separated string to array
+ if [[ -n "$DISKS" ]]; then
+ IFS=',' read -ra SELECTED_DISKS <<< "$DISKS"
+ fi
+
+ UNATTENDED=true
+ info "Running in unattended mode"
+}
+
+check_config() {
+ # Only use config when explicitly specified with --config-file
+ # This prevents accidental disk destruction from an unnoticed config file
+ if [[ -n "$CONFIG_FILE" ]]; then
+ load_config "$CONFIG_FILE"
+ fi
+}
+
+#############################
+# Config Validation
+#############################
+
+validate_config() {
+ local errors=0
+
+ [[ -z "$HOSTNAME" ]] && { warn "HOSTNAME not set"; ((errors++)); }
+ [[ -z "$TIMEZONE" ]] && { warn "TIMEZONE not set"; ((errors++)); }
+ [[ ${#SELECTED_DISKS[@]} -eq 0 ]] && { warn "No disks selected"; ((errors++)); }
+ [[ -z "$ROOT_PASSWORD" ]] && { warn "ROOT_PASSWORD not set"; ((errors++)); }
+
+ # Validate disks exist
+ for disk in "${SELECTED_DISKS[@]}"; do
+ [[ -b "$disk" ]] || { warn "Disk not found: $disk"; ((errors++)); }
+ done
+
+ # Validate timezone
+ if [[ -n "$TIMEZONE" && ! -f "/usr/share/zoneinfo/$TIMEZONE" ]]; then
+ warn "Invalid timezone: $TIMEZONE"
+ ((errors++))
+ fi
+
+ if [[ $errors -gt 0 ]]; then
+ error "Config validation failed with $errors error(s)"
+ fi
+ info "Config validation passed"
+}
diff --git a/installer/lib/disk.sh b/installer/lib/disk.sh
new file mode 100644
index 0000000..2e7deb3
--- /dev/null
+++ b/installer/lib/disk.sh
@@ -0,0 +1,204 @@
+#!/usr/bin/env bash
+# disk.sh - Disk partitioning functions for archangel installer
+# Source this file after common.sh
+
+#############################
+# Partition Disks
+#############################
+
+# Partition a single disk for ZFS/Btrfs installation
+# Creates: EFI partition (512M) + root partition (rest)
+# Uses global FILESYSTEM variable to determine partition type
+partition_disk() {
+ local disk="$1"
+ local efi_size="${2:-512M}"
+
+ # Determine root partition type based on filesystem
+ local root_type="BF00" # ZFS (Solaris root)
+ if [[ "$FILESYSTEM" == "btrfs" ]]; then
+ root_type="8300" # Linux filesystem
+ fi
+
+ info "Partitioning $disk..."
+
+ # Wipe existing partition table
+ sgdisk --zap-all "$disk" || error "Failed to wipe $disk"
+
+ # Create EFI partition (512M, type EF00)
+ sgdisk -n 1:0:+${efi_size} -t 1:EF00 -c 1:"EFI" "$disk" || error "Failed to create EFI partition on $disk"
+
+ # Create root partition (rest of disk)
+ sgdisk -n 2:0:0 -t 2:$root_type -c 2:"ROOT" "$disk" || error "Failed to create root partition on $disk"
+
+ # Notify kernel of partition changes
+ partprobe "$disk" 2>/dev/null || true
+ sleep 1
+
+ info "Partitioned $disk: EFI=${efi_size}, ROOT=remainder"
+}
+
+# Partition multiple disks (for RAID configurations)
+partition_disks() {
+ local efi_size="${1:-512M}"
+ shift
+ local disks=("$@")
+
+ for disk in "${disks[@]}"; do
+ partition_disk "$disk" "$efi_size"
+ done
+}
+
+#############################
+# Partition Helpers
+#############################
+
+# Get EFI partition path for a disk
+get_efi_partition() {
+ local disk="$1"
+ if [[ "$disk" =~ nvme ]]; then
+ echo "${disk}p1"
+ else
+ echo "${disk}1"
+ fi
+}
+
+# Get root partition path for a disk
+get_root_partition() {
+ local disk="$1"
+ if [[ "$disk" =~ nvme ]]; then
+ echo "${disk}p2"
+ else
+ echo "${disk}2"
+ fi
+}
+
+# Get all root partitions from disk array
+get_root_partitions() {
+ local disks=("$@")
+ local parts=()
+ for disk in "${disks[@]}"; do
+ parts+=("$(get_root_partition "$disk")")
+ done
+ printf '%s\n' "${parts[@]}"
+}
+
+# Get all EFI partitions from disk array
+get_efi_partitions() {
+ local disks=("$@")
+ local parts=()
+ for disk in "${disks[@]}"; do
+ parts+=("$(get_efi_partition "$disk")")
+ done
+ printf '%s\n' "${parts[@]}"
+}
+
+#############################
+# EFI Partition Management
+#############################
+
+# Format EFI partition
+format_efi() {
+ local partition="$1"
+ local label="${2:-EFI}"
+
+ info "Formatting EFI partition: $partition"
+ mkfs.fat -F32 -n "$label" "$partition" || error "Failed to format EFI: $partition"
+}
+
+# Format all EFI partitions
+format_efi_partitions() {
+ local disks=("$@")
+ local first=true
+
+ for disk in "${disks[@]}"; do
+ local efi
+ efi=$(get_efi_partition "$disk")
+ if $first; then
+ format_efi "$efi" "EFI"
+ first=false
+ else
+ format_efi "$efi" "EFI2"
+ fi
+ done
+}
+
+# Mount EFI partition
+mount_efi() {
+ local partition="$1"
+ local mount_point="${2:-/mnt/efi}"
+
+ mkdir -p "$mount_point"
+ mount "$partition" "$mount_point" || error "Failed to mount EFI at $mount_point"
+ info "Mounted EFI: $partition -> $mount_point"
+}
+
+#############################
+# Disk Selection (Interactive)
+#############################
+
+# Interactive disk selection using fzf
+select_disks() {
+ local available
+ available=$(list_available_disks)
+
+ if [[ -z "$available" ]]; then
+ error "No available disks found"
+ fi
+
+ step "Select installation disk(s)"
+ prompt "Use Tab to select multiple disks for RAID, Enter to confirm"
+
+ local selected
+ if has_fzf; then
+ selected=$(echo "$available" | fzf --multi --prompt="Select disk(s): " --height=15 --reverse)
+ else
+ echo "$available"
+ read -rp "Enter disk path(s) separated by space: " selected
+ fi
+
+ if [[ -z "$selected" ]]; then
+ error "No disk selected"
+ fi
+
+ # Extract just the device paths (remove size/model info)
+ SELECTED_DISKS=()
+ while IFS= read -r line; do
+ local disk
+ disk=$(echo "$line" | cut -d' ' -f1)
+ SELECTED_DISKS+=("$disk")
+ done <<< "$selected"
+
+ info "Selected disks: ${SELECTED_DISKS[*]}"
+}
+
+#############################
+# RAID Level Selection
+#############################
+
+select_raid_level() {
+ local num_disks=${#SELECTED_DISKS[@]}
+
+ if [[ $num_disks -eq 1 ]]; then
+ RAID_LEVEL=""
+ info "Single disk - no RAID"
+ return
+ fi
+
+ step "Select RAID level"
+
+ local options=()
+ options+=("mirror - Mirror data across disks (recommended)")
+
+ if [[ $num_disks -ge 3 ]]; then
+ options+=("raidz1 - Single parity, lose 1 disk capacity")
+ fi
+ if [[ $num_disks -ge 4 ]]; then
+ options+=("raidz2 - Double parity, lose 2 disks capacity")
+ fi
+
+ local selected
+ selected=$(fzf_select "RAID level:" "${options[@]}")
+ RAID_LEVEL=$(echo "$selected" | cut -d' ' -f1)
+
+ info "Selected RAID level: $RAID_LEVEL"
+}
diff --git a/installer/lib/zfs.sh b/installer/lib/zfs.sh
new file mode 100644
index 0000000..feda91d
--- /dev/null
+++ b/installer/lib/zfs.sh
@@ -0,0 +1,359 @@
+#!/usr/bin/env bash
+# zfs.sh - ZFS-specific functions for archangel installer
+# Source this file after common.sh, config.sh, disk.sh
+
+#############################
+# ZFS Constants
+#############################
+
+POOL_NAME="${POOL_NAME:-zroot}"
+ASHIFT="${ASHIFT:-12}"
+COMPRESSION="${COMPRESSION:-zstd}"
+
+#############################
+# ZFS Pre-flight
+#############################
+
+zfs_preflight() {
+ # Check ZFS module
+ if ! lsmod | grep -q zfs; then
+ info "Loading ZFS module..."
+ modprobe zfs || error "Failed to load ZFS module. Is zfs-linux-lts installed?"
+ fi
+ info "ZFS module loaded successfully."
+}
+
+#############################
+# ZFS Pool Creation
+#############################
+
+create_zfs_pool() {
+ local encryption="${1:-true}"
+ local passphrase="$2"
+
+ step "Creating ZFS Pool"
+
+ # Destroy existing pool if present
+ if zpool list "$POOL_NAME" &>/dev/null; then
+ warn "Pool $POOL_NAME already exists. Destroying..."
+ zpool destroy -f "$POOL_NAME"
+ fi
+
+ # Get root partitions
+ local zfs_parts=()
+ for disk in "${SELECTED_DISKS[@]}"; do
+ zfs_parts+=("$(get_root_partition "$disk")")
+ done
+
+ # Build pool configuration based on RAID level
+ local pool_config
+ if [[ "$RAID_LEVEL" == "stripe" ]]; then
+ pool_config="${zfs_parts[*]}"
+ info "Creating striped pool with ${#zfs_parts[@]} disks (NO redundancy)..."
+ warn "Data loss will occur if ANY disk fails!"
+ elif [[ -n "$RAID_LEVEL" ]]; then
+ pool_config="$RAID_LEVEL ${zfs_parts[*]}"
+ info "Creating $RAID_LEVEL pool with ${#zfs_parts[@]} disks..."
+ else
+ pool_config="${zfs_parts[0]}"
+ info "Creating single-disk pool..."
+ fi
+
+ # Base pool options
+ local pool_opts=(
+ -f
+ -o ashift="$ASHIFT"
+ -o autotrim=on
+ -O acltype=posixacl
+ -O atime=off
+ -O canmount=off
+ -O compression="$COMPRESSION"
+ -O dnodesize=auto
+ -O normalization=formD
+ -O relatime=on
+ -O xattr=sa
+ -O mountpoint=none
+ -R /mnt
+ )
+
+ # Create pool (with or without encryption)
+ if [[ "$encryption" == "false" ]]; then
+ warn "Creating pool WITHOUT encryption"
+ zpool create "${pool_opts[@]}" "$POOL_NAME" $pool_config
+ else
+ info "Creating encrypted pool..."
+ echo "$passphrase" | zpool create "${pool_opts[@]}" \
+ -O encryption=aes-256-gcm \
+ -O keyformat=passphrase \
+ -O keylocation=prompt \
+ "$POOL_NAME" $pool_config
+ fi
+
+ info "ZFS pool created successfully."
+ zpool status "$POOL_NAME"
+}
+
+#############################
+# ZFS Dataset Creation
+#############################
+
+create_zfs_datasets() {
+ step "Creating ZFS Datasets"
+
+ # Root dataset container
+ zfs create -o mountpoint=none -o canmount=off "$POOL_NAME/ROOT"
+
+ # Calculate reservation (20% of pool, capped 5-20G)
+ local pool_size_bytes
+ pool_size_bytes=$(zpool get -Hp size "$POOL_NAME" | awk '{print $3}')
+ local pool_size_gb=$((pool_size_bytes / 1024 / 1024 / 1024))
+ local reserve_gb=$((pool_size_gb / 5))
+ [[ $reserve_gb -gt 20 ]] && reserve_gb=20
+ [[ $reserve_gb -lt 5 ]] && reserve_gb=5
+
+ # Main root filesystem
+ zfs create -o mountpoint=/ -o canmount=noauto -o reservation=${reserve_gb}G "$POOL_NAME/ROOT/default"
+ zfs mount "$POOL_NAME/ROOT/default"
+
+ # Home
+ zfs create -o mountpoint=/home "$POOL_NAME/home"
+ zfs create -o mountpoint=/root "$POOL_NAME/home/root"
+
+ # Media - compression off for already-compressed files
+ zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media"
+
+ # VMs - 64K recordsize for VM disk images
+ zfs create -o mountpoint=/vms -o recordsize=64K "$POOL_NAME/vms"
+
+ # Var datasets
+ zfs create -o mountpoint=/var -o canmount=off "$POOL_NAME/var"
+ zfs create -o mountpoint=/var/log "$POOL_NAME/var/log"
+ zfs create -o mountpoint=/var/cache "$POOL_NAME/var/cache"
+ zfs create -o mountpoint=/var/lib -o canmount=off "$POOL_NAME/var/lib"
+ zfs create -o mountpoint=/var/lib/pacman "$POOL_NAME/var/lib/pacman"
+ zfs create -o mountpoint=/var/lib/docker "$POOL_NAME/var/lib/docker"
+
+ # Temp directories - excluded from snapshots
+ zfs create -o mountpoint=/var/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/var/tmp"
+ zfs create -o mountpoint=/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/tmp"
+ chmod 1777 /mnt/tmp /mnt/var/tmp
+
+ info "Datasets created:"
+ zfs list -r "$POOL_NAME" -o name,mountpoint,compression
+}
+
+#############################
+# ZFSBootMenu Configuration
+#############################
+
+configure_zfsbootmenu() {
+ step "Configuring ZFSBootMenu"
+
+ # Ensure hostid exists
+ if [[ ! -f /etc/hostid ]]; then
+ zgenhostid
+ fi
+ local host_id
+ host_id=$(hostid)
+
+ # Copy hostid to installed system
+ cp /etc/hostid /mnt/etc/hostid
+
+ # Create ZFSBootMenu directory on EFI
+ mkdir -p /mnt/efi/EFI/ZBM
+
+ # Download ZFSBootMenu release EFI binary
+ 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."
+
+ # Set kernel command line on the ROOT PARENT dataset
+ local cmdline="rw loglevel=3"
+
+ # Add AMD GPU workarounds if needed
+ 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
+
+ zfs set org.zfsbootmenu:commandline="$cmdline" "$POOL_NAME/ROOT"
+ info "Kernel command line set on $POOL_NAME/ROOT"
+
+ # Set bootfs property
+ 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
+ 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
+
+ info "Creating EFI boot entry: $label on $disk"
+ efibootmgr --create \
+ --disk "$disk" \
+ --part 1 \
+ --label "$label" \
+ --loader '\EFI\ZBM\zfsbootmenu.efi' \
+ --unicode "$zbm_cmdline" \
+ --quiet
+ done
+
+ # Set as primary boot option
+ local bootnum
+ bootnum=$(efibootmgr | grep "ZFSBootMenu" | head -1 | grep -oP 'Boot\K[0-9A-F]+')
+ if [[ -n "$bootnum" ]]; then
+ 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 "ZFSBootMenu configuration complete."
+}
+
+#############################
+# ZFS Services
+#############################
+
+configure_zfs_services() {
+ step "Configuring ZFS Services"
+
+ arch-chroot /mnt systemctl enable zfs.target
+ arch-chroot /mnt systemctl disable zfs-import-cache.service
+ arch-chroot /mnt systemctl enable zfs-import-scan.service
+ arch-chroot /mnt systemctl enable zfs-mount.service
+ arch-chroot /mnt systemctl enable zfs-import.target
+
+ # Disable cachefile - we use zfs-import-scan
+ zpool set cachefile=none "$POOL_NAME"
+ rm -f /mnt/etc/zfs/zpool.cache
+
+ info "ZFS services configured."
+}
+
+#############################
+# Pacman Snapshot Hook
+#############################
+
+configure_zfs_pacman_hook() {
+ step "Configuring Pacman Snapshot Hook"
+
+ mkdir -p /mnt/etc/pacman.d/hooks
+
+ cat > /mnt/etc/pacman.d/hooks/zfs-snapshot.hook << EOF
+[Trigger]
+Operation = Upgrade
+Operation = Install
+Operation = Remove
+Type = Package
+Target = *
+
+[Action]
+Description = Creating ZFS snapshot before pacman transaction...
+When = PreTransaction
+Exec = /usr/local/bin/zfs-pre-snapshot
+EOF
+
+ cat > /mnt/usr/local/bin/zfs-pre-snapshot << 'EOF'
+#!/bin/bash
+POOL="zroot"
+DATASET="$POOL/ROOT/default"
+TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
+SNAPSHOT_NAME="pre-pacman_$TIMESTAMP"
+
+if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then
+ echo "Created snapshot: $DATASET@$SNAPSHOT_NAME"
+else
+ echo "Warning: Failed to create snapshot" >&2
+fi
+EOF
+
+ chmod +x /mnt/usr/local/bin/zfs-pre-snapshot
+ info "Pacman hook configured."
+}
+
+#############################
+# ZFS Tools
+#############################
+
+install_zfs_tools() {
+ step "Installing ZFS Management Tools"
+
+ # Copy ZFS management scripts
+ 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/zfssnapshot
+ chmod +x /mnt/usr/local/bin/zfsrollback
+
+ info "ZFS management scripts installed: zfssnapshot, zfsrollback"
+}
+
+#############################
+# EFI Sync (Multi-disk)
+#############################
+
+sync_zfs_efi_partitions() {
+ local efi_parts=()
+ for disk in "${SELECTED_DISKS[@]}"; do
+ efi_parts+=("$(get_efi_partition "$disk")")
+ done
+
+ # Skip if only one disk
+ [[ ${#efi_parts[@]} -le 1 ]] && return
+
+ step "Syncing EFI partitions for redundancy"
+
+ for ((i=1; i<${#efi_parts[@]}; i++)); do
+ local secondary="${efi_parts[$i]}"
+ local tmp_mount="/tmp/efi_sync_$$"
+
+ mkdir -p "$tmp_mount"
+ mount "$secondary" "$tmp_mount"
+ rsync -a /mnt/efi/ "$tmp_mount/"
+ umount "$tmp_mount"
+ rmdir "$tmp_mount"
+
+ info "Synced EFI to $secondary"
+ done
+}
+
+#############################
+# Genesis Snapshot
+#############################
+
+create_zfs_genesis_snapshot() {
+ step "Creating Genesis Snapshot"
+
+ local snapshot_name="genesis"
+ zfs snapshot -r "$POOL_NAME@$snapshot_name"
+
+ info "Genesis snapshot created: $POOL_NAME@$snapshot_name"
+ info "You can restore to this point anytime with: zfsrollback $snapshot_name"
+}
+
+#############################
+# ZFS Cleanup
+#############################
+
+zfs_cleanup() {
+ step "Cleaning up ZFS"
+
+ # Unmount all ZFS datasets
+ zfs unmount -a 2>/dev/null || true
+
+ # Unmount EFI
+ umount /mnt/efi 2>/dev/null || true
+
+ # Export pool (important for clean import on boot)
+ zpool export "$POOL_NAME"
+
+ info "ZFS pool exported cleanly."
+}
diff --git a/installer/zfsrollback b/installer/zfsrollback
new file mode 100755
index 0000000..a99a4d3
--- /dev/null
+++ b/installer/zfsrollback
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+# Craig Jennings (github.com/cjennings)
+# Roll back ZFS datasets to a selected snapshot using fzf.
+
+set -euo pipefail
+
+# Usage info
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [-h] [-s]
+Roll back ZFS datasets to a selected snapshot.
+
+ -h display this help and exit
+ -s single dataset mode (roll back only the selected dataset,
+ not all datasets with matching snapshot name)
+
+By default, rolling back a snapshot will roll back ALL datasets that share
+that snapshot name. Use -s for single dataset rollback.
+
+WARNING: Rolling back destroys all data and snapshots newer than the target.
+ This operation cannot be undone!
+
+Requires: fzf, zfs
+EOF
+}
+
+# Check dependencies
+for cmd in zfs fzf; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "Error: $cmd command not found"
+ exit 1
+ fi
+done
+
+# Check for root/sudo
+if [ "$EUID" -ne 0 ]; then
+ echo "Error: This script must be run as root (use sudo)"
+ exit 1
+fi
+
+# Parse arguments
+single_mode=false
+while getopts ":hs" opt; do
+ case ${opt} in
+ h)
+ show_help
+ exit 0
+ ;;
+ s)
+ single_mode=true
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+# Get all snapshots
+snapshots=$(zfs list -t snapshot -H -o name 2>/dev/null)
+
+if [ -z "$snapshots" ]; then
+ echo "No ZFS snapshots found"
+ exit 0
+fi
+
+if $single_mode; then
+ # Single mode: show full dataset@snapshot names (sorted newest first)
+ selected=$(zfs list -t snapshot -H -o name -S creation | fzf --height=70% --reverse \
+ --header="Select snapshot to roll back (ESC to cancel)" \
+ --preview="zfs list -t snapshot -o name,creation,used,refer -r {1}" \
+ --preview-window=down:5)
+
+ if [ -z "$selected" ]; then
+ echo "No snapshot selected, exiting"
+ exit 0
+ fi
+
+ dataset="${selected%@*}"
+ snap_name="${selected#*@}"
+ targets=("$selected")
+else
+ # Multi mode: show unique snapshot names, roll back all matching datasets
+ # Sort reverse so newest (latest date) appears at top
+ unique_snaps=$(echo "$snapshots" | sed 's/.*@//' | sort -ru)
+
+ snap_name=$(echo "$unique_snaps" | fzf --height=70% --reverse \
+ --header="Select snapshot name to roll back ALL matching datasets (ESC to cancel)" \
+ --preview="zfs list -t snapshot -o name,creation,used -H | grep '@{}$' | column -t" \
+ --preview-window=down:10)
+
+ if [ -z "$snap_name" ]; then
+ echo "No snapshot selected, exiting"
+ exit 0
+ fi
+
+ # Find all datasets with this snapshot, sorted by depth (deepest first)
+ # This ensures children are rolled back before parents
+ mapfile -t targets < <(echo "$snapshots" | grep "@${snap_name}$" | awk -F'@' '{print length($1), $0}' | sort -rn | cut -d' ' -f2-)
+
+ if [ ${#targets[@]} -eq 0 ]; then
+ echo "Error: No datasets found with snapshot @${snap_name}"
+ exit 1
+ fi
+fi
+
+# Display what will happen
+echo ""
+echo "═══════════════════════════════════════════════════════════════════"
+echo " ⚠️ WARNING ⚠️"
+echo "═══════════════════════════════════════════════════════════════════"
+echo ""
+
+# Special warning for genesis rollback
+if [[ "$snap_name" == "genesis" ]]; then
+ echo " 🚨 GENESIS ROLLBACK DETECTED 🚨"
+ echo ""
+ echo " Rolling back to genesis will destroy ALL changes since installation!"
+ echo " This includes all packages installed, configurations, and user data."
+ echo ""
+fi
+
+echo "You are about to roll back to snapshot: @${snap_name}"
+echo ""
+echo "The following datasets will be rolled back:"
+for target in "${targets[@]}"; do
+ dataset="${target%@*}"
+ echo " • $dataset"
+
+ # Show newer snapshots that will be destroyed
+ newer=$(zfs list -t snapshot -H -o name -S creation "$dataset" 2>/dev/null | \
+ awk -v snap="$target" 'found {print " ✗ " $0 " (will be DESTROYED)"} $0 == snap {found=1}')
+ if [ -n "$newer" ]; then
+ echo "$newer"
+ fi
+done
+
+echo ""
+echo "═══════════════════════════════════════════════════════════════════"
+echo " THIS OPERATION CANNOT BE UNDONE!"
+echo " All data written after the snapshot will be permanently lost."
+echo " All snapshots newer than the target will be destroyed."
+echo "═══════════════════════════════════════════════════════════════════"
+echo ""
+
+# Require explicit confirmation
+read -r -p "Type 'yes' to confirm rollback: " confirmation
+
+if [ "$confirmation" != "yes" ]; then
+ echo "Rollback cancelled"
+ exit 0
+fi
+
+echo ""
+echo "Rolling back..."
+
+# Perform rollbacks
+failed=0
+for target in "${targets[@]}"; do
+ dataset="${target%@*}"
+ echo -n " Rolling back $dataset... "
+ if zfs rollback -r "$target" 2>&1; then
+ echo "✓"
+ else
+ echo "✗ FAILED"
+ ((failed++))
+ fi
+done
+
+echo ""
+if [ $failed -eq 0 ]; then
+ echo "Rollback complete."
+ echo ""
+ echo "Note: ZFSBootMenu auto-detects snapshots - no menu regeneration needed."
+else
+ echo "Rollback completed with $failed failure(s)"
+ exit 1
+fi
diff --git a/installer/zfssnapshot b/installer/zfssnapshot
new file mode 100755
index 0000000..90331c3
--- /dev/null
+++ b/installer/zfssnapshot
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# Craig Jennings (github.com/cjennings)
+# Create a ZFS snapshot across all datasets with a dated, descriptive name.
+
+set -euo pipefail
+
+# Usage info
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [-h] [DESCRIPTION]
+Create a ZFS snapshot across all datasets.
+
+ -h display this help and exit
+ DESCRIPTION short description for the snapshot (optional, will prompt if omitted)
+
+Snapshot names are formatted as: YYYY-MM-DD_HH-MM-SS_description
+Only alphanumeric characters, hyphens, and underscores are allowed in descriptions.
+Spaces are converted to underscores automatically.
+
+Examples:
+ ${0##*/} before-upgrade
+ ${0##*/} "pre system update"
+ ${0##*/} # prompts for description
+EOF
+}
+
+# Check for ZFS
+if ! command -v zfs &> /dev/null; then
+ echo "Error: zfs command not found. Is ZFS installed?"
+ exit 1
+fi
+
+# Check for root/sudo
+if [ "$EUID" -ne 0 ]; then
+ echo "Error: This script must be run as root (use sudo)"
+ exit 1
+fi
+
+# Parse arguments
+while getopts ":h" opt; do
+ case ${opt} in
+ h)
+ show_help
+ exit 0
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ show_help
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+# Get description from argument or prompt
+if [ $# -ge 1 ]; then
+ description="$*"
+else
+ read -r -p "Enter snapshot description: " description
+ if [ -z "$description" ]; then
+ echo "Error: Description cannot be empty"
+ exit 1
+ fi
+fi
+
+# Sanitize description: convert spaces to underscores, lowercase
+description=$(echo "$description" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
+
+# Validate description: only allow alphanumeric, hyphens, underscores
+if [[ ! "$description" =~ ^[a-z0-9_-]+$ ]]; then
+ echo "Error: Description contains invalid characters"
+ echo "Only letters, numbers, hyphens, and underscores are allowed"
+ echo "Sanitized input was: $description"
+ exit 1
+fi
+
+# Create snapshot name with timestamp prefix (matches pre-pacman format)
+timestamp=$(date +%Y-%m-%d_%H-%M-%S)
+snapshot_name="${timestamp}_${description}"
+
+# Get all pools
+pools=$(zpool list -H -o name)
+
+if [ -z "$pools" ]; then
+ echo "Error: No ZFS pools found"
+ exit 1
+fi
+
+echo "Creating snapshots with name: @${snapshot_name}"
+echo ""
+
+# Create recursive snapshots on each pool
+for pool in $pools; do
+ echo "Snapshotting pool: $pool"
+ if zfs snapshot -r "${pool}@${snapshot_name}"; then
+ echo " ✓ Created ${pool}@${snapshot_name} (recursive)"
+ else
+ echo " ✗ Failed to snapshot $pool"
+ fi
+done
+
+echo ""
+echo "Snapshot complete. Verify with: zfs list -t snapshot | grep $snapshot_name"
+echo ""
+echo "To boot from this snapshot: reboot and press Ctrl+D at ZFSBootMenu"