{%- set role = image_role -%} #!/bin/bash # nixos-{{ role.name }} — build image and create Hetzner snapshot # provisioning {{ provisioning_version }} — {{ now }} # Output: SNAPSHOT_ID written to stdout on success set -euo pipefail {%- if debug is defined and debug == "yes" %} {%- endif %} HCLOUD_TOKEN="${HCLOUD_TOKEN:?HCLOUD_TOKEN required}" PROVISIONING="${PROVISIONING:-/usr/local/provisioning}" FLAKE_DIR="{{ flake_dir }}" SSH_KEY="{{ ssh_key }}" LOCATION="{{ location | default(value='nbg1') }}" ROLE="{{ role.name }}" # Build the raw-efi image via nix # Requires: nix with aarch64-linux builder (Nix Desktop on Apple Silicon, or remote builder) echo "=== Building NixOS $ROLE image ===" # Find nix executable (handle both installed and path variations) NIX_BIN=$(which nix 2>/dev/null || find /nix/store -name "nix" -type f 2>/dev/null | head -1) if [ -z "$NIX_BIN" ]; then echo "ERROR: nix not found in PATH or /nix/store" exit 1 fi export PROVISIONING $NIX_BIN build "${FLAKE_DIR}#packages.aarch64-linux.${ROLE}-image" \ --out-link "/tmp/nixos-${ROLE}-image" \ --print-build-logs \ --impure IMAGE_STORE=$(readlink -f "/tmp/nixos-${ROLE}-image") # nixos-generators raw-efi output is a .img file inside the store path IMAGE_PATH=$(find "$IMAGE_STORE" -name "*.img" | head -1) if [ -z "$IMAGE_PATH" ]; then echo "ERROR: no .img found in $IMAGE_STORE" exit 1 fi echo "Image: $IMAGE_PATH ($(du -sh "$IMAGE_PATH" | cut -f1))" # Create temporary build server (cheapest ARM64 on Hetzner) TEMP_NAME="imgbuild-${ROLE}-$$" echo "=== Creating temp server $TEMP_NAME ===" hcloud server create \ --name "$TEMP_NAME" \ --type cax11 \ --location "$LOCATION" \ --image debian-12 \ --ssh-key "$SSH_KEY" \ --output json > /tmp/imgbuild-create.json SERVER_ID=$(hcloud server describe "$TEMP_NAME" -o format='{{ "{{.ID}}" }}') SERVER_IP=$(hcloud server describe "$TEMP_NAME" -o format='{{ "{{.PublicNet.IPv4.IP}}" }}') echo "Server $TEMP_NAME: id=$SERVER_ID ip=$SERVER_IP" cleanup() { echo "=== Cleanup: deleting $TEMP_NAME ===" hcloud server delete "$SERVER_ID" || true rm -f /tmp/imgbuild-create.json "/tmp/nixos-${ROLE}-image" } trap cleanup EXIT # Enable rescue and reboot echo "=== Booting into rescue ===" hcloud server enable-rescue "$SERVER_ID" --type linux64 --ssh-key "$SSH_KEY" hcloud server reboot "$SERVER_ID" echo "Waiting for rescue SSH..." for i in $(seq 1 24); do ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \ -o BatchMode=yes root@"$SERVER_IP" true 2>/dev/null && break sleep 10 done ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \ -o BatchMode=yes root@"$SERVER_IP" true || { echo "ERROR: rescue SSH unreachable after 240s" exit 1 } # Write image to disk echo "=== Writing image to /dev/sda ===" gzip -dc "$IMAGE_PATH" | ssh -o StrictHostKeyChecking=no root@"$SERVER_IP" \ "dd of=/dev/sda bs=4M conv=fsync status=progress" echo "=== Powering off ===" hcloud server poweroff "$SERVER_ID" sleep 15 # Create snapshot DESCRIPTION="nixos-${ROLE}-aarch64-{{ now | replace(from=' ', to='T') | replace(from=':', to='') }}" echo "=== Creating snapshot: $DESCRIPTION ===" SNAPSHOT_ID=$(hcloud server create-image "$SERVER_ID" \ --type snapshot \ --description "$DESCRIPTION" \ -o format='{{ "{{.ID}}" }}') echo "=== Done ===" echo "SNAPSHOT_ID=$SNAPSHOT_ID" echo "Update servers.ncl for role '$ROLE': image = \"$SNAPSHOT_ID\"" # Disable cleanup trap (keep snapshot, delete server still happens) trap - EXIT hcloud server delete "$SERVER_ID"