diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-23 11:54:25 -0600 |
| commit | fd2ea796b20bcbebea19c43978fb08e3cd6754ed (patch) | |
| tree | 344efeff361b1c8953bd4f56d304a8ec70637899 /installer/lib/zfs.sh | |
| parent | 8560e2a2798f9318fb28283d5ef7242fed20d447 (diff) | |
| download | archangel-fd2ea796b20bcbebea19c43978fb08e3cd6754ed.tar.gz archangel-fd2ea796b20bcbebea19c43978fb08e3cd6754ed.zip | |
refactor: rename custom/ to installer/ for clarity
The custom/ directory name was an archiso implementation detail. Renamed
to installer/ which clearly communicates that this directory contains the
installer scripts and utilities that ship on the ISO.
Updated all references in build.sh, Makefile, test-install.sh, and README.
Diffstat (limited to 'installer/lib/zfs.sh')
| -rw-r--r-- | installer/lib/zfs.sh | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/installer/lib/zfs.sh b/installer/lib/zfs.sh new file mode 100644 index 0000000..feda91d --- /dev/null +++ b/installer/lib/zfs.sh @@ -0,0 +1,359 @@ +#!/usr/bin/env bash +# zfs.sh - ZFS-specific functions for archangel installer +# Source this file after common.sh, config.sh, disk.sh + +############################# +# ZFS Constants +############################# + +POOL_NAME="${POOL_NAME:-zroot}" +ASHIFT="${ASHIFT:-12}" +COMPRESSION="${COMPRESSION:-zstd}" + +############################# +# ZFS Pre-flight +############################# + +zfs_preflight() { + # Check ZFS module + if ! lsmod | grep -q zfs; then + info "Loading ZFS module..." + modprobe zfs || error "Failed to load ZFS module. Is zfs-linux-lts installed?" + fi + info "ZFS module loaded successfully." +} + +############################# +# ZFS Pool Creation +############################# + +create_zfs_pool() { + local encryption="${1:-true}" + local passphrase="$2" + + step "Creating ZFS Pool" + + # Destroy existing pool if present + if zpool list "$POOL_NAME" &>/dev/null; then + warn "Pool $POOL_NAME already exists. Destroying..." + zpool destroy -f "$POOL_NAME" + fi + + # Get root partitions + local zfs_parts=() + for disk in "${SELECTED_DISKS[@]}"; do + zfs_parts+=("$(get_root_partition "$disk")") + done + + # Build pool configuration based on RAID level + local pool_config + if [[ "$RAID_LEVEL" == "stripe" ]]; then + pool_config="${zfs_parts[*]}" + info "Creating striped pool with ${#zfs_parts[@]} disks (NO redundancy)..." + warn "Data loss will occur if ANY disk fails!" + elif [[ -n "$RAID_LEVEL" ]]; then + pool_config="$RAID_LEVEL ${zfs_parts[*]}" + info "Creating $RAID_LEVEL pool with ${#zfs_parts[@]} disks..." + else + pool_config="${zfs_parts[0]}" + info "Creating single-disk pool..." + fi + + # Base pool options + local pool_opts=( + -f + -o ashift="$ASHIFT" + -o autotrim=on + -O acltype=posixacl + -O atime=off + -O canmount=off + -O compression="$COMPRESSION" + -O dnodesize=auto + -O normalization=formD + -O relatime=on + -O xattr=sa + -O mountpoint=none + -R /mnt + ) + + # Create pool (with or without encryption) + if [[ "$encryption" == "false" ]]; then + warn "Creating pool WITHOUT encryption" + zpool create "${pool_opts[@]}" "$POOL_NAME" $pool_config + else + info "Creating encrypted pool..." + echo "$passphrase" | zpool create "${pool_opts[@]}" \ + -O encryption=aes-256-gcm \ + -O keyformat=passphrase \ + -O keylocation=prompt \ + "$POOL_NAME" $pool_config + fi + + info "ZFS pool created successfully." + zpool status "$POOL_NAME" +} + +############################# +# ZFS Dataset Creation +############################# + +create_zfs_datasets() { + step "Creating ZFS Datasets" + + # Root dataset container + zfs create -o mountpoint=none -o canmount=off "$POOL_NAME/ROOT" + + # Calculate reservation (20% of pool, capped 5-20G) + local pool_size_bytes + pool_size_bytes=$(zpool get -Hp size "$POOL_NAME" | awk '{print $3}') + local pool_size_gb=$((pool_size_bytes / 1024 / 1024 / 1024)) + local reserve_gb=$((pool_size_gb / 5)) + [[ $reserve_gb -gt 20 ]] && reserve_gb=20 + [[ $reserve_gb -lt 5 ]] && reserve_gb=5 + + # Main root filesystem + zfs create -o mountpoint=/ -o canmount=noauto -o reservation=${reserve_gb}G "$POOL_NAME/ROOT/default" + zfs mount "$POOL_NAME/ROOT/default" + + # Home + zfs create -o mountpoint=/home "$POOL_NAME/home" + zfs create -o mountpoint=/root "$POOL_NAME/home/root" + + # Media - compression off for already-compressed files + zfs create -o mountpoint=/media -o compression=off "$POOL_NAME/media" + + # VMs - 64K recordsize for VM disk images + zfs create -o mountpoint=/vms -o recordsize=64K "$POOL_NAME/vms" + + # Var datasets + zfs create -o mountpoint=/var -o canmount=off "$POOL_NAME/var" + zfs create -o mountpoint=/var/log "$POOL_NAME/var/log" + zfs create -o mountpoint=/var/cache "$POOL_NAME/var/cache" + zfs create -o mountpoint=/var/lib -o canmount=off "$POOL_NAME/var/lib" + zfs create -o mountpoint=/var/lib/pacman "$POOL_NAME/var/lib/pacman" + zfs create -o mountpoint=/var/lib/docker "$POOL_NAME/var/lib/docker" + + # Temp directories - excluded from snapshots + zfs create -o mountpoint=/var/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/var/tmp" + zfs create -o mountpoint=/tmp -o com.sun:auto-snapshot=false "$POOL_NAME/tmp" + chmod 1777 /mnt/tmp /mnt/var/tmp + + info "Datasets created:" + zfs list -r "$POOL_NAME" -o name,mountpoint,compression +} + +############################# +# ZFSBootMenu Configuration +############################# + +configure_zfsbootmenu() { + step "Configuring ZFSBootMenu" + + # Ensure hostid exists + if [[ ! -f /etc/hostid ]]; then + zgenhostid + fi + local host_id + host_id=$(hostid) + + # Copy hostid to installed system + cp /etc/hostid /mnt/etc/hostid + + # Create ZFSBootMenu directory on EFI + mkdir -p /mnt/efi/EFI/ZBM + + # Download ZFSBootMenu release EFI binary + info "Downloading ZFSBootMenu..." + local zbm_url="https://get.zfsbootmenu.org/efi" + if ! curl -fsSL -o /mnt/efi/EFI/ZBM/zfsbootmenu.efi "$zbm_url"; then + error "Failed to download ZFSBootMenu" + fi + info "ZFSBootMenu binary installed." + + # Set kernel command line on the ROOT PARENT dataset + local cmdline="rw loglevel=3" + + # Add AMD GPU workarounds if needed + if lspci | grep -qi "amd.*display\|amd.*vga"; then + info "AMD GPU detected - adding workaround parameters" + cmdline="$cmdline amdgpu.pg_mask=0 amdgpu.cwsr_enable=0" + fi + + zfs set org.zfsbootmenu:commandline="$cmdline" "$POOL_NAME/ROOT" + info "Kernel command line set on $POOL_NAME/ROOT" + + # Set bootfs property + zpool set bootfs="$POOL_NAME/ROOT/default" "$POOL_NAME" + info "Default boot filesystem set to $POOL_NAME/ROOT/default" + + # Create EFI boot entries for each disk + local zbm_cmdline="spl_hostid=0x${host_id} zbm.timeout=3 zbm.prefer=${POOL_NAME} zbm.import_policy=hostid" + + for i in "${!SELECTED_DISKS[@]}"; do + local disk="${SELECTED_DISKS[$i]}" + local label="ZFSBootMenu" + if [[ ${#SELECTED_DISKS[@]} -gt 1 ]]; then + label="ZFSBootMenu-disk$((i+1))" + fi + + info "Creating EFI boot entry: $label on $disk" + efibootmgr --create \ + --disk "$disk" \ + --part 1 \ + --label "$label" \ + --loader '\EFI\ZBM\zfsbootmenu.efi' \ + --unicode "$zbm_cmdline" \ + --quiet + done + + # Set as primary boot option + local bootnum + bootnum=$(efibootmgr | grep "ZFSBootMenu" | head -1 | grep -oP 'Boot\K[0-9A-F]+') + if [[ -n "$bootnum" ]]; then + local current_order + current_order=$(efibootmgr | grep "BootOrder" | cut -d: -f2 | tr -d ' ') + efibootmgr --bootorder "$bootnum,$current_order" --quiet + info "ZFSBootMenu set as primary boot option" + fi + + info "ZFSBootMenu configuration complete." +} + +############################# +# ZFS Services +############################# + +configure_zfs_services() { + step "Configuring ZFS Services" + + arch-chroot /mnt systemctl enable zfs.target + arch-chroot /mnt systemctl disable zfs-import-cache.service + arch-chroot /mnt systemctl enable zfs-import-scan.service + arch-chroot /mnt systemctl enable zfs-mount.service + arch-chroot /mnt systemctl enable zfs-import.target + + # Disable cachefile - we use zfs-import-scan + zpool set cachefile=none "$POOL_NAME" + rm -f /mnt/etc/zfs/zpool.cache + + info "ZFS services configured." +} + +############################# +# Pacman Snapshot Hook +############################# + +configure_zfs_pacman_hook() { + step "Configuring Pacman Snapshot Hook" + + mkdir -p /mnt/etc/pacman.d/hooks + + cat > /mnt/etc/pacman.d/hooks/zfs-snapshot.hook << EOF +[Trigger] +Operation = Upgrade +Operation = Install +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Creating ZFS snapshot before pacman transaction... +When = PreTransaction +Exec = /usr/local/bin/zfs-pre-snapshot +EOF + + cat > /mnt/usr/local/bin/zfs-pre-snapshot << 'EOF' +#!/bin/bash +POOL="zroot" +DATASET="$POOL/ROOT/default" +TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) +SNAPSHOT_NAME="pre-pacman_$TIMESTAMP" + +if zfs snapshot "$DATASET@$SNAPSHOT_NAME"; then + echo "Created snapshot: $DATASET@$SNAPSHOT_NAME" +else + echo "Warning: Failed to create snapshot" >&2 +fi +EOF + + chmod +x /mnt/usr/local/bin/zfs-pre-snapshot + info "Pacman hook configured." +} + +############################# +# ZFS Tools +############################# + +install_zfs_tools() { + step "Installing ZFS Management Tools" + + # Copy ZFS management scripts + cp /usr/local/bin/zfssnapshot /mnt/usr/local/bin/zfssnapshot + cp /usr/local/bin/zfsrollback /mnt/usr/local/bin/zfsrollback + chmod +x /mnt/usr/local/bin/zfssnapshot + chmod +x /mnt/usr/local/bin/zfsrollback + + info "ZFS management scripts installed: zfssnapshot, zfsrollback" +} + +############################# +# EFI Sync (Multi-disk) +############################# + +sync_zfs_efi_partitions() { + local efi_parts=() + for disk in "${SELECTED_DISKS[@]}"; do + efi_parts+=("$(get_efi_partition "$disk")") + done + + # Skip if only one disk + [[ ${#efi_parts[@]} -le 1 ]] && return + + step "Syncing EFI partitions for redundancy" + + for ((i=1; i<${#efi_parts[@]}; i++)); do + local secondary="${efi_parts[$i]}" + local tmp_mount="/tmp/efi_sync_$$" + + mkdir -p "$tmp_mount" + mount "$secondary" "$tmp_mount" + rsync -a /mnt/efi/ "$tmp_mount/" + umount "$tmp_mount" + rmdir "$tmp_mount" + + info "Synced EFI to $secondary" + done +} + +############################# +# Genesis Snapshot +############################# + +create_zfs_genesis_snapshot() { + step "Creating Genesis Snapshot" + + local snapshot_name="genesis" + zfs snapshot -r "$POOL_NAME@$snapshot_name" + + info "Genesis snapshot created: $POOL_NAME@$snapshot_name" + info "You can restore to this point anytime with: zfsrollback $snapshot_name" +} + +############################# +# ZFS Cleanup +############################# + +zfs_cleanup() { + step "Cleaning up ZFS" + + # Unmount all ZFS datasets + zfs unmount -a 2>/dev/null || true + + # Unmount EFI + umount /mnt/efi 2>/dev/null || true + + # Export pool (important for clean import on boot) + zpool export "$POOL_NAME" + + info "ZFS pool exported cleanly." +} |
