146 lines
4.9 KiB
Django/Jinja
146 lines
4.9 KiB
Django/Jinja
{%- 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"
|