Build server: infra/build-server/provision.sh (installimage mode run on igneum-build-1, provision mode for Ubuntu 24.04)

Idempotent provisioning of the Hetzner AX162 build box: install mode (rescue system, Ubuntu 24.04, software RAID 1 over
the two NVMe drives, no swap, rescue keys taken over, run 6 October 2026 17:16 to 17:20 UTC) and provision mode (user build,
compilers, mingw-w64 posix, rustup 1.99.0 with the Windows target, sccache 100 GB, Node 22, bare mirrors, /srv/builds, sshd
key-only, ufw 22 + seed p2p ports). shellcheck clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 17:20:39 +00:00
parent 0be505b8e2
commit f83f8df1c7

View file

@ -0,0 +1,398 @@
#!/usr/bin/env bash
# Provision igneum-build-1, the Hetzner dedicated build server (AX162-1-LTD: EPYC 9454P 48 cores / 96 threads, 128 GB,
# 2x 3.84 TB NVMe, Falkenstein; ordered 6 October 2026). Idempotent: every step checks before it changes anything and
# says "ok" (nothing to do) or "changed". Run it over ssh as root; nothing here reads a secret.
#
# infra/build-server/run-from-mac.sh <ip> the usual way (ships this file, fills WORKTREES, writes the host file)
# ssh root@<ip> 'bash -s' < infra/build-server/provision.sh the bare way
# ssh root@<ip> 'MODE=install bash -s' < infra/build-server/provision.sh force the rescue-system path
#
# Two modes, chosen by MODE (auto, install, provision; default auto):
# install the box booted into Hetzner's rescue system (installimage present, hostname rescue*): run installimage in
# batch mode for Ubuntu 24.04 with software RAID 1 over the two NVMe drives, no swap, the rescue system's
# authorized_keys taken over, the image signature checked, then reboot. Run the script again after the reboot.
# Ran on igneum-build-1 on 6 October 2026 at 17:16 to 17:20 UTC (16 steps, no prompt).
# provision a running Ubuntu 24.04: user `build` with root's key, the compiler and cross toolchains, rustup pinned to
# RUST_TOOLCHAIN with the x86_64-pc-windows-gnu target, sccache with a 100 GB disk cache, Node 22, git, tmux,
# a swap-free tuned sysctl, the two bare mirrors (/srv/igneum.git, /srv/igneum-node.git), /srv/builds with one
# directory per agent worktree, sshd key-only, ufw with 22 and the seed p2p ports.
#
# Settings (environment, all optional):
# RUST_TOOLCHAIN 1.99.0 the Mac's `rustc --version` on 6 October 2026. Neither the repo nor the fork carries a
# rust-toolchain file (checked 6 October 2026), so the pin lives here; the fork's Cargo.toml says
# rust-version 1.91.0. tools/build-remote.sh compares the two sides and refuses a mismatch.
# SCCACHE_GB 100 the local disk cache at /srv/sccache
# SCCACHE_VERSION (unset) a `cargo install sccache --version` pin; unset = the newest on crates.io
# NODE_MAJOR 22
# WORKTREES "" space-separated agent worktree names, one /srv/builds/<name> each (run-from-mac.sh fills it from
# `git worktree list` on the Mac; tools/build-remote.sh creates a missing one on first use)
# SLOTS 1 remote build slots (tools/build-remote.sh takes one; with 1 slot every build gets the box)
# P2P_PORTS "26611 26811" TCP ports ufw opens beside 22: the devnet seed's p2p (infra/seed-nodes/config.sh devnet
# P2P_PORT=26611) and the testnet seed's (26811). A suffixed devnet (Devnet 2, the fleet's staging
# chain) listens on the same 26611 (infra/cloud-devnet/config.sh P2P_PORT=26611). RPC ports
# (26610, 28610, 26790 and the 268xx set) stay on loopback as on every seed, so they are not opened.
# BOX_HOSTNAME igneum-build-1
# SSH_PUBKEY (unset) a public key line for the build user when root has no authorized_keys (installimage installs it)
#
# Mirrors: the Mac pushes to them (never a clone from GitHub; the fork vendor/igneum-node exists only on the Mac):
# git -C /Users/joshm/Projects/igneum remote add build build@<ip>:/srv/igneum.git
# git -C /Users/joshm/Projects/igneum/vendor/igneum-node remote add build build@<ip>:/srv/igneum-node.git
# git push build --all (tools/build-remote.sh pushes the branch it builds before every build)
#
# What this script does NOT do: install zig or cargo-zigbuild (the Mac's glibc 2.36 Linux cross-build, infra/cross/build-linux.sh,
# stays on the Mac until the box is proven; a native build here links glibc 2.39, which Debian 13 seeds accept and HiveOS
# does not), start any node, or copy a secret. The installimage flags and the image name were read from the live rescue system
# on 6 October 2026 (`installimage -h`, /root/.oldroot/nfs/images); the script still reads the image list instead of hard-coding a name.
set -euo pipefail
MODE="${MODE:-auto}"
RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-1.99.0}"
SCCACHE_GB="${SCCACHE_GB:-100}"
SCCACHE_VERSION="${SCCACHE_VERSION:-}"
NODE_MAJOR="${NODE_MAJOR:-22}"
WORKTREES="${WORKTREES:-}"
SLOTS="${SLOTS:-1}"
P2P_PORTS="${P2P_PORTS:-26611 26811}"
BOX_HOSTNAME="${BOX_HOSTNAME:-igneum-build-1}"
SSH_PUBKEY="${SSH_PUBKEY:-}"
BUILD_USER=build
BUILD_HOME=/home/$BUILD_USER
log() { printf '%s provision: %s\n' "$(date -u +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
changed() { log "$1: changed${2:+ ($2)}"; }
ok() { log "$1: ok${2:+ ($2)}"; }
as_build() { su - "$BUILD_USER" -c "$*"; }
[ "$(id -u)" = 0 ] || die "run as root"
# ----------------------------------------------------------------------------------------------------------------------
# install mode: the rescue system
# ----------------------------------------------------------------------------------------------------------------------
INSTALLIMAGE=/root/.oldroot/nfs/install/installimage # not on PATH in a non-interactive ssh shell (read 6 Oct 2026)
in_rescue() {
[ -x "$INSTALLIMAGE" ] || return 1
case "$(hostname)" in rescue*) return 0 ;; esac
[ -d /root/.oldroot/nfs/images ]
}
do_install() {
local images drives image parts
images=/root/.oldroot/nfs/images
[ -d "$images" ] || die "no image directory at $images: not the Hetzner rescue system"
# the Ubuntu 24.04 (noble) amd64 base image the rescue system offers (read on 6 October 2026: Ubuntu-2404-noble-amd64-base.tar.zst
# with a detached .sig; read, not hard-coded, because Hetzner refreshes the names)
image=$(find "$images" -maxdepth 1 -type f -iregex '.*/ubuntu-2404.*amd64.*\.tar\.\(zst\|gz\|xz\)' -printf '%f\n' | sort | tail -1 || true)
[ -n "$image" ] || die "no Ubuntu 24.04 amd64 image under $images: $(find "$images" -maxdepth 1 -type f -printf '%f ' )"
mapfile -t drives < <(lsblk -dn -o NAME,TYPE | awk '$2 == "disk" && $1 ~ /^nvme/ { print $1 }' | sort)
[ "${#drives[@]}" = 2 ] || die "expected exactly two NVMe drives for RAID 1, found ${#drives[@]}: ${drives[*]:-none}"
[ -s /root/.ssh/authorized_keys ] || die "/root/.ssh/authorized_keys is empty in the rescue system; -t yes would carry nothing into the image"
[ -d /sys/firmware/efi ] || log "WARNING: no /sys/firmware/efi, the box booted in BIOS mode; the esp partition is harmless but grub goes to the MBR"
# no swap partition: 128 GB of RAM and a swap-free sysctl (the provision mode checks no swap is active)
parts="/boot/efi:esp:512M,/boot:ext4:1G,/:ext4:all"
log "installimage: image $image, drives ${drives[*]} as software RAID 1, partitions $parts, hostname $BOX_HOSTNAME, rescue ssh keys taken over (-t yes), image signature checked (-g)"
log "this WIPES ${drives[*]}"
# flag form, read from `installimage -h` on 6 October 2026: -a batch, -n hostname, -r raid, -l level, -i image, -g verify
# the detached signature, -p partitions mount:fs:size, -d drives, -t yes take over the rescue system's ssh keys (root's
# authorized_keys), -G yes new host keys. The -c config form forbids every other flag, so the keys could not travel with it.
TERM="${TERM:-xterm}" "$INSTALLIMAGE" -a -n "$BOX_HOSTNAME" -r yes -l 1 -i "$images/$image" -g -p "$parts" -d "$(IFS=,; echo "${drives[*]}")" -t yes -G yes
log "installimage finished; rebooting into Ubuntu. Run this script again (MODE=provision or auto) once ssh answers (the host key is new: -G yes)."
sync; reboot
}
# ----------------------------------------------------------------------------------------------------------------------
# provision mode: the installed Ubuntu
# ----------------------------------------------------------------------------------------------------------------------
step_hostname() {
if [ "$(hostnamectl --static 2>/dev/null || hostname)" = "$BOX_HOSTNAME" ]; then ok hostname "$BOX_HOSTNAME"; return; fi
hostnamectl set-hostname "$BOX_HOSTNAME"; grep -q "$BOX_HOSTNAME" /etc/hosts || printf '127.0.1.1 %s\n' "$BOX_HOSTNAME" >> /etc/hosts
changed hostname "$BOX_HOSTNAME"
}
step_os_check() {
. /etc/os-release
[ "${ID:-}" = ubuntu ] && [ "${VERSION_ID:-}" = 24.04 ] || die "this is ${PRETTY_NAME:-unknown}, not Ubuntu 24.04"
ok os "$PRETTY_NAME, $(nproc) threads, $(awk '/MemTotal/ { printf "%d GB", $2 / 1024 / 1024 }' /proc/meminfo)"
}
# the proven Ubuntu 24.04 set: the PC build job's APT lists (app/igneum-app/src/jobbuild.rs: mingw-w64 posix threads so
# libstdc++ has std::thread for rocksdb, clang for librocksdb-sys's bindgen, protoc for the node's proto crates) plus the
# task's list (build-essential, clang, lld, pkg-config, libssl-dev, cmake, git, tmux) and what the scripts here call
APT_PACKAGES=(
build-essential clang lld llvm libclang-dev pkg-config libssl-dev cmake protobuf-compiler
gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 binutils-mingw-w64-x86-64 mingw-w64-x86-64-dev mingw-w64-tools
git tmux curl ca-certificates xz-utils zstd unzip rsync jq python3 ufw htop file
)
step_apt() {
local need=() p
for p in "${APT_PACKAGES[@]}"; do dpkg -s "$p" >/dev/null 2>&1 || need+=("$p"); done
if [ "${#need[@]}" = 0 ]; then ok apt "${#APT_PACKAGES[@]} packages present"; return; fi
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq --no-install-recommends "${need[@]}"
changed apt "installed ${need[*]}"
}
step_mingw_alternatives() {
# Ubuntu ships -posix and -win32 variants behind update-alternatives; the Windows exes want posix threads (jobbuild.rs)
local tool want cur any=0
for tool in gcc g++; do
want="/usr/bin/x86_64-w64-mingw32-$tool-posix"
cur=$(readlink -f "/etc/alternatives/x86_64-w64-mingw32-$tool" 2>/dev/null || true)
[ -x "$want" ] || die "no $want after apt"
if [ "$cur" != "$want" ]; then update-alternatives --set "x86_64-w64-mingw32-$tool" "$want" >/dev/null; any=1; fi
done
[ "$any" = 1 ] && changed mingw-alternatives "posix threads" || ok mingw-alternatives "posix threads"
}
step_no_swap() {
local any=0
if [ -n "$(swapon --noheadings --show 2>/dev/null)" ]; then swapoff -a; any=1; fi
if grep -qE '^[^#].*\sswap\s' /etc/fstab; then sed -i -E 's/^([^#].*\sswap\s.*)$/# \1 (disabled by infra\/build-server\/provision.sh)/' /etc/fstab; any=1; fi
[ "$any" = 1 ] && changed swap "off, fstab entry commented" || ok swap "none"
}
step_sysctl() {
local f=/etc/sysctl.d/90-igneum-build.conf tmp
tmp=$(mktemp)
cat > "$tmp" <<'EOF'
# igneum-build-1: a compile box with no swap (infra/build-server/provision.sh)
vm.swappiness = 0
vm.overcommit_memory = 0
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
vm.max_map_count = 1048576
fs.file-max = 4194304
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192
kernel.pid_max = 4194304
kernel.threads-max = 1048576
net.core.somaxconn = 4096
net.ipv4.tcp_fin_timeout = 15
EOF
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok sysctl "$f"; return; fi
install -m 644 "$tmp" "$f"; rm -f "$tmp"; sysctl --system >/dev/null
changed sysctl "$f applied"
}
step_limits() {
local f=/etc/security/limits.d/90-igneum-build.conf
if [ -f "$f" ]; then ok limits; return; fi
printf '%s soft nofile 1048576\n%s hard nofile 1048576\n%s soft nproc unlimited\n' "$BUILD_USER" "$BUILD_USER" "$BUILD_USER" > "$f"
changed limits "$f"
}
step_user() {
local keys
if ! id -u "$BUILD_USER" >/dev/null 2>&1; then useradd -m -s /bin/bash -G users "$BUILD_USER"; changed user "$BUILD_USER created"; else ok user "$BUILD_USER"; fi
install -d -m 700 -o "$BUILD_USER" -g "$BUILD_USER" "$BUILD_HOME/.ssh"
if [ -n "$SSH_PUBKEY" ]; then keys="$SSH_PUBKEY"; elif [ -s /root/.ssh/authorized_keys ]; then keys=$(cat /root/.ssh/authorized_keys); else die "no key for $BUILD_USER: root has no authorized_keys and SSH_PUBKEY is unset"; fi
if [ -f "$BUILD_HOME/.ssh/authorized_keys" ] && [ "$(cat "$BUILD_HOME/.ssh/authorized_keys")" = "$keys" ]; then ok authorized_keys; else
printf '%s\n' "$keys" > "$BUILD_HOME/.ssh/authorized_keys"; chmod 600 "$BUILD_HOME/.ssh/authorized_keys"; chown "$BUILD_USER:$BUILD_USER" "$BUILD_HOME/.ssh/authorized_keys"
changed authorized_keys "$(printf '%s\n' "$keys" | grep -c .) key(s) from root"
fi
}
worktree_count() { find /srv/builds -mindepth 1 -maxdepth 1 -type d -not -name '_*' | wc -l | tr -d ' '; }
step_dirs() {
local d any=0
for d in /srv/builds /srv/builds/_locks /srv/sccache /srv/artefacts; do
if [ ! -d "$d" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$d"; any=1; fi
done
if [ ! -f /srv/builds/_locks/slots ] || [ "$(cat /srv/builds/_locks/slots)" != "$SLOTS" ]; then printf '%s\n' "$SLOTS" > /srv/builds/_locks/slots; chown "$BUILD_USER:$BUILD_USER" /srv/builds/_locks/slots; any=1; fi
for d in $WORKTREES; do
case "$d" in */*|.*|_*) die "worktree name '$d' is not a plain directory name" ;; esac
if [ ! -d "/srv/builds/$d" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "/srv/builds/$d"; any=1; fi
done
[ "$any" = 1 ] && changed dirs "/srv/builds ($(worktree_count) worktree dirs), /srv/sccache, slots=$SLOTS" || ok dirs "$(worktree_count) worktree dirs, slots=$SLOTS"
}
step_mirrors() {
local r any=0
for r in /srv/igneum.git /srv/igneum-node.git; do
if [ ! -d "$r" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$r"; as_build "git init -q --bare -b master $r"; any=1; fi
done
as_build "git config --global --get safe.directory >/dev/null 2>&1 || git config --global --add safe.directory '*'"
as_build "git config --global init.defaultBranch master; git config --global gc.auto 0"
[ "$any" = 1 ] && changed mirrors "bare /srv/igneum.git and /srv/igneum-node.git (push from the Mac, see the header)" || ok mirrors
}
step_rustup() {
local cargo="$BUILD_HOME/.cargo/bin/cargo" rustup="$BUILD_HOME/.cargo/bin/rustup" any=0 t
if [ ! -x "$rustup" ]; then
as_build "curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path --default-toolchain $RUST_TOOLCHAIN" >/dev/null
any=1
fi
if ! as_build "$rustup toolchain list" | grep -q "^$RUST_TOOLCHAIN-"; then as_build "$rustup toolchain install $RUST_TOOLCHAIN --profile minimal" >/dev/null; any=1; fi
if [ "$(as_build "$rustup default" | cut -d- -f1)" != "$RUST_TOOLCHAIN" ]; then as_build "$rustup default $RUST_TOOLCHAIN" >/dev/null; any=1; fi
for t in x86_64-pc-windows-gnu x86_64-unknown-linux-gnu; do
as_build "$rustup target list --installed --toolchain $RUST_TOOLCHAIN" | grep -qx "$t" || { as_build "$rustup target add $t --toolchain $RUST_TOOLCHAIN" >/dev/null; any=1; }
done
as_build "$rustup component list --installed --toolchain $RUST_TOOLCHAIN" | grep -q '^clippy' || { as_build "$rustup component add clippy rustfmt --toolchain $RUST_TOOLCHAIN" >/dev/null; any=1; }
[ "$any" = 1 ] && changed rustup "$(as_build "$cargo --version"), targets: $(as_build "$rustup target list --installed" | tr '\n' ' ')" || ok rustup "$(as_build "$cargo --version"), targets: $(as_build "$rustup target list --installed" | tr '\n' ' ')"
}
step_sccache() {
local cargo="$BUILD_HOME/.cargo/bin/cargo" bin="$BUILD_HOME/.cargo/bin/sccache" cfgdir="$BUILD_HOME/.config/sccache" any=0 bytes tmp
if [ ! -x "$bin" ] || { [ -n "$SCCACHE_VERSION" ] && ! "$bin" --version | grep -q " $SCCACHE_VERSION\$"; }; then
as_build "$cargo install sccache --locked ${SCCACHE_VERSION:+--version $SCCACHE_VERSION}" >/dev/null 2>&1 || as_build "$cargo install sccache ${SCCACHE_VERSION:+--version $SCCACHE_VERSION}" >/dev/null
any=1
fi
bytes=$(( SCCACHE_GB * 1024 * 1024 * 1024 ))
install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$cfgdir"
tmp=$(mktemp)
printf '[cache.disk]\ndir = "/srv/sccache"\nsize = %s\n' "$bytes" > "$tmp"
if ! cmp -s "$tmp" "$cfgdir/config"; then install -m 644 -o "$BUILD_USER" -g "$BUILD_USER" "$tmp" "$cfgdir/config"; any=1; fi
rm -f "$tmp"
[ "$any" = 1 ] && changed sccache "$(as_build "$bin --version"), disk cache /srv/sccache, $SCCACHE_GB GB" || ok sccache "$(as_build "$bin --version"), /srv/sccache $SCCACHE_GB GB"
}
step_cargo_config() {
# the build user's cargo defaults: sccache in front of rustc, 90 jobs (96 threads, 6 left for ssh, rsync and the
# system), lld for the native target through clang. The Windows target's compilers and flags are NOT here: they are
# set per build by tools/cross-remote.sh, the same variables as the Mac's proto-cuda/windows-node/cross-build.sh and
# the PC's jobbuild.rs, so a build's flags are visible in the script that runs it.
local f="$BUILD_HOME/.cargo/config.toml" tmp
tmp=$(mktemp)
cat > "$tmp" <<'EOF'
# igneum-build-1 (infra/build-server/provision.sh)
[build]
rustc-wrapper = "/home/build/.cargo/bin/sccache"
jobs = 90
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[net]
git-fetch-with-cli = true
EOF
if cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok cargo-config "$f"; return; fi
install -m 644 -o "$BUILD_USER" -g "$BUILD_USER" "$tmp" "$f"; rm -f "$tmp"
changed cargo-config "$f"
}
step_profile() {
# sourced by tools/build-remote.sh's remote script (a non-login ssh shell reads no profile) and by login shells
local f=/etc/profile.d/igneum-build.sh tmp
tmp=$(mktemp)
cat > "$tmp" <<EOF
# igneum-build-1 (infra/build-server/provision.sh)
export PATH="/home/build/.cargo/bin:/usr/local/bin:\$PATH"
export SCCACHE_DIR=/srv/sccache
export SCCACHE_CACHE_SIZE=${SCCACHE_GB}G
export CARGO_INCREMENTAL=0
export IGNEUM_BUILD_SLOTS_DIR=/srv/builds/_locks
export IGNEUM_BUILD_ROOT=/srv/builds
export IGNEUM_RUST_TOOLCHAIN=$RUST_TOOLCHAIN
EOF
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok profile "$f"; return; fi
install -m 644 "$tmp" "$f"; rm -f "$tmp"; changed profile "$f"
}
step_node() {
local want have shasums tarball ver dir
have=$(/usr/local/bin/node --version 2>/dev/null || true)
case "$have" in v$NODE_MAJOR.*) ok node "$have"; return ;; esac
# the newest $NODE_MAJOR release from nodejs.org, checked against its SHASUMS256.txt (https, the official host)
shasums=$(curl -fsSL "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/SHASUMS256.txt")
tarball=$(printf '%s\n' "$shasums" | awk '$2 ~ /linux-x64\.tar\.xz$/ { print $2 }' | head -1)
[ -n "$tarball" ] || die "no linux-x64 tarball in the Node $NODE_MAJOR SHASUMS"
ver=${tarball#node-}; ver=${ver%-linux-x64.tar.xz}
dir=/usr/local/lib/nodejs
install -d "$dir"
( cd "$dir" && curl -fsSLO "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/$tarball" && printf '%s\n' "$shasums" | grep " $tarball\$" | sha256sum -c --quiet - && tar -xJf "$tarball" && rm -f "$tarball" )
ln -sfn "$dir/node-$ver-linux-x64/bin/node" /usr/local/bin/node
ln -sfn "$dir/node-$ver-linux-x64/bin/npm" /usr/local/bin/npm
ln -sfn "$dir/node-$ver-linux-x64/bin/npx" /usr/local/bin/npx
changed node "$(/usr/local/bin/node --version) from nodejs.org (sha256 checked)"
}
step_sshd() {
local f=/etc/ssh/sshd_config.d/10-igneum-build.conf tmp
tmp=$(mktemp)
cat > "$tmp" <<'EOF'
# igneum-build-1 (infra/build-server/provision.sh): keys only
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
PermitEmptyPasswords no
X11Forwarding no
MaxAuthTries 4
ClientAliveInterval 60
ClientAliveCountMax 10
EOF
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok sshd "$f"; return; fi
install -m 644 "$tmp" "$f"; rm -f "$tmp"
# Hetzner's installimage may leave a cloud-init drop-in that sets PasswordAuthentication yes; the lowest-numbered file wins
if [ -f /etc/ssh/sshd_config.d/50-cloud-init.conf ] && grep -qi '^PasswordAuthentication yes' /etc/ssh/sshd_config.d/50-cloud-init.conf; then
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config.d/50-cloud-init.conf
fi
sshd -t || die "sshd -t rejected the configuration; the drop-in $f was NOT activated"
systemctl reload ssh 2>/dev/null || systemctl reload sshd
changed sshd "key-only, root prohibit-password"
}
step_ufw() {
local p want=() any=0 status
status=$(ufw status verbose 2>/dev/null || true)
grep -q 'Default: deny (incoming), allow (outgoing)' <<<"$status" || { ufw --force default deny incoming >/dev/null; ufw --force default allow outgoing >/dev/null; any=1; }
want=(22)
for p in $P2P_PORTS; do want+=("$p"); done
for p in "${want[@]}"; do
grep -qE "^$p/tcp +ALLOW IN +Anywhere *$" <<<"$status" || { ufw allow "$p/tcp" >/dev/null; any=1; }
done
grep -q '^Status: active' <<<"$status" || { ufw --force enable >/dev/null; any=1; }
[ "$any" = 1 ] && changed ufw "22 and ${P2P_PORTS} open, everything else denied" || ok ufw "22 and ${P2P_PORTS}"
}
step_summary() {
log "summary:"
{
printf 'host %s, %s threads, %s RAM, root fs %s free\n' "$(hostname)" "$(nproc)" "$(awk '/MemTotal/ { printf "%d GB", $2 / 1024 / 1024 }' /proc/meminfo)" "$(df -h / | awk 'NR == 2 { print $4 }')"
printf 'raid: %s\n' "$(grep -E '^md' /proc/mdstat 2>/dev/null | tr '\n' ';' || echo none)"
printf 'rust: %s | %s | targets %s\n' "$(as_build "$BUILD_HOME/.cargo/bin/rustc --version")" "$(as_build "$BUILD_HOME/.cargo/bin/cargo --version")" "$(as_build "$BUILD_HOME/.cargo/bin/rustup target list --installed" | tr '\n' ' ')"
printf 'sccache: %s, %s\n' "$(as_build "$BUILD_HOME/.cargo/bin/sccache --version")" "$(cat "$BUILD_HOME/.config/sccache/config" | tr '\n' ' ')"
printf 'mingw: %s\n' "$(x86_64-w64-mingw32-gcc-posix --version | head -1)"
printf 'clang: %s | lld: %s\n' "$(clang --version | head -1)" "$(ld.lld --version | head -1)"
printf 'node: %s | git: %s | tmux: %s\n' "$(/usr/local/bin/node --version)" "$(git --version)" "$(tmux -V)"
printf 'swap: %s\n' "$(swapon --noheadings --show 2>/dev/null | wc -l | awk '{ print ($1 == 0) ? "none" : $1 " device(s) ACTIVE" }')"
printf 'mirrors: /srv/igneum.git (%s) /srv/igneum-node.git (%s)\n' "$(as_build 'git -C /srv/igneum.git branch --list | wc -l') branches" "$(as_build 'git -C /srv/igneum-node.git branch --list | wc -l') branches"
printf 'builds: %s worktree dirs under /srv/builds, %s slot(s)\n' "$(worktree_count)" "$(cat /srv/builds/_locks/slots)"
printf 'ufw: %s\n' "$(ufw status | grep -E 'ALLOW' | awk '{ print $1 }' | tr '\n' ' ')"
printf 'ssh line: ssh -i ~/.ssh/igneum_ed25519 build@%s\n' "$(hostname -I 2>/dev/null | awk '{ print $1 }')"
} | sed 's/^/ /'
}
do_provision() {
step_os_check
step_hostname
step_apt
step_mingw_alternatives
step_no_swap
step_sysctl
step_limits
step_user
step_dirs
step_mirrors
step_rustup
step_sccache
step_cargo_config
step_profile
step_node
step_sshd
step_ufw
step_summary
log "done"
}
case "$MODE" in
install) do_install ;;
provision) do_provision ;;
auto) if in_rescue; then log "rescue system detected: install mode"; do_install; else do_provision; fi ;;
*) die "MODE must be auto, install or provision" ;;
esac