diff options
| -rw-r--r-- | .gitignore | 10 | ||||
| -rwxr-xr-x | build.sh | 188 | ||||
| -rwxr-xr-x | custom/archsetup-zfs | 438 | ||||
| -rwxr-xr-x | custom/install-archzfs | 831 | ||||
| -rwxr-xr-x | custom/install-claude | 24 | ||||
| -rwxr-xr-x | download-archzfs-iso.sh | 145 | ||||
| -rwxr-xr-x | scripts/test-vm.sh | 208 | ||||
| -rw-r--r-- | vm/OVMF_VARS.fd | bin | 0 -> 540672 bytes | |||
| -rw-r--r-- | vm/archzfs-test.qcow2 | bin | 0 -> 197408 bytes |
9 files changed, 1844 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e19341 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build artifacts +work/ +out/ +profile/ + +# Downloaded packages +zfs-packages/ + +# Session notes (private) +docs/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..012455e --- /dev/null +++ b/build.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# build.sh - Build the custom Arch ZFS installation ISO +# Must be run as root + +set -e + +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" +ZFS_PKG_DIR="$SCRIPT_DIR/zfs-packages" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +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 root +[[ $EUID -ne 0 ]] && error "This script must be run as root" + +# Check dependencies +command -v mkarchiso >/dev/null 2>&1 || { + info "Installing archiso..." + pacman -Sy --noconfirm archiso +} + +# Get current kernel version +KERNEL_VER=$(pacman -Si linux | grep Version | awk '{print $3}') +info "Current Arch kernel version: $KERNEL_VER" + +# Download ZFS packages from GitHub releases +info "Downloading ZFS packages for kernel $KERNEL_VER..." +mkdir -p "$ZFS_PKG_DIR" + +# Find matching ZFS packages from experimental release +ZFS_LINUX_URL=$(curl -s https://api.github.com/repos/archzfs/archzfs/releases/tags/experimental | \ + jq -r ".assets[] | select(.name | contains(\"zfs-linux-\") and contains(\"${KERNEL_VER}\") and (contains(\"-headers\") | not) and contains(\".pkg.tar.zst\") and (contains(\".sig\") | not)) | .browser_download_url" | head -1) + +ZFS_UTILS_URL=$(curl -s https://api.github.com/repos/archzfs/archzfs/releases/tags/experimental | \ + jq -r '.assets[] | select(.name | contains("zfs-utils-") and contains(".pkg.tar.zst") and (contains(".sig") | not) and (contains("debug") | not)) | .browser_download_url' | head -1) + +if [[ -z "$ZFS_LINUX_URL" ]]; then + warn "No ZFS package found for kernel $KERNEL_VER in experimental" + warn "Checking other releases..." + + # Try to find any recent zfs-linux package + ZFS_LINUX_URL=$(curl -s https://api.github.com/repos/archzfs/archzfs/releases | \ + jq -r ".[].assets[] | select(.name | contains(\"zfs-linux-\") and contains(\"6.18\") and (contains(\"-headers\") | not) and contains(\".pkg.tar.zst\") and (contains(\".sig\") | not)) | .browser_download_url" | head -1) +fi + +if [[ -z "$ZFS_LINUX_URL" || -z "$ZFS_UTILS_URL" ]]; then + error "Could not find matching ZFS packages. The archzfs repo may not have packages for kernel $KERNEL_VER yet." +fi + +info "Downloading: $(basename "$ZFS_LINUX_URL")" +wget -q -N -P "$ZFS_PKG_DIR" "$ZFS_LINUX_URL" || error "Failed to download zfs-linux" + +info "Downloading: $(basename "$ZFS_UTILS_URL")" +wget -q -N -P "$ZFS_PKG_DIR" "$ZFS_UTILS_URL" || error "Failed to download zfs-utils" + +# Clean previous builds +if [[ -d "$WORK_DIR" ]]; then + warn "Removing previous work directory..." + rm -rf "$WORK_DIR" +fi + +# Always start fresh from releng profile +info "Copying base releng profile..." +rm -rf "$PROFILE_DIR" +cp -r /usr/share/archiso/configs/releng "$PROFILE_DIR" + +# Add our custom packages (NOT zfs - we'll install that separately) +info "Adding custom packages..." +cat >> "$PROFILE_DIR/packages.x86_64" << 'EOF' + +# Additional networking +wget + +# Development tools for Claude Code +nodejs +npm +jq + +# Additional utilities +zsh +htop +ripgrep +eza +fd +fzf + +# For installation scripts +dialog +EOF + +# Update profiledef.sh with our ISO name +info "Updating ISO metadata..." +sed -i 's/^iso_name=.*/iso_name="archzfs-claude"/' "$PROFILE_DIR/profiledef.sh" + +# Create airootfs directories +mkdir -p "$PROFILE_DIR/airootfs/usr/local/bin" +mkdir -p "$PROFILE_DIR/airootfs/code" +mkdir -p "$PROFILE_DIR/airootfs/var/cache/zfs-packages" + +# Copy ZFS packages to airootfs for installation during boot +info "Copying ZFS packages to ISO..." +cp "$ZFS_PKG_DIR"/*.pkg.tar.zst "$PROFILE_DIR/airootfs/var/cache/zfs-packages/" + +# Copy our custom scripts +info "Copying custom scripts..." +cp "$CUSTOM_DIR/install-archzfs" "$PROFILE_DIR/airootfs/usr/local/bin/" +cp "$CUSTOM_DIR/install-claude" "$PROFILE_DIR/airootfs/usr/local/bin/" +cp "$CUSTOM_DIR/archsetup-zfs" "$PROFILE_DIR/airootfs/usr/local/bin/" + +# Create ZFS setup script that runs on boot +cat > "$PROFILE_DIR/airootfs/usr/local/bin/zfs-setup" << 'ZFSSETUP' +#!/bin/bash +# Install ZFS packages and load module +# Run this first after booting the ISO + +set -e + +echo "Installing ZFS packages..." +pacman -U --noconfirm /var/cache/zfs-packages/*.pkg.tar.zst + +echo "Loading ZFS module..." +modprobe zfs + +echo "" +echo "ZFS is ready! You can now run:" +echo " install-archzfs" +echo "" +ZFSSETUP + +# Set permissions in profiledef.sh +info "Setting file permissions..." +if grep -q "file_permissions=" "$PROFILE_DIR/profiledef.sh"; then + sed -i '/^file_permissions=(/,/)/ { + /)/ i\ ["/usr/local/bin/install-archzfs"]="0:0:755" + }' "$PROFILE_DIR/profiledef.sh" + sed -i '/^file_permissions=(/,/)/ { + /)/ i\ ["/usr/local/bin/install-claude"]="0:0:755" + }' "$PROFILE_DIR/profiledef.sh" + sed -i '/^file_permissions=(/,/)/ { + /)/ i\ ["/usr/local/bin/archsetup-zfs"]="0:0:755" + }' "$PROFILE_DIR/profiledef.sh" + sed -i '/^file_permissions=(/,/)/ { + /)/ i\ ["/usr/local/bin/zfs-setup"]="0:0:755" + }' "$PROFILE_DIR/profiledef.sh" +fi + +# Copy archsetup into airootfs +if [[ -d /home/cjennings/code/archsetup ]]; then + info "Copying archsetup into ISO..." + cp -r /home/cjennings/code/archsetup "$PROFILE_DIR/airootfs/code/" + rm -rf "$PROFILE_DIR/airootfs/code/archsetup/.git" + rm -rf "$PROFILE_DIR/airootfs/code/archsetup/.claude" +fi + +# Ensure scripts are executable in the profile +chmod +x "$PROFILE_DIR/airootfs/usr/local/bin/"* + +# Build the ISO +info "Building ISO (this will take a while)..." +mkarchiso -v -w "$WORK_DIR" -o "$OUT_DIR" "$PROFILE_DIR" + +# Report results +ISO_FILE=$(ls -t "$OUT_DIR"/*.iso 2>/dev/null | head -1) +if [[ -f "$ISO_FILE" ]]; then + echo "" + info "Build complete!" + info "ISO location: $ISO_FILE" + info "ISO size: $(du -h "$ISO_FILE" | cut -f1)" + echo "" + info "To test: ./scripts/test-vm.sh" + echo "" + info "After booting, run:" + echo " zfs-setup # Install ZFS and load module" + echo " install-archzfs # Run the installer" +else + error "Build failed - no ISO file found" +fi diff --git a/custom/archsetup-zfs b/custom/archsetup-zfs new file mode 100755 index 0000000..89aeb08 --- /dev/null +++ b/custom/archsetup-zfs @@ -0,0 +1,438 @@ +#!/bin/sh +# archsetup-zfs - Post-installation setup for Arch Linux on ZFS +# Craig Jennings <craigmartinjennings@gmail.com> +# License: GNU GPLv3 +# +# This is a ZFS-specific variant of archsetup. +# It replaces btrfs snapshot tooling with ZFS equivalents. +# +# Run this after install-archzfs completes and you've rebooted. + +# Commentary +# +# This script is based on the original archsetup but modified for ZFS: +# - Removes: timeshift-autosnap, grub-btrfs (btrfs-specific) +# - Adds: sanoid/syncoid configuration verification +# - Keeps: All other package installations and configurations +# +# The ZFS snapshot infrastructure is already configured by install-archzfs: +# - sanoid.timer for automatic snapshots +# - pacman hook for pre-upgrade snapshots +# - syncoid script for TrueNAS replication + +set -e + +### Root Check +if [ "$EUID" -ne 0 ]; then + echo "ERROR: This script must be run as root" + echo "Usage: sudo $0" + exit 1 +fi + +### Parse Arguments +skip_slow_packages=false +fresh_install=false +show_status_only=false + +while [ $# -gt 0 ]; do + case "$1" in + --skip-slow-packages) + skip_slow_packages=true + shift + ;; + --fresh) + fresh_install=true + shift + ;; + --status) + show_status_only=true + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --skip-slow-packages Skip texlive-meta and topgrade" + echo " --fresh Start fresh, ignore previous progress" + echo " --status Show installation progress and exit" + echo " --help, -h Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--skip-slow-packages] [--fresh] [--status]" + exit 1 + ;; + esac +done + +### Constants +username="cjennings" +password="welcome" # will be changed on first login + +archsetup_dir="$(dirname "$(readlink -f "$0")")" + +# Check if we're running from the ISO copy or user's home +if [ -d "/home/$username/code/archsetup" ]; then + archsetup_dir="/home/$username/code/archsetup" +fi + +dotfiles_dir="$archsetup_dir/dotfiles" + +dwm_repo="https://git.cjennings.net/dwm.git" +dmenu_repo="https://git.cjennings.net/dmenu.git" +st_repo="https://git.cjennings.net/st.git" +slock_repo="https://git.cjennings.net/slock.git" +dotemacs_repo="https://git.cjennings.net/dotemacs.git" + +logfile="/var/log/archsetup-zfs-$(date +'%Y-%m-%d-%H-%M-%S').log" +source_dir="/home/$username/.local/src" +packages_before="/var/log/archsetup-preexisting-package-list.txt" +packages_after="/var/log/archsetup-post-install-package-list.txt" +archsetup_packages="/var/log/archsetup-installed-packages.txt" + +min_disk_space_gb=20 +state_dir="/var/lib/archsetup-zfs/state" + +### ZFS Check +check_zfs() { + if ! command -v zfs >/dev/null 2>&1; then + echo "ERROR: ZFS tools not found. This script is for ZFS systems." + echo "Use the regular 'archsetup' for btrfs systems." + exit 1 + fi + + if ! zpool list zroot >/dev/null 2>&1; then + echo "ERROR: ZFS pool 'zroot' not found." + echo "This script expects a ZFS root pool named 'zroot'." + exit 1 + fi + + echo "ZFS system detected: $(zpool list -H -o name,size,health zroot)" +} + +### State Tracking +step_completed() { + [ -f "$state_dir/$1" ] +} + +mark_complete() { + mkdir -p "$state_dir" + echo "$(date +'%Y-%m-%d %H:%M:%S')" > "$state_dir/$1" +} + +run_step() { + step_name="$1" + step_func="$2" + + if step_completed "$step_name"; then + printf "Skipping %s (already completed)\n" "$step_name" + return 0 + fi + + if $step_func; then + mark_complete "$step_name" + return 0 + else + printf "FAILED: %s\n" "$step_name" + printf "To retry this step, remove: %s/%s\n" "$state_dir" "$step_name" + return 1 + fi +} + +show_status() { + echo "Archsetup-ZFS State Status" + echo "==========================" + echo "State directory: $state_dir" + echo "" + if [ ! -d "$state_dir" ]; then + echo "No state found. Script has not been run or was run with --fresh." + exit 0 + fi + echo "Completed steps:" + for step in intro prerequisites user_customizations \ + aur_installer essential_services xorg dwm \ + desktop_environment developer_workstation \ + supplemental_software boot_ux zfs_services; do + if step_completed "$step"; then + timestamp=$(cat "$state_dir/$step") + printf " [x] %-25s (%s)\n" "$step" "$timestamp" + else + printf " [ ] %-25s\n" "$step" + fi + done + exit 0 +} + +if $show_status_only; then + show_status +fi + +if $fresh_install; then + echo "Starting fresh installation (removing previous state)..." + rm -rf "$state_dir" +fi + +### General Functions +error () { + case "$1" in + "error") + printf "ERROR: %s failed with error code %s @ %s\n" \ + "$2" "$3" "$(date +'%T')" | tee -a "$logfile" + return 1 + ;; + *) + printf "CRASH: %s failed with error: %s @ %s. Script halted.\n" \ + "$2" "$3" "$(date +'%T')" | tee -a "$logfile" + exit 1 + ;; + esac +} + +display () { + case "$1" in + "title") + printf "\n##### %s\n" "$2" | tee -a "$logfile" + ;; + "subtitle") + printf "\n%s\n" "$2" | tee -a "$logfile" + ;; + "task") + printf "...%s @ %s\n" "$2" "$(date +'%T')" | tee -a "$logfile" + ;; + *) + printf "CRASH: display() called with incorrect arguments.\n" + exit 1 + ;; + esac +} + +pacman_install() { + action="installing $1 via pacman" && display "task" "$action" + if ! (pacman --noconfirm --needed -S "$1" >> "$logfile" 2>&1); then + action="retrying $1" && display "task" "$action" + if ! (pacman --noconfirm --needed -S "$1" >> "$logfile" 2>&1); then + action="retrying $1 once more" && display "task" "$action" + (pacman --noconfirm --needed -S "$1" >> "$logfile" 2>&1) || + error "error" "$action" "$?" + fi + fi +} + +aur_install() { + action="installing $1 via the AUR" && display "task" "$action" + if ! (sudo -u "$username" yay -S --noconfirm "$1" >> "$logfile" 2>&1); then + action="retrying $1" && display "task" "$action" + if ! (sudo -u "$username" yay -S --noconfirm "$1" >> "$logfile" 2>&1); then + action="retrying $1 once more" && display "task" "$action" + (sudo -u "$username" yay -S --noconfirm "$1" >> "$logfile" 2>&1) || + error "error" "$action" "$?" + fi + fi +} + +git_install() { + prog_name="$(basename "$1" .git)" + build_dir="$source_dir/$prog_name" + action="building & installing $prog_name from source" + display "task" "$action" + + if ! (sudo -u "$username" git clone --depth 1 "$1" "$build_dir" >> "$logfile" 2>&1); then + error "error" "cloning $prog_name - removing and retrying" "$?" + rm -rf "$build_dir" + (sudo -u "$username" git clone --depth 1 "$1" "$build_dir" >> "$logfile" 2>&1) || \ + error "crash" "re-cloning $prog_name" "$?" + fi + + (cd "$build_dir" && make install >> "$logfile" 2>&1) || \ + error "error" "building $prog_name" "$?" +} + +### ZFS-Specific Services +zfs_services() { + display "title" "ZFS Services Verification" + + display "subtitle" "Verifying ZFS Snapshot Services" + + # Verify sanoid is configured and running + action="checking sanoid configuration" && display "task" "$action" + if [ -f /etc/sanoid/sanoid.conf ]; then + echo " Sanoid config found" | tee -a "$logfile" + else + warn "Sanoid config not found - snapshots may not be automatic" + fi + + action="enabling sanoid timer" && display "task" "$action" + systemctl enable --now sanoid.timer >> "$logfile" 2>&1 || \ + error "error" "$action" "$?" + + # Verify pacman hook + action="checking pacman ZFS hook" && display "task" "$action" + if [ -f /etc/pacman.d/hooks/zfs-snapshot.hook ]; then + echo " Pacman ZFS hook found" | tee -a "$logfile" + else + error "error" "Pacman ZFS hook not found" "1" + fi + + # Show current ZFS status + display "subtitle" "ZFS Pool Status" + zpool status >> "$logfile" 2>&1 + zpool status + + display "subtitle" "ZFS Datasets" + zfs list >> "$logfile" 2>&1 + zfs list + + display "subtitle" "Recent Snapshots" + zfs list -t snapshot -o name,creation -s creation | tail -10 +} + +### Essential Services (ZFS variant - no btrfs tools) +essential_services() { + display "title" "Essential Services" + + # ... [Keep all the same services from original archsetup EXCEPT:] + # - Remove: timeshift-autosnap + # - Remove: grub-btrfs + # - Remove: grub-btrfsd configuration + + display "subtitle" "Randomness" + pacman_install rng-tools + systemctl enable rngd >> "$logfile" 2>&1 + + display "subtitle" "Networking" + pacman_install networkmanager + + display "subtitle" "Power" + pacman_install upower + systemctl enable upower >> "$logfile" 2>&1 + + display "subtitle" "Secure Shell" + pacman_install openssh + systemctl enable sshd >> "$logfile" 2>&1 + + display "subtitle" "Firewall" + pacman_install ufw + ufw default deny incoming >> "$logfile" 2>&1 + + for protocol in \ + "80,443,8080/tcp" \ + "ssh" \ + "22000/tcp" "22000/udp" "21027/udp" \ + ; do + action="adding ufw rule for $protocol" && display "task" "$action" + ufw allow $protocol >> "$logfile" 2>&1 || error "error" "$action" "$?" + done + + ufw limit 22/tcp >> "$logfile" 2>&1 + systemctl enable ufw.service >> "$logfile" 2>&1 + + display "subtitle" "Network Service Discovery" + pacman_install nss-mdns + pacman_install avahi + systemctl enable avahi-daemon.service >> "$logfile" 2>&1 + + display "subtitle" "Job Scheduling" + pacman_install cronie + systemctl enable cronie >> "$logfile" 2>&1 + + display "subtitle" "Package Repository Cache" + pacman_install pacman-contrib + systemctl enable --now paccache.timer >> "$logfile" 2>&1 + + # ZFS snapshot services (already configured by install-archzfs) + display "subtitle" "ZFS Snapshot Services" + action="verifying sanoid timer" && display "task" "$action" + systemctl enable --now sanoid.timer >> "$logfile" 2>&1 || \ + error "error" "$action" "$?" + + # NOTE: No grub-btrfs equivalent needed - ZFS boot environments + # are handled differently (via GRUB entries or zfsbootmenu) +} + +### Intro +intro() { + printf "\n\nArchSetup-ZFS launched @ %s\n" "$(date +'%D %T')" | tee -a "$logfile" + check_zfs + + STARTTIME=$(date +%s) + errors_encountered=0 + + [ -f "$logfile" ] && rm -f "$logfile" + touch "$logfile" + + pacman -Q > "$packages_before" || error "crash" "generating package list" "$?" +} + +### Outro +outro() { + display "title" "Cleanup" + + action="forcing password change on first login" && display "task" "$action" + chage -d 0 "$username" >> "$logfile" 2>&1 || error "error" "$action" "$?" + + display "subtitle" "Statistics" + pacman -Q > "$packages_after" + comm -13 --nocheck-order "$packages_before" "$packages_after" > "$archsetup_packages" + + ENDTIME=$(date +%s) + totalsecs=$((ENDTIME - STARTTIME)) + mins=$((totalsecs / 60)) + secs=$((totalsecs % 60)) + new_packages=$(wc -l < "$archsetup_packages") + + printf "\n" + printf "Completion time : %s\n" "$(date +'%D %T')" | tee -a "$logfile" + printf "Elapsed time : %s minutes, %s seconds\n" "$mins" "$secs" | tee -a "$logfile" + printf "Errors encountered : %s\n" "$errors_encountered" | tee -a "$logfile" + printf "Log file location : %s\n" "$logfile" + printf "Packages installed : %s\n" "$new_packages" + printf "\n" + + display "subtitle" "ZFS Quick Reference" + printf " List snapshots: zfs list -t snapshot\n" + printf " Manual snapshot: sudo zfs snapshot zroot/home@my-backup\n" + printf " Rollback: sudo zfs rollback zroot/home@my-backup\n" + printf " Pool status: zpool status\n" + printf " Replicate to NAS: sudo zfs-replicate\n" + printf "\n" + printf "Please reboot before working with your new workstation.\n\n" + + printf "=== ARCHSETUP_ZFS_EXECUTION_COMPLETE ===\n" | tee -a "$logfile" +} + +### Main - Placeholder for full implementation +main() { + echo "" + echo "==============================================" + echo " archsetup-zfs - ZFS Post-Installation Setup" + echo "==============================================" + echo "" + echo "This is a skeleton script. The full implementation" + echo "will include all sections from archsetup, modified" + echo "for ZFS systems." + echo "" + echo "For now, run the original archsetup from:" + echo " ~/code/archsetup/archsetup" + echo "" + echo "The ZFS-specific services (sanoid, pacman hooks," + echo "syncoid) were already configured by install-archzfs." + echo "" + + # When fully implemented, this will call: + # run_step "intro" intro + # run_step "prerequisites" prerequisites + # run_step "user_customizations" user_customizations + # run_step "aur_installer" aur_installer + # run_step "essential_services" essential_services # ZFS variant + # run_step "xorg" xorg + # run_step "dwm" dwm + # run_step "desktop_environment" desktop_environment + # run_step "developer_workstation" developer_workstation + # run_step "supplemental_software" supplemental_software + # run_step "boot_ux" boot_ux + # run_step "zfs_services" zfs_services # New ZFS-specific step + # outro +} + +main "$@" diff --git a/custom/install-archzfs b/custom/install-archzfs new file mode 100755 index 0000000..2afc9b6 --- /dev/null +++ b/custom/install-archzfs @@ -0,0 +1,831 @@ +#!/bin/bash +# install-archzfs - Arch Linux ZFS Root Installation Script +# Craig Jennings <c@cjennings.net> +# +# Installs Arch Linux on ZFS root with native encryption. +# Designed to be run from the custom archzfs ISO. + +set -e + +# These will be set interactively +HOSTNAME="" +USERNAME="" +TIMEZONE="" +LOCALE="en_US.UTF-8" +KEYMAP="us" + +# 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' + +# 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}"; } + +# 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" +fi + +### Interactive Configuration ### +configure_install() { + step "Installation Configuration" + echo "" + + # 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 + prompt "Select timezone region:" + PS3="Region: " + select region in "America" "Europe" "Asia" "Australia" "Pacific" "Other"; do + if [[ -n "$region" ]]; then + break + fi + done + + if [[ "$region" == "Other" ]]; then + prompt "Enter timezone (e.g., Etc/UTC):" + read -p "> " TIMEZONE + 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 + if [[ -n "$city" ]]; then + TIMEZONE="$region/$city" + break + fi + done + fi + + echo "" + + # Locale selection + 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 + if [[ -n "$loc" ]]; then + if [[ "$loc" == "Other" ]]; then + prompt "Enter locale (e.g., ja_JP.UTF-8):" + read -p "> " LOCALE + else + LOCALE="$loc" + fi + break + fi + done + + echo "" + + # Keymap selection + prompt "Select keyboard layout:" + PS3="Keymap: " + select km in "us" "uk" "de" "fr" "es" "dvorak" "Other"; do + if [[ -n "$km" ]]; then + if [[ "$km" == "Other" ]]; then + prompt "Enter keymap (e.g., jp106):" + read -p "> " KEYMAP + else + KEYMAP="$km" + fi + break + fi + done + + # Confirm settings + 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 "" + + read -p "Is this correct? [Y/n]: " confirm + if [[ "$confirm" == "n" || "$confirm" == "N" ]]; then + configure_install + fi +} + +### Disk Selection ### +select_disk() { + step "Disk Selection" + + echo "" + echo "Available disks:" + echo "----------------" + lsblk -d -o NAME,SIZE,MODEL,TYPE | grep disk + echo "" + + # Get list of disks + mapfile -t DISKS < <(lsblk -d -n -o NAME,TYPE | awk '$2=="disk"{print $1}') + + if [[ ${#DISKS[@]} -eq 0 ]]; then + error "No disks found!" + fi + + PS3="Select disk for installation (number): " + select disk in "${DISKS[@]}"; do + if [[ -n "$disk" ]]; then + DISK="/dev/$disk" + break + fi + done + + echo "" + warn "Selected disk: $DISK" + echo "" + lsblk "$DISK" + echo "" + + read -p "This will DESTROY all data on $DISK. Type 'yes' to continue: " confirm + [[ "$confirm" != "yes" ]] && error "Aborted by user" +} + +### 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" + else + EFI_PART="${DISK}1" + ZFS_PART="${DISK}2" + fi + + # 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" + + info "Partitioning complete." + lsblk "$DISK" +} + +### 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 "" + + # Create encrypted pool + 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" "$ZFS_PART" + + info "ZFS pool created successfully." +} + +### 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" + + # Mount root first + zfs mount "$POOL_NAME/ROOT/default" + + # Home datasets + 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 + zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media" + + # VMs dataset - larger 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" + + # Exclude temp directories 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 +} + +### Mount EFI ### +mount_efi() { + step "Mounting EFI Partition" + + mkdir -p /mnt/boot + mount "$EFI_PART" /mnt/boot + + info "EFI partition mounted at /mnt/boot" +} + +### Install Base System ### +install_base() { + step "Installing Base System" + + info "Updating pacman keys..." + pacman-key --init + pacman-key --populate archlinux + pacman-key -r DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true + pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true + + info "Installing base packages (this takes a while)..." + pacstrap -K /mnt \ + base \ + base-devel \ + linux \ + linux-headers \ + linux-firmware \ + zfs-linux \ + zfs-utils \ + grub \ + efibootmgr \ + networkmanager \ + openssh \ + git \ + vim \ + sudo \ + zsh \ + nodejs \ + npm \ + sanoid + + info "Base system installed." +} + +### Configure System ### +configure_system() { + step "Configuring System" + + # Generate fstab (only for EFI, ZFS handles the rest) + 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 + + # 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 to installed system + info "Adding archzfs repository..." + cat >> /mnt/etc/pacman.conf << 'EOF' + +[archzfs] +Server = https://archzfs.com/$repo/$arch +SigLevel = Optional TrustAll +EOF + + # Import archzfs key in chroot + arch-chroot /mnt pacman-key -r DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true + arch-chroot /mnt pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76 2>/dev/null || true +} + +### 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" + + # Configure GRUB defaults + cat > /mnt/etc/default/grub << EOF +GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR="Arch Linux (ZFS)" +GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet" +GRUB_CMDLINE_LINUX="root=ZFS=$POOL_NAME/ROOT/default" +GRUB_PRELOAD_MODULES="part_gpt part_msdos zfs" +GRUB_TERMINAL_OUTPUT="console" +GRUB_DISABLE_OS_PROBER=true +GRUB_GFXMODE=auto +GRUB_GFXPAYLOAD_LINUX=keep +EOF + + info "Installing GRUB..." + arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB + + info "Generating GRUB configuration..." + arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg +} + +### 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 + arch-chroot /mnt systemctl enable zfs-import.target + + # Generate zpool cache + mkdir -p /mnt/etc/zfs + zpool set cachefile=/etc/zfs/zpool.cache "$POOL_NAME" + cp /etc/zfs/zpool.cache /mnt/etc/zfs/ + + # Set bootfs property + zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" + + # Enable other services + arch-chroot /mnt systemctl enable NetworkManager + arch-chroot /mnt systemctl enable sshd + + 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" + + 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 +# 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 + echo "Warning: Failed to create snapshot" >&2 +fi +EOF + + chmod +x /mnt/usr/local/bin/zfs-pre-snapshot + + info "Pacman hook configured. Snapshots will be created before each transaction." +} + +### Create User ### +create_user() { + step "Creating User: $USERNAME" + + arch-chroot /mnt useradd -m -G wheel -s /bin/zsh "$USERNAME" 2>/dev/null || \ + warn "User $USERNAME may already exist" + + # Set ownership of home dataset + arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" + + # Configure sudo + echo "%wheel ALL=(ALL:ALL) ALL" > /mnt/etc/sudoers.d/wheel + chmod 440 /mnt/etc/sudoers.d/wheel + + 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" + + echo "" + info "Set password for root:" + arch-chroot /mnt passwd +} + +### Copy archsetup ### +copy_archsetup() { + step "Copying archsetup to New System" + + 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 +} + +### Create Syncoid Script for TrueNAS ### +create_syncoid_script() { + step "Creating Syncoid Replication Script" + + cat > /mnt/usr/local/bin/zfs-replicate << 'SCRIPT' +#!/bin/bash +# zfs-replicate - Replicate ZFS datasets to TrueNAS +# Usage: zfs-replicate [dataset] [target] +# +# Examples: +# zfs-replicate # Replicate all configured datasets +# zfs-replicate zroot/home user@truenas:/tank/backup/laptop + +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 + +# Full replication mode +info "Starting ZFS replication to $TRUENAS_HOST" +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 "" +done + +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 + + cat > /mnt/etc/systemd/system/zfs-replicate.timer << 'EOF' +[Unit] +Description=Run ZFS replication nightly + +[Timer] +OnCalendar=*-*-* 02:00:00 +RandomizedDelaySec=1800 +Persistent=true + +[Install] +WantedBy=timers.target +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" +} + +### Unmount and Export ### +cleanup() { + step "Cleaning Up" + + info "Unmounting filesystems..." + umount /mnt/boot 2>/dev/null || true + + info "Exporting ZFS pool..." + zpool export "$POOL_NAME" + + info "Cleanup complete." +} + +### Print Summary ### +print_summary() { + echo "" + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Installation Complete! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e "${BOLD}System Configuration:${NC}" + 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 " - 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 "" + 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 "" + 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 "" + 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" + echo "" +} + +### Main Installation Flow ### +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 "" + + info "Installation log: $LOGFILE" + echo "" + + configure_install + select_disk + partition_disk + create_zfs_pool + create_datasets + mount_efi + install_base + configure_system + configure_initramfs + configure_bootloader + configure_zfs_services + configure_sanoid + configure_pacman_hook + create_user + copy_archsetup + create_syncoid_script + cleanup + print_summary +} + +# Handle interrupts +trap 'error "Installation interrupted!"' INT TERM + +# Run main +main "$@" diff --git a/custom/install-claude b/custom/install-claude new file mode 100755 index 0000000..e467108 --- /dev/null +++ b/custom/install-claude @@ -0,0 +1,24 @@ +#!/bin/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/download-archzfs-iso.sh b/download-archzfs-iso.sh new file mode 100755 index 0000000..65b5f3d --- /dev/null +++ b/download-archzfs-iso.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# download-archzfs-iso.sh - Download the official archzfs ISO and add our scripts +# +# The archzfs project maintains ISOs with matched kernel+ZFS versions. +# This script downloads their ISO and creates a script bundle to use with it. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="$SCRIPT_DIR/out" +CUSTOM_DIR="$SCRIPT_DIR/custom" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +mkdir -p "$OUT_DIR" + +echo "" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo " ArchZFS ISO Setup" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Check for existing archzfs ISO +EXISTING_ISO=$(ls "$OUT_DIR"/archlinux-*-zfs-*.iso 2>/dev/null | head -1) + +if [[ -n "$EXISTING_ISO" ]]; then + info "Found existing archzfs ISO: $(basename "$EXISTING_ISO")" + read -p "Use this ISO? [Y/n]: " use_existing + if [[ "$use_existing" != "n" && "$use_existing" != "N" ]]; then + ISO_FILE="$EXISTING_ISO" + fi +fi + +if [[ -z "$ISO_FILE" ]]; then + info "Fetching latest archzfs ISO URL..." + + # Get the latest ISO from archzfs releases + RELEASE_URL="https://github.com/archzfs/archzfs/releases" + + echo "" + echo "Please download the latest archzfs ISO from:" + echo -e " ${CYAN}$RELEASE_URL${NC}" + echo "" + echo "Look for: archlinux-YYYY.MM.DD-zfs-linux-lts-x86_64.iso" + echo "Save it to: $OUT_DIR/" + echo "" + read -p "Press Enter once downloaded, or Ctrl+C to abort..." + + ISO_FILE=$(ls "$OUT_DIR"/archlinux-*-zfs-*.iso 2>/dev/null | head -1) + + if [[ -z "$ISO_FILE" ]]; then + echo "No archzfs ISO found in $OUT_DIR/" + exit 1 + fi +fi + +info "Using ISO: $ISO_FILE" + +# Create a tarball of our custom scripts +info "Creating script bundle..." + +BUNDLE_DIR=$(mktemp -d) +mkdir -p "$BUNDLE_DIR/archzfs-scripts" + +# Copy our scripts +cp "$CUSTOM_DIR/install-archzfs" "$BUNDLE_DIR/archzfs-scripts/" +cp "$CUSTOM_DIR/install-claude" "$BUNDLE_DIR/archzfs-scripts/" +cp "$CUSTOM_DIR/archsetup-zfs" "$BUNDLE_DIR/archzfs-scripts/" + +# Copy archsetup if available +if [[ -d /home/cjennings/code/archsetup ]]; then + info "Including archsetup..." + cp -r /home/cjennings/code/archsetup "$BUNDLE_DIR/archzfs-scripts/" + rm -rf "$BUNDLE_DIR/archzfs-scripts/archsetup/.git" + rm -rf "$BUNDLE_DIR/archzfs-scripts/archsetup/.claude" +fi + +# Create setup script +cat > "$BUNDLE_DIR/archzfs-scripts/setup.sh" << 'SETUP' +#!/bin/bash +# Run this after booting the archzfs ISO +# It copies the installation scripts to the right places + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Setting up archzfs installation scripts..." + +# Copy scripts to /usr/local/bin +cp "$SCRIPT_DIR/install-archzfs" /usr/local/bin/ +cp "$SCRIPT_DIR/install-claude" /usr/local/bin/ +cp "$SCRIPT_DIR/archsetup-zfs" /usr/local/bin/ +chmod +x /usr/local/bin/install-archzfs +chmod +x /usr/local/bin/install-claude +chmod +x /usr/local/bin/archsetup-zfs + +# Copy archsetup to /code +if [[ -d "$SCRIPT_DIR/archsetup" ]]; then + mkdir -p /code + cp -r "$SCRIPT_DIR/archsetup" /code/ + echo "archsetup copied to /code/archsetup" +fi + +echo "" +echo "Setup complete! You can now run:" +echo " install-archzfs" +echo "" +SETUP +chmod +x "$BUNDLE_DIR/archzfs-scripts/setup.sh" + +# Create the tarball +BUNDLE_FILE="$OUT_DIR/archzfs-scripts.tar.gz" +tar -czf "$BUNDLE_FILE" -C "$BUNDLE_DIR" archzfs-scripts +rm -rf "$BUNDLE_DIR" + +info "Script bundle created: $BUNDLE_FILE" + +echo "" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${GREEN}Setup Complete!${NC}" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" +echo "To install Arch on ZFS:" +echo "" +echo "1. Boot from the archzfs ISO:" +echo " $(basename "$ISO_FILE")" +echo "" +echo "2. Connect to network, then download and extract scripts:" +echo " # If you have a web server or USB drive with the bundle:" +echo " tar -xzf archzfs-scripts.tar.gz" +echo " cd archzfs-scripts && ./setup.sh" +echo "" +echo "3. Run the installer:" +echo " install-archzfs" +echo "" +echo "Alternative: Copy scripts via SSH from another machine" +echo "" diff --git a/scripts/test-vm.sh b/scripts/test-vm.sh new file mode 100755 index 0000000..581fa6c --- /dev/null +++ b/scripts/test-vm.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# test-vm.sh - Test the archzfs ISO in a QEMU virtual machine +# +# Usage: +# ./test-vm.sh # Create new VM and boot ISO +# ./test-vm.sh --boot-disk # Boot from existing virtual disk (after install) +# ./test-vm.sh --clean # Remove VM disk and start fresh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# VM Configuration +VM_NAME="archzfs-test" +VM_DIR="$PROJECT_DIR/vm" +VM_DISK="$VM_DIR/$VM_NAME.qcow2" +VM_DISK_SIZE="50G" +VM_RAM="4096" +VM_CPUS="4" + +# UEFI firmware (adjust path for your system) +OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" +OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd" +OVMF_VARS="$VM_DIR/OVMF_VARS.fd" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +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; } + +# Find the ISO +find_iso() { + ISO_FILE=$(ls -t "$PROJECT_DIR/out/"*.iso 2>/dev/null | head -1) + if [[ -z "$ISO_FILE" ]]; then + error "No ISO found in $PROJECT_DIR/out/" + echo "Build the ISO first with: sudo ./build.sh" + exit 1 + fi + info "Using ISO: $ISO_FILE" +} + +# Check dependencies +check_deps() { + local missing=() + + command -v qemu-system-x86_64 >/dev/null 2>&1 || missing+=("qemu") + + if [[ ! -f "$OVMF_CODE" ]]; then + missing+=("edk2-ovmf") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Missing dependencies: ${missing[*]}" + echo "Install with: sudo pacman -S ${missing[*]}" + exit 1 + fi +} + +# Create VM directory and disk +setup_vm() { + mkdir -p "$VM_DIR" + + if [[ ! -f "$VM_DISK" ]]; then + info "Creating virtual disk: $VM_DISK ($VM_DISK_SIZE)" + qemu-img create -f qcow2 "$VM_DISK" "$VM_DISK_SIZE" + else + info "Using existing disk: $VM_DISK" + fi + + # Copy OVMF vars if needed + if [[ ! -f "$OVMF_VARS" ]]; then + info "Setting up UEFI variables" + cp "$OVMF_VARS_ORIG" "$OVMF_VARS" + fi +} + +# Clean up VM files +clean_vm() { + warn "Removing VM files..." + rm -f "$VM_DISK" + rm -f "$OVMF_VARS" + info "VM files removed. Ready for fresh install." +} + +# Boot VM from ISO +boot_iso() { + find_iso + setup_vm + + info "Starting VM with ISO..." + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo " VM: $VM_NAME" + echo " RAM: ${VM_RAM}MB | CPUs: $VM_CPUS" + echo " Disk: $VM_DISK_SIZE" + echo " ISO: $(basename "$ISO_FILE")" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo "Tips:" + echo " - Press Ctrl+Alt+G to release mouse grab" + echo " - Press Ctrl+Alt+F to toggle fullscreen" + echo " - Run 'install-archzfs' to start installation" + echo "" + + qemu-system-x86_64 \ + -name "$VM_NAME" \ + -machine q35,accel=kvm \ + -cpu host \ + -smp "$VM_CPUS" \ + -m "$VM_RAM" \ + -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \ + -drive if=pflash,format=raw,file="$OVMF_VARS" \ + -drive file="$VM_DISK",format=qcow2,if=virtio \ + -cdrom "$ISO_FILE" \ + -boot d \ + -netdev user,id=net0,hostfwd=tcp::2222-:22 \ + -device virtio-net-pci,netdev=net0 \ + -device virtio-vga-gl \ + -display gtk,gl=on \ + -audiodev pipewire,id=audio0 \ + -device ich9-intel-hda \ + -device hda-duplex,audiodev=audio0 \ + -usb \ + -device usb-tablet +} + +# Boot VM from disk (after installation) +boot_disk() { + setup_vm + + if [[ ! -f "$VM_DISK" ]]; then + error "No disk found. Run without --boot-disk first to install." + fi + + info "Booting from installed disk..." + echo "" + echo "SSH access: ssh -p 2222 localhost" + echo "" + + qemu-system-x86_64 \ + -name "$VM_NAME" \ + -machine q35,accel=kvm \ + -cpu host \ + -smp "$VM_CPUS" \ + -m "$VM_RAM" \ + -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \ + -drive if=pflash,format=raw,file="$OVMF_VARS" \ + -drive file="$VM_DISK",format=qcow2,if=virtio \ + -boot c \ + -netdev user,id=net0,hostfwd=tcp::2222-:22 \ + -device virtio-net-pci,netdev=net0 \ + -device virtio-vga-gl \ + -display gtk,gl=on \ + -audiodev pipewire,id=audio0 \ + -device ich9-intel-hda \ + -device hda-duplex,audiodev=audio0 \ + -usb \ + -device usb-tablet +} + +# Show help +show_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " (none) Create VM and boot from ISO for installation" + echo " --boot-disk Boot from existing virtual disk (after install)" + echo " --clean Remove VM disk and start fresh" + echo " --help Show this help message" + echo "" + echo "VM Configuration (edit this script to change):" + echo " Disk size: $VM_DISK_SIZE" + echo " RAM: ${VM_RAM}MB" + echo " CPUs: $VM_CPUS" + echo "" + echo "SSH into running VM:" + echo " ssh -p 2222 localhost" +} + +# Main +check_deps + +case "${1:-}" in + --boot-disk) + boot_disk + ;; + --clean) + clean_vm + ;; + --help|-h) + show_help + ;; + "") + boot_iso + ;; + *) + error "Unknown option: $1" + show_help + exit 1 + ;; +esac diff --git a/vm/OVMF_VARS.fd b/vm/OVMF_VARS.fd Binary files differnew file mode 100644 index 0000000..90366ac --- /dev/null +++ b/vm/OVMF_VARS.fd diff --git a/vm/archzfs-test.qcow2 b/vm/archzfs-test.qcow2 Binary files differnew file mode 100644 index 0000000..1436695 --- /dev/null +++ b/vm/archzfs-test.qcow2 |
