X: First working version for RasPi Zero 2W
This commit is contained in:
@@ -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`.
|
||||
@@ -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}"
|
||||
@@ -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 "============================================================================"
|
||||
Executable
+507
@@ -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."
|
||||
Executable
+530
@@ -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
File diff suppressed because it is too large
Load Diff
Executable
+6
@@ -0,0 +1,6 @@
|
||||
country=<COUNTRY_CODE>
|
||||
|
||||
network={
|
||||
ssid="<SSID>"
|
||||
psk=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
}
|
||||
Reference in New Issue
Block a user