Files

1713 lines
48 KiB
Bash
Executable File

#!/bin/sh
set -eu
# Monotonic time is unaffected by the RTC seed and later network time updates.
INSTALL_START_UPTIME_SECONDS="$(awk '{ print int($1); exit }' /proc/uptime)"
# ==============================================================================
# Configuration
# ==============================================================================
NENJIM_USER="nenjim"
NENJIM_PASS="nenjim"
NENJIM_UID="1001"
SYSOP_USER="sysop"
SYSOP_PASS="sysop"
SYSOP_UID="1000"
MY_IFACE="wlan0"
MY_TIMEZONE="Europe/Copenhagen"
ALPINE_BRANCH="3.24"
ALPINE_MIRROR="https://dl-cdn.alpinelinux.org/alpine"
APK_MAIN_REPOSITORY="$ALPINE_MIRROR/v$ALPINE_BRANCH/main"
APK_COMMUNITY_REPOSITORY="$ALPINE_MIRROR/v$ALPINE_BRANCH/community"
BUILD_TIME_FILE_NAME="nenjim-build-time"
CONFIG_FILE_NAME="NenjimHub.conf"
SOFTWARE_ROOT="/usr/local/software"
SYSOP_DATA_ROOT="/media/sysop"
VIRT_MOUNT="/media/virt"
ZRAM_SWAP_DEVICE="/dev/zram0"
ZRAM_INSTALL_MEMORY_PERCENT="100"
ZREPL_CONFIG_DIRECTORY="$SYSOP_DATA_ROOT/zrepl"
ZREPL_CONFIG_FILE="$ZREPL_CONFIG_DIRECTORY/zrepl.yml"
EARLY_PACKAGES="
btop
eudev
eudev-openrc
htop
kitty-terminfo
musl-locales
tmux
udev-init-scripts
udev-init-scripts-openrc
zfs~2.4.3
zfs-openrc~2.4.3
zfs-scripts~2.4.3
zfs-udev~2.4.3
zram-init
zram-init-openrc
"
RUNTIME_PACKAGES="
bash
ca-certificates
containerd
containerd-openrc
doas
docker-cli
docker-engine
docker-openrc
e2fsprogs
git
libcrypto3
libssl3
libstdc++
openssh-server
openssh-server-common-openrc
wpa_supplicant
wpa_supplicant-openrc
"
# ==============================================================================
# Helpers
# ==============================================================================
die()
{
echo "ERROR: $*" >&2
exit 1
}
require_uint()
{
name="$1"
value="$2"
case "$value" in
''|*[!0-9]*) die "$name must be a non-negative integer." ;;
esac
}
require_swap_priority()
{
name="$1"
value="$2"
require_uint "$name" "$value"
[ "$value" -le 32767 ] ||
die "$name must be between 0 and 32767."
}
format_duration()
{
total_seconds="$1"
hours=$((total_seconds / 3600))
minutes=$(((total_seconds % 3600) / 60))
seconds=$((total_seconds % 60))
printf '%02d:%02d:%02d' "$hours" "$minutes" "$seconds"
}
is_mounted()
{
awk -v path="$1" '$2 == path { found=1 } END { exit !found }' /proc/mounts
}
wait_for_block_device()
{
path="$1"
attempt=0
while [ "$attempt" -lt 50 ]; do
[ -b "$path" ] && return 0
sleep 1
attempt=$((attempt + 1))
done
return 1
}
blkid_value()
{
blkid_device="$1"
blkid_key="$2"
blkid_output="$(blkid "$blkid_device" 2>/dev/null || true)"
printf '%s\n' "$blkid_output" |
sed -n "s/.*[[:space:]]${blkid_key}=\"\([^\"]*\)\".*/\1/p" |
head -1
}
enable_service()
{
service="$1"
runlevel="$2"
[ -x "/etc/init.d/$service" ] ||
die "Missing OpenRC service: $service"
rc-update add "$service" "$runlevel"
}
write_zram_config()
{
memory_percent="$1"
case "$memory_percent" in
''|*[!0-9]*) die "Invalid ZRAM memory percentage: $memory_percent" ;;
esac
mkdir -p /etc/conf.d
cat >/etc/conf.d/zram-init <<EOF
load_on_start=yes
unload_on_stop=yes
num_devices=1
type0=swap
flag0=$SWAP_PRI_ZRAM
size0="\$(awk -v percent=$memory_percent '/^MemTotal:/ { print int((\$2 / 1024) * percent / 100); exit }' /proc/meminfo)"
mlim0=
back0=
icmp0=
idle0=
wlim0=
notr0=
algo0=
para0=
labl0=nenjim_zram
uuid0=
args0=
EOF
}
ensure_empty_path_can_be_linked()
{
path="$1"
if [ -L "$path" ]; then
rm -f "$path"
return 0
fi
if [ -d "$path" ]; then
if find "$path" -mindepth 1 -print -quit | grep -q .; then
die "Refusing to replace non-empty directory: $path"
fi
rmdir "$path"
elif [ -e "$path" ]; then
die "Refusing to replace non-directory path: $path"
fi
}
create_or_validate_user()
{
username="$1"
uid="$2"
home="$3"
shell="$4"
if id "$username" >/dev/null 2>&1; then
actual_uid="$(id -u "$username")"
[ "$actual_uid" = "$uid" ] ||
die "$username has UID $actual_uid, expected $uid."
else
adduser \
-D \
-u "$uid" \
-h "$home" \
-s "$shell" \
"$username"
fi
sed -i \
"s#^\($username:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*:[^:]*#\1$home:$shell#" \
/etc/passwd
}
# ==============================================================================
# Locate the FAT boot media and first-boot inputs
# ==============================================================================
ovl="$(
dmesg |
grep -o 'Loading user settings from .*:' |
awk '{print $5}' |
sed 's/:.*$//' |
head -1
)"
if [ -n "${ovl:-}" ] && [ -f "$ovl" ]; then
BOOT_MEDIA="$(dirname "$ovl")"
else
BOOT_MEDIA="$(
find /media \
-maxdepth 3 \
-type f \
-name headless.apkovl.tar.gz \
-exec dirname {} \; |
head -1
)"
fi
[ -n "${BOOT_MEDIA:-}" ] && [ -d "$BOOT_MEDIA" ] ||
die "Could not locate the Alpine boot media."
mount -o remount,rw "$BOOT_MEDIA" 2>/dev/null || true
[ -f "$BOOT_MEDIA/wpa_supplicant.conf" ] ||
die "Missing wpa_supplicant.conf on the boot partition."
[ -f "$BOOT_MEDIA/installZeroTier.sh" ] ||
die "Missing installZeroTier.sh on the boot partition."
[ -f "$BOOT_MEDIA/$CONFIG_FILE_NAME" ] ||
die "Missing $CONFIG_FILE_NAME on the boot partition; rerun flash.sh."
# ==============================================================================
# Logging
# ==============================================================================
INSTALL_LOG="$BOOT_MEDIA/NenjimHub-Install.log"
LEGACY_INSTALL_LOG="$BOOT_MEDIA/nenjim-install.log"
if [ -f "$LEGACY_INSTALL_LOG" ] && [ ! -e "$INSTALL_LOG" ]; then
mv "$LEGACY_INSTALL_LOG" "$INSTALL_LOG"
fi
exec >>"$INSTALL_LOG" 2>&1
logger -t nenjim-install "Starting unattended Alpine installation"
echo
echo "============================================================================"
echo "Nenjim Alpine/ZFS bootstrap"
echo "Boot media: $BOOT_MEDIA"
echo "Log: $INSTALL_LOG"
echo "============================================================================"
# ==============================================================================
# Load and validate the shared provisioning configuration
# ==============================================================================
CONFIG_PATH="$BOOT_MEDIA/$CONFIG_FILE_NAME"
# shellcheck source=NenjimHub.conf
. "$CONFIG_PATH"
require_uint BOOT_PARTITION_SIZE_MIB "${BOOT_PARTITION_SIZE_MIB:-}"
require_uint VIRT_ZVOL_SIZE_MIB "${VIRT_ZVOL_SIZE_MIB:-}"
require_uint SWAP_SIZE_MIB_PART "${SWAP_SIZE_MIB_PART:-}"
require_uint SWAP_SIZE_MIB_ZFS "${SWAP_SIZE_MIB_ZFS:-}"
require_uint SWAP_SIZE_PCT_ZRAM "${SWAP_SIZE_PCT_ZRAM:-}"
require_uint DISKLESS_ROOT_SIZE_MIB "${DISKLESS_ROOT_SIZE_MIB:-}"
require_uint ZFS_ARC_MAX_MIB "${ZFS_ARC_MAX_MIB:-}"
require_uint ZEROTIER_REQUIRED_SWAP_MIB "${ZEROTIER_REQUIRED_SWAP_MIB:-}"
require_uint ZEROTIER_MAKE_JOBS "${ZEROTIER_MAKE_JOBS:-}"
require_uint ZEROTIER_ENABLE_SSO "${ZEROTIER_ENABLE_SSO:-}"
require_swap_priority SWAP_PRI_ZRAM "${SWAP_PRI_ZRAM:-}"
require_swap_priority SWAP_PRI_ZFS "${SWAP_PRI_ZFS:-}"
require_swap_priority SWAP_PRI_PART "${SWAP_PRI_PART:-}"
[ "$BOOT_PARTITION_SIZE_MIB" -gt 0 ] ||
die "BOOT_PARTITION_SIZE_MIB must be greater than zero."
[ "$VIRT_ZVOL_SIZE_MIB" -gt 0 ] ||
die "VIRT_ZVOL_SIZE_MIB must be greater than zero."
[ "$DISKLESS_ROOT_SIZE_MIB" -gt 0 ] ||
die "DISKLESS_ROOT_SIZE_MIB must be greater than zero."
[ "$ZFS_ARC_MAX_MIB" -gt 0 ] ||
die "ZFS_ARC_MAX_MIB must be greater than zero."
[ "$ZEROTIER_REQUIRED_SWAP_MIB" -gt 0 ] ||
die "ZEROTIER_REQUIRED_SWAP_MIB must be greater than zero."
[ "$ZEROTIER_MAKE_JOBS" -gt 0 ] ||
die "ZEROTIER_MAKE_JOBS must be greater than zero."
case "$ZEROTIER_ENABLE_SSO" in
0|1) ;;
*) die "ZEROTIER_ENABLE_SSO must be either 0 or 1." ;;
esac
[ -n "${ZEROTIER_VERSION:-}" ] ||
die "ZEROTIER_VERSION must not be empty."
printf '%s\n' "$ZEROTIER_VERSION" |
grep -Eq '^[0-9]+([.][0-9]+){2}([._-][[:alnum:]]+)*$' ||
die "Invalid ZEROTIER_VERSION: $ZEROTIER_VERSION"
printf '%s\n' "${ZEROTIER_PREBUILT_BINARY:-}" |
grep -Eq '^[[:alnum:]][[:alnum:]._-]*$' ||
die "ZEROTIER_PREBUILT_BINARY must be a plain filename without directories."
[ "$SWAP_SIZE_PCT_ZRAM" -le 100 ] ||
die "SWAP_SIZE_PCT_ZRAM must be between 0 and 100."
printf '%s\n' "${NENJIMHUB_HOSTNAME:-}" |
grep -Eq '^[[:alnum:]]([[:alnum:]-]{0,61}[[:alnum:]])?$' ||
die "NENJIMHUB_HOSTNAME must be one 1-63 character DNS hostname label."
printf '%s\n' "${CONSOLE_KEYMAP_LAYOUT:-}" |
grep -Eq '^[A-Za-z0-9][A-Za-z0-9_-]*$' ||
die "Invalid CONSOLE_KEYMAP_LAYOUT: ${CONSOLE_KEYMAP_LAYOUT:-empty}"
printf '%s\n' "${CONSOLE_KEYMAP_VARIANT:-}" |
grep -Eq '^[A-Za-z0-9][A-Za-z0-9_-]*$' ||
die "Invalid CONSOLE_KEYMAP_VARIANT: ${CONSOLE_KEYMAP_VARIANT:-empty}"
printf '%s\n' "${SYSTEM_LOCALE:-}" |
grep -Eq '^[A-Za-z][A-Za-z0-9_.@-]*$' ||
die "Invalid SYSTEM_LOCALE: ${SYSTEM_LOCALE:-empty}"
printf '%s\n' "$POOL_NAME" | grep -Eq '^[A-Za-z][A-Za-z0-9_.:-]*$' ||
die "Invalid POOL_NAME: $POOL_NAME"
printf '%s\n' "$SWAP_PART_LABEL" | grep -Eq '^[[:alnum:]_-]{1,15}$' ||
die "Invalid SWAP_PART_LABEL: $SWAP_PART_LABEL"
SWAP_ZVOL="$POOL_NAME/swap"
SWAP_SIZE_BYTES=$((SWAP_SIZE_MIB_ZFS * 1024 * 1024))
SWAP_DEVICE="/dev/zvol/$POOL_NAME/swap"
SWAP_PART_DEVICE="/dev/disk/by-label/$SWAP_PART_LABEL"
VIRT_ZVOL="$POOL_NAME/virt"
VIRT_DEVICE="/dev/zvol/$POOL_NAME/virt"
VIRT_SIZE_BYTES=$((VIRT_ZVOL_SIZE_MIB * 1024 * 1024))
DISKLESS_ROOT_SIZE="${DISKLESS_ROOT_SIZE_MIB}M"
ZFS_ARC_MAX_BYTES=$((ZFS_ARC_MAX_MIB * 1024 * 1024))
echo "Swap profile:"
echo " ZRAM: ${SWAP_SIZE_PCT_ZRAM}% at priority $SWAP_PRI_ZRAM"
echo " raw p2: ${SWAP_SIZE_MIB_PART} MiB at priority $SWAP_PRI_PART"
echo " ZFS zvol: ${SWAP_SIZE_MIB_ZFS} MiB at priority $SWAP_PRI_ZFS"
if [ "$SWAP_SIZE_MIB_PART" -gt 0 ] && \
[ "$SWAP_SIZE_MIB_ZFS" -gt 0 ] && \
[ "$SWAP_PRI_ZFS" -gt "$SWAP_PRI_PART" ]; then
echo "WARNING: ZFS swap has higher priority than raw partition swap."
echo " This is allowed, but raw swap is safer under severe memory pressure."
fi
# ==============================================================================
# Activate stable raw swap before setup-alpine and package installation
# ==============================================================================
if [ "$SWAP_SIZE_MIB_PART" -gt 0 ]; then
echo
echo "=== Activating early raw partition swap ==="
BOOT_BLOCK_DEVICE="$(
awk -v path="$BOOT_MEDIA" '$2 == path { print $1; exit }' /proc/mounts
)"
case "$BOOT_BLOCK_DEVICE" in
*p1) EARLY_SWAP_PARTITION="${BOOT_BLOCK_DEVICE%p1}p2" ;;
*1) EARLY_SWAP_PARTITION="${BOOT_BLOCK_DEVICE%1}2" ;;
*) die "Could not derive partition 2 from boot device $BOOT_BLOCK_DEVICE." ;;
esac
wait_for_block_device "$EARLY_SWAP_PARTITION" ||
die "Raw swap partition did not appear: $EARLY_SWAP_PARTITION"
[ "$(blkid_value "$EARLY_SWAP_PARTITION" TYPE)" = "swap" ] ||
die "$EARLY_SWAP_PARTITION is not formatted as Linux swap."
[ "$(blkid_value "$EARLY_SWAP_PARTITION" LABEL)" = "$SWAP_PART_LABEL" ] ||
die "$EARLY_SWAP_PARTITION does not have label $SWAP_PART_LABEL."
EARLY_SWAP_KERNEL_DEVICE="$(readlink -f "$EARLY_SWAP_PARTITION")"
if ! awk -v device="$EARLY_SWAP_KERNEL_DEVICE" \
'NR > 1 && $1 == device { found=1 } END { exit !found }' /proc/swaps
then
swapon -p "$SWAP_PRI_PART" "$EARLY_SWAP_PARTITION"
fi
fi
# ==============================================================================
# Seed a trustworthy clock before HTTPS (the Raspberry Pi has no RTC)
# ==============================================================================
echo
echo "=== Seeding the clock for HTTPS package access ==="
BUILD_TIME_FILE="$BOOT_MEDIA/$BUILD_TIME_FILE_NAME"
[ -f "$BUILD_TIME_FILE" ] ||
die "Missing $BUILD_TIME_FILE_NAME; rerun the updated flash.sh."
BUILD_TIME="$(sed -n '1p' "$BUILD_TIME_FILE")"
printf '%s\n' "$BUILD_TIME" | grep -Eq \
'^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$' ||
die "Invalid UTC build time in $BUILD_TIME_FILE."
echo "Clock before seed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
date -u -s "$BUILD_TIME" >/dev/null
echo "Clock after seed: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
# ==============================================================================
# Persistent diskless root size
# ==============================================================================
echo
echo "=== Configuring diskless root size: $DISKLESS_ROOT_SIZE ==="
CMDLINE_FILE="$BOOT_MEDIA/cmdline.txt"
[ -f "$CMDLINE_FILE" ] || die "Missing Raspberry Pi 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"
# ==============================================================================
# Base Alpine diskless configuration
# ==============================================================================
echo
echo "=== Validating console keymap: $CONSOLE_KEYMAP_LAYOUT $CONSOLE_KEYMAP_VARIANT ==="
# setup-keymap installs this package itself, but an invalid non-interactive
# variant would fall back to an interactive prompt. Keep a temporary reference
# while validating so setup-keymap can use the same already-installed files.
apk add \
--quiet \
--no-cache \
--virtual .nenjim-keymap-check \
kbd-bkeymaps
KEYMAP_SOURCE_FOUND=0
for keymap_source in \
"/usr/share/bkeymaps/$CONSOLE_KEYMAP_LAYOUT/$CONSOLE_KEYMAP_VARIANT.bmap" \
"/usr/share/bkeymaps/$CONSOLE_KEYMAP_LAYOUT/$CONSOLE_KEYMAP_VARIANT.bmap.gz"
do
if [ -f "$keymap_source" ]; then
KEYMAP_SOURCE_FOUND=1
break
fi
done
if [ "$KEYMAP_SOURCE_FOUND" -ne 1 ]; then
echo "Available variants for layout $CONSOLE_KEYMAP_LAYOUT:" >&2
ls -1 "/usr/share/bkeymaps/$CONSOLE_KEYMAP_LAYOUT" 2>/dev/null >&2 || true
die "Console keymap does not exist: $CONSOLE_KEYMAP_LAYOUT $CONSOLE_KEYMAP_VARIANT"
fi
cat >/tmp/ANSWERFILE <<EOF
KEYMAPOPTS="$CONSOLE_KEYMAP_LAYOUT $CONSOLE_KEYMAP_VARIANT"
HOSTNAMEOPTS="$NENJIMHUB_HOSTNAME"
DEVDOPTS=udev
INTERFACESOPTS="auto lo
iface lo inet loopback
auto $MY_IFACE
iface $MY_IFACE inet dhcp
"
DNSOPTS=""
TIMEZONEOPTS="$MY_TIMEZONE"
PROXYOPTS=none
APKREPOSOPTS="$BOOT_MEDIA/apks $APK_MAIN_REPOSITORY $APK_COMMUNITY_REPOSITORY"
USEROPTS=none
SSHDOPTS=openssh
NTPOPTS=chrony
DISKOPTS=none
LBUOPTS="$BOOT_MEDIA"
APKCACHEOPTS="$BOOT_MEDIA/cache"
EOF
# Keep the headless bootstrap SSH session alive while setup-alpine configures
# the final OpenSSH service.
SSH_CONNECTION="FAKE" setup-alpine -ef /tmp/ANSWERFILE
[ -s /etc/conf.d/loadkmap ] ||
die "setup-keymap did not create /etc/conf.d/loadkmap."
apk del --quiet --no-progress .nenjim-keymap-check
for repository in \
"$APK_MAIN_REPOSITORY" \
"$APK_COMMUNITY_REPOSITORY"
do
grep -qxF "$repository" /etc/apk/repositories ||
die "Required APK repository is missing: $repository"
done
echo "Configured APK repositories:"
cat /etc/apk/repositories
# ==============================================================================
# Persistent APK cache and LBU destination
# ==============================================================================
echo
echo "=== Configuring persistent APK cache and LBU ==="
mkdir -p "$BOOT_MEDIA/cache" /etc/lbu
touch "$BOOT_MEDIA/cache/.boot_repository"
BOOT_AVAILABLE_KB="$(df -Pk "$BOOT_MEDIA" | awk 'NR == 2 { print $4 }')"
[ "$BOOT_AVAILABLE_KB" -ge $((400 * 1024)) ] ||
die "Partition 1 needs at least 400 MB free for the APK cache."
rm -f /etc/apk/cache
ln -s "$BOOT_MEDIA/cache" /etc/apk/cache
cat >/etc/lbu/lbu.conf <<EOF
DEFAULT_CIPHER=aes-256-cbc
LBU_BACKUPDIR=$BOOT_MEDIA
BACKUP_LIMIT=3
EOF
install -m 0644 "$CONFIG_PATH" /etc/NenjimHub.conf
# ==============================================================================
# Install ZFS, udev and ZRAM before the larger runtime package transaction
# ==============================================================================
echo
echo "=== Installing early OpenZFS, udev and ZRAM packages ==="
apk update
# Intentional word splitting of the newline-separated package list.
# shellcheck disable=SC2086
apk add $EARLY_PACKAGES
echo
echo "=== Configuring UTF-8 terminals ==="
mkdir -p /etc/profile.d
cat >/etc/profile.d/99-nenjim-utf8.sh <<EOF
export LANG="$SYSTEM_LOCALE"
export LC_CTYPE="$SYSTEM_LOCALE"
EOF
chmod 0644 /etc/profile.d/99-nenjim-utf8.sh
# Alpine's console setup reads this OpenRC setting during normal boots.
touch /etc/rc.conf
sed -i '/^[[:space:]]*unicode=/d' /etc/rc.conf
echo 'unicode="YES"' >>/etc/rc.conf
export LANG="$SYSTEM_LOCALE"
export LC_CTYPE="$SYSTEM_LOCALE"
lbu include /etc/profile.d/99-nenjim-utf8.sh
lbu include /etc/rc.conf
echo "Early monitoring tools are ready:"
echo " tmux new-session -s install"
echo " htop"
echo " btop"
echo " tail -f $INSTALL_LOG"
mkdir -p /etc/modprobe.d /etc/zfs
cat >/etc/modprobe.d/zfs.conf <<EOF
options zfs zfs_arc_max=$ZFS_ARC_MAX_BYTES
EOF
modprobe zfs
udevadm settle
ZFS_VERSION_OUTPUT="$(zfs version)"
printf '%s\n' "$ZFS_VERSION_OUTPUT"
printf '%s\n' "$ZFS_VERSION_OUTPUT" |
grep -Eq "^zfs-${EXPECTED_ZFS_VERSION}([.-]|$)" ||
die "OpenZFS userspace is not $EXPECTED_ZFS_VERSION."
printf '%s\n' "$ZFS_VERSION_OUTPUT" |
grep -Eq "^zfs-kmod-${EXPECTED_ZFS_VERSION}([.-]|$)" ||
die "The Raspberry Pi OpenZFS kernel module is not $EXPECTED_ZFS_VERSION."
[ -f "/usr/share/zfs/compatibility.d/$POOL_COMPATIBILITY" ] ||
die "Missing pool compatibility definition: $POOL_COMPATIBILITY"
# ==============================================================================
# Activate the optional raw swap partition before importing ZFS
# ==============================================================================
echo
if [ "$SWAP_SIZE_MIB_PART" -gt 0 ]; then
echo "=== Activating raw partition swap at priority $SWAP_PRI_PART ==="
udevadm trigger --subsystem-match=block 2>/dev/null || true
udevadm settle
wait_for_block_device "$SWAP_PART_DEVICE" ||
die "Raw swap partition did not appear as $SWAP_PART_DEVICE."
PART_SWAP_KERNEL_DEVICE="$(readlink -f "$SWAP_PART_DEVICE")"
[ -b "$PART_SWAP_KERNEL_DEVICE" ] ||
die "Could not resolve raw swap partition $SWAP_PART_DEVICE."
[ "$(blkid_value "$SWAP_PART_DEVICE" TYPE)" = "swap" ] ||
die "$SWAP_PART_DEVICE is not formatted as Linux swap."
if ! awk -v device="$PART_SWAP_KERNEL_DEVICE" \
'NR > 1 && $1 == device { found=1 } END { exit !found }' /proc/swaps
then
swapon -p "$SWAP_PRI_PART" "$SWAP_PART_DEVICE"
fi
cat >/etc/init.d/nenjim-part-swap <<EOF
#!/sbin/openrc-run
description="Nenjim raw partition swap"
device_link="$SWAP_PART_DEVICE"
priority="$SWAP_PRI_PART"
depend()
{
after udev-settle
before zfs-import containerd docker zrepl
}
start()
{
ebegin "Enabling raw partition swap"
device="\$(readlink -f "\$device_link")"
[ -b "\$device" ] || {
eend 1 "Could not resolve \$device_link"
return 1
}
if awk -v device="\$device" \
'NR > 1 && \$1 == device { found=1 } END { exit !found }' /proc/swaps
then
eend 0
return 0
fi
swapon -p "\$priority" "\$device_link"
eend \$?
}
stop()
{
ebegin "Disabling raw partition swap"
device="\$(readlink -f "\$device_link")"
if [ ! -b "\$device" ] || ! awk -v device="\$device" \
'NR > 1 && \$1 == device { found=1 } END { exit !found }' /proc/swaps
then
eend 0
return 0
fi
swapoff "\$device"
eend \$?
}
EOF
chmod 0755 /etc/init.d/nenjim-part-swap
enable_service nenjim-part-swap boot
lbu include /etc/init.d/nenjim-part-swap
else
echo "=== Raw partition swap is disabled ==="
fi
# ==============================================================================
# Start optional provisioning ZRAM before importing ZFS
# ==============================================================================
if [ "$SWAP_SIZE_PCT_ZRAM" -gt 0 ]; then
echo
echo "=== Starting temporary $ZRAM_INSTALL_MEMORY_PERCENT% provisioning ZRAM ==="
write_zram_config "$ZRAM_INSTALL_MEMORY_PERCENT"
enable_service zram-init boot
if ! rc-service zram-init start; then
# Some kernels need a short moment after loading the zram module before
# /dev/zram0 becomes available.
sleep 1
rc-service zram-init start
fi
awk -v device="$ZRAM_SWAP_DEVICE" \
'NR > 1 && $1 == device { found=1 } END { exit !found }' /proc/swaps ||
die "$ZRAM_SWAP_DEVICE did not become active."
zramctl
else
echo
echo "=== ZRAM swap is disabled ==="
fi
# ==============================================================================
# Import rpool without enabling any newer feature flags
# ==============================================================================
echo
echo "=== Importing $POOL_NAME from partition 3 ==="
if ! zpool list -H -o name "$POOL_NAME" >/dev/null 2>&1; then
zpool import -N -d /dev "$POOL_NAME"
fi
[ "$(zpool get -H -o value compatibility "$POOL_NAME")" = "$POOL_COMPATIBILITY" ] ||
die "$POOL_NAME does not use compatibility=$POOL_COMPATIBILITY."
zpool set cachefile=/etc/zfs/zpool.cache "$POOL_NAME"
for dataset_mount in \
"$POOL_NAME/home:/home" \
"$POOL_NAME/software:$SOFTWARE_ROOT" \
"$POOL_NAME/sysop:$SYSOP_DATA_ROOT"
do
dataset="${dataset_mount%%:*}"
expected_mount="${dataset_mount#*:}"
actual_mount="$(zfs get -H -o value mountpoint "$dataset")"
[ "$actual_mount" = "$expected_mount" ] ||
die "$dataset has mountpoint $actual_mount, expected $expected_mount."
done
zfs mount -a
for mount_path in /home "$SOFTWARE_ROOT" "$SYSOP_DATA_ROOT"; do
is_mounted "$mount_path" || die "ZFS dataset is not mounted at $mount_path."
done
# ==============================================================================
# Create, format and activate optional ZFS swap using the Pi's page size
# ==============================================================================
if [ "$SWAP_SIZE_MIB_ZFS" -gt 0 ]; then
echo
echo "=== Configuring ${SWAP_SIZE_MIB_ZFS} MiB ZFS swap zvol ==="
PAGE_SIZE="$(getconf PAGESIZE)"
case "$PAGE_SIZE" in
''|*[!0-9]*) die "Could not determine the Raspberry Pi page size." ;;
esac
if ! zfs list -H "$SWAP_ZVOL" >/dev/null 2>&1; then
zfs create \
-V "${SWAP_SIZE_MIB_ZFS}M" \
-b "$PAGE_SIZE" \
-o volmode=dev \
-o compression=off \
-o dedup=off \
-o sync=always \
-o logbias=throughput \
-o primarycache=metadata \
-o secondarycache=none \
-o com.sun:auto-snapshot=false \
"$SWAP_ZVOL"
else
[ "$(zfs get -Hp -o value volsize "$SWAP_ZVOL")" = "$SWAP_SIZE_BYTES" ] ||
die "$SWAP_ZVOL is not exactly ${SWAP_SIZE_MIB_ZFS} MiB."
[ "$(zfs get -Hp -o value volblocksize "$SWAP_ZVOL")" = "$PAGE_SIZE" ] ||
die "$SWAP_ZVOL volblocksize does not match getconf PAGESIZE ($PAGE_SIZE)."
fi
zfs set volmode=dev "$SWAP_ZVOL"
udevadm trigger --subsystem-match=block 2>/dev/null || true
udevadm settle
wait_for_block_device "$SWAP_DEVICE" ||
die "Swap zvol device did not appear: $SWAP_DEVICE"
SWAP_KERNEL_DEVICE="$(readlink -f "$SWAP_DEVICE")"
[ -b "$SWAP_KERNEL_DEVICE" ] ||
die "Could not resolve $SWAP_DEVICE to its kernel block device."
if [ "$(blkid_value "$SWAP_DEVICE" TYPE)" != "swap" ]; then
mkswap "$SWAP_DEVICE"
fi
if ! awk -v device="$SWAP_KERNEL_DEVICE" \
'NR > 1 && $1 == device { found=1 } END { exit !found }' /proc/swaps
then
swapon -p "$SWAP_PRI_ZFS" "$SWAP_DEVICE"
fi
cat >/etc/init.d/nenjim-zfs-swap <<EOF
#!/sbin/openrc-run
description="Nenjim swap on $SWAP_ZVOL"
zvol="$SWAP_ZVOL"
device_link="$SWAP_DEVICE"
priority="$SWAP_PRI_ZFS"
wait_seconds=60
depend()
{
need zfs-import
after udev-settle
before containerd docker zrepl
}
start()
{
ebegin "Enabling ZFS swap"
if ! zfs list -H -o name "\$zvol" >/dev/null 2>&1; then
eend 1 "Required zvol does not exist: \$zvol"
return 1
fi
udevadm trigger --subsystem-match=block >/dev/null 2>&1 || true
attempt=0
while [ "\$attempt" -lt "\$wait_seconds" ]; do
[ -b "\$device_link" ] && break
sleep 1
attempt=\$((attempt + 1))
done
device="\$(readlink -f "\$device_link")"
[ -b "\$device" ] || {
eend 1 "Could not resolve \$device_link"
return 1
}
if awk -v device="\$device" \\
'NR > 1 && \$1 == device { found=1 } END { exit !found }' /proc/swaps
then
eend 0
return 0
fi
swapon -p "\$priority" "\$device_link"
eend \$?
}
stop()
{
ebegin "Disabling ZFS swap"
device="\$(readlink -f "\$device_link")"
if [ ! -b "\$device" ]; then
eend 0
return 0
fi
if ! awk -v device="\$device" \\
'NR > 1 && \$1 == device { found=1 } END { exit !found }' /proc/swaps
then
eend 0
return 0
fi
swapoff "\$device"
eend \$?
}
EOF
chmod 0755 /etc/init.d/nenjim-zfs-swap
enable_service nenjim-zfs-swap boot
lbu include /etc/init.d/nenjim-zfs-swap
else
echo
echo "=== ZFS swap is disabled ==="
if zfs list -H "$SWAP_ZVOL" >/dev/null 2>&1; then
echo "An existing $SWAP_ZVOL is left intact but is not enabled as swap."
fi
fi
echo "Active swap devices before runtime package installation:"
cat /proc/swaps
if [ "$SWAP_SIZE_PCT_ZRAM" -gt 0 ]; then
zramctl
fi
ROOT_FILESYSTEM="$(awk '$2 == "/" { print $3; exit }' /proc/mounts)"
if [ "$ROOT_FILESYSTEM" = "tmpfs" ]; then
mount -o "remount,size=$DISKLESS_ROOT_SIZE" /
fi
grep -E 'MemTotal|SwapTotal|SwapFree' /proc/meminfo
# ==============================================================================
# Install the remaining runtime packages with swap active
# ==============================================================================
echo
echo "=== Installing runtime packages ==="
# shellcheck disable=SC2086
apk add $RUNTIME_PACKAGES
# A blanket apk upgrade is deliberately avoided. Diskless kernel and modloop
# updates belong in flash.sh so kernel and zfs-rpi remain matched.
# ==============================================================================
# Wi-Fi, DHCP and SSH persistence
# ==============================================================================
echo
echo "=== Configuring Wi-Fi and DHCP ==="
mkdir -p /etc/wpa_supplicant /etc/conf.d /etc/network /etc/modprobe.d
# Load the Raspberry Pi Broadcom Wi-Fi driver deterministically. The headless
# bootstrap uses these stability options only on first boot and then removes
# its temporary file, so keep the final system's own copy.
grep -qxF brcmfmac /etc/modules 2>/dev/null || echo brcmfmac >>/etc/modules
cat >/etc/modprobe.d/nenjim-brcmfmac.conf <<'EOF'
options brcmfmac roamoff=1 feature_disable=0x282000
EOF
cat >/etc/init.d/nenjim-wifi-ready <<EOF
#!/sbin/openrc-run
description="Wait for the Nenjim Wi-Fi interface"
interface="$MY_IFACE"
wait_seconds=60
depend()
{
need modules
after udev-settle
before wpa_supplicant networking
}
start()
{
ebegin "Waiting for Wi-Fi interface \$interface"
if ! modprobe brcmfmac; then
eend 1 "Could not load brcmfmac"
return 1
fi
udevadm trigger --subsystem-match=net >/dev/null 2>&1 || true
attempt=0
while [ "\$attempt" -lt "\$wait_seconds" ]; do
if [ -d "/sys/class/net/\$interface" ]; then
eend 0
return 0
fi
sleep 1
attempt=\$((attempt + 1))
done
eerror "Wi-Fi interface \$interface did not appear after \$wait_seconds seconds"
dmesg | grep -Ei 'brcm|firmware|wlan' | tail -20 >&2 || true
eend 1
return 1
}
EOF
chmod 0755 /etc/init.d/nenjim-wifi-ready
enable_service nenjim-wifi-ready boot
lbu include /etc/init.d/nenjim-wifi-ready
install \
-m 0600 \
"$BOOT_MEDIA/wpa_supplicant.conf" \
/etc/wpa_supplicant/wpa_supplicant.conf
cat >/etc/conf.d/wpa_supplicant <<EOF
wpa_supplicant_args="-i$MY_IFACE"
rc_need="nenjim-wifi-ready"
EOF
touch /etc/conf.d/networking
sed -i '/^[[:space:]]*rc_need=/d' /etc/conf.d/networking
echo 'rc_need="nenjim-wifi-ready"' >>/etc/conf.d/networking
cat >/etc/network/interfaces <<EOF
auto lo
iface lo inet loopback
auto $MY_IFACE
iface $MY_IFACE inet dhcp
EOF
enable_service wpa_supplicant boot
enable_service networking boot
lbu include /etc/conf.d/wpa_supplicant
lbu include /etc/conf.d/networking
lbu include /etc/modprobe.d/nenjim-brcmfmac.conf
# ==============================================================================
# OpenZFS boot services
# ==============================================================================
echo
echo "=== Enabling OpenZFS services ==="
grep -qxF zfs /etc/modules 2>/dev/null || echo zfs >>/etc/modules
enable_service modules boot
enable_service zfs-import boot
enable_service zfs-mount boot
enable_service zfs-zed default
# ==============================================================================
# Mount rpool/virt as ext4 before Docker and containerd
# ==============================================================================
echo
echo "=== Configuring shared Docker/containerd storage ==="
zfs list -H "$VIRT_ZVOL" >/dev/null 2>&1 ||
die "Missing zvol created by prepare-storage.sh: $VIRT_ZVOL"
[ "$(zfs get -Hp -o value volsize "$VIRT_ZVOL")" = "$VIRT_SIZE_BYTES" ] ||
die "$VIRT_ZVOL does not match VIRT_ZVOL_SIZE_MIB=$VIRT_ZVOL_SIZE_MIB."
# This is a host block device, not a guest disk with partitions. Persist an
# explicit visible device mode instead of relying on the platform default.
zfs set volmode=dev "$VIRT_ZVOL"
udevadm trigger --subsystem-match=block 2>/dev/null || true
udevadm settle
wait_for_block_device "$VIRT_DEVICE" ||
die "Virt zvol device did not appear: $VIRT_DEVICE"
FSCK_STATUS=0
e2fsck -p "$VIRT_DEVICE" || FSCK_STATUS=$?
[ "$FSCK_STATUS" -le 1 ] ||
die "e2fsck could not repair $VIRT_DEVICE (status $FSCK_STATUS)."
mkdir -p "$VIRT_MOUNT"
if ! is_mounted "$VIRT_MOUNT"; then
mount -t ext4 -o rw,noatime "$VIRT_DEVICE" "$VIRT_MOUNT"
fi
mkdir -p "$VIRT_MOUNT/docker" "$VIRT_MOUNT/containerd"
chmod 0711 "$VIRT_MOUNT/docker" "$VIRT_MOUNT/containerd"
if [ -f /etc/fstab ]; then
sed -i '\|[[:space:]]/media/virt[[:space:]]|d' /etc/fstab
fi
echo "$VIRT_DEVICE $VIRT_MOUNT ext4 noauto,rw,noatime 0 0" >>/etc/fstab
cat >/etc/init.d/nenjim-virt <<EOF
#!/sbin/openrc-run
description="Nenjim Docker/containerd ext4 zvol"
pool="$POOL_NAME"
zvol="$VIRT_ZVOL"
device="$VIRT_DEVICE"
mountpoint="$VIRT_MOUNT"
wait_seconds=60
depend()
{
need zfs-import
after udev-settle
before containerd docker
}
start()
{
ebegin "Checking and mounting Nenjim virt storage"
if mountpoint -q "\$mountpoint"; then
eend 0
return 0
fi
if ! zpool list -H -o name "\$pool" >/dev/null 2>&1; then
eerror "ZFS pool \$pool was not imported by zfs-import"
eend 1
return 1
fi
if ! zfs list -H -o name "\$zvol" >/dev/null 2>&1; then
eerror "Required zvol does not exist: \$zvol"
eend 1
return 1
fi
# ZFS creates the kernel zvol asynchronously and udev subsequently creates
# /dev/zvol/<pool>/<name>. Wait for the exact link with a finite timeout so
# a udev problem cannot block the boot forever.
udevadm trigger --subsystem-match=block >/dev/null 2>&1 || true
attempt=0
while [ "\$attempt" -lt "\$wait_seconds" ]; do
[ -b "\$device" ] && break
sleep 1
attempt=\$((attempt + 1))
done
if [ ! -b "\$device" ]; then
eerror "Zvol device \$device did not appear after \$wait_seconds seconds"
ls -l /dev/zd* /dev/zvol/"\$pool" 2>/dev/null >&2 || true
eend 1
return 1
fi
e2fsck -p "\$device"
status=\$?
[ "\$status" -le 1 ] || {
eend "\$status"
return "\$status"
}
mount -t ext4 -o rw,noatime "\$device" "\$mountpoint"
eend \$?
}
stop()
{
ebegin "Unmounting Nenjim virt storage"
umount "\$mountpoint"
eend \$?
}
EOF
chmod 0755 /etc/init.d/nenjim-virt
enable_service nenjim-virt boot
lbu include /etc/init.d/nenjim-virt
# ==============================================================================
# User accounts and persistent homes
# ==============================================================================
echo
echo "=== Configuring sysop and nenjim accounts ==="
if [ -f "$BOOT_MEDIA/sysop-preserve.tar.gz" ]; then
echo "Restoring the preserved /sysop rescue home"
tar -xzf "$BOOT_MEDIA/sysop-preserve.tar.gz" -C /
fi
mkdir -p /sysop "/home/$NENJIM_USER" "$SYSOP_DATA_ROOT"
create_or_validate_user "$SYSOP_USER" "$SYSOP_UID" /sysop /bin/bash
create_or_validate_user "$NENJIM_USER" "$NENJIM_UID" "/home/$NENJIM_USER" /bin/bash
addgroup "$SYSOP_USER" wheel 2>/dev/null || true
addgroup "$SYSOP_USER" docker 2>/dev/null || true
delgroup "$NENJIM_USER" wheel 2>/dev/null || true
delgroup "$NENJIM_USER" docker 2>/dev/null || true
echo "$SYSOP_USER:$SYSOP_PASS" | chpasswd
echo "$NENJIM_USER:$NENJIM_PASS" | chpasswd
chown "$SYSOP_USER:$SYSOP_USER" /sysop
chmod 0750 /sysop
chown "$NENJIM_USER:$NENJIM_USER" "/home/$NENJIM_USER"
chmod 0750 "/home/$NENJIM_USER"
cat >/etc/profile.d/nenjim-java.sh <<'PROFILE'
export JAVA_HOME=/usr/local/software/jre-25
export PATH="${JAVA_HOME}/bin:${PATH}"
PROFILE
chmod 0644 /etc/profile.d/nenjim-java.sh
mkdir -p /var/log
rm -f /var/log/NenjimHub-Install.log
ln -s "$INSTALL_LOG" /var/log/NenjimHub-Install.log
lbu include /sysop
lbu include /var/log/NenjimHub-Install.log
# ==============================================================================
# Password-required doas for sysop only
# ==============================================================================
echo
echo "=== Configuring password-required doas for sysop ==="
mkdir -p /etc/doas.d
rm -f /etc/doas.d/20-wheel.conf /etc/doas.d/nenjim.conf /etc/doas.d/99-nenjim.conf
cat >/etc/doas.d/99-sysop.conf <<EOF
permit persist $SYSOP_USER as root
EOF
chmod 0400 /etc/doas.d/99-sysop.conf
doas -C /etc/doas.d/99-sysop.conf
id "$SYSOP_USER" | grep -qw wheel || die "sysop is not in wheel."
id "$SYSOP_USER" | grep -qw docker || die "sysop is not in the docker group."
if id "$NENJIM_USER" | grep -qw wheel; then
die "nenjim must not be in wheel."
fi
if id "$NENJIM_USER" | grep -qw docker; then
die "nenjim must not be in the docker group."
fi
# ==============================================================================
# Disable direct root login from console and SSH
# ==============================================================================
echo
echo "=== Locking direct root login ==="
ROOT_PASSWORD_FIELD="$(awk -F: '$1 == "root" { print $2; exit }' /etc/shadow)"
case "$ROOT_PASSWORD_FIELD" in
'!'*|'*'*)
;;
*)
sed -i 's/^root:[^:]*:/root:!:/' /etc/shadow
;;
esac
mkdir -p /etc/ssh/sshd_config.d
if ! grep -Eq \
'^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config.d/\*\.conf' \
/etc/ssh/sshd_config
then
{
echo 'Include /etc/ssh/sshd_config.d/*.conf'
cat /etc/ssh/sshd_config
} >/etc/ssh/sshd_config.new
mv /etc/ssh/sshd_config.new /etc/ssh/sshd_config
fi
cat >/etc/ssh/sshd_config.d/10-nenjim.conf <<EOF
PermitRootLogin no
PasswordAuthentication yes
PubkeyAuthentication yes
AllowUsers $SYSOP_USER $NENJIM_USER
EOF
chmod 0600 /etc/ssh/sshd_config.d/10-nenjim.conf
ssh-keygen -A
sshd -t
enable_service sshd default
rc-service sshd restart
# ==============================================================================
# Remove MOTD on every diskless boot before login services start
# ==============================================================================
echo
echo "=== Removing /etc/motd ==="
rm -f /etc/motd
cat >/etc/init.d/nenjim-clean-login <<'OPENRC'
#!/sbin/openrc-run
description="Remove the default Alpine MOTD"
depend()
{
before sshd
}
start()
{
rm -f /etc/motd
}
OPENRC
chmod 0755 /etc/init.d/nenjim-clean-login
enable_service nenjim-clean-login boot
lbu include /etc/init.d/nenjim-clean-login
# Alpine starts its physical console login directly from BusyBox inittab; it is
# not an OpenRC service. Ensure tty1 has a respawning login prompt and persist
# the inittab entry in the diskless overlay. Root remains locked, while sysop
# can use the physical console for recovery.
if ! grep -Eq '^[[:space:]]*tty1::respawn:/sbin/(a?getty|busybox[[:space:]]+getty)([[:space:]]|$)' /etc/inittab; then
echo 'tty1::respawn:/sbin/getty 38400 tty1' >>/etc/inittab
fi
lbu include /etc/inittab
# ==============================================================================
# Docker and standalone containerd on rpool/virt
# ==============================================================================
echo
echo "=== Configuring Docker and containerd ==="
mkdir -p /var/lib /etc/conf.d
ensure_empty_path_can_be_linked /var/lib/docker
ensure_empty_path_can_be_linked /var/lib/containerd
ln -s "$VIRT_MOUNT/docker" /var/lib/docker
ln -s "$VIRT_MOUNT/containerd" /var/lib/containerd
for service in docker containerd; do
conf_file="/etc/conf.d/$service"
touch "$conf_file"
sed -i '/^[[:space:]]*rc_need=/d' "$conf_file"
echo 'rc_need="nenjim-virt"' >>"$conf_file"
done
lbu include /var/lib/docker
lbu include /var/lib/containerd
lbu include /etc/conf.d/docker
lbu include /etc/conf.d/containerd
enable_service cgroups default
enable_service containerd default
enable_service docker default
# Docker group access is root-equivalent. It is granted only to sysop; nenjim
# deliberately remains outside both wheel and docker.
# ==============================================================================
# Verify Java installed by prepare-storage.sh
# ==============================================================================
echo
echo "=== Verifying Java on rpool/software ==="
[ -x "$SOFTWARE_ROOT/jre-25/bin/java" ] ||
die "Java is missing from $SOFTWARE_ROOT; rerun prepare-storage.sh on a fresh card."
"$SOFTWARE_ROOT/jre-25/bin/java" -version
# ==============================================================================
# Build/install ZeroTier; state lives in sysop's ZFS-backed area
# ==============================================================================
echo
echo "=== Running the ZeroTier build/install module ==="
echo "nenjim can follow this with: tail -f /var/log/NenjimHub-Install.log"
SOFTWARE_ROOT="$SOFTWARE_ROOT" \
SYSOP_DATA_ROOT="$SYSOP_DATA_ROOT" \
ZEROTIER_VERSION="$ZEROTIER_VERSION" \
REQUIRED_SWAP_MIB="$ZEROTIER_REQUIRED_SWAP_MIB" \
ROOTFS_BUILD_SIZE="$DISKLESS_ROOT_SIZE" \
MAKE_JOBS="$ZEROTIER_MAKE_JOBS" \
ENABLE_SSO="$ZEROTIER_ENABLE_SSO" \
PREBUILT_BINARY="$BOOT_MEDIA/$ZEROTIER_PREBUILT_BINARY" \
/bin/bash "$BOOT_MEDIA/installZeroTier.sh"
# ==============================================================================
# zrepl binary, no-op configuration and daemon
# ==============================================================================
echo
echo "=== Configuring zrepl with an empty job list ==="
ZREPL_BINARY="$SOFTWARE_ROOT/zrepl/current/zrepl"
[ -x "$ZREPL_BINARY" ] ||
die "zrepl is missing from $SOFTWARE_ROOT; rerun prepare-storage.sh on a fresh card."
mkdir -p /usr/local/bin "$ZREPL_CONFIG_DIRECTORY" /etc/zrepl /sysop
rm -f /usr/local/bin/zrepl
ln -s "$ZREPL_BINARY" /usr/local/bin/zrepl
if [ -e /sysop/zrepl ] || [ -L /sysop/zrepl ]; then
if [ -L /sysop/zrepl ]; then
rm -f /sysop/zrepl
elif [ -d /sysop/zrepl ] &&
! find /sysop/zrepl -mindepth 1 -print -quit | grep -q .
then
rmdir /sysop/zrepl
else
die "Refusing to replace non-empty /sysop/zrepl."
fi
fi
ln -s "$ZREPL_CONFIG_DIRECTORY" /sysop/zrepl
if [ ! -f "$ZREPL_CONFIG_FILE" ]; then
cat >"$ZREPL_CONFIG_FILE" <<'ZREPL'
jobs: []
ZREPL
fi
chown -R "$SYSOP_USER:$SYSOP_USER" "$ZREPL_CONFIG_DIRECTORY"
chmod 0750 "$ZREPL_CONFIG_DIRECTORY"
chmod 0640 "$ZREPL_CONFIG_FILE"
rm -f /etc/zrepl/zrepl.yml
ln -s /sysop/zrepl/zrepl.yml /etc/zrepl/zrepl.yml
/usr/local/bin/zrepl configcheck
cat >/etc/init.d/zrepl <<'OPENRC'
#!/sbin/openrc-run
description="zrepl ZFS replication daemon"
command="/usr/local/bin/zrepl"
command_args="daemon"
command_background="yes"
pidfile="/run/${RC_SVCNAME}.pid"
depend()
{
need zfs-mount
use net logger
after networking
}
start_pre()
{
mkdir -p /run/zrepl
chmod 0700 /run/zrepl
"$command" configcheck
}
OPENRC
chmod 0755 /etc/init.d/zrepl
enable_service zrepl default
lbu include /etc/init.d/zrepl
echo "Starting zrepl with the no-op configuration"
rc-service zrepl start
ZREPL_CONTROL_SOCKET="/var/run/zrepl/control"
ZREPL_READY=0
attempt=0
while [ "$attempt" -lt 30 ]; do
if [ -S "$ZREPL_CONTROL_SOCKET" ]; then
ZREPL_READY=1
break
fi
sleep 1
attempt=$((attempt + 1))
done
[ "$ZREPL_READY" -eq 1 ] ||
die "zrepl started, but its control socket did not appear: $ZREPL_CONTROL_SOCKET"
lbu include /usr/local/bin/zrepl
lbu include /sysop/zrepl
# ==============================================================================
# Synchronize the diskless package cache and verify the finished design
# ==============================================================================
echo
echo "=== Synchronizing the persistent APK cache ==="
# setup-alpine may have installed eudev before /etc/apk/cache pointed at the
# boot partition. `apk add` then sees it as already installed and does not
# download its APK. A full cache sync is therefore required so every installed
# package can be reconstructed in RAM before networking exists on the next boot.
apk cache -v sync
echo
echo "=== Verifying packages, pool, users and services ==="
for package in \
bash btop containerd containerd-openrc doas docker-cli docker-engine \
docker-openrc e2fsprogs eudev eudev-openrc git htop kitty-terminfo \
musl-locales openssh-server tmux \
udev-init-scripts udev-init-scripts-openrc \
wpa_supplicant zfs zfs-openrc zfs-scripts zfs-udev \
zram-init zram-init-openrc
do
apk info -e "$package" >/dev/null ||
die "Required package is not installed: $package"
done
for service in udev udev-trigger udev-settle
do
[ -x "/etc/init.d/$service" ] ||
die "Required udev service is missing after APK cache sync: $service"
done
zpool status "$POOL_NAME"
zfs list -r "$POOL_NAME"
is_mounted /home || die "$POOL_NAME/home is not mounted."
is_mounted "$SOFTWARE_ROOT" || die "$POOL_NAME/software is not mounted."
is_mounted "$SYSOP_DATA_ROOT" || die "$POOL_NAME/sysop is not mounted."
is_mounted "$VIRT_MOUNT" || die "$POOL_NAME/virt ext4 is not mounted."
if [ "$SWAP_SIZE_MIB_PART" -gt 0 ]; then
PART_SWAP_KERNEL_DEVICE="$(readlink -f "$SWAP_PART_DEVICE")"
[ -b "$PART_SWAP_KERNEL_DEVICE" ] ||
die "Could not resolve $SWAP_PART_DEVICE during final verification."
ACTIVE_PART_PRIORITY="$(
awk -v device="$PART_SWAP_KERNEL_DEVICE" \
'NR > 1 && $1 == device { print $5; exit }' /proc/swaps
)"
[ "$ACTIVE_PART_PRIORITY" = "$SWAP_PRI_PART" ] ||
die "$SWAP_PART_DEVICE has priority ${ACTIVE_PART_PRIORITY:-inactive}, expected $SWAP_PRI_PART."
fi
if [ "$SWAP_SIZE_PCT_ZRAM" -gt 0 ]; then
ACTIVE_ZRAM_PRIORITY="$(
awk -v device="$ZRAM_SWAP_DEVICE" \
'NR > 1 && $1 == device { print $5; exit }' /proc/swaps
)"
[ "$ACTIVE_ZRAM_PRIORITY" = "$SWAP_PRI_ZRAM" ] ||
die "$ZRAM_SWAP_DEVICE has priority ${ACTIVE_ZRAM_PRIORITY:-inactive}, expected $SWAP_PRI_ZRAM."
fi
if [ "$SWAP_SIZE_MIB_ZFS" -gt 0 ]; then
SWAP_KERNEL_DEVICE="$(readlink -f "$SWAP_DEVICE")"
[ -b "$SWAP_KERNEL_DEVICE" ] ||
die "Could not resolve $SWAP_DEVICE during final verification."
ACTIVE_ZFS_PRIORITY="$(
awk -v device="$SWAP_KERNEL_DEVICE" \
'NR > 1 && $1 == device { print $5; exit }' /proc/swaps
)"
[ "$ACTIVE_ZFS_PRIORITY" = "$SWAP_PRI_ZFS" ] ||
die "$SWAP_DEVICE has priority ${ACTIVE_ZFS_PRIORITY:-inactive}, expected $SWAP_PRI_ZFS."
fi
[ "$(readlink /var/lib/docker)" = "$VIRT_MOUNT/docker" ] ||
die "Docker data is not linked to rpool/virt."
[ "$(readlink /var/lib/containerd)" = "$VIRT_MOUNT/containerd" ] ||
die "containerd data is not linked to rpool/virt."
[ "$(readlink /sysop/zerotier)" = "$SYSOP_DATA_ROOT/zerotier" ] ||
die "ZeroTier state is not linked into /sysop."
[ "$(readlink /sysop/zrepl)" = "$ZREPL_CONFIG_DIRECTORY" ] ||
die "zrepl configuration is not linked into /sysop."
ROOT_PASSWORD_FIELD="$(awk -F: '$1 == "root" { print $2; exit }' /etc/shadow)"
case "$ROOT_PASSWORD_FIELD" in
'!'*|'*'*) echo "Root password login is locked." ;;
*) die "Root password login is not locked." ;;
esac
REQUIRED_SERVICES="
cgroups
containerd
docker
nenjim-clean-login
nenjim-virt
nenjim-wifi-ready
networking
sshd
wpa_supplicant
zerotier-one
zfs-import
zfs-mount
zfs-zed
zrepl
"
if [ "$SWAP_SIZE_MIB_PART" -gt 0 ]; then
REQUIRED_SERVICES="$REQUIRED_SERVICES nenjim-part-swap"
fi
if [ "$SWAP_SIZE_MIB_ZFS" -gt 0 ]; then
REQUIRED_SERVICES="$REQUIRED_SERVICES nenjim-zfs-swap"
fi
if [ "$SWAP_SIZE_PCT_ZRAM" -gt 0 ]; then
REQUIRED_SERVICES="$REQUIRED_SERVICES zram-init"
fi
for service in $REQUIRED_SERVICES
do
[ -x "/etc/init.d/$service" ] ||
die "Required OpenRC service file is missing: /etc/init.d/$service"
rc-update show | grep -Eq "^[[:space:]]*$service[[:space:]]+\\|" ||
die "Required boot service is not enabled: $service"
done
/usr/sbin/zerotier-one -v
/usr/local/bin/zrepl configcheck
/usr/local/bin/zrepl version
# ==============================================================================
# Persist the requested final ZRAM size for normal boots
# ==============================================================================
if [ "$SWAP_SIZE_PCT_ZRAM" -gt 0 ]; then
echo
echo "=== Persisting final $SWAP_SIZE_PCT_ZRAM% ZRAM policy ==="
# The active provisioning ZRAM remains larger until the imminent reboot.
# OpenRC reads the requested final policy on the next boot.
write_zram_config "$SWAP_SIZE_PCT_ZRAM"
else
rm -f /etc/conf.d/zram-init
fi
# ==============================================================================
# Commit the final APKoVL
# ==============================================================================
echo
echo "=== Creating final APKoVL ==="
lbu status -a || true
lbu commit -d
sync
# ==============================================================================
# Remove one-time bootstrap inputs
# ==============================================================================
echo
echo "=== Removing one-time bootstrap files ==="
rm -f "$BOOT_MEDIA/headless.apkovl.tar.gz"
rm -f "$BOOT_MEDIA/unattended.sh"
rm -f "$BOOT_MEDIA/installZeroTier.sh"
rm -f "$BOOT_MEDIA/$ZEROTIER_PREBUILT_BINARY"
rm -f "$BOOT_MEDIA/$BUILD_TIME_FILE_NAME"
rm -f "$BOOT_MEDIA/wpa_supplicant.conf"
rm -f "$BOOT_MEDIA/sysop-preserve.tar.gz"
sync
# ==============================================================================
# Finished
# ==============================================================================
echo
echo "============================================================================"
echo "Installation complete."
echo
echo "sysop/sysop is the rescue, password-doas and Docker administrator account."
echo "nenjim/nenjim cannot use doas, su or Docker directly."
echo "Root password login and SSH root login are disabled."
echo "ZeroTier is enabled but a fresh installation is not joined to a network."
echo "Docker, containerd, OpenZFS and no-op zrepl are enabled for next boot."
echo "The system will now reboot."
echo "============================================================================"
logger -t nenjim-install "Installation complete - rebooting"
INSTALL_END_UPTIME_SECONDS="$(awk '{ print int($1); exit }' /proc/uptime)"
INSTALL_DURATION_SECONDS=$((INSTALL_END_UPTIME_SECONDS - INSTALL_START_UPTIME_SECONDS))
INSTALL_DURATION="$(format_duration "$INSTALL_DURATION_SECONDS")"
echo "Total installation time: $INSTALL_DURATION (HH:MM:SS, $INSTALL_DURATION_SECONDS seconds)."
sync
reboot