aboutsummaryrefslogtreecommitdiff
path: root/custom
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-23 11:54:25 -0600
committerCraig Jennings <c@cjennings.net>2026-02-23 11:54:25 -0600
commitfd2ea796b20bcbebea19c43978fb08e3cd6754ed (patch)
tree344efeff361b1c8953bd4f56d304a8ec70637899 /custom
parent8560e2a2798f9318fb28283d5ef7242fed20d447 (diff)
downloadarchangel-fd2ea796b20bcbebea19c43978fb08e3cd6754ed.tar.gz
archangel-fd2ea796b20bcbebea19c43978fb08e3cd6754ed.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 'custom')
-rw-r--r--custom/RESCUE-GUIDE.txt2618
-rwxr-xr-xcustom/archangel1688
-rw-r--r--custom/archangel.conf.example96
-rwxr-xr-xcustom/install-claude24
-rw-r--r--custom/lib/btrfs.sh900
-rw-r--r--custom/lib/common.sh173
-rw-r--r--custom/lib/config.sh131
-rw-r--r--custom/lib/disk.sh204
-rw-r--r--custom/lib/zfs.sh359
-rwxr-xr-xcustom/zfsrollback179
-rwxr-xr-xcustom/zfssnapshot105
11 files changed, 0 insertions, 6477 deletions
diff --git a/custom/RESCUE-GUIDE.txt b/custom/RESCUE-GUIDE.txt
deleted file mode 100644
index e241125..0000000
--- a/custom/RESCUE-GUIDE.txt
+++ /dev/null
@@ -1,2618 +0,0 @@
-================================================================================
- 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/custom/archangel b/custom/archangel
deleted file mode 100755
index 023115e..0000000
--- a/custom/archangel
+++ /dev/null
@@ -1,1688 +0,0 @@
-#!/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/custom/archangel.conf.example b/custom/archangel.conf.example
deleted file mode 100644
index c3c1877..0000000
--- a/custom/archangel.conf.example
+++ /dev/null
@@ -1,96 +0,0 @@
-# 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/custom/install-claude b/custom/install-claude
deleted file mode 100755
index f312861..0000000
--- a/custom/install-claude
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/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/custom/lib/btrfs.sh b/custom/lib/btrfs.sh
deleted file mode 100644
index 321c05c..0000000
--- a/custom/lib/btrfs.sh
+++ /dev/null
@@ -1,900 +0,0 @@
-#!/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/custom/lib/common.sh b/custom/lib/common.sh
deleted file mode 100644
index 0f02e37..0000000
--- a/custom/lib/common.sh
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/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/custom/lib/config.sh b/custom/lib/config.sh
deleted file mode 100644
index 358a5f4..0000000
--- a/custom/lib/config.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/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/custom/lib/disk.sh b/custom/lib/disk.sh
deleted file mode 100644
index 2e7deb3..0000000
--- a/custom/lib/disk.sh
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/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/custom/lib/zfs.sh b/custom/lib/zfs.sh
deleted file mode 100644
index feda91d..0000000
--- a/custom/lib/zfs.sh
+++ /dev/null
@@ -1,359 +0,0 @@
-#!/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/custom/zfsrollback b/custom/zfsrollback
deleted file mode 100755
index a99a4d3..0000000
--- a/custom/zfsrollback
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/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/custom/zfssnapshot b/custom/zfssnapshot
deleted file mode 100755
index 90331c3..0000000
--- a/custom/zfssnapshot
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/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"