aboutsummaryrefslogtreecommitdiff
path: root/installer/lib/disk.sh
blob: ae7801bb996a379bb604e0fca2a4cae27f3933e8 (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
#!/usr/bin/env bash
# disk.sh - Disk partitioning functions for archangel installer
# Source this file after common.sh

#############################
# Partition Disks
#############################

# Partition a single disk for ZFS or Btrfs installation. Wipes
# non-GPT signatures (LVM, mdadm, ext) with wipefs, zaps the GPT
# with sgdisk, then lays down a 512M EFI partition plus a root
# partition that fills the rest. Root partition type code is
# selected from FILESYSTEM (BF00 for ZFS, 8300 for Btrfs).
partition_disk() {
    local disk="$1"
    local efi_size="${2:-512M}"

    local root_type="BF00"
    if [[ "$FILESYSTEM" == "btrfs" ]]; then
        root_type="8300"
    fi

    info "Partitioning $disk..."

    wipefs -af "$disk" || error "Failed to wipe signatures on $disk"
    sgdisk --zap-all "$disk" || error "Failed to zap GPT on $disk"
    sgdisk -n 1:0:+${efi_size} -t 1:EF00 -c 1:"EFI" "$disk" || error "Failed to create EFI partition on $disk"
    sgdisk -n 2:0:0 -t 2:$root_type -c 2:"ROOT" "$disk" || error "Failed to create root partition on $disk"

    partprobe "$disk" 2>/dev/null || true
    sleep 1

    info "Partitioned $disk: EFI=${efi_size}, ROOT=remainder"
}

# Partition every disk in SELECTED_DISKS, format each EFI partition,
# and populate the EFI_PARTS + ROOT_PARTS arrays for downstream
# callers (create_zfs_pool, btrfs_open_encryption,
# sync_efi_partitions, fstab generation).
#
# EFI labels are EFI0, EFI1, ... in selection order so multi-disk
# layouts get a stable, distinguishable scheme that lsblk -f can
# show. Errors out if SELECTED_DISKS is empty so a misconfigured
# install can't silently skip partitioning.
partition_disks() {
    if [[ ${#SELECTED_DISKS[@]} -eq 0 ]]; then
        error "partition_disks: SELECTED_DISKS is empty"
    fi

    step "Partitioning ${#SELECTED_DISKS[@]} disk(s)"

    EFI_PARTS=()
    ROOT_PARTS=()

    for disk in "${SELECTED_DISKS[@]}"; do
        partition_disk "$disk"
        EFI_PARTS+=("$(get_efi_partition "$disk")")
        ROOT_PARTS+=("$(get_root_partition "$disk")")
    done

    sleep 2

    for i in "${!EFI_PARTS[@]}"; do
        info "Formatting EFI partition ${EFI_PARTS[$i]}..."
        mkfs.fat -F32 -n "EFI$i" "${EFI_PARTS[$i]}" || error "Failed to format ${EFI_PARTS[$i]}"
    done

    info "Partitioning complete. Created ${#EFI_PARTS[@]} EFI and ${#ROOT_PARTS[@]} ROOT partitions."
}

#############################
# Partition Helpers
#############################

# Get EFI partition path for a disk
get_efi_partition() {
    local disk="$1"
    if [[ "$disk" =~ nvme ]]; then
        echo "${disk}p1"
    else
        echo "${disk}1"
    fi
}

# Get root partition path for a disk
get_root_partition() {
    local disk="$1"
    if [[ "$disk" =~ nvme ]]; then
        echo "${disk}p2"
    else
        echo "${disk}2"
    fi
}

#############################
# Disk Selection (Interactive)
#############################

# Interactive disk selection using fzf
select_disks() {
    local available
    available=$(list_available_disks)

    if [[ -z "$available" ]]; then
        error "No available disks found"
    fi

    step "Select installation disk(s)"
    prompt "Use Tab to select multiple disks for RAID, Enter to confirm"

    local selected
    if has_fzf; then
        selected=$(echo "$available" | fzf --multi --prompt="Select disk(s): " --height=15 --reverse)
    else
        echo "$available"
        read -rp "Enter disk path(s) separated by space: " selected
    fi

    if [[ -z "$selected" ]]; then
        error "No disk selected"
    fi

    # Extract just the device paths (remove size/model info)
    SELECTED_DISKS=()
    while IFS= read -r line; do
        local disk
        disk=$(echo "$line" | cut -d' ' -f1)
        SELECTED_DISKS+=("$disk")
    done <<< "$selected"

    info "Selected disks: ${SELECTED_DISKS[*]}"
}

#############################
# Pre-flight: Disk Safety
#############################

# Minimum usable install disk. Root plus the 50G reservation, packages, and
# snapshots needs real headroom; below this the install fails partway
# through. 20 GB is a hard floor (validate_install_targets errors out).
# Decimal GB (disk-vendor sizing) on purpose: it reads as the natural "20GB"
# minimum and clears a 20 GiB disk image with headroom rather than sitting
# exactly on the boundary.
MIN_DISK_BYTES=20000000000   # 20 * 10^9 (20 GB)

# Pure size predicate: succeed only when <bytes> is a non-negative integer
# meeting MIN_DISK_BYTES. Non-numeric or empty input fails (treated as an
# unknown size, which is itself a reason not to proceed).
disk_meets_min_size() {
    local bytes="$1"
    [[ "$bytes" =~ ^[0-9]+$ ]] || return 1
    (( bytes >= MIN_DISK_BYTES ))
}

# Size of a block device in bytes (live query). Thin wrapper over blockdev;
# exercised by the VM integration harness rather than unit tests.
disk_size_bytes() {
    blockdev --getsize64 "$1" 2>/dev/null
}

# Succeed (return 0) when <disk> is in active use and must NOT be wiped:
# any partition mounted, active swap on it, or membership in an imported
# zpool or assembled md array. Over-detection errs on the safe side
# (refuse). Live-state predicate — validated in the VM harness, where the
# install disks are deliberately idle so the happy path returns 1.
disk_in_use() {
    local disk="$1"
    local base
    base=$(basename "$disk")

    # Any mountpoint on the disk or its children.
    if lsblk -nro MOUNTPOINT "$disk" 2>/dev/null | grep -q .; then
        return 0
    fi
    # Active swap on the disk or a partition of it.
    if swapon --show=NAME --noheadings 2>/dev/null | grep -q "^${disk}"; then
        return 0
    fi
    # Member of an imported zpool. -P prints full device paths (/dev/vda2),
    # so a fixed-string match on the disk path catches partition members too
    # — a plain word match on the bare name would miss "vda2".
    if command_exists zpool && zpool status -LP 2>/dev/null | grep -qF "$disk"; then
        return 0
    fi
    # Member of an assembled md array. /proc/mdstat lists bare partition names
    # (vda1[0]); substring-match the disk name (over-match errs toward refuse).
    if grep -qsF "$base" /proc/mdstat 2>/dev/null; then
        return 0
    fi
    return 1
}