diff options
Diffstat (limited to 'custom')
| -rw-r--r-- | custom/RESCUE-GUIDE.txt | 2618 | ||||
| -rwxr-xr-x | custom/archangel | 1688 | ||||
| -rw-r--r-- | custom/archangel.conf.example | 96 | ||||
| -rwxr-xr-x | custom/install-claude | 24 | ||||
| -rw-r--r-- | custom/lib/btrfs.sh | 900 | ||||
| -rw-r--r-- | custom/lib/common.sh | 173 | ||||
| -rw-r--r-- | custom/lib/config.sh | 131 | ||||
| -rw-r--r-- | custom/lib/disk.sh | 204 | ||||
| -rw-r--r-- | custom/lib/zfs.sh | 359 | ||||
| -rwxr-xr-x | custom/zfsrollback | 179 | ||||
| -rwxr-xr-x | custom/zfssnapshot | 105 |
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" |
