aboutsummaryrefslogtreecommitdiff
path: root/custom/lib/zfs.sh
blob: bb948fb951019577ade5ab50f804e7b8908dbce9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#!/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"

    local primary="${efi_parts[0]}"
    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."
}