diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
| commit | 6631521d9f45b73f9b4df76db9148d82af8d57b9 (patch) | |
| tree | 344efeff361b1c8953bd4f56d304a8ec70637899 /installer | |
| parent | c7608c8ba7757fa49ca02c068a4b0473e7f825dc (diff) | |
| download | archangel-6631521d9f45b73f9b4df76db9148d82af8d57b9.tar.gz archangel-6631521d9f45b73f9b4df76db9148d82af8d57b9.zip | |
refactor: rename custom/ to installer/ for clarity
The custom/ directory name was an archiso implementation detail. Renamed
to installer/ which clearly communicates that this directory contains the
installer scripts and utilities that ship on the ISO.
Updated all references in build.sh, Makefile, test-install.sh, and README.
Diffstat (limited to 'installer')
| -rw-r--r-- | installer/RESCUE-GUIDE.txt | 2618 | ||||
| -rwxr-xr-x | installer/archangel | 1688 | ||||
| -rw-r--r-- | installer/archangel.conf.example | 96 | ||||
| -rwxr-xr-x | installer/install-claude | 24 | ||||
| -rw-r--r-- | installer/lib/btrfs.sh | 900 | ||||
| -rw-r--r-- | installer/lib/common.sh | 173 | ||||
| -rw-r--r-- | installer/lib/config.sh | 131 | ||||
| -rw-r--r-- | installer/lib/disk.sh | 204 | ||||
| -rw-r--r-- | installer/lib/zfs.sh | 359 | ||||
| -rwxr-xr-x | installer/zfsrollback | 179 | ||||
| -rwxr-xr-x | installer/zfssnapshot | 105 |
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" |
