diff options
| author | Craig Jennings <c@cjennings.net> | 2026-01-23 20:05:01 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-01-23 20:05:01 -0600 |
| commit | f757fedf2a88760d5ddd3a815998f15572a20069 (patch) | |
| tree | 08b1839949401308b6659078fa8f1ffd65078e95 /custom/install-archzfs | |
| parent | 53e82d802dba330ec8e5a568d6250e04f1193f0c (diff) | |
| download | archangel-f757fedf2a88760d5ddd3a815998f15572a20069.tar.gz archangel-f757fedf2a88760d5ddd3a815998f15572a20069.zip | |
Phase 1.5: Rename to archangel
- Rename install-archzfs → archangel
- Rename install-archzfs.conf.example → archangel.conf.example
- Update build.sh to use new names
- Update script header with dual-filesystem description
- Update log file names to archangel-*
The installer is now called "archangel" reflecting its expanded
scope: snapshot-based recovery for both ZFS and Btrfs filesystems.
Diffstat (limited to 'custom/install-archzfs')
| -rwxr-xr-x | custom/install-archzfs | 1432 |
1 files changed, 0 insertions, 1432 deletions
diff --git a/custom/install-archzfs b/custom/install-archzfs deleted file mode 100755 index f604f5c..0000000 --- a/custom/install-archzfs +++ /dev/null @@ -1,1432 +0,0 @@ -#!/bin/bash -# install-archzfs - Arch Linux ZFS Root Installation Script -# Craig Jennings <c@cjennings.net> -# -# Installs Arch Linux on ZFS root with optional 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 -# - Optional ZFS native encryption (passphrase required at boot) -# - Pre-pacman ZFS snapshots for safe upgrades -# -# UNATTENDED MODE: -# Use --config-file /path/to/install-archzfs.conf for automated installs. -# Config file must be explicitly specified to prevent accidental disk wipes. -# See /root/install-archzfs.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" - -############################# -# 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/install-archzfs-$(date +'%Y-%m-%d-%H-%M-%S').log" -exec > >(tee -a "$LOGFILE") 2>&1 - -# Log header with timestamp -echo "" -echo "================================================================================" -echo "install-archzfs 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 - zfs_preflight -} - -############################# -# 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 not yet implemented - if [[ "$FILESYSTEM" == "btrfs" ]]; then - error "Btrfs support not yet implemented. Use FILESYSTEM=zfs" - 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 - - # Check for btrfs (not yet implemented) - if [[ "$FILESYSTEM" == "btrfs" ]]; then - error "Btrfs support not yet implemented. Please select ZFS." - fi - - get_hostname - get_timezone - get_locale - get_keymap - get_disks - get_raid_level - get_wifi - get_encryption_choice - [[ "$NO_ENCRYPT" != "yes" ]] && 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 -} - -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=$(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=$(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=$(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_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 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 - 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 " 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=$(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 - 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=$(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 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." - # 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." -} - -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 - info "Adding archzfs repository..." - cat >> /mnt/etc/pacman.conf << 'EOF' - -[archzfs] -Server = https://archzfs.com/$repo/$arch -SigLevel = Optional TrustAll -EOF - - # 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 - - # 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 "Run archsetup to harden SSH (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 "Note: Install sanoid via archsetup for automated snapshot retention." -} - -copy_archsetup() { - step "Installing archsetup Launcher" - - 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" -} - -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 " - Sanoid/syncoid configured by archsetup" - 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 "" -} - -############################# -# Main -############################# - -main() { - parse_args "$@" - preflight_checks - check_config - gather_input - - # Unattended installation begins - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Beginning unattended installation..." - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - - 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 - copy_archsetup - sync_efi_partitions - create_genesis_snapshot - cleanup - print_summary -} - -trap 'error "Installation interrupted!"' INT TERM - -main "$@" |
