{%- set role = image_role -%} #!/bin/bash # base-{{ role.name }} — build Debian base image with custom partition layout # provisioning {{ provisioning_version }} — {{ now }} # Output: SNAPSHOT_ID written to stdout on success set -euo pipefail HCLOUD_TOKEN="${HCLOUD_TOKEN:?HCLOUD_TOKEN required}" SSH_KEY="{{ ssh_key }}" LOCATION="{{ location | default(value='nbg1') }}" ROLE="{{ role.name }}" BUILD_TYPE="{{ role.build_server_type | default(value='cx22') }}" ROOT_SIZE_GB="{{ role.partition_layout.root_size_gb | default(value=5) }}" DATA_FMT="{{ role.partition_layout.data_part | default(value='ext4') }}" # Create temporary x86_64 builder server (cx22: 2 vCPU, 4GB RAM, 40GB disk) TEMP_NAME="basebuild-${ROLE}-$$" echo "=== Creating builder $TEMP_NAME (${BUILD_TYPE}, location=${LOCATION}) ===" hcloud server create \ --name "$TEMP_NAME" \ --type "$BUILD_TYPE" \ --location "$LOCATION" \ --image debian-12 \ --ssh-key "$SSH_KEY" \ --output json > /tmp/basebuild-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 (id=$SERVER_ID) ===" hcloud server delete "$SERVER_ID" || true rm -f /tmp/basebuild-create.json } trap cleanup EXIT # Enable rescue mode and reboot echo "=== Enabling rescue mode ===" hcloud server enable-rescue "$SERVER_ID" --type linux64 --ssh-key "$SSH_KEY" hcloud server reboot "$SERVER_ID" # Wait for rescue SSH (30 attempts × 10s = 300s timeout) echo "Waiting for rescue SSH..." for i in $(seq 1 30); 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 300s" exit 1 } # Execute partitioning + Debian install in rescue mode # ROOT_GB and DATA_FMT are passed as env vars; script is quoted to prevent local expansion echo "=== Running rescue setup script ===" ssh -o StrictHostKeyChecking=no root@"$SERVER_IP" \ ROOT_GB="$ROOT_SIZE_GB" DATA_FMT="$DATA_FMT" \ bash -s <<'RESCUE' set -euo pipefail # Hetzner rescue may not include debootstrap apt-get install -y debootstrap 2>&1 | tail -10 # GPT partition layout: # sda1 1MiB – 2MiB bios_grub (required for GRUB on GPT+BIOS) # sda2 2MiB – ${ROOT_GB}GiB ext4 root (boot flag set) # sda3 ${ROOT_GB}GiB – 100% ext4 data (or left unformatted) parted -s /dev/sda mklabel gpt parted -s /dev/sda mkpart bios_boot 1MiB 2MiB parted -s /dev/sda set 1 bios_grub on parted -s /dev/sda mkpart primary ext4 2MiB "${ROOT_GB}GiB" parted -s /dev/sda set 2 boot on parted -s /dev/sda mkpart primary ext4 "${ROOT_GB}GiB" 100% partprobe /dev/sda sleep 2 # Format root; data partition formatted only when requested mkfs.ext4 -L root /dev/sda2 [ "$DATA_FMT" = "ext4" ] && mkfs.ext4 -L data /dev/sda3 || true # Bootstrap minimal Debian bookworm onto root partition mount /dev/sda2 /mnt debootstrap --arch amd64 bookworm /mnt https://deb.debian.org/debian # Bind mounts required for chroot operations (grub-install needs /dev) mount --bind /dev /mnt/dev mount --bind /proc /mnt/proc mount --bind /sys /mnt/sys # Configure base system inside chroot chroot /mnt bash <<'CHROOT' set -euo pipefail export DEBIAN_FRONTEND=noninteractive # Filesystem table: root on sda2 (LABEL=root), data on sda3 (LABEL=data) printf 'LABEL=root / ext4 defaults 0 1\nLABEL=data /data ext4 defaults 0 2\n' > /etc/fstab mkdir -p /data # Minimal loopback config; cloud-init manages real network interfaces printf 'auto lo\niface lo inet loopback\n' > /etc/network/interfaces apt-get update apt-get install -y --no-install-recommends \ linux-image-amd64 \ grub-pc \ openssh-server \ cloud-init \ cloud-guest-utils \ isc-dhcp-client \ net-tools # grub-install targets the whole disk; sda1 (bios_grub) provides the embedding area grub-install /dev/sda update-grub systemctl enable ssh apt-get clean rm -rf /var/lib/apt/lists/* CHROOT # Unmount all bind mounts before poweroff umount -l /mnt/dev /mnt/proc /mnt/sys /mnt RESCUE # Power off before snapshot to ensure disk consistency echo "=== Powering off builder ===" hcloud server poweroff "$SERVER_ID" sleep 15 # Create snapshot from powered-off server DESCRIPTION="base-{{ role.name }}-debian12-{{ 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 images.ncl for role '{{ role.name }}': snapshot_id = \"$SNAPSHOT_ID\"" trap - EXIT hcloud server delete "$SERVER_ID"