Files
r35157_nenjimhub-os/prepare-storage.sh

531 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# ==============================================================================
# Configuration
# ==============================================================================
CONFIG_FILE="NenjimHub.conf"
JAVA_ARCHIVE="zulu25.36.205-ca-jre25.0.4.1-linux_musl_aarch64.tar.gz"
JAVA_DIRECTORY="zulu25.36.205-ca-jre25.0.4.1-linux_musl_aarch64"
JAVA_VERSION_DIRECTORY="jre-25.0.4.1"
JAVA_MAJOR_DIRECTORY="jre-25"
ZREPL_VERSION="v0.7.0"
ZREPL_ASSET="zrepl-linux-arm64"
ZREPL_LOCAL_BINARY="$ZREPL_ASSET"
# Cards marketed as 16 GB are commonly a little smaller than 16,000,000,000
# usable bytes. 14 GiB actual capacity is the baseline; the configured
# partition and zvol sizes are also checked below.
MINIMUM_DEVICE_BYTES=$((14 * 1024 * 1024 * 1024))
# Raspberry Pi Zero 2 W boots most reliably from a classic MBR partition table.
# 0x0c is FAT32 LBA, 0x82 is Linux swap, and 0xbf identifies Solaris/ZFS.
BOOT_PARTITION_TYPE="c"
SWAP_PARTITION_TYPE="82"
ZFS_PARTITION_TYPE="bf"
TEMP_DIRECTORY=""
STAGING_ROOT=""
VIRT_MOUNT=""
POOL_CREATED=0
# ==============================================================================
# Helpers
# ==============================================================================
die()
{
echo "ERROR: $*" >&2
exit 1
}
require_uint()
{
local name="$1"
local value="${!name-}"
[[ "$value" =~ ^[0-9]+$ ]] ||
die "$name must be a non-negative integer."
}
require_swap_priority()
{
local name="$1"
local value="${!name-}"
require_uint "$name"
(( value <= 32767 )) ||
die "$name must be between 0 and 32767."
}
partition_path()
{
local number="$1"
if [[ "$DEVICE" =~ [0-9]$ ]]; then
printf '%sp%s\n' "$DEVICE" "$number"
else
printf '%s%s\n' "$DEVICE" "$number"
fi
}
wait_for_block_device()
{
local path="$1"
local attempt
for attempt in {1..50}; do
[[ -b "$path" ]] && return 0
sleep 0.2
done
return 1
}
cleanup()
{
set +e
if [[ -n "$VIRT_MOUNT" ]] && mountpoint -q "$VIRT_MOUNT" 2>/dev/null; then
umount "$VIRT_MOUNT"
fi
if (( POOL_CREATED == 1 )) && zpool list -H -o name "$POOL_NAME" >/dev/null 2>&1; then
zpool export "$POOL_NAME"
fi
if [[ -n "$TEMP_DIRECTORY" && -d "$TEMP_DIRECTORY" ]]; then
case "$TEMP_DIRECTORY" in
/tmp/nenjim-storage.*)
rm -rf -- "$TEMP_DIRECTORY"
;;
*)
echo "WARNING: Refusing to remove unexpected path: $TEMP_DIRECTORY" >&2
;;
esac
fi
}
download_zrepl()
{
local destination="$1"
local metadata asset_data asset_url asset_digest actual_digest
echo "Downloading official zrepl ${ZREPL_VERSION} ARM64 binary"
metadata="$({
curl \
--fail \
--location \
--proto '=https' \
--silent \
--show-error \
"https://api.github.com/repos/zrepl/zrepl/releases/tags/${ZREPL_VERSION}"
})"
asset_data="$({
printf '%s' "$metadata" |
ZREPL_ASSET="$ZREPL_ASSET" python3 -c '
import json
import os
import sys
release = json.load(sys.stdin)
name = os.environ["ZREPL_ASSET"]
for asset in release.get("assets", []):
if asset.get("name") == name:
print(asset.get("browser_download_url", ""))
print(asset.get("digest", ""))
break
else:
raise SystemExit(f"Release asset not found: {name}")
'
})" || die "Could not read the zrepl release metadata."
asset_url="$(printf '%s\n' "$asset_data" | sed -n '1p')"
asset_digest="$(printf '%s\n' "$asset_data" | sed -n '2p')"
[[ "$asset_url" == https://github.com/zrepl/zrepl/releases/download/* ]] ||
die "Unexpected zrepl download URL returned by GitHub."
[[ "$asset_digest" =~ ^sha256:[[:xdigit:]]{64}$ ]] ||
die "GitHub did not provide a SHA-256 digest for $ZREPL_ASSET."
curl \
--fail \
--location \
--proto '=https' \
--silent \
--show-error \
--output "$destination" \
"$asset_url"
actual_digest="sha256:$(sha256sum "$destination" | awk '{print $1}')"
[[ "$actual_digest" == "$asset_digest" ]] ||
die "SHA-256 validation failed for the downloaded zrepl binary."
}
# ==============================================================================
# Initialization and validation
# ==============================================================================
cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
[[ -f "$CONFIG_FILE" ]] || die "Missing shared configuration: $CONFIG_FILE"
# shellcheck source=NenjimHub.conf
source "./$CONFIG_FILE"
[[ $EUID -eq 0 ]] || die "This script must be run as root."
for command in \
awk blkid blockdev curl file findmnt install lsblk mkfs.ext4 mkfs.vfat mkswap \
mount mountpoint partprobe python3 sed sfdisk sha256sum tar udevadm \
umount wipefs zfs zpool
do
command -v "$command" >/dev/null 2>&1 ||
die "Missing required command: $command"
done
[[ -f "$JAVA_ARCHIVE" ]] || die "Missing input file: $JAVA_ARCHIVE"
[[ -b "$DEVICE" ]] || die "Not a block device: $DEVICE"
for name in \
BOOT_PARTITION_SIZE_MIB VIRT_ZVOL_SIZE_MIB \
SWAP_SIZE_MIB_PART SWAP_SIZE_MIB_ZFS SWAP_SIZE_PCT_ZRAM
do
require_uint "$name"
done
for name in SWAP_PRI_ZRAM SWAP_PRI_ZFS SWAP_PRI_PART; do
require_swap_priority "$name"
done
(( BOOT_PARTITION_SIZE_MIB > 0 )) ||
die "BOOT_PARTITION_SIZE_MIB must be greater than zero."
(( VIRT_ZVOL_SIZE_MIB > 0 )) ||
die "VIRT_ZVOL_SIZE_MIB must be greater than zero."
(( SWAP_SIZE_PCT_ZRAM <= 100 )) ||
die "SWAP_SIZE_PCT_ZRAM must be between 0 and 100."
[[ "$NENJIMHUB_HOSTNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]] ||
die "NENJIMHUB_HOSTNAME must be one 1-63 character DNS hostname label."
[[ "$CONSOLE_KEYMAP_LAYOUT" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] ||
die "Invalid CONSOLE_KEYMAP_LAYOUT: $CONSOLE_KEYMAP_LAYOUT"
[[ "$CONSOLE_KEYMAP_VARIANT" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] ||
die "Invalid CONSOLE_KEYMAP_VARIANT: $CONSOLE_KEYMAP_VARIANT"
[[ "$BOOT_LABEL" =~ ^[[:alnum:]_-]{1,11}$ ]] ||
die "BOOT_LABEL must be 1-11 letters, digits, underscores or hyphens."
[[ "$SWAP_PART_LABEL" =~ ^[[:alnum:]_-]{1,15}$ ]] ||
die "SWAP_PART_LABEL must be 1-15 letters, digits, underscores or hyphens."
DEVICE_BYTES="$(blockdev --getsize64 "$DEVICE")"
CONFIGURED_MINIMUM_DEVICE_BYTES=$((
(1 + BOOT_PARTITION_SIZE_MIB + SWAP_SIZE_MIB_PART +
VIRT_ZVOL_SIZE_MIB + SWAP_SIZE_MIB_ZFS + 2048) * 1024 * 1024
))
if (( CONFIGURED_MINIMUM_DEVICE_BYTES > MINIMUM_DEVICE_BYTES )); then
REQUIRED_DEVICE_BYTES="$CONFIGURED_MINIMUM_DEVICE_BYTES"
else
REQUIRED_DEVICE_BYTES="$MINIMUM_DEVICE_BYTES"
fi
(( DEVICE_BYTES >= REQUIRED_DEVICE_BYTES )) ||
die "The card has $DEVICE_BYTES bytes; the selected layout needs at least $REQUIRED_DEVICE_BYTES bytes."
if lsblk -nrpo MOUNTPOINT "$DEVICE" | grep -q '[^[:space:]]'; then
die "A partition on $DEVICE is mounted. Unmount it before continuing."
fi
if zpool list -H -o name "$POOL_NAME" >/dev/null 2>&1; then
die "A pool named $POOL_NAME is already imported on this PC."
fi
if zpool status -P 2>/dev/null | grep -Fq "$DEVICE"; then
die "$DEVICE belongs to a pool that is currently imported. Export it first."
fi
HOST_ZFS_VERSION="$(zpool --version | sed -n 's/^zfs-\([^ -]*\).*/\1/p' | head -1)"
[[ "$HOST_ZFS_VERSION" == "$EXPECTED_ZFS_VERSION" ]] ||
die "This design expects OpenZFS $EXPECTED_ZFS_VERSION; the PC has ${HOST_ZFS_VERSION:-unknown}."
COMPATIBILITY_FILE="/usr/share/zfs/compatibility.d/$POOL_COMPATIBILITY"
[[ -f "$COMPATIBILITY_FILE" ]] ||
die "Missing OpenZFS compatibility file: $COMPATIBILITY_FILE"
BOOT_PARTITION="$(partition_path 1)"
SWAP_PARTITION="$(partition_path 2)"
ZFS_PARTITION="$(partition_path 3)"
TEMP_DIRECTORY="$(mktemp -d /tmp/nenjim-storage.XXXXXX)"
STAGING_ROOT="$TEMP_DIRECTORY/root"
VIRT_MOUNT="$TEMP_DIRECTORY/virt"
mkdir -p "$STAGING_ROOT" "$VIRT_MOUNT"
trap cleanup EXIT
# ==============================================================================
# Destructive-action confirmation
# ==============================================================================
echo
echo "WARNING: ALL DATA ON ${DEVICE} WILL BE DESTROYED."
echo
echo "The new layout will be:"
echo " partition 1: ${BOOT_PARTITION_SIZE_MIB} MiB FAT32 Alpine boot partition"
if (( SWAP_SIZE_MIB_PART > 0 )); then
echo " partition 2: ${SWAP_SIZE_MIB_PART} MiB raw Linux swap (priority ${SWAP_PRI_PART})"
else
echo " partition 2: omitted (raw swap is disabled)"
fi
echo " partition 3: remaining space, OpenZFS pool $POOL_NAME"
if (( SWAP_SIZE_MIB_ZFS > 0 )); then
echo " $POOL_NAME/swap: ${SWAP_SIZE_MIB_ZFS} MiB (priority ${SWAP_PRI_ZFS})"
else
echo " $POOL_NAME/swap: disabled"
fi
echo " ZRAM: ${SWAP_SIZE_PCT_ZRAM}% of RAM (priority ${SWAP_PRI_ZRAM})"
echo
lsblk "$DEVICE"
echo
read -r -p "Type YES to create the new storage layout: " answer
[[ "$answer" == "YES" ]] || die "Aborted."
[[ -b "$DEVICE" ]] || die "The block device disappeared: $DEVICE"
# ==============================================================================
# Create the physical partitions
# ==============================================================================
echo
echo "=== Creating FAT32, optional raw swap and OpenZFS partitions ==="
wipefs --all "$DEVICE"
LOGICAL_SECTOR_BYTES="$(blockdev --getss "$DEVICE")"
case "$LOGICAL_SECTOR_BYTES" in
512|4096)
;;
*)
die "Unsupported logical sector size: $LOGICAL_SECTOR_BYTES bytes."
;;
esac
BOOT_START_SECTOR=$((1024 * 1024 / LOGICAL_SECTOR_BYTES))
BOOT_SIZE_SECTORS=$((BOOT_PARTITION_SIZE_MIB * 1024 * 1024 / LOGICAL_SECTOR_BYTES))
SWAP_SIZE_SECTORS=$((SWAP_SIZE_MIB_PART * 1024 * 1024 / LOGICAL_SECTOR_BYTES))
NEXT_START_SECTOR=$((BOOT_START_SECTOR + BOOT_SIZE_SECTORS))
if (( SWAP_SIZE_MIB_PART > 0 )); then
SWAP_START_SECTOR="$NEXT_START_SECTOR"
ZFS_START_SECTOR=$((SWAP_START_SECTOR + SWAP_SIZE_SECTORS))
else
ZFS_START_SECTOR="$NEXT_START_SECTOR"
fi
{
echo 'label: dos'
echo 'unit: sectors'
echo
echo "$BOOT_PARTITION : start=$BOOT_START_SECTOR, size=$BOOT_SIZE_SECTORS, type=$BOOT_PARTITION_TYPE, bootable"
if (( SWAP_SIZE_MIB_PART > 0 )); then
echo "$SWAP_PARTITION : start=$SWAP_START_SECTOR, size=$SWAP_SIZE_SECTORS, type=$SWAP_PARTITION_TYPE"
fi
echo "$ZFS_PARTITION : start=$ZFS_START_SECTOR, type=$ZFS_PARTITION_TYPE"
} | sfdisk --wipe always "$DEVICE"
partprobe "$DEVICE"
udevadm settle
wait_for_block_device "$BOOT_PARTITION" ||
die "Boot partition did not appear: $BOOT_PARTITION"
if (( SWAP_SIZE_MIB_PART > 0 )); then
wait_for_block_device "$SWAP_PARTITION" ||
die "Swap partition did not appear: $SWAP_PARTITION"
fi
wait_for_block_device "$ZFS_PARTITION" ||
die "ZFS partition did not appear: $ZFS_PARTITION"
mkfs.vfat -F 32 -n "$BOOT_LABEL" "$BOOT_PARTITION"
if (( SWAP_SIZE_MIB_PART > 0 )); then
mkswap -L "$SWAP_PART_LABEL" "$SWAP_PARTITION"
fi
# ==============================================================================
# Create the ZFS pool and datasets
# ==============================================================================
echo
echo "=== Creating OpenZFS pool $POOL_NAME ==="
zpool create \
-f \
-R "$STAGING_ROOT" \
-o "compatibility=$POOL_COMPATIBILITY" \
-o ashift=12 \
-O xattr=sa \
-O compression=lz4 \
-O atime=off \
-O canmount=off \
-O mountpoint=none \
"$POOL_NAME" \
"$ZFS_PARTITION"
POOL_CREATED=1
zfs create -o mountpoint=/home "$POOL_NAME/home"
zfs create -o mountpoint=/usr/local/software "$POOL_NAME/software"
zfs create -o mountpoint=/media/sysop "$POOL_NAME/sysop"
# ==============================================================================
# Install Java on rpool/software
# ==============================================================================
echo
echo "=== Installing Azul Zulu JRE on rpool/software ==="
SOFTWARE_ROOT="$STAGING_ROOT/usr/local/software"
mkdir -p "$SOFTWARE_ROOT"
tar -xzf "$JAVA_ARCHIVE" -C "$SOFTWARE_ROOT"
[[ -d "$SOFTWARE_ROOT/$JAVA_DIRECTORY" ]] ||
die "The Java archive did not create $JAVA_DIRECTORY."
ln -s "$JAVA_DIRECTORY" "$SOFTWARE_ROOT/$JAVA_VERSION_DIRECTORY"
ln -s "$JAVA_VERSION_DIRECTORY" "$SOFTWARE_ROOT/$JAVA_MAJOR_DIRECTORY"
# ==============================================================================
# Install the official static zrepl ARM64 binary on rpool/software
# ==============================================================================
echo
echo "=== Installing zrepl ${ZREPL_VERSION} on rpool/software ==="
ZREPL_VERSION_NUMBER="${ZREPL_VERSION#v}"
ZREPL_VERSION_DIRECTORY="$SOFTWARE_ROOT/zrepl/versions/$ZREPL_VERSION_NUMBER"
ZREPL_TEMP_BINARY="$TEMP_DIRECTORY/$ZREPL_ASSET"
mkdir -p "$ZREPL_VERSION_DIRECTORY"
if [[ -f "$ZREPL_LOCAL_BINARY" ]]; then
echo "Using local zrepl binary: $ZREPL_LOCAL_BINARY"
cp "$ZREPL_LOCAL_BINARY" "$ZREPL_TEMP_BINARY"
else
download_zrepl "$ZREPL_TEMP_BINARY"
fi
file "$ZREPL_TEMP_BINARY" | grep -Eq 'ELF 64-bit.*ARM aarch64' ||
die "$ZREPL_ASSET is not an ARM64 ELF binary."
install -m 0755 "$ZREPL_TEMP_BINARY" "$ZREPL_VERSION_DIRECTORY/zrepl"
ln -s "versions/$ZREPL_VERSION_NUMBER" "$SOFTWARE_ROOT/zrepl/current"
# ==============================================================================
# Create and format the shared Docker/containerd zvol
# ==============================================================================
echo
echo "=== Creating ${POOL_NAME}/virt (${VIRT_ZVOL_SIZE_MIB} MiB, ext4) ==="
zfs create \
-V "${VIRT_ZVOL_SIZE_MIB}M" \
-o volmode=dev \
-o logbias=throughput \
-o primarycache=metadata \
-o secondarycache=none \
"$POOL_NAME/virt"
udevadm settle
VIRT_DEVICE="/dev/zvol/$POOL_NAME/virt"
wait_for_block_device "$VIRT_DEVICE" ||
die "The zvol device did not appear: $VIRT_DEVICE"
mkfs.ext4 -F -L NENJIMVIRT "$VIRT_DEVICE"
mount -t ext4 -o rw,noatime "$VIRT_DEVICE" "$VIRT_MOUNT"
mkdir -p "$VIRT_MOUNT/docker" "$VIRT_MOUNT/containerd"
chmod 0711 "$VIRT_MOUNT/docker" "$VIRT_MOUNT/containerd"
sync
umount "$VIRT_MOUNT"
# ==============================================================================
# Verification and clean export
# ==============================================================================
echo
echo "=== Verifying and exporting the pool ==="
zpool get -H -o value compatibility "$POOL_NAME" |
grep -qx "$POOL_COMPATIBILITY" ||
die "The pool compatibility setting is incorrect."
for dataset in home software sysop virt; do
zfs list "$POOL_NAME/$dataset" >/dev/null ||
die "Missing ZFS object: $POOL_NAME/$dataset"
done
JAVA_BINARY="$SOFTWARE_ROOT/$JAVA_DIRECTORY/bin/java"
[[ -x "$JAVA_BINARY" ]] ||
die "The Java archive did not provide an executable bin/java."
# The preparation PC is normally amd64 and therefore cannot execute this
# Raspberry Pi binary. Verify its format and target architecture instead.
file "$JAVA_BINARY" | grep -Eq 'ELF 64-bit.*ARM aarch64' ||
die "The installed Java binary is not an ARM64 ELF executable."
zpool set cachefile=none "$POOL_NAME"
zpool export "$POOL_NAME"
POOL_CREATED=0
sync
# ==============================================================================
# Finished
# ==============================================================================
cleanup
trap - EXIT
echo
echo "============================================================================"
echo "Storage preparation is complete."
echo
echo "Partition 1 is ready for flash.sh."
if (( SWAP_SIZE_MIB_PART > 0 )); then
echo "Partition 2 is raw swap and will not be touched by flash.sh."
else
echo "Partition 2 is intentionally unused."
fi
echo "Partition 3 contains $POOL_NAME and will not be touched by flash.sh."
if (( SWAP_SIZE_MIB_ZFS > 0 )); then
echo "The Pi will create ${POOL_NAME}/swap (${SWAP_SIZE_MIB_ZFS} MiB) using its real page size."
fi
echo "============================================================================"