diff options
Diffstat (limited to 'custom')
| -rwxr-xr-x | custom/install-archzfs | 999 |
1 files changed, 586 insertions, 413 deletions
diff --git a/custom/install-archzfs b/custom/install-archzfs index 2afc9b6..733660b 100755 --- a/custom/install-archzfs +++ b/custom/install-archzfs @@ -4,75 +4,107 @@ # # Installs Arch Linux on ZFS root with native encryption. # Designed to be run from the custom archzfs ISO. +# +# Features: +# - All questions asked upfront, then unattended installation +# - Optional WiFi configuration with connection test +# - ZFS native encryption (passphrase required at boot) +# - Pre-pacman ZFS snapshots for safe upgrades set -e +############################# +# Configuration +############################# + # These will be set interactively HOSTNAME="" -USERNAME="" 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) -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' +# 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) # Logging LOGFILE="/tmp/install-archzfs.log" exec > >(tee -a "$LOGFILE") 2>&1 -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 -e "\n${BLUE}==>${NC} ${CYAN}$1${NC}"; } -prompt() { echo -e "${BOLD}$1${NC}"; } +info() { echo "[INFO] $1"; } +warn() { echo "[WARN] $1"; } +error() { echo "[ERROR] $1"; exit 1; } +step() { echo ""; echo "==> $1"; } +prompt() { echo "$1"; } -# Check root -[[ $EUID -ne 0 ]] && error "This script must be run as root" +############################# +# Pre-flight Checks +############################# -# Check ZFS module -if ! lsmod | grep -q zfs; then - info "Loading ZFS module..." - modprobe zfs || error "Failed to load ZFS module" -fi +preflight_checks() { + # Check root + [[ $EUID -ne 0 ]] && error "This script must be run as root" + + # 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." +} + +############################# +# Phase 1: Gather All Input +############################# -### Interactive Configuration ### -configure_install() { - step "Installation Configuration" +gather_input() { + echo "" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ Arch Linux ZFS Root ║" + echo "║ Configuration and Installation ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo "" + info "Answer all questions now. Installation will run unattended afterward." echo "" - # Hostname + get_hostname + get_timezone + get_locale + get_keymap + get_disks + get_raid_level + get_wifi + get_zfs_passphrase + 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 +} - echo "" - - # Username - prompt "Enter primary username:" - read -p "> " USERNAME - while [[ -z "$USERNAME" || ! "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]]; do - warn "Invalid username. Use lowercase letters, numbers, underscore, hyphen." - read -p "> " USERNAME - done - - echo "" - - # Timezone selection +get_timezone() { + step "Timezone" prompt "Select timezone region:" PS3="Region: " select region in "America" "Europe" "Asia" "Australia" "Pacific" "Other"; do @@ -87,7 +119,6 @@ configure_install() { else echo "" prompt "Select city:" - # List cities for selected region mapfile -t cities < <(find /usr/share/zoneinfo/"$region" -maxdepth 1 -type f -printf '%f\n' | sort) PS3="City: " select city in "${cities[@]}"; do @@ -97,10 +128,10 @@ configure_install() { fi done fi +} - echo "" - - # Locale selection +get_locale() { + step "Locale" prompt "Select locale:" PS3="Locale: " select loc in "en_US.UTF-8" "en_GB.UTF-8" "de_DE.UTF-8" "fr_FR.UTF-8" "es_ES.UTF-8" "Other"; do @@ -114,10 +145,10 @@ configure_install() { break fi done +} - echo "" - - # Keymap selection +get_keymap() { + step "Keyboard Layout" prompt "Select keyboard layout:" PS3="Keymap: " select km in "us" "uk" "de" "fr" "es" "dvorak" "Other"; do @@ -131,126 +162,325 @@ configure_install() { break fi done +} - # Confirm settings +get_disks() { + step "Disk Selection" echo "" - echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD}Configuration Summary:${NC}" - echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo " Hostname: $HOSTNAME" - echo " Username: $USERNAME" - echo " Timezone: $TIMEZONE" - echo " Locale: $LOCALE" - echo " Keymap: $KEYMAP" - echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "Available disks:" + echo "----------------" + lsblk -d -o NAME,SIZE,MODEL,TYPE | grep disk echo "" - read -p "Is this correct? [Y/n]: " confirm - if [[ "$confirm" == "n" || "$confirm" == "N" ]]; then - configure_install + # Get list of available disks + mapfile -t AVAILABLE_DISKS < <(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}') + + if [[ ${#AVAILABLE_DISKS[@]} -eq 0 ]]; then + error "No disks found!" + fi + + # Build dialog checklist items + local dialog_items=() + for disk in "${AVAILABLE_DISKS[@]}"; do + local size=$(lsblk -d -n -o SIZE "/dev/$disk" | tr -d ' ') + local model=$(lsblk -d -n -o MODEL "/dev/$disk" | tr -d ' ' | head -c 20) + dialog_items+=("$disk" "$size $model" "off") + done + + # Use dialog for multi-select + local result + result=$(dialog --stdout --checklist "Select disks for installation (SPACE to select, ENTER to confirm):" \ + 20 70 10 "${dialog_items[@]}") || error "Disk selection cancelled" + + if [[ -z "$result" ]]; then + error "No disks selected!" + fi + + # Parse selected disks + SELECTED_DISKS=() + for disk in $result; do + SELECTED_DISKS+=("/dev/$disk") + done + + clear + echo "" + warn "Selected ${#SELECTED_DISKS[@]} disk(s):" + for disk in "${SELECTED_DISKS[@]}"; do + echo " - $disk" + lsblk "$disk" | sed 's/^/ /' + 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 } -### Disk Selection ### -select_disk() { - step "Disk Selection" +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 "" - echo "Available disks:" - echo "----------------" - lsblk -d -o NAME,SIZE,MODEL,TYPE | grep disk + echo "You have selected $disk_count disks." echo "" - # Get list of disks - mapfile -t DISKS < <(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}') + # Build options based on disk count + local options=("mirror" "stripe") + local descriptions=( + "mirror - All disks mirror each other (max redundancy, ${disk_count}x durability)" + "stripe - Combine disks for max capacity (NO redundancy, ${disk_count}x space)" + ) - if [[ ${#DISKS[@]} -eq 0 ]]; then - error "No disks found!" + if [[ $disk_count -ge 3 ]]; then + options+=("raidz1") + descriptions+=("raidz1 - Single parity (can lose 1 disk)") + fi + if [[ $disk_count -ge 4 ]]; then + options+=("raidz2") + descriptions+=("raidz2 - Double parity (can lose 2 disks)") + fi + if [[ $disk_count -ge 5 ]]; then + options+=("raidz3") + descriptions+=("raidz3 - Triple parity (can lose 3 disks)") fi - PS3="Select disk for installation (number): " - select disk in "${DISKS[@]}"; do - if [[ -n "$disk" ]]; then - DISK="/dev/$disk" + echo "Available RAID levels:" + for i in "${!descriptions[@]}"; do + echo " $((i+1))) ${descriptions[$i]}" + done + echo "" + + PS3="Select RAID level: " + select level in "${options[@]}"; do + if [[ -n "$level" ]]; then + RAID_LEVEL="$level" break fi done + info "RAID level: $RAID_LEVEL" +} + +get_wifi() { + step "WiFi Configuration (Optional)" echo "" - warn "Selected disk: $DISK" + prompt "Do you want to configure WiFi? [y/N]:" + read -p "> " configure_wifi + + if [[ "$configure_wifi" =~ ^[Yy]$ ]]; 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 2 + echo "" + echo "Available networks:" + nmcli device wifi list + echo "" + + prompt "Enter WiFi SSID:" + read -p "> " WIFI_SSID + + prompt "Enter WiFi password:" + 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_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 "" - lsblk "$DISK" + + 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 "" - read -p "This will DESTROY all data on $DISK. Type 'yes' to continue: " confirm - [[ "$confirm" != "yes" ]] && error "Aborted by user" + 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 } -### Partitioning ### -partition_disk() { - step "Partitioning $DISK" - - # Wipe existing signatures - info "Wiping existing signatures..." - wipefs -af "$DISK" - sgdisk --zap-all "$DISK" - - # Create partitions - # 1: EFI System Partition (1GB) - # 2: ZFS partition (rest) - info "Creating partitions..." - sgdisk -n 1:0:+1G -t 1:ef00 -c 1:"EFI" "$DISK" - sgdisk -n 2:0:0 -t 2:bf00 -c 2:"ZFS" "$DISK" - - # Determine partition names (handle nvme vs sda naming) - if [[ "$DISK" == *"nvme"* ]] || [[ "$DISK" == *"mmcblk"* ]]; then - EFI_PART="${DISK}p1" - ZFS_PART="${DISK}p2" +get_ssh_config() { + step "SSH Configuration" + echo "" + info "SSH enables remote access after installation." + info "Recommended for headless servers. Harden with archsetup later." + 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 - EFI_PART="${DISK}1" - ZFS_PART="${DISK}2" + ENABLE_SSH="yes" + info "SSH will be enabled with root password login." + warn "Remember to harden SSH (key auth, fail2ban) with archsetup!" fi +} + +show_summary() { + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Configuration Summary:" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + 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=$(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)" + echo " ZFS Pool: $POOL_NAME (encrypted)" + echo " Boot: EFI on all disks (redundant)" + 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: 1G EFI + rest for ZFS + sgdisk -n 1:0:+1G -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 - # Wait for partitions to appear - sleep 2 - partprobe "$DISK" sleep 2 - # Format EFI partition - info "Formatting EFI partition..." - mkfs.fat -F32 -n EFI "$EFI_PART" + # 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." - lsblk "$DISK" + info "Partitioning complete. Created ${#EFI_PARTS[@]} EFI and ${#ZFS_PARTS[@]} ZFS partitions." } -### ZFS Pool Creation ### create_zfs_pool() { step "Creating ZFS Pool with Native Encryption" - # Check if pool already exists if zpool list "$POOL_NAME" &>/dev/null; then warn "Pool $POOL_NAME already exists. Destroying..." zpool destroy -f "$POOL_NAME" fi - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD}ZFS Encryption Passphrase${NC}" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - echo "You will now create an encryption passphrase." - 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 -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" + # 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 encrypted pool - zpool create -f \ + # Create encrypted pool using passphrase from variable + echo "$ZFS_PASSPHRASE" | zpool create -f \ -o ashift="$ASHIFT" \ -o autotrim=on \ -O acltype=posixacl \ @@ -266,33 +496,37 @@ create_zfs_pool() { -O keylocation=prompt \ -O mountpoint=none \ -R /mnt \ - "$POOL_NAME" "$ZFS_PART" + "$POOL_NAME" $pool_config info "ZFS pool created successfully." + zpool status "$POOL_NAME" } -### Dataset Creation ### create_datasets() { step "Creating ZFS Datasets" # Root dataset container zfs create -o mountpoint=none -o canmount=off "$POOL_NAME/ROOT" - # Main root filesystem with reservation for safety - zfs create -o mountpoint=/ -o canmount=noauto -o reservation=50G "$POOL_NAME/ROOT/default" + # Main root filesystem + # Reserve 20% of pool or 20G max to prevent pool from filling completely + local 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 - # Mount root first + zfs create -o mountpoint=/ -o canmount=noauto -o reservation=${reserve_gb}G "$POOL_NAME/ROOT/default" zfs mount "$POOL_NAME/ROOT/default" - # Home datasets + # Home (archsetup will create user subdataset) zfs create -o mountpoint=/home "$POOL_NAME/home" zfs create -o mountpoint=/root "$POOL_NAME/home/root" - zfs create -o mountpoint="/home/$USERNAME" "$POOL_NAME/home/$USERNAME" - # Media dataset - compression off for already-compressed files + # Media - compression off for already-compressed files zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media" - # VMs dataset - larger recordsize for VM disk images + # VMs - 64K recordsize for VM disk images zfs create -o mountpoint=/vms -o recordsize=64K "$POOL_NAME/vms" # Var datasets @@ -303,46 +537,56 @@ create_datasets() { zfs create -o mountpoint=/var/lib/pacman "$POOL_NAME/var/lib/pacman" zfs create -o mountpoint=/var/lib/docker "$POOL_NAME/var/lib/docker" - # Exclude temp directories from snapshots + # 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:" - echo "" - zfs list -r "$POOL_NAME" -o name,mountpoint,compression,reservation + zfs list -r "$POOL_NAME" -o name,mountpoint,compression } -### Mount EFI ### mount_efi() { step "Mounting EFI Partition" - mkdir -p /mnt/boot - mount "$EFI_PART" /mnt/boot - - info "EFI partition mounted at /mnt/boot" + # Mount primary (first) EFI partition + mount "${EFI_PARTS[0]}" /mnt/boot + info "Primary EFI partition ${EFI_PARTS[0]} mounted at /mnt/boot" } -### Install Base System ### install_base() { step "Installing Base System" info "Updating pacman keys..." pacman-key --init pacman-key --populate archlinux + + # Add archzfs key pacman-key -r DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true + # Add archzfs repo to pacman.conf for pacstrap + if ! grep -q "\[archzfs\]" /etc/pacman.conf; then + cat >> /etc/pacman.conf << 'EOF' + +[archzfs] +Server = https://archzfs.com/$repo/$arch +SigLevel = Optional TrustAll +EOF + fi + info "Installing base packages (this takes a while)..." + info "ZFS will be built from source via DKMS - this ensures kernel compatibility." pacstrap -K /mnt \ base \ base-devel \ - linux \ - linux-headers \ + linux-lts \ + linux-lts-headers \ linux-firmware \ - zfs-linux \ + zfs-dkms \ zfs-utils \ grub \ + freetype2 \ efibootmgr \ networkmanager \ openssh \ @@ -352,16 +596,15 @@ install_base() { zsh \ nodejs \ npm \ - sanoid + ttf-dejavu info "Base system installed." } -### Configure System ### configure_system() { step "Configuring System" - # Generate fstab (only for EFI, ZFS handles the rest) + # fstab (only for EFI) info "Generating fstab..." echo "# /boot - EFI System Partition" > /mnt/etc/fstab echo "UUID=$(blkid -s UUID -o value "$EFI_PART") /boot vfat defaults,noatime 0 2" >> /mnt/etc/fstab @@ -389,7 +632,7 @@ configure_system() { 127.0.1.1 $HOSTNAME.localdomain $HOSTNAME EOF - # Add archzfs repo to installed system + # Add archzfs repo info "Adding archzfs repository..." cat >> /mnt/etc/pacman.conf << 'EOF' @@ -398,27 +641,60 @@ Server = https://archzfs.com/$repo/$arch SigLevel = Optional TrustAll EOF - # Import archzfs key in chroot + # Import archzfs key arch-chroot /mnt pacman-key -r DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true arch-chroot /mnt pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true + + # 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 "Run archsetup to harden SSH (key auth, fail2ban)." + else + info "SSH not enabled. Enable manually if needed." + fi } -### Configure mkinitcpio ### configure_initramfs() { step "Configuring Initramfs for ZFS" - # Backup original cp /mnt/etc/mkinitcpio.conf /mnt/etc/mkinitcpio.conf.bak # Configure hooks for ZFS - # Order matters: keyboard before zfs for passphrase entry sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block zfs filesystems fsck)/' /mnt/etc/mkinitcpio.conf info "Regenerating initramfs..." arch-chroot /mnt mkinitcpio -P } -### Configure Bootloader ### configure_bootloader() { step "Configuring GRUB Bootloader" @@ -434,20 +710,49 @@ GRUB_TERMINAL_OUTPUT="console" GRUB_DISABLE_OS_PROBER=true GRUB_GFXMODE=auto GRUB_GFXPAYLOAD_LINUX=keep +GRUB_FONT=/boot/grub/fonts/DejaVuSansMono32.pf2 EOF - info "Installing GRUB..." - arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB + # Install GRUB to each EFI partition for boot redundancy + info "Installing GRUB to ${#EFI_PARTS[@]} EFI partition(s)..." + + for i in "${!EFI_PARTS[@]}"; do + local efi_part="${EFI_PARTS[$i]}" + local bootloader_id="GRUB" + if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then + bootloader_id="GRUB-disk$((i+1))" + fi + + # Unmount current boot if mounted, mount this EFI partition + umount /mnt/boot 2>/dev/null || true + mount "$efi_part" /mnt/boot + + # Create directories and font + mkdir -p /mnt/boot/grub/fonts + arch-chroot /mnt grub-mkfont -s 32 -o /boot/grub/fonts/DejaVuSansMono32.pf2 \ + /usr/share/fonts/TTF/DejaVuSansMono.ttf 2>/dev/null || true - info "Generating GRUB configuration..." - arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg + # Install GRUB + info "Installing GRUB to $efi_part (bootloader-id: $bootloader_id)..." + arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot \ + --bootloader-id="$bootloader_id" --recheck + + # Generate configuration + arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg + done + + # Remount primary EFI for rest of installation + umount /mnt/boot 2>/dev/null || true + mount "${EFI_PARTS[0]}" /mnt/boot + + if [[ ${#EFI_PARTS[@]} -gt 1 ]]; then + info "GRUB installed to all ${#EFI_PARTS[@]} disks for boot redundancy." + fi } -### Configure ZFS Services ### configure_zfs_services() { step "Configuring ZFS Services" - # Enable ZFS services arch-chroot /mnt systemctl enable zfs.target arch-chroot /mnt systemctl enable zfs-import-cache arch-chroot /mnt systemctl enable zfs-mount @@ -458,7 +763,7 @@ configure_zfs_services() { zpool set cachefile=/etc/zfs/zpool.cache "$POOL_NAME" cp /etc/zfs/zpool.cache /mnt/etc/zfs/ - # Set bootfs property + # Set bootfs zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" # Enable other services @@ -468,93 +773,8 @@ configure_zfs_services() { info "ZFS services configured." } -### Configure Sanoid (Snapshot Management) ### -configure_sanoid() { - step "Configuring Sanoid Snapshot Management" - - mkdir -p /mnt/etc/sanoid - - cat > /mnt/etc/sanoid/sanoid.conf << EOF -# Sanoid configuration for ZFS snapshots -# https://github.com/jimsalterjrs/sanoid - -############################# -# Templates -############################# - -[template_production] - # Frequent snapshots for active data - hourly = 24 - daily = 7 - weekly = 4 - monthly = 12 - yearly = 0 - autosnap = yes - autoprune = yes - -[template_backup] - # Less frequent for large/static data - hourly = 0 - daily = 7 - weekly = 4 - monthly = 6 - yearly = 0 - autosnap = yes - autoprune = yes - -[template_none] - # No automatic snapshots (for tmp, cache) - autosnap = no - autoprune = yes - -############################# -# Datasets -############################# - -# Root filesystem -[$POOL_NAME/ROOT/default] - use_template = production - -# Home directories -[$POOL_NAME/home] - use_template = production - recursive = yes - -# Media (large files, less frequent snapshots) -[$POOL_NAME/media] - use_template = backup - -# VMs (snapshot before changes manually, or less frequently) -[$POOL_NAME/vms] - use_template = backup - -# Var data -[$POOL_NAME/var/log] - use_template = production - -[$POOL_NAME/var/lib/pacman] - use_template = production - -# No snapshots for cache/tmp (handled by dataset property, but explicit here) -[$POOL_NAME/var/cache] - use_template = none - -[$POOL_NAME/var/tmp] - use_template = none - -[$POOL_NAME/tmp] - use_template = none -EOF - - # Enable sanoid timer - arch-chroot /mnt systemctl enable sanoid.timer - - info "Sanoid configured. Snapshots will run automatically." -} - -### Configure Pacman ZFS Snapshot Hook ### configure_pacman_hook() { - step "Configuring Pacman Pre-Upgrade Snapshot Hook" + step "Configuring Pacman Snapshot Hook" mkdir -p /mnt/etc/pacman.d/hooks @@ -574,15 +794,11 @@ EOF cat > /mnt/usr/local/bin/zfs-pre-snapshot << 'EOF' #!/bin/bash -# Create a ZFS snapshot before pacman transactions -# This allows easy rollback if an upgrade breaks something - POOL="zroot" DATASET="$POOL/ROOT/default" TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) SNAPSHOT_NAME="pre-pacman_$TIMESTAMP" -# Create the snapshot if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then echo "Created snapshot: $DATASET@$SNAPSHOT_NAME" else @@ -592,152 +808,121 @@ EOF chmod +x /mnt/usr/local/bin/zfs-pre-snapshot - info "Pacman hook configured. Snapshots will be created before each transaction." + info "Pacman hook configured." } -### Create User ### -create_user() { - step "Creating User: $USERNAME" +copy_archsetup() { + step "Installing archsetup Launcher" - arch-chroot /mnt useradd -m -G wheel -s /bin/zsh "$USERNAME" 2>/dev/null || \ - warn "User $USERNAME may already exist" + cat > /mnt/usr/local/bin/archsetup << 'EOF' +#!/bin/bash +curl -fsSL https://cjennings.net/archsetup | bash +EOF + chmod +x /mnt/usr/local/bin/archsetup + info "archsetup launcher installed to /usr/local/bin/archsetup" +} - # Set ownership of home dataset - arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" +sync_efi_partitions() { + # Skip if only one disk + if [[ ${#EFI_PARTS[@]} -le 1 ]]; then + return + fi - # Configure sudo - echo "%wheel ALL=(ALL:ALL) ALL" > /mnt/etc/sudoers.d/wheel - chmod 440 /mnt/etc/sudoers.d/wheel + step "Syncing EFI Partitions for Redundancy" - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD}Set User Password${NC}" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - info "Set password for $USERNAME:" - arch-chroot /mnt passwd "$USERNAME" + local primary_efi="${EFI_PARTS[0]}" + local temp_mount="/mnt/efi_sync" - echo "" - info "Set password for root:" - arch-chroot /mnt passwd -} + for i in "${!EFI_PARTS[@]}"; do + if [[ $i -eq 0 ]]; then + continue # Skip primary + fi -### Copy archsetup ### -copy_archsetup() { - step "Copying archsetup to New System" + local efi_part="${EFI_PARTS[$i]}" + info "Syncing to EFI partition $((i+1)): $efi_part" - if [[ -d /code/archsetup ]]; then - mkdir -p "/mnt/home/$USERNAME/code" - cp -r /code/archsetup "/mnt/home/$USERNAME/code/" - arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/code" - info "archsetup copied to /home/$USERNAME/code/archsetup" - else - warn "archsetup not found in ISO, skipping..." - fi + mkdir -p "$temp_mount" + mount "$efi_part" "$temp_mount" + + # Sync all content from primary EFI (mounted at /mnt/boot) to secondary + rsync -a --delete /mnt/boot/ "$temp_mount/" + + umount "$temp_mount" + done + + rmdir "$temp_mount" 2>/dev/null || true + info "All EFI partitions synchronized." } -### Create Syncoid Script for TrueNAS ### -create_syncoid_script() { - step "Creating Syncoid Replication Script" +create_genesis_snapshot() { + step "Creating Genesis Snapshot" - cat > /mnt/usr/local/bin/zfs-replicate << 'SCRIPT' + # 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 -# zfs-replicate - Replicate ZFS datasets to TrueNAS -# Usage: zfs-replicate [dataset] [target] +# rollback-to-genesis - Roll back all datasets to the genesis snapshot # -# Examples: -# zfs-replicate # Replicate all configured datasets -# zfs-replicate zroot/home user@truenas:/tank/backup/laptop +# 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 -# Configuration - edit these for your TrueNAS setup -TRUENAS_HOST="truenas" # TrueNAS hostname or IP -TRUENAS_USER="root" # User with ZFS permissions -TRUENAS_POOL="tank" # Destination pool -BACKUP_PATH="backup/laptop" # Path under the pool - -# Datasets to replicate (space-separated) -DATASETS="zroot/ROOT/default zroot/home zroot/media zroot/vms" - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -info() { echo -e "${GREEN}[INFO]${NC} $1"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } - -# Check if syncoid is installed -command -v syncoid >/dev/null 2>&1 || error "syncoid not found. Install sanoid package." - -# Single dataset mode -if [[ -n "$1" ]] && [[ -n "$2" ]]; then - info "Replicating $1 to $2" - syncoid --recursive "$1" "$2" - exit 0 -fi +POOL_NAME="zroot" -# Full replication mode -info "Starting ZFS replication to $TRUENAS_HOST" +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 "" -for dataset in $DATASETS; do - dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}" - info "Replicating $dataset -> $dest" - - if syncoid --recursive "$dataset" "$dest"; then - info " Success" - else - warn " Failed (will retry next run)" - fi - 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 "" -info "Replication complete." -SCRIPT - - chmod +x /mnt/usr/local/bin/zfs-replicate - - # Create systemd service and timer for automatic replication - cat > /mnt/etc/systemd/system/zfs-replicate.service << 'EOF' -[Unit] -Description=ZFS Replication to TrueNAS -After=network-online.target -Wants=network-online.target - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/zfs-replicate -User=root - -[Install] -WantedBy=multi-user.target -EOF +read -p "Type 'ROLLBACK' to confirm: " confirm +if [[ "$confirm" != "ROLLBACK" ]]; then + echo "Aborted." + exit 1 +fi - cat > /mnt/etc/systemd/system/zfs-replicate.timer << 'EOF' -[Unit] -Description=Run ZFS replication nightly +echo "" +echo "Rolling back to genesis..." -[Timer] -OnCalendar=*-*-* 02:00:00 -RandomizedDelaySec=1800 -Persistent=true +# 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 -[Install] -WantedBy=timers.target -EOF +echo "" +echo "Rollback complete!" +echo "Reboot to complete the process: reboot" +ROLLBACK_EOF - info "Syncoid replication script created." - info "Edit /usr/local/bin/zfs-replicate to configure your TrueNAS connection." - info "Enable with: systemctl enable --now zfs-replicate.timer" + chmod +x /mnt/root/rollback-to-genesis + info "Genesis snapshot created. Rollback script: /root/rollback-to-genesis" } -### Unmount and Export ### cleanup() { step "Cleaning Up" + # Clear sensitive variables + ROOT_PASSWORD="" + ZFS_PASSPHRASE="" + info "Unmounting filesystems..." umount /mnt/boot 2>/dev/null || true @@ -747,85 +932,73 @@ cleanup() { info "Cleanup complete." } -### Print Summary ### print_summary() { echo "" - echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ Installation Complete! ║${NC}" - echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ Installation Complete! ║" + echo "╚═══════════════════════════════════════════════════════════════╝" echo "" - echo -e "${BOLD}System Configuration:${NC}" + echo "System Configuration:" echo " Hostname: $HOSTNAME" - echo " Username: $USERNAME" echo " Timezone: $TIMEZONE" echo " ZFS Pool: $POOL_NAME (encrypted)" echo "" - echo -e "${BOLD}ZFS Features Configured:${NC}" - echo " - Automatic snapshots via sanoid (hourly/daily/weekly/monthly)" + echo "ZFS Features:" + echo " - Genesis snapshot: pristine post-install state" echo " - Pre-pacman snapshots for safe upgrades" - echo " - Replication script ready for TrueNAS" - echo "" - echo -e "${BOLD}Next Steps:${NC}" - echo " 1. Reboot: ${CYAN}reboot${NC}" - echo " 2. Enter your ZFS encryption passphrase at boot" - echo " 3. Log in as $USERNAME" - echo " 4. Run archsetup: ${CYAN}cd ~/code/archsetup && sudo ./archsetup${NC}" + echo " - Sanoid/syncoid configured by archsetup" echo "" - echo -e "${BOLD}Configure TrueNAS Replication:${NC}" - echo " 1. Set up SSH key auth to TrueNAS" - echo " 2. Edit: ${CYAN}/usr/local/bin/zfs-replicate${NC}" - echo " 3. Enable: ${CYAN}sudo systemctl enable --now zfs-replicate.timer${NC}" + echo "Next Steps:" + echo " 1. Reboot: reboot" + echo " 2. Enter ZFS encryption passphrase at boot" + echo " 3. Log in as root" + echo " 4. Run archsetup: archsetup" echo "" - echo -e "${BOLD}Useful ZFS Commands:${NC}" - echo " List snapshots: ${CYAN}zfs list -t snapshot${NC}" - echo " Manual snapshot: ${CYAN}sudo zfs snapshot zroot/home@my-snapshot${NC}" - echo " Rollback: ${CYAN}sudo zfs rollback zroot/home@my-snapshot${NC}" - echo " Check pool status: ${CYAN}zpool status${NC}" + 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 "" - echo -e "${BOLD}If Something Goes Wrong:${NC}" - echo " Boot from this ISO, then:" - echo " ${CYAN}zpool import -R /mnt zroot${NC}" - echo " ${CYAN}zfs load-key zroot${NC}" - echo " ${CYAN}zfs mount zroot/ROOT/default${NC}" - echo "" - info "Installation log saved to: $LOGFILE" + info "Installation log: $LOGFILE" echo "" } -### Main Installation Flow ### +############################# +# Main +############################# + main() { - echo "" - echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ Arch Linux ZFS Root Installation ║${NC}" - echo -e "${CYAN}║ with Native Encryption ║${NC}" - echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}" - echo "" + preflight_checks + gather_input - info "Installation log: $LOGFILE" + # Unattended installation begins + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Beginning unattended installation..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - configure_install - select_disk - partition_disk + partition_disks create_zfs_pool create_datasets mount_efi install_base configure_system + configure_wifi + configure_ssh configure_initramfs configure_bootloader configure_zfs_services - configure_sanoid configure_pacman_hook - create_user copy_archsetup - create_syncoid_script + sync_efi_partitions + create_genesis_snapshot cleanup print_summary } -# Handle interrupts trap 'error "Installation interrupted!"' INT TERM -# Run main main "$@" |
