First version
This commit is contained in:
commit
243e92a9dd
84 files changed
+4012
No files matched your search
@@ -0,0 +1,247 @@
|
||||
#!/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
|
||||
Reference in new issue
Block a user