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