248 lines
10 KiB
Bash
Executable File
248 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# Builds the ACPC 2026 contest image from the official ICPC image.
|
|
#
|
|
# 1. Download the ICPC image (streamed: checksum, decompression and a
|
|
# sparse raw file in one pass, the .gz is not kept).
|
|
# 2. Create build/acpc2026.qcow2, an overlay on top of the ICPC image.
|
|
# The ICPC image itself is never modified.
|
|
# 3. Allow SSH password login in the overlay, offline (debugfs through a
|
|
# qemu-storage-daemon FUSE export, no root needed).
|
|
# 4. Boot the overlay in QEMU/KVM and configure it with Ansible (site.yml).
|
|
# 5. Power off. The result is written to USB sticks with write_sticks.sh,
|
|
# or exported to a raw image with --export.
|
|
#
|
|
# Usage: ./build.sh --admin-pass PASSWORD [options] (./build.sh -h for the list)
|
|
set -euo pipefail
|
|
|
|
IMG_URL=${IMG_URL:-https://image.icpc.global/icpc2027/icpc2027-2026-09-29_image-amd64.img.gz}
|
|
IMG_SHA256=${IMG_SHA256:-e81f1cb46abd19782c2bcbccbad2dca63a198650b38ba5b4f7b54f3f29fd1045}
|
|
BASE_IMG=${BASE_IMG:-}
|
|
ADMIN_PASS=${ADMIN_PASS:-}
|
|
TEAM_PASS=${TEAM_PASS:-}
|
|
STOCK_PASS=contest # password of the team account in the ICPC image
|
|
RAM=${RAM:-4096}
|
|
CPUS=${CPUS:-4}
|
|
PORT_SSH=${PORT_SSH:-2222}
|
|
FRESH=0
|
|
EXPORT=0
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./build.sh --admin-pass PASSWORD [options]
|
|
|
|
--admin-pass PASS New password of the administrator account (icpc), also
|
|
asked to edit the boot menu (user admin). Required.
|
|
--team-pass PASS New password of the team account, used by the teams.
|
|
Default: unchanged (contest, as in the ICPC image).
|
|
--base-image FILE Use an ICPC image already decompressed (raw .img)
|
|
instead of downloading it.
|
|
--fresh Start again from the ICPC image (drops build/acpc2026.qcow2).
|
|
--export After the build, also write build/acpc2026.img (raw,
|
|
sparse), for tools that cannot read qcow2.
|
|
-h, --help Show this help.
|
|
|
|
Environment: IMG_URL, IMG_SHA256, RAM (MB), CPUS, PORT_SSH.
|
|
EOF
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--admin-pass) ADMIN_PASS=$2; shift 2 ;;
|
|
--team-pass) TEAM_PASS=$2; shift 2 ;;
|
|
--base-image) BASE_IMG=$2; shift 2 ;;
|
|
--fresh) FRESH=1; shift ;;
|
|
--export) EXPORT=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
cd "$(dirname "$0")"
|
|
ROOT=$(pwd)
|
|
BUILD=$ROOT/build
|
|
mkdir -p "$BUILD"
|
|
[ -n "$BASE_IMG" ] || BASE_IMG=$BUILD/$(basename "${IMG_URL%.gz}")
|
|
BASE_IMG=$(realpath -m "$BASE_IMG")
|
|
OVERLAY=$BUILD/acpc2026.qcow2
|
|
VARS=$BUILD/OVMF_VARS.fd
|
|
MON=$BUILD/qemu-monitor.sock
|
|
FUSE=$BUILD/fuse.raw
|
|
|
|
log() { printf '\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
|
fail() { printf '\033[1;31m[%s] ERROR\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; exit 1; }
|
|
|
|
[ -n "$ADMIN_PASS" ] || { usage >&2; fail "--admin-pass is required"; }
|
|
for tool in qemu-system-x86_64 qemu-img qemu-storage-daemon fusermount3 debugfs \
|
|
sfdisk ansible-playbook sshpass curl python3; do
|
|
command -v "$tool" >/dev/null || fail "missing tool: $tool"
|
|
done
|
|
[ -w /dev/kvm ] || fail "/dev/kvm is not available or not writable"
|
|
|
|
OVMF_CODE=""
|
|
for dir in /usr/share/edk2/x64 /usr/share/OVMF /usr/share/edk2/ovmf; do
|
|
for code in OVMF_CODE.4m.fd OVMF_CODE_4M.fd OVMF_CODE.fd; do
|
|
if [ -f "$dir/$code" ]; then
|
|
OVMF_CODE=$dir/$code
|
|
OVMF_VARS=$dir/${code/CODE/VARS}
|
|
break 2
|
|
fi
|
|
done
|
|
done
|
|
[ -n "$OVMF_CODE" ] && [ -f "$OVMF_VARS" ] || fail "UEFI firmware (OVMF) not found, install edk2-ovmf"
|
|
|
|
vm_running() { pgrep -f "^qemu-system-x86_64 -name acpc2026-build" >/dev/null; }
|
|
monitor() {
|
|
python3 -I - "$MON" "$1" <<'PY'
|
|
import socket, sys, time
|
|
s = socket.socket(socket.AF_UNIX)
|
|
s.connect(sys.argv[1])
|
|
time.sleep(0.2)
|
|
s.sendall((sys.argv[2] + "\n").encode())
|
|
time.sleep(0.5)
|
|
PY
|
|
}
|
|
stop_vm() {
|
|
vm_running || return 0
|
|
monitor system_powerdown 2>/dev/null || true
|
|
for _ in $(seq 1 100); do vm_running || return 0; sleep 3; done
|
|
monitor quit 2>/dev/null || true
|
|
sleep 2
|
|
}
|
|
cleanup() {
|
|
stop_vm
|
|
if mountpoint -q "$FUSE" 2>/dev/null; then fusermount3 -u "$FUSE"; fi
|
|
pkill -f "^qemu-storage-daemon .*$OVERLAY" 2>/dev/null || true
|
|
rm -f "$BUILD/vars.yml"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# --- 1. ICPC image ------------------------------------------------------------
|
|
if [ -f "$BASE_IMG" ]; then
|
|
log "1. ICPC image already present: $BASE_IMG"
|
|
else
|
|
log "1. Downloading $IMG_URL"
|
|
curl -fL --retry 5 "$IMG_URL" \
|
|
| tee >(sha256sum | awk '{print $1}' > "$BASE_IMG.sha256") \
|
|
| gzip -dc | dd of="$BASE_IMG.part" bs=4M conv=sparse status=none
|
|
sleep 1
|
|
got=$(cat "$BASE_IMG.sha256")
|
|
[ "$got" = "$IMG_SHA256" ] || fail "SHA-256 mismatch: got $got, expected $IMG_SHA256"
|
|
mv "$BASE_IMG.part" "$BASE_IMG"
|
|
log " SHA-256 OK"
|
|
fi
|
|
|
|
# --- 2. Overlay ---------------------------------------------------------------
|
|
if [ $FRESH -eq 1 ] || [ ! -f "$OVERLAY" ]; then
|
|
log "2. Creating the overlay $OVERLAY"
|
|
qemu-img create -q -f qcow2 -b "$BASE_IMG" -F raw "$OVERLAY"
|
|
cp "$OVMF_VARS" "$VARS"
|
|
else
|
|
log "2. Reusing the overlay $OVERLAY (--fresh to start again)"
|
|
[ -f "$VARS" ] || cp "$OVMF_VARS" "$VARS"
|
|
fi
|
|
|
|
# --- 3. Offline changes for the build -----------------------------------------
|
|
# The ICPC image refuses SSH password logins and denies the group "teams", to
|
|
# which the team account belongs. For the build, icpc.conf is removed and
|
|
# 00-build.conf allows password logins. finish.yml puts icpc.conf back. The
|
|
# firewall is turned off for the boot, in case the overlay was already built
|
|
# (site.yml turns it back on).
|
|
log "3. Allowing SSH password login in the overlay"
|
|
touch "$FUSE"
|
|
qemu-storage-daemon \
|
|
--blockdev driver=file,node-name=file,filename="$OVERLAY" \
|
|
--blockdev driver=qcow2,node-name=disk,file=file \
|
|
--export type=fuse,id=exp,node-name=disk,mountpoint="$FUSE",writable=on \
|
|
> "$BUILD/qsd.log" 2>&1 &
|
|
for _ in $(seq 1 20); do mountpoint -q "$FUSE" && break; sleep 0.5; done
|
|
mountpoint -q "$FUSE" || fail "qemu-storage-daemon did not start (see $BUILD/qsd.log)"
|
|
# The root file system is the largest partition
|
|
start=$(sfdisk -J "$FUSE" | python3 -I -c '
|
|
import json, sys
|
|
parts = json.load(sys.stdin)["partitiontable"]["partitions"]
|
|
print(max(parts, key=lambda p: p["size"])["start"])')
|
|
DEV="$FUSE?offset=$((start * 512))"
|
|
printf 'PasswordAuthentication yes\nKbdInteractiveAuthentication yes\nPermitRootLogin no\n' > "$BUILD/00-build.conf"
|
|
dfs() { debugfs -w -R "$1" "$DEV" >/dev/null 2>&1 || true; }
|
|
dfs "rm /etc/ssh/sshd_config.d/icpc.conf"
|
|
dfs "rm /etc/ssh/sshd_config.d/00-build.conf"
|
|
dfs "write $BUILD/00-build.conf /etc/ssh/sshd_config.d/00-build.conf"
|
|
for field in "mode 0100644" "uid 0" "gid 0"; do
|
|
dfs "set_inode_field /etc/ssh/sshd_config.d/00-build.conf $field"
|
|
done
|
|
dfs "rm /etc/systemd/system/multi-user.target.wants/firewall.service"
|
|
debugfs -R "ls /etc/ssh/sshd_config.d" "$DEV" 2>/dev/null | grep -q 00-build.conf \
|
|
|| fail "could not write 00-build.conf in the image"
|
|
fusermount3 -u "$FUSE"
|
|
pkill -f "^qemu-storage-daemon .*$OVERLAY" || true
|
|
sleep 2
|
|
rm -f "$FUSE" "$BUILD/00-build.conf"
|
|
|
|
# --- 4. Boot and configure ----------------------------------------------------
|
|
log "4. Booting the image (SSH on 127.0.0.1:$PORT_SSH)"
|
|
vm_running && fail "a build VM is already running"
|
|
qemu-system-x86_64 -name acpc2026-build -machine q35,accel=kvm -cpu host \
|
|
-m "$RAM" -smp "$CPUS" \
|
|
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
|
-drive if=pflash,format=raw,file="$VARS" \
|
|
-drive id=disk0,file="$OVERLAY",format=qcow2,if=none \
|
|
-device virtio-blk-pci,drive=disk0,bootindex=1 \
|
|
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$PORT_SSH"-:22 \
|
|
-device virtio-net-pci,netdev=net0 \
|
|
-display none -monitor unix:"$MON",server,nowait \
|
|
-serial file:"$BUILD/serial.log" &
|
|
|
|
ssh_ok() {
|
|
sshpass -p "$2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
-o LogLevel=ERROR -o ConnectTimeout=5 -o PreferredAuthentications=password \
|
|
-p "$PORT_SSH" "$1@127.0.0.1" 'sudo -n true 2>/dev/null; id -nG' 2>/dev/null | grep -qw sudo
|
|
}
|
|
# First build: the team account of the ICPC image (password contest, sudo).
|
|
# Next builds: the icpc account with the new password (team lost sudo).
|
|
LOGIN_USER=""
|
|
for _ in $(seq 1 120); do
|
|
vm_running || fail "QEMU stopped (see $BUILD/serial.log)"
|
|
if ssh_ok icpc "$ADMIN_PASS"; then LOGIN_USER=icpc; LOGIN_PASS=$ADMIN_PASS; break; fi
|
|
if ssh_ok team "$STOCK_PASS"; then LOGIN_USER=team; LOGIN_PASS=$STOCK_PASS; break; fi
|
|
sleep 5
|
|
done
|
|
[ -n "$LOGIN_USER" ] || fail "no SSH login with sudo after 10 minutes (icpc with --admin-pass, or team/contest)"
|
|
log " SSH login OK ($LOGIN_USER)"
|
|
|
|
# Passwords go through a file readable only by the user, not the command line
|
|
( umask 077; python3 -I - "$BUILD/vars.yml" "$LOGIN_PASS" "$ADMIN_PASS" "$TEAM_PASS" <<'PY'
|
|
import json, sys
|
|
path, login, admin, team = sys.argv[1:]
|
|
json.dump({"ansible_password": login, "ansible_become_password": login,
|
|
"admin_pass": admin, "team_pass": team}, open(path, "w"))
|
|
PY
|
|
)
|
|
log " Running site.yml"
|
|
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i "127.0.0.1," -u "$LOGIN_USER" \
|
|
-e ansible_port="$PORT_SSH" -e ansible_become_exe=sudo.ws \
|
|
-e "@$BUILD/vars.yml" -e start_firewall=false site.yml
|
|
rm -f "$BUILD/vars.yml"
|
|
|
|
# --- 5. Power off -------------------------------------------------------------
|
|
# finish.yml powers the machine off as its last task
|
|
log "5. Waiting for the image to power off"
|
|
for _ in $(seq 1 200); do vm_running || break; sleep 3; done
|
|
stop_vm
|
|
vm_running && fail "the VM did not power off"
|
|
|
|
if [ $EXPORT -eq 1 ]; then
|
|
log " Exporting $BUILD/acpc2026.img"
|
|
qemu-img convert -p -O raw "$OVERLAY" "$BUILD/acpc2026.img"
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
Image ready: $OVERLAY
|
|
Teams: team (automatic login, no sudo), password ${TEAM_PASS:-contest}
|
|
Administrator: icpc (sudo), password given with --admin-pass
|
|
Boot menu: user admin, same password
|
|
|
|
Write it to USB sticks with: sudo ./write_sticks.sh $OVERLAY
|
|
Try it in a window with: ./run_vm.sh
|
|
EOF
|