1 Commits
Author SHA1 Message Date
minimons 3301acebcf X: First working version for RasPi Zero 2W 2026-08-26 09:46:05 +02:00
7 changed files with 3663 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
# NenjimHub: Alpine, valgfri rå swap og OpenZFS
Designet bruger en klassisk MBR-partitionstabel:
- partition 1: FAT32 til Alpine diskless, APKoVL og APK-cache
- partition 2: valgfri rå Linux-swap
- partition 3: resten af kortet som OpenZFS-poolen `rpool`
ZFS er altid partition 3. Hvis den rå swapstørrelse er `0`, oprettes partition
2 ikke; partitionsnummer 2 står blot tomt. Det gør layoutet entydigt for alle
profiler.
`flash.sh` skriver kun til partition 1. Partition 2 og 3 ændres ikke, så en
senere Alpine-opdatering ikke overskriver swap eller ZFS-data.
## Filer i samme mappe
```text
alpine-rpi-3.24.1-aarch64.img
flash.sh
headless.apkovl.tar.gz
installZeroTier.sh
NenjimHub.conf
prepare-storage.sh
unattended.sh
wpa_supplicant.conf
zerotier-one-1.16.2-linux-musl-aarch64-sso0 # valgfri
zulu25.36.205-ca-jre25.0.4.1-linux_musl_aarch64.tar.gz
```
`prepare-storage.sh` downloader som udgangspunkt det officielle statiske
`zrepl-linux-arm64`-program fra zrepl v0.7.0 og kontrollerer SHA-256-værdien fra
GitHub. Det kan i stedet placeres lokalt ved siden af scriptet med navnet
`zrepl-linux-arm64`.
## Fælles konfiguration
Ret kun `NenjimHub.conf`; både `prepare-storage.sh`, `flash.sh` og første boot
bruger den samme fil. `DEVICE` skal være hele SD-kortet, eksempelvis `/dev/sdc`,
og aldrig en partition som `/dev/sdc1`.
Standardprofilen til Raspberry Pi Zero 2W er:
```ini
DEVICE="/dev/sdc"
NENJIMHUB_HOSTNAME="alpine-nenjim"
CONSOLE_KEYMAP_LAYOUT="dk"
CONSOLE_KEYMAP_VARIANT="dk"
SYSTEM_LOCALE="C.UTF-8"
VIRT_ZVOL_SIZE_MIB="4096"
SWAP_SIZE_MIB_PART="2048"
SWAP_SIZE_MIB_ZFS="0"
SWAP_SIZE_PCT_ZRAM="25"
SWAP_PRI_ZRAM="300"
SWAP_PRI_ZFS="200"
SWAP_PRI_PART="100"
ZEROTIER_ENABLE_SSO="0"
ZEROTIER_VERSION="1.16.2"
```
Sæt `NENJIMHUB_HOSTNAME` til et unikt navn for hvert apparat, eksempelvis
`nenjimhub-01`. Værdien må være ét hostname-led på 1-63 bogstaver, tal eller
bindestreger og må ikke begynde eller slutte med en bindestreg.
`CONSOLE_KEYMAP_LAYOUT="dk"` og `CONSOLE_KEYMAP_VARIANT="dk"` vælger det
almindelige danske PC-tastatur på Linux-konsollen. Første boot kontrollerer
keymap-navnet før `setup-alpine`, så en stavefejl stopper med en tydelig fejl i
stedet for at åbne en interaktiv menu.
`SYSTEM_LOCALE="C.UTF-8"` giver et neutralt UTF-8-loginmiljø uden at vælge
engelske eller danske dato- og talformater. Installationen aktiverer samtidig
Unicode på Linux-konsollen.
En swaptype slås fra ved at sætte dens størrelse eller procent til `0`.
Prioritet `0` er derimod en gyldig Linux-swapprioritet og slår ikke noget fra.
PC'en skal have OpenZFS 2.4.3, ZFS-kernemodulet indlæst, FAT/ext4-værktøjer,
Python 3 og enten Podman eller Docker. Minimumskapaciteten beregnes ud fra
konfigurationen; standardprofilen er beregnet til et SD-kort solgt som 16 GB.
## Trin 1: klargør hele kortet
```bash
sudo ./prepare-storage.sh
```
Dette trin sletter hele SD-kortet. Det opretter de valgte partitioner,
`rpool`, datasets `rpool/home`, `rpool/software` og `rpool/sysop` samt det
ext4-formaterede zvol `rpool/virt`. Java og zrepl lægges på
`rpool/software`.
Størrelsen på `rpool/virt` styres særskilt af `VIRT_ZVOL_SIZE_MIB`; den har
ingen forbindelse til ZFS-swapstørrelsen.
## Trin 2: installer eller opdater Alpine
```bash
sudo ./flash.sh
```
Dette trin erstatter kun partition 1. Det bygger på PC'en en matchende Alpine
Raspberry Pi-kernel/modloop med `zfs-rpi` og lægger first-boot-filerne ind.
Som sikkerhed skal du skrive `NENJIMHUB`, når scriptet beder om bekræftelse.
Scriptet gemmer også PC'ens aktuelle UTC-tid på kortet, så Pi'en uden RTC kan
validere HTTPS-certifikater, inden netværkstiden er synkroniseret. Vent på
slutteksten `Alpine partition 1 is ready`.
Ved en senere Alpine-opdatering køres kun trin 2. `flash.sh` bevarer `/sysop`
fra det gamle APKoVL, mens alle ZFS-data ligger urørt på partition 3.
## Første boot
Pi'en gør følgende automatisk:
1. aktiverer den rå swap-partition, hvis den er konfigureret
2. aktiverer midlertidig ZRAM til installationen, hvis ZRAM er konfigureret
3. importerer `rpool` fra partition 3
4. opretter og aktiverer `rpool/swap`, hvis dens størrelse er større end `0`
5. installerer pakker og genbruger en prekompileret ZeroTier-binærfil eller
bygger ZeroTier med ét compile-job, hvis filen ikke findes
6. skriver den endelige ZRAM-størrelse, opretter APKoVL og genstarter
`rpool/swap` oprettes på Pi'en, så `volblocksize` kan følge den faktiske værdi
fra `getconf PAGESIZE`. ZeroTier-kontrollen summerer al aktiv swap; den er ikke
afhængig af, om pladsen kommer fra partition, ZRAM eller ZFS.
På Pi Zero 2W er `ZEROTIER_ENABLE_SSO="0"` standard. Det udelader den meget
store Rust/Cargo-toolchain og bygger ZeroTier med GCC og ét job. Sæt værdien til
`1`, hvis hubben senere skal kunne bruge ZeroTier-netværk, som kræver SSO; det
gør førstegangsinstallationen væsentligt tungere.
### Genbrug den færdigbyggede ZeroTier-fil
Når en Pi har bygget ZeroTier færdig, kopieres binærfilen til PC'en fra den
persistente ZFS-placering. Kør dette på PC'en i mappen med scripts og erstat
`PI-ADRESSE` med Pi'ens IP-adresse:
```bash
scp sysop@PI-ADRESSE:/usr/local/software/zerotier/current/zerotier-one \
./zerotier-one-1.16.2-linux-musl-aarch64-sso0
chmod 0755 ./zerotier-one-1.16.2-linux-musl-aarch64-sso0
```
Ved næste kørsel kopierer `flash.sh` automatisk filen til kortet. Første boot
installerer den direkte og testkører den på Pi'en; både arkitektur og den
forventede version skal passe. Filnavnet indeholder også SSO-profilen, så et
SSO-build ikke forveksles med standardbuildet uden SSO.
Hvis den navngivne fil ikke findes ved siden af `flash.sh`, falder installationen
automatisk tilbage til lokal kompilering på Pi'en. Det er derfor nok at slette
eller flytte filen på PC'en, før et nyt SD-kort klargøres, for at fremtvinge en
ny kompilering.
`tmux`, `htop` og `btop` installeres i den tidlige pakkegruppe. Pakken
`kitty-terminfo` gør, at de kan bruges direkte fra Kitty med
`TERM=xterm-kitty`, og `musl-locales` plus `SYSTEM_LOCALE` giver UTF-8 til
tekstbaserede programmer. Installationen kan
følges lokalt sådan:
```bash
tmux new-session -s install
tail -f /media/mmcblk0p1/NenjimHub-Install.log
```
Når brugeren `nenjim` er oprettet, findes samme log her:
```bash
tail -f /var/log/NenjimHub-Install.log
```
Den sidste linje før genstart viser den samlede tid fra start af unattended
installation til reboot, målt monotont og vist som `HH:MM:SS`.
På normale boots venter `nenjim-wifi-ready` op til 60 sekunder på `wlan0`, før
Wi-Fi og networking startes. Tilsvarende venter `nenjim-virt` på det asynkront
oprettede `/dev/zvol/rpool/virt`-link, før ext4 kontrolleres og Docker samt
containerd får adgang til deres datalager. Begge ventetider er afgrænsede, så
en hardware- eller udev-fejl ikke kan blokere boot uendeligt.
Loginprompten på HDMI-konsollens `tty1` startes af BusyBox `init` via
`/etc/inittab`, ikke som en OpenRC-service. Installationen sikrer og gemmer
denne entry, så `sysop` kan logge ind lokalt efter boot.
Inden det endelige APKoVL oprettes, køres en fuld `apk cache sync`. Det er
vigtigt i diskless mode, fordi også pakker som `eudev`, der blev installeret
før den persistente cache blev aktiveret, skal ligge som APK-filer på
bootpartitionen. De egne storage- og Wi-Fi-services bruger `udev-settle` som
valgfri rækkefølge og ikke som en hård afhængighed.
`apk cache sync` er den autoritative cachekontrol. Tomme metadatapakker som
`udev-init-scripts` kræver ikke nødvendigvis en separat payloadfil med samme
navn; installationen kontrollerer derfor de leverede udev-services frem for
at gætte cachefilnavne.
Konti efter installation:
```text
sysop / sysop administrator via password-beskyttet doas og Docker-gruppen
nenjim / nenjim almindelig bruger uden wheel, doas og Docker-adgang
```
Medlemskab af Docker-gruppen er i praksis root-adgang. Det gives derfor kun
til `sysop`; `nenjim` fjernes eksplicit fra både `wheel` og `docker`.
Direkte root-login er låst på både normal konsol og SSH. Skift de to
standard-passwords, før maskinen placeres på et netværk, som andre har adgang
til.
## Persistente placeringer
```text
/etc/NenjimHub.conf kopi af den anvendte provisioningprofil
/home rpool/home
/usr/local/software rpool/software
/media/sysop rpool/sysop
/media/virt ext4 på rpool/virt
/var/lib/docker -> /media/virt/docker
/var/lib/containerd -> /media/virt/containerd
/sysop/zerotier -> /media/sysop/zerotier
/sysop/zrepl -> /media/sysop/zrepl
/etc/zrepl/zrepl.yml -> /sysop/zrepl/zrepl.yml
```
Selve `/sysop` ligger i Alpine/APKoVL, så sysop stadig kan logge ind, hvis ZFS
ikke kan importeres. De vigtige ZeroTier- og zrepl-data ligger på ZFS.
zrepl starter med den gyldige no-op-konfiguration `jobs: []`. Efter redigering
kan konfigurationen kontrolleres og servicen genstartes sådan:
```bash
doas zrepl configcheck
doas rc-service zrepl restart
```
## Swap-profiler og prioriteter
Linux foretrækker det højeste prioritetsnummer. Scripts accepterer enhver
selvstændig prioritet fra 0 til 32767. Derfor kan ZFS-swap godt gives højere
prioritet end partitions-swap. Første boot skriver en advarsel, hvis dette
valg bruges, men afviser det ikke.
En profil til en Pi med mere RAM kan eksempelvis være:
```ini
SWAP_SIZE_MIB_PART="1024"
SWAP_SIZE_MIB_ZFS="4096"
SWAP_SIZE_PCT_ZRAM="75"
SWAP_PRI_ZRAM="300"
SWAP_PRI_ZFS="200"
SWAP_PRI_PART="100"
```
ZRAM-størrelsen beregnes ved hver normal opstart ud fra `MemTotal`; pladsen
bruges kun efter behov. Under førstegangsinstallationen anvendes midlertidigt
100 % logisk ZRAM-størrelse, hvorefter den ønskede procent gemmes til næste
boot. Sættes `SWAP_SIZE_PCT_ZRAM="0"`, oprettes og aktiveres ZRAM slet ikke.
Swap på et ZFS-zvol kan under ekstremt hukommelsespres skabe en uheldig
afhængighed mellem ZFS og swap. Den rå partition er derfor den sikreste høje
belastningsbuffer på Pi Zero 2W. På en større Pi kan ZFS-swap prioriteres højere,
hvis det er det ønskede kompromis. ZFS ARC begrænses separat via
`ZFS_ARC_MAX_MIB`.
+79
View File
@@ -0,0 +1,79 @@
# Shared NenjimHub provisioning configuration.
#
# This file is sourced as shell syntax by prepare-storage.sh and flash.sh on
# the PC and by unattended.sh during the Raspberry Pi's first boot.
# ==============================================================================
# System identity
# ==============================================================================
# One DNS hostname label: 1-63 letters, digits or hyphens. It may not begin or
# end with a hyphen. Give every NenjimHub a unique name on the same network.
NENJIMHUB_HOSTNAME="<hostname>"
# Alpine console keymap, expressed as setup-keymap LAYOUT and VARIANT.
# "dk dk-winkeys" is the standard Danish PC keyboard layout.
CONSOLE_KEYMAP_LAYOUT="dk"
CONSOLE_KEYMAP_VARIANT="dk-winkeys"
# Neutral UTF-8 login locale for SSH and console shells. C.UTF-8 avoids tying
# program output and date/number formatting to a particular spoken language.
SYSTEM_LOCALE="C.UTF-8"
# ==============================================================================
# Target and storage layout
# ==============================================================================
# Always use the whole SD-card device, never a partition such as /dev/sdc1.
DEVICE="/dev/sdXXXXX"
BOOT_PARTITION_SIZE_MIB="1024"
BOOT_LABEL="NenjimHub"
POOL_NAME="rpool"
POOL_COMPATIBILITY="openzfs-2.4-linux"
EXPECTED_ZFS_VERSION="2.4.3"
# Shared ext4 zvol for /var/lib/docker and /var/lib/containerd.
VIRT_ZVOL_SIZE_MIB="4096"
# ==============================================================================
# Swap policy
# ==============================================================================
# A size/percentage of 0 disables that swap type. Partition 3 is always ZFS;
# partition 2 is created only when SWAP_SIZE_MIB_PART is greater than zero.
SWAP_PART_LABEL="NenjimSwap"
SWAP_SIZE_MIB_PART="4096"
SWAP_SIZE_MIB_ZFS="0"
SWAP_SIZE_PCT_ZRAM="50"
# Linux swap priorities are independent. A larger number is preferred.
# Priority 0 is valid and does not disable a swap type.
SWAP_PRI_ZRAM="300"
SWAP_PRI_ZFS="200"
SWAP_PRI_PART="100"
# ==============================================================================
# First-boot resource limits
# ==============================================================================
DISKLESS_ROOT_SIZE_MIB="1400"
ZFS_ARC_MAX_MIB="96"
# ZeroTier is compiled with one job. The check covers all active swap types.
ZEROTIER_VERSION="1.16.2"
ZEROTIER_REQUIRED_SWAP_MIB="2048"
ZEROTIER_MAKE_JOBS="1"
# SSO requires the large Rust/Cargo toolchain. Keep it disabled on Pi Zero 2W.
# Set this to 1 only if the hub must join ZeroTier networks requiring SSO.
ZEROTIER_ENABLE_SSO="0"
# Optional binary built by a Pi. Put this exact filename beside flash.sh.
# The SSO profile is part of the name so an incompatible build is not reused.
ZEROTIER_PREBUILT_BINARY="zerotier-one-${ZEROTIER_VERSION}-linux-musl-aarch64-sso${ZEROTIER_ENABLE_SSO}"
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
+6
View File
@@ -0,0 +1,6 @@
country=<COUNTRY_CODE>
network={
ssid="<SSID>"
psk=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}