First working version for RasPi Zero 2W

This commit is contained in:
2026-08-26 09:36:41 +02:00
parent c8a8964189
commit 52e7f012ee
4 changed files with 3310 additions and 0 deletions
Executable
+561
View File
@@ -0,0 +1,561 @@
#!/usr/bin/env bash
set -euo pipefail
# ==============================================================================
# Configuration
# ==============================================================================
VERSION="3.24.1"
BRANCH="3.24"
CONFIG_FILE="NenjimHub.conf"
IMAGE="alpine-rpi-${VERSION}-aarch64.img"
HEADLESS_OVERLAY="headless.apkovl.tar.gz"
WPA_CONFIG="wpa_supplicant.conf"
UNATTENDED_SCRIPT="unattended.sh"
ZEROTIER_INSTALLER="installZeroTier.sh"
BUILD_TIME_FILE="nenjim-build-time"
BUILDER_IMAGE="alpine:${BRANCH}"
KERNEL_MKSQUASHFS_MEMORY="128M"
BOOT_MOUNT="/mnt/usb"
TEMP_DIRECTORY=""
SOURCE_MOUNT=""
LOOP_DEVICE=""
ZEROTIER_PREBUILT_AVAILABLE=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 "$SOURCE_MOUNT" ]] && mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
umount "$SOURCE_MOUNT"
fi
if mountpoint -q "$BOOT_MOUNT" 2>/dev/null; then
umount "$BOOT_MOUNT"
fi
if [[ -n "$LOOP_DEVICE" ]]; then
losetup -d "$LOOP_DEVICE" 2>/dev/null || true
fi
if [[ -n "$TEMP_DIRECTORY" && -d "$TEMP_DIRECTORY" ]]; then
case "$TEMP_DIRECTORY" in
/tmp/nenjim-flash.*)
rm -rf -- "$TEMP_DIRECTORY"
;;
*)
echo "WARNING: Refusing to remove unexpected path: $TEMP_DIRECTORY" >&2
;;
esac
fi
}
preserve_sysop_home()
{
local overlay overlay_count extract_root
local -a final_overlays=()
mapfile -t final_overlays < <(
find "$BOOT_MOUNT" \
-maxdepth 1 \
-type f \
-name '*.apkovl.tar.gz' \
! -name 'headless.apkovl.tar.gz' \
-print
)
overlay_count="${#final_overlays[@]}"
if (( overlay_count == 0 )); then
echo "No final APKoVL found; this appears to be a fresh installation."
return 0
fi
if (( overlay_count > 1 )); then
printf 'Found more than one final APKoVL:\n' >&2
printf ' %s\n' "${final_overlays[@]}" >&2
die "Keep only the active final APKoVL before updating partition 1."
fi
overlay="${final_overlays[0]}"
extract_root="$TEMP_DIRECTORY/old-overlay"
mkdir -p "$extract_root"
echo "Preserving /sysop from $(basename "$overlay")"
tar \
--extract \
--gzip \
--file "$overlay" \
--directory "$extract_root" \
--no-same-owner
if [[ ! -d "$extract_root/sysop" ]]; then
echo "The previous APKoVL does not contain /sysop; nothing to preserve."
return 0
fi
tar \
--create \
--gzip \
--file "$TEMP_DIRECTORY/sysop-preserve.tar.gz" \
--directory "$extract_root" \
sysop
}
run_kernel_builder()
{
local runtime="$1"
local kernel_work="$TEMP_DIRECTORY/kernel-work"
mkdir -p "$kernel_work"
"$runtime" run \
--rm \
--volume "$BOOT_MOUNT:/media/boot" \
--volume "$kernel_work:/work" \
--env KERNEL_MKSQUASHFS_MEMORY="$KERNEL_MKSQUASHFS_MEMORY" \
"$BUILDER_IMAGE" \
/bin/sh -euxc '
apk add --no-cache alpine-conf alpine-keys squashfs-tools xz
# update-kernel creates a temporary aarch64 APK root. The
# architecture-specific keys are required when the builder
# container itself is x86_64. The minimal Docker image can have
# the shared key files removed even though alpine-keys is marked
# as installed, so restore all package-owned key files first.
apk fix --no-cache alpine-keys
APK_KEYS_DIR=/usr/share/apk/keys
find "$APK_KEYS_DIR" -maxdepth 1 -type f -name "*.rsa.pub" -print
test -n "$(find "$APK_KEYS_DIR" -maxdepth 1 -type f -name "*.rsa.pub" -print -quit)"
apk \
--arch aarch64 \
--keys-dir "$APK_KEYS_DIR" \
update
MKSQUASHFS_OPTS="-processors 1 -mem $KERNEL_MKSQUASHFS_MEMORY" \
TMPDIR=/work \
update-kernel \
-a aarch64 \
-f rpi \
-p zfs-rpi \
--keys-dir "$APK_KEYS_DIR" \
--repositories-file /etc/apk/repositories \
-M /media/boot
test -s /media/boot/boot/vmlinuz-rpi
test -s /media/boot/boot/initramfs-rpi
test -s /media/boot/boot/modloop-rpi
unsquashfs -ll /media/boot/boot/modloop-rpi |
grep -E "/zfs[.]ko([.](gz|xz|zst))?$"
'
}
# ==============================================================================
# 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 name in \
BOOT_PARTITION_SIZE_MIB VIRT_ZVOL_SIZE_MIB \
SWAP_SIZE_MIB_PART SWAP_SIZE_MIB_ZFS SWAP_SIZE_PCT_ZRAM \
DISKLESS_ROOT_SIZE_MIB ZFS_ARC_MAX_MIB ZEROTIER_REQUIRED_SWAP_MIB \
ZEROTIER_MAKE_JOBS ZEROTIER_ENABLE_SSO
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."
(( DISKLESS_ROOT_SIZE_MIB > 0 )) ||
die "DISKLESS_ROOT_SIZE_MIB must be greater than zero."
(( ZFS_ARC_MAX_MIB > 0 )) ||
die "ZFS_ARC_MAX_MIB must be greater than zero."
(( ZEROTIER_REQUIRED_SWAP_MIB > 0 )) ||
die "ZEROTIER_REQUIRED_SWAP_MIB must be greater than zero."
(( ZEROTIER_MAKE_JOBS > 0 )) ||
die "ZEROTIER_MAKE_JOBS must be greater than zero."
(( ZEROTIER_ENABLE_SSO == 0 || ZEROTIER_ENABLE_SSO == 1 )) ||
die "ZEROTIER_ENABLE_SSO must be either 0 or 1."
(( 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"
[[ "$ZEROTIER_VERSION" =~ ^[0-9]+([.][0-9]+){2}([._-][A-Za-z0-9]+)*$ ]] ||
die "Invalid ZEROTIER_VERSION: $ZEROTIER_VERSION"
[[ "$ZEROTIER_PREBUILT_BINARY" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] ||
die "ZEROTIER_PREBUILT_BINARY must be a plain filename without directories."
DISKLESS_ROOT_SIZE="${DISKLESS_ROOT_SIZE_MIB}M"
for command in \
blkid blockdev cp date find install losetup lsblk mkfs.vfat mount mountpoint \
sed sync tar udevadm umount
do
command -v "$command" >/dev/null 2>&1 ||
die "Missing required command: $command"
done
for file in \
"$CONFIG_FILE" \
"$IMAGE" \
"$HEADLESS_OVERLAY" \
"$WPA_CONFIG" \
"$UNATTENDED_SCRIPT" \
"$ZEROTIER_INSTALLER"
do
[[ -f "$file" ]] || die "Missing input file: $file"
done
if [[ -f "$ZEROTIER_PREBUILT_BINARY" ]]; then
[[ -s "$ZEROTIER_PREBUILT_BINARY" ]] ||
die "Optional ZeroTier binary is empty: $ZEROTIER_PREBUILT_BINARY"
ZEROTIER_PREBUILT_AVAILABLE=1
echo "Using optional precompiled ZeroTier binary: $ZEROTIER_PREBUILT_BINARY"
elif [[ -e "$ZEROTIER_PREBUILT_BINARY" ]]; then
die "Optional ZeroTier binary is not a regular file: $ZEROTIER_PREBUILT_BINARY"
else
echo "No precompiled ZeroTier binary found; the Pi will compile it on first boot."
fi
if command -v podman >/dev/null 2>&1; then
CONTAINER_RUNTIME="podman"
elif command -v docker >/dev/null 2>&1; then
CONTAINER_RUNTIME="docker"
else
die "Podman or Docker is required to build the matched ARM64 kernel/modloop."
fi
[[ -b "$DEVICE" ]] || die "Not a block device: $DEVICE"
[[ "$(lsblk -dnro PTTYPE "$DEVICE" 2>/dev/null || true)" == "dos" ]] ||
die "The card does not use the Raspberry Pi-compatible MBR layout. Run the updated prepare-storage.sh first."
if mountpoint -q "$BOOT_MOUNT" 2>/dev/null; then
die "$BOOT_MOUNT is already in use. Unmount it before continuing."
fi
if lsblk -nrpo MOUNTPOINT "$DEVICE" | grep -q '[^[:space:]]'; then
die "A partition on $DEVICE is mounted. Export rpool and unmount the card first."
fi
BOOT_PARTITION="$(partition_path 1)"
SWAP_PARTITION="$(partition_path 2)"
ZFS_PARTITION="$(partition_path 3)"
[[ -b "$BOOT_PARTITION" ]] || die "Missing partition 1: run prepare-storage.sh first."
[[ -b "$ZFS_PARTITION" ]] || die "Missing ZFS partition 3: run the updated prepare-storage.sh first."
EXPECTED_BOOT_PARTITION_BYTES=$((BOOT_PARTITION_SIZE_MIB * 1024 * 1024))
ACTUAL_BOOT_PARTITION_BYTES="$(blockdev --getsize64 "$BOOT_PARTITION")"
[[ "$ACTUAL_BOOT_PARTITION_BYTES" == "$EXPECTED_BOOT_PARTITION_BYTES" ]] ||
die "$BOOT_PARTITION is $ACTUAL_BOOT_PARTITION_BYTES bytes, but $CONFIG_FILE specifies $EXPECTED_BOOT_PARTITION_BYTES bytes."
[[ "$(blkid -s TYPE -o value "$BOOT_PARTITION" 2>/dev/null || true)" == "vfat" ]] ||
die "$BOOT_PARTITION is not the expected FAT32 Alpine partition."
if (( SWAP_SIZE_MIB_PART > 0 )); then
[[ -b "$SWAP_PARTITION" ]] ||
die "Missing configured raw swap partition 2: run prepare-storage.sh first."
EXPECTED_SWAP_PARTITION_BYTES=$((SWAP_SIZE_MIB_PART * 1024 * 1024))
ACTUAL_SWAP_PARTITION_BYTES="$(blockdev --getsize64 "$SWAP_PARTITION")"
[[ "$ACTUAL_SWAP_PARTITION_BYTES" == "$EXPECTED_SWAP_PARTITION_BYTES" ]] ||
die "$SWAP_PARTITION is $ACTUAL_SWAP_PARTITION_BYTES bytes, but $CONFIG_FILE specifies $EXPECTED_SWAP_PARTITION_BYTES bytes."
[[ "$(blkid -s TYPE -o value "$SWAP_PARTITION" 2>/dev/null || true)" == "swap" ]] ||
die "$SWAP_PARTITION is not a Linux swap area."
[[ "$(blkid -s LABEL -o value "$SWAP_PARTITION" 2>/dev/null || true)" == "$SWAP_PART_LABEL" ]] ||
die "$SWAP_PARTITION does not have the expected label $SWAP_PART_LABEL."
elif [[ -b "$SWAP_PARTITION" ]]; then
die "Partition 2 exists although SWAP_SIZE_MIB_PART=0; rerun prepare-storage.sh with the selected layout."
fi
[[ "$(blkid -s TYPE -o value "$ZFS_PARTITION" 2>/dev/null || true)" == "zfs_member" ]] ||
die "$ZFS_PARTITION is not an OpenZFS pool member."
TEMP_DIRECTORY="$(mktemp -d /tmp/nenjim-flash.XXXXXX)"
SOURCE_MOUNT="$TEMP_DIRECTORY/image-boot"
mkdir -p "$SOURCE_MOUNT" "$BOOT_MOUNT"
trap cleanup EXIT
# ==============================================================================
# Confirmation: only partition 1 is replaced
# ==============================================================================
echo
echo "Partition 1 (${BOOT_PARTITION}) will be replaced with Alpine ${VERSION}."
if (( SWAP_SIZE_MIB_PART > 0 )); then
echo "Partition 2 (${SWAP_PARTITION}) is raw swap and will NOT be changed."
else
echo "Partition 2 is unused."
fi
echo "Partition 3 (${ZFS_PARTITION}) and $POOL_NAME will NOT be changed."
echo
lsblk -f "$DEVICE"
echo
read -r -p "Type NENJIMHUB to update partition 1: " answer
[[ "$answer" == "NENJIMHUB" ]] || die "Aborted."
# ==============================================================================
# Attach the Alpine image read-only
# ==============================================================================
echo
echo "=== Reading the Alpine image ==="
LOOP_DEVICE="$(losetup --find --show --partscan --read-only "$IMAGE")"
SOURCE_PARTITION="${LOOP_DEVICE}p1"
wait_for_block_device "$SOURCE_PARTITION" ||
die "The image partition did not appear: $SOURCE_PARTITION"
mount -o ro "$SOURCE_PARTITION" "$SOURCE_MOUNT"
[[ -f "$SOURCE_MOUNT/cmdline.txt" ]] ||
die "The image does not contain a Raspberry Pi cmdline.txt."
# ==============================================================================
# Preserve the rescue home from the previous Alpine overlay
# ==============================================================================
echo
echo "=== Preserving the sysop rescue home, if present ==="
mount "$BOOT_PARTITION" "$BOOT_MOUNT"
preserve_sysop_home
ACTUAL_BOOT_LABEL="$(blkid -s LABEL -o value "$BOOT_PARTITION" 2>/dev/null || true)"
BOOT_UUID="$(blkid -s UUID -o value "$BOOT_PARTITION" 2>/dev/null || true)"
[[ "$ACTUAL_BOOT_LABEL" == "$BOOT_LABEL" ]] ||
die "$BOOT_PARTITION has label '${ACTUAL_BOOT_LABEL:-none}', expected '$BOOT_LABEL'."
umount "$BOOT_MOUNT"
# ==============================================================================
# Replace only the FAT32 Alpine partition
# ==============================================================================
echo
echo "=== Replacing Alpine partition 1 ==="
BOOT_VOLUME_ID="$(printf '%s' "$BOOT_UUID" | tr -d '-')"
if [[ "$BOOT_VOLUME_ID" =~ ^[[:xdigit:]]{8}$ ]]; then
mkfs.vfat -F 32 -n "$BOOT_LABEL" -i "$BOOT_VOLUME_ID" "$BOOT_PARTITION"
else
mkfs.vfat -F 32 -n "$BOOT_LABEL" "$BOOT_PARTITION"
fi
mount "$BOOT_PARTITION" "$BOOT_MOUNT"
cp -a "$SOURCE_MOUNT/." "$BOOT_MOUNT/"
CMDLINE_FILE="$BOOT_MOUNT/cmdline.txt"
CMDLINE_WITHOUT_ROOTFLAGS="$(
sed 's/rootflags=[^[:space:]]*[[:space:]]*//g' "$CMDLINE_FILE"
)"
printf '%s rootflags=size=%s\n' \
"$CMDLINE_WITHOUT_ROOTFLAGS" \
"$DISKLESS_ROOT_SIZE" \
>"$CMDLINE_FILE"
# ==============================================================================
# Build a matched Raspberry Pi kernel/modloop with zfs-rpi on the PC
# ==============================================================================
echo
echo "=== Adding Alpine's matched Raspberry Pi OpenZFS module ==="
if ! run_kernel_builder "$CONTAINER_RUNTIME"; then
die "Kernel/modloop build failed. The SD card is not ready to boot."
fi
# ==============================================================================
# Install first-boot inputs
# ==============================================================================
echo
echo "=== Installing unattended first-boot files ==="
install -m 0644 \
"$HEADLESS_OVERLAY" \
"$BOOT_MOUNT/headless.apkovl.tar.gz"
install -m 0600 \
"$WPA_CONFIG" \
"$BOOT_MOUNT/wpa_supplicant.conf"
install -m 0755 \
"$UNATTENDED_SCRIPT" \
"$BOOT_MOUNT/unattended.sh"
install -m 0755 \
"$ZEROTIER_INSTALLER" \
"$BOOT_MOUNT/installZeroTier.sh"
install -m 0644 \
"$CONFIG_FILE" \
"$BOOT_MOUNT/NenjimHub.conf"
if (( ZEROTIER_PREBUILT_AVAILABLE == 1 )); then
install -m 0755 \
"$ZEROTIER_PREBUILT_BINARY" \
"$BOOT_MOUNT/$ZEROTIER_PREBUILT_BINARY"
fi
# Raspberry Pi Zero 2 W has no battery-backed RTC. Give first boot a recent,
# trusted starting point before it contacts HTTPS package repositories.
date -u '+%Y-%m-%d %H:%M:%S' >"$BOOT_MOUNT/$BUILD_TIME_FILE"
chmod 0644 "$BOOT_MOUNT/$BUILD_TIME_FILE"
if [[ -f "$TEMP_DIRECTORY/sysop-preserve.tar.gz" ]]; then
install -m 0600 \
"$TEMP_DIRECTORY/sysop-preserve.tar.gz" \
"$BOOT_MOUNT/sysop-preserve.tar.gz"
fi
# ==============================================================================
# Verification and finish
# ==============================================================================
echo
echo "=== Verifying and syncing partition 1 ==="
for boot_file in \
bootcode.bin \
config.txt \
fixup.dat \
start.elf \
boot/vmlinuz-rpi \
boot/initramfs-rpi \
boot/modloop-rpi \
headless.apkovl.tar.gz \
unattended.sh \
installZeroTier.sh \
NenjimHub.conf \
nenjim-build-time \
wpa_supplicant.conf
do
[[ -s "$BOOT_MOUNT/$boot_file" ]] ||
die "Missing prepared boot file: $boot_file"
done
if (( ZEROTIER_PREBUILT_AVAILABLE == 1 )); then
[[ -s "$BOOT_MOUNT/$ZEROTIER_PREBUILT_BINARY" ]] ||
die "Missing copied ZeroTier binary: $ZEROTIER_PREBUILT_BINARY"
fi
sync
umount "$SOURCE_MOUNT"
umount "$BOOT_MOUNT"
losetup -d "$LOOP_DEVICE"
LOOP_DEVICE=""
cleanup
trap - EXIT
echo
echo "============================================================================"
echo "Alpine partition 1 is ready. $POOL_NAME on partition 3 was left untouched."
if (( ZEROTIER_PREBUILT_AVAILABLE == 1 )); then
echo "ZeroTier $ZEROTIER_VERSION will use the supplied precompiled binary."
else
echo "ZeroTier $ZEROTIER_VERSION will be compiled by the Pi."
fi
echo
echo "First boot performs the unattended setup and reboots."
echo "After the second boot, use the sysop rescue/admin account:"
echo " user: sysop"
echo " password: sysop"
echo "============================================================================"
+507
View File
@@ -0,0 +1,507 @@
#!/usr/bin/env bash
set -euo pipefail
# ==============================================================================
# Configuration (may be overridden by unattended.sh)
# ==============================================================================
ZEROTIER_VERSION="${ZEROTIER_VERSION:-1.16.2}"
SOFTWARE_ROOT="${SOFTWARE_ROOT:-/usr/local/software}"
SYSOP_DATA_ROOT="${SYSOP_DATA_ROOT:-/media/sysop}"
REQUIRED_SWAP_MIB="${REQUIRED_SWAP_MIB:-2048}"
ROOTFS_BUILD_SIZE="${ROOTFS_BUILD_SIZE:-1400M}"
MAKE_JOBS="${MAKE_JOBS:-1}"
ENABLE_SSO="${ENABLE_SSO:-0}"
PREBUILT_BINARY="${PREBUILT_BINARY:-}"
ZEROTIER_ROOT="$SOFTWARE_ROOT/zerotier"
ZEROTIER_VERSIONS="$ZEROTIER_ROOT/versions"
ZEROTIER_BUILD_PROFILE="${ZEROTIER_VERSION}-linux-musl-aarch64-sso${ENABLE_SSO}"
ZEROTIER_INSTALL="$ZEROTIER_VERSIONS/$ZEROTIER_BUILD_PROFILE"
ZEROTIER_STATE="$SYSOP_DATA_ROOT/zerotier"
BUILD_DIRECTORY="$SOFTWARE_ROOT/.zerotier-build-$ZEROTIER_BUILD_PROFILE"
SOURCE_DIRECTORY="$BUILD_DIRECTORY/ZeroTierOne"
BUILD_DEPS=(
build-base
linux-headers
openssl-dev
pkgconf
)
if [[ "$ENABLE_SSO" == "1" ]]; then
BUILD_DEPS+=(cargo)
fi
# ==============================================================================
# Helpers and cleanup
# ==============================================================================
BUILD_DEPS_INSTALLED=0
BUILD_REQUIRED=1
PREBUILT_AVAILABLE=0
STATE_WAS_PRESENT=0
die()
{
echo "ERROR: $*" >&2
exit 1
}
safe_remove_build_directory()
{
case "$BUILD_DIRECTORY" in
"$SOFTWARE_ROOT"/.zerotier-build-*)
rm -rf -- "$BUILD_DIRECTORY"
;;
*)
echo "WARNING: Refusing to remove unexpected path: $BUILD_DIRECTORY" >&2
;;
esac
}
release_build_resources()
{
local result=0
if (( BUILD_DEPS_INSTALLED == 1 )); then
echo "=== Removing ZeroTier build dependencies ==="
if apk del .zerotier-builddeps; then
BUILD_DEPS_INSTALLED=0
else
result=1
fi
fi
safe_remove_build_directory
return "$result"
}
cleanup_on_exit()
{
local status=$?
trap - EXIT
set +e
release_build_resources
exit "$status"
}
migrate_directory_to_state()
{
local source="$1"
if [[ -L "$source" ]]; then
rm -f -- "$source"
return 0
fi
if [[ -d "$source" ]]; then
cp -a "$source/." "$ZEROTIER_STATE/" 2>/dev/null || true
rm -rf -- "$source"
elif [[ -e "$source" ]]; then
die "Refusing to replace non-directory path: $source"
fi
}
trap cleanup_on_exit EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
# ==============================================================================
# Validate the Alpine diskless/ZFS environment
# ==============================================================================
[[ $EUID -eq 0 ]] || die "This installer must be run as root."
for command in apk git lbu mount rc-update
do
command -v "$command" >/dev/null 2>&1 ||
die "Missing required command: $command"
done
[[ -d "$SOFTWARE_ROOT" ]] || die "Missing software dataset mount: $SOFTWARE_ROOT"
[[ -d "$SYSOP_DATA_ROOT" ]] || die "Missing sysop dataset mount: $SYSOP_DATA_ROOT"
SOFTWARE_FILESYSTEM="$({
awk -v path="$SOFTWARE_ROOT" '$2 == path { print $3; exit }' /proc/mounts
} || true)"
[[ "$SOFTWARE_FILESYSTEM" == "zfs" ]] ||
die "$SOFTWARE_ROOT must be a mounted ZFS dataset, not '${SOFTWARE_FILESYSTEM:-unknown}'."
case "$REQUIRED_SWAP_MIB" in
''|*[!0-9]*) die "REQUIRED_SWAP_MIB must be a positive integer." ;;
esac
case "$MAKE_JOBS" in
''|*[!0-9]*) die "MAKE_JOBS must be a positive integer." ;;
esac
case "$ENABLE_SSO" in
0|1) ;;
*) die "ENABLE_SSO must be either 0 or 1." ;;
esac
(( REQUIRED_SWAP_MIB > 0 )) || die "REQUIRED_SWAP_MIB must be greater than zero."
(( MAKE_JOBS > 0 )) || die "MAKE_JOBS must be greater than zero."
if [[ -x "$ZEROTIER_INSTALL/zerotier-one" ]]; then
EXISTING_VERSION="$(
"$ZEROTIER_INSTALL/zerotier-one" -v 2>/dev/null || true
)"
if [[ "$EXISTING_VERSION" == "$ZEROTIER_VERSION" ]]; then
BUILD_REQUIRED=0
else
echo "WARNING: Existing ZeroTier binary reports version '${EXISTING_VERSION:-unknown}'; replacing it."
fi
fi
if (( BUILD_REQUIRED == 1 )) &&
[[ -n "$PREBUILT_BINARY" && -s "$PREBUILT_BINARY" ]]
then
PREBUILT_AVAILABLE=1
BUILD_REQUIRED=0
fi
# ==============================================================================
# Check swap and free space when compilation is needed
# ==============================================================================
if (( BUILD_REQUIRED == 1 )); then
ACTIVE_SWAP_KB="$(awk 'NR > 1 { total += $3 } END { print total + 0 }' /proc/swaps)"
REQUIRED_SWAP_KB=$((REQUIRED_SWAP_MIB * 1024))
(( ACTIVE_SWAP_KB >= REQUIRED_SWAP_KB )) ||
die "ZeroTier compilation requires at least ${REQUIRED_SWAP_MIB} MiB active swap."
AVAILABLE_KB="$(df -Pk "$SOFTWARE_ROOT" | awk 'NR == 2 { print $4 }')"
REQUIRED_FREE_KB=$((1200 * 1024))
(( AVAILABLE_KB >= REQUIRED_FREE_KB )) ||
die "At least 1200 MB must be free on $SOFTWARE_ROOT for the ZeroTier build."
echo
echo "=== Reusing all active swap devices for compilation ==="
grep -E 'MemTotal|SwapTotal|SwapFree' /proc/meminfo
elif (( PREBUILT_AVAILABLE == 1 )); then
echo
echo "=== Using precompiled ZeroTier ${ZEROTIER_VERSION} binary ==="
echo "Source: $PREBUILT_BINARY"
else
echo
echo "=== Reusing the existing ZeroTier ${ZEROTIER_VERSION} binary ==="
fi
# ==============================================================================
# Enlarge the temporary RAM root filesystem
# ==============================================================================
if (( BUILD_REQUIRED == 1 )); then
ROOT_FILESYSTEM="$(awk '$2 == "/" { print $3; exit }' /proc/mounts)"
if [[ "$ROOT_FILESYSTEM" == "tmpfs" ]]; then
echo
echo "=== Enlarging temporary rootfs limit to $ROOTFS_BUILD_SIZE ==="
mount -o "remount,size=$ROOTFS_BUILD_SIZE" /
fi
fi
# ==============================================================================
# Install build-only packages
# ==============================================================================
if (( BUILD_REQUIRED == 1 )); then
echo
echo "=== Installing ZeroTier build dependencies ==="
if [[ "$ENABLE_SSO" == "1" ]]; then
echo "ZeroTier SSO is enabled; Rust/Cargo will be installed."
else
echo "ZeroTier SSO is disabled; Rust/Cargo are not required."
fi
# --no-cache keeps the compiler toolchain out of the persistent APK cache.
# Retry transient Wi-Fi/download failures, cleaning a partial virtual
# package before each new attempt.
APK_ADD_SUCCEEDED=0
for attempt in 1 2 3; do
if apk add \
--no-cache \
--virtual .zerotier-builddeps \
"${BUILD_DEPS[@]}"
then
APK_ADD_SUCCEEDED=1
break
fi
echo "WARNING: Build dependency installation attempt $attempt failed."
apk del .zerotier-builddeps >/dev/null 2>&1 || true
if (( attempt < 3 )); then
echo "Retrying after $((attempt * 10)) seconds..."
sleep $((attempt * 10))
fi
done
(( APK_ADD_SUCCEEDED == 1 )) ||
die "Could not install ZeroTier build dependencies after 3 attempts."
BUILD_DEPS_INSTALLED=1
for command in gcc g++ make pkgconf strip
do
command -v "$command" >/dev/null 2>&1 ||
die "Build dependency did not provide required command: $command"
done
if [[ "$ENABLE_SSO" == "1" ]]; then
command -v cargo >/dev/null 2>&1 ||
die "SSO build dependency did not provide cargo."
fi
fi
# ==============================================================================
# Download the exact pinned ZeroTier source tag
# ==============================================================================
if (( BUILD_REQUIRED == 1 )); then
echo
echo "=== Downloading ZeroTier ${ZEROTIER_VERSION} source ==="
safe_remove_build_directory
mkdir -p "$BUILD_DIRECTORY"
git init "$SOURCE_DIRECTORY"
git -C "$SOURCE_DIRECTORY" remote add \
origin \
https://github.com/zerotier/ZeroTierOne.git
# Fetch the exact tag ref. This avoids both the earlier git-describe error
# on a shallow checkout and ambiguity between branch and tag names.
git -C "$SOURCE_DIRECTORY" fetch \
--depth 1 \
origin \
"refs/tags/$ZEROTIER_VERSION:refs/tags/$ZEROTIER_VERSION"
git -C "$SOURCE_DIRECTORY" checkout \
--detach \
"refs/tags/$ZEROTIER_VERSION"
ACTUAL_COMMIT="$(git -C "$SOURCE_DIRECTORY" rev-parse HEAD)"
TAG_COMMIT="$(
git -C "$SOURCE_DIRECTORY" \
rev-parse --verify "refs/tags/$ZEROTIER_VERSION^{commit}"
)"
[[ "$ACTUAL_COMMIT" == "$TAG_COMMIT" ]] ||
die "Checked-out commit does not match tag $ZEROTIER_VERSION."
fi
# ==============================================================================
# Build conservatively for the Raspberry Pi Zero 2W
# ==============================================================================
if (( BUILD_REQUIRED == 1 )); then
echo
echo "=== Building ZeroTier with ${MAKE_JOBS} make job(s) ==="
(
cd "$SOURCE_DIRECTORY"
# BCM2710A1 does not provide the optional ARMv8 crypto extensions that
# ZeroTier's aarch64 makefile otherwise enables by default.
sed -i \
's/-march=armv8-a+crypto/-march=armv8-a/g' \
make-linux.mk
if grep -q -- '-march=armv8-a+crypto' make-linux.mk; then
die "Could not disable ARM crypto-extension instructions."
fi
export TMPDIR="$BUILD_DIRECTORY/tmp"
if [[ "$ENABLE_SSO" == "1" ]]; then
export CARGO_BUILD_JOBS=1
export CARGO_INCREMENTAL=0
export CARGO_HOME="$BUILD_DIRECTORY/cargo-home"
mkdir -p "$CARGO_HOME"
fi
mkdir -p "$TMPDIR"
make \
-j "$MAKE_JOBS" \
CC=gcc \
CXX=g++ \
ZT_CONTROLLER=0 \
ZT_SSO_SUPPORTED="$ENABLE_SSO" \
one
strip zerotier-one
)
fi
# ==============================================================================
# Install the binary on rpool/software
# ==============================================================================
if (( PREBUILT_AVAILABLE == 1 )); then
echo
echo "=== Installing precompiled ZeroTier on rpool/software ==="
mkdir -p "$ZEROTIER_INSTALL"
install -m 0755 \
"$PREBUILT_BINARY" \
"$ZEROTIER_INSTALL/zerotier-one"
elif (( BUILD_REQUIRED == 1 )); then
echo
echo "=== Installing ZeroTier on rpool/software ==="
mkdir -p "$ZEROTIER_INSTALL"
install -m 0755 \
"$SOURCE_DIRECTORY/zerotier-one" \
"$ZEROTIER_INSTALL/zerotier-one"
fi
mkdir -p "$ZEROTIER_ROOT"
ln -sfn "versions/$ZEROTIER_BUILD_PROFILE" "$ZEROTIER_ROOT/current"
rm -f \
/usr/sbin/zerotier-one \
/usr/sbin/zerotier-cli \
/usr/sbin/zerotier-idtool
ln -s "$ZEROTIER_ROOT/current/zerotier-one" /usr/sbin/zerotier-one
ln -s zerotier-one /usr/sbin/zerotier-cli
ln -s zerotier-one /usr/sbin/zerotier-idtool
# ==============================================================================
# Keep identity and future network state under sysop's ZFS-backed area
# ==============================================================================
if [[ -d "$ZEROTIER_STATE" ]] &&
find "$ZEROTIER_STATE" -mindepth 1 -print -quit | grep -q .
then
STATE_WAS_PRESENT=1
fi
mkdir -p "$ZEROTIER_STATE" /sysop /var/lib
chmod 0700 "$ZEROTIER_STATE"
if id sysop >/dev/null 2>&1; then
chown sysop:sysop "$ZEROTIER_STATE"
fi
migrate_directory_to_state /sysop/zerotier
migrate_directory_to_state /var/lib/zerotier-one
ln -s "$ZEROTIER_STATE" /sysop/zerotier
ln -s /sysop/zerotier /var/lib/zerotier-one
# ==============================================================================
# OpenRC service
# ==============================================================================
cat >/etc/init.d/zerotier-one <<'OPENRC'
#!/sbin/openrc-run
description="ZeroTier One network virtualization service"
command="/usr/sbin/zerotier-one"
command_background="yes"
pidfile="/run/${RC_SVCNAME}.pid"
depend()
{
need zfs-mount
use net logger
after networking
}
start_pre()
{
if [ ! -d /var/lib/zerotier-one ]; then
eerror "Persistent ZeroTier state is unavailable"
return 1
fi
modprobe tun >/dev/null 2>&1 || true
}
OPENRC
chmod 0755 /etc/init.d/zerotier-one
grep -qxF tun /etc/modules 2>/dev/null || echo tun >>/etc/modules
rc-update add zerotier-one default
# ==============================================================================
# Persist diskless symlinks and service files
# ==============================================================================
for path in \
/usr/sbin/zerotier-one \
/usr/sbin/zerotier-cli \
/usr/sbin/zerotier-idtool \
/sysop/zerotier \
/var/lib/zerotier-one \
/etc/init.d/zerotier-one
do
lbu include "$path"
done
# ==============================================================================
# Verify without joining a network
# ==============================================================================
echo
echo "=== Verifying the ZeroTier installation ==="
if ! INSTALLED_VERSION="$(/usr/sbin/zerotier-one -v 2>/dev/null)"; then
die "The ZeroTier binary cannot run on this Raspberry Pi/Alpine system."
fi
echo "$INSTALLED_VERSION"
[[ "$INSTALLED_VERSION" == "$ZEROTIER_VERSION" ]] ||
die "ZeroTier reports version '$INSTALLED_VERSION', expected '$ZEROTIER_VERSION'."
if ldd /usr/sbin/zerotier-one 2>&1 | grep -q 'not found'; then
ldd /usr/sbin/zerotier-one >&2 || true
die "The ZeroTier binary has missing runtime libraries."
fi
if (( STATE_WAS_PRESENT == 0 )) && [[ -d "$ZEROTIER_STATE/networks.d" ]]; then
die "A fresh installation unexpectedly contains joined-network configuration."
fi
# ==============================================================================
# Remove toolchain and source tree
# ==============================================================================
echo
release_build_resources
trap - EXIT HUP INT TERM
echo
echo "ZeroTier ${ZEROTIER_VERSION} is installed and enabled for the next boot."
echo "Existing identity/configuration is preserved; a fresh install is not joined."
+530
View File
@@ -0,0 +1,530 @@
#!/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 "============================================================================"
Executable
+1712
View File
File diff suppressed because it is too large Load Diff