Skip to main content

max / mountaineer

6.7 KB · 218 lines History Blame Raw
1 #!/bin/sh
2 # vm-up.sh — start the long-lived Mountaineer builder VM.
3 #
4 # First run: generates a NoCloud cloud-init seed (hostname, ssh keys,
5 # build-deps packages, runcmd), boots the cloud qcow2 with the seed
6 # attached, polls until sshd is reachable. Subsequent runs: boots without
7 # the seed (cloud-init has already run).
8 #
9 # Usage:
10 # tools/qemu/vm-up.sh # idempotent — provision if needed, then start
11 # tools/qemu/vm-up.sh --reprovision # wipe state and re-run cloud-init from scratch
12 # tools/qemu/vm-up.sh --stop # graceful shutdown via qemu monitor
13 #
14 # Apple Silicon caveat: x86_64 guests run via TCG. First boot (cloud-init
15 # installing the build deps) is the slowest part — expect 5-10 minutes.
16
17 . "$(dirname "$0")/common.sh"
18 ensure_dirs
19
20 CLOUD_IMG="$IMG_DIR/alpine-cloud.qcow2"
21 BUILDER_DISK="$DISK_DIR/builder.qcow2"
22 SEED_ISO="$DISK_DIR/builder-seed.iso"
23 KEYS_DIR="$VMS/keys"
24 BUILDER_KEY="$KEYS_DIR/builder_ed25519"
25 LOGS_DIR="$VMS/logs"
26 SOCKETS_DIR="$VMS/sockets"
27 PROVISIONED_MARKER="$VMS/.builder-provisioned"
28 PIDFILE="$VMS/builder.pid"
29
30 mkdir -p "$LOGS_DIR" "$SOCKETS_DIR"
31
32 mode=auto
33 case "${1:-}" in
34 --reprovision) mode=reprovision ;;
35 --stop) mode=stop ;;
36 "") mode=auto ;;
37 *) echo "unknown flag: $1" >&2; exit 2 ;;
38 esac
39
40 # --- stop --------------------------------------------------------------
41 if [ "$mode" = stop ]; then
42 if [ -S "$SOCKETS_DIR/builder-mon.sock" ]; then
43 printf 'system_powerdown\n' | nc -U "$SOCKETS_DIR/builder-mon.sock" >/dev/null 2>&1 || true
44 echo "==> sent powerdown; VM will exit gracefully"
45 elif [ -f "$PIDFILE" ]; then
46 kill "$(cat "$PIDFILE")" 2>/dev/null || true
47 echo "==> sent SIGTERM to qemu (no monitor socket found)"
48 else
49 echo "==> no running builder VM detected"
50 fi
51 exit 0
52 fi
53
54 # --- preconditions ------------------------------------------------------
55 if [ ! -f "$CLOUD_IMG" ]; then
56 cat >&2 <<EOF
57 error: cloud image not found at $CLOUD_IMG
58
59 Download with:
60 curl -fL -o $CLOUD_IMG \\
61 $ALPINE_CLOUD_URL
62 qemu-img resize $CLOUD_IMG +30G
63 EOF
64 exit 1
65 fi
66
67 if [ ! -f "$BUILDER_KEY" ] || [ ! -f "$BUILDER_KEY.pub" ]; then
68 echo "==> generating builder SSH key at $BUILDER_KEY"
69 mkdir -p "$KEYS_DIR"
70 ssh-keygen -t ed25519 -N '' -C 'mountaineer-builder' -f "$BUILDER_KEY"
71 fi
72
73 # --- reprovision: clear state -------------------------------------------
74 if [ "$mode" = reprovision ]; then
75 echo "==> reprovisioning: wiping builder state"
76 rm -f "$BUILDER_DISK" "$SEED_ISO" "$PROVISIONED_MARKER" "$NVRAM_DIR/builder_VARS.fd"
77 fi
78
79 # --- builder disk: copy-on-write from the cloud image -------------------
80 if [ ! -f "$BUILDER_DISK" ]; then
81 echo "==> creating builder disk as overlay on $CLOUD_IMG"
82 qemu-img create -f qcow2 -b "$CLOUD_IMG" -F qcow2 "$BUILDER_DISK"
83 fi
84
85 # --- generate cidata seed -----------------------------------------------
86 need_provision=0
87 if [ ! -f "$PROVISIONED_MARKER" ]; then
88 need_provision=1
89 fi
90
91 if [ "$need_provision" = 1 ]; then
92 echo "==> building cloud-init seed.iso"
93 pubkey=$(cat "$BUILDER_KEY.pub")
94 seed_stage=$(mktemp -d)
95 cat > "$seed_stage/meta-data" <<EOF
96 instance-id: mountaineer-builder-001
97 local-hostname: mountaineer-builder
98 EOF
99 cat > "$seed_stage/user-data" <<EOF
100 #cloud-config
101 hostname: mountaineer-builder
102 manage_etc_hosts: true
103
104 ssh_pwauth: false
105 disable_root: false
106
107 users:
108 - name: root
109 ssh_authorized_keys:
110 - $pubkey
111 - name: build
112 groups: [abuild, wheel]
113 shell: /bin/sh
114 sudo: ALL=(ALL) NOPASSWD:ALL
115 ssh_authorized_keys:
116 - $pubkey
117
118 growpart:
119 mode: auto
120 devices: ['/']
121
122 package_update: true
123 packages:
124 - alpine-sdk
125 - abuild
126 - xorriso
127 - squashfs-tools
128 - mkinitfs
129 - grub-efi
130 - mtools
131 - dosfstools
132 - git
133 - tar
134 - rsync
135 - coreutils
136
137 runcmd:
138 - [ sh, -c, "su build -c 'abuild-keygen -a -i -n' || true" ]
139 - [ sh, -c, "mkdir -p /var/lib/cloud/instance && touch /var/lib/cloud/instance/mountaineer-ready" ]
140
141 final_message: "Mountaineer builder ready after \$UPTIME seconds."
142 EOF
143
144 # macOS: hdiutil makehybrid produces an ISO9660 image with the
145 # volume label cloud-init's NoCloud datasource looks for.
146 if command -v hdiutil >/dev/null 2>&1; then
147 rm -f "$SEED_ISO"
148 hdiutil makehybrid -iso -joliet -default-volume-name CIDATA -o "$SEED_ISO" "$seed_stage" >/dev/null
149 elif command -v xorriso >/dev/null 2>&1; then
150 xorriso -as mkisofs -volid CIDATA -joliet -rock -output "$SEED_ISO" "$seed_stage" >/dev/null 2>&1
151 elif command -v genisoimage >/dev/null 2>&1; then
152 genisoimage -V CIDATA -J -r -o "$SEED_ISO" "$seed_stage" >/dev/null 2>&1
153 else
154 echo "error: no ISO builder found (need hdiutil on macOS, or xorriso/genisoimage)" >&2
155 rm -rf "$seed_stage"
156 exit 1
157 fi
158 rm -rf "$seed_stage"
159 fi
160
161 # --- boot ---------------------------------------------------------------
162 serial_log="$LOGS_DIR/builder-serial.log"
163 monitor_sock="$SOCKETS_DIR/builder-mon.sock"
164
165 # Cleanup stale sockets/pidfile.
166 rm -f "$monitor_sock" "$PIDFILE"
167
168 seed_drive=""
169 if [ "$need_provision" = 1 ]; then
170 seed_drive="-drive file=$SEED_ISO,if=virtio,format=raw,readonly=on"
171 fi
172
173 echo "==> booting builder VM (serial log: $serial_log)"
174 # shellcheck disable=SC2046,SC2086
175 qemu-system-x86_64 \
176 $(qemu_base_args builder) \
177 -serial file:"$serial_log" \
178 -drive file="$BUILDER_DISK",if=virtio,format=qcow2 \
179 $seed_drive \
180 -netdev user,id=net0,hostfwd=tcp:127.0.0.1:$BUILDER_SSH_PORT-:22 \
181 -device virtio-net-pci,netdev=net0 \
182 -monitor unix:"$monitor_sock",server,nowait \
183 -daemonize \
184 -pidfile "$PIDFILE"
185
186 if [ "$need_provision" = 1 ]; then
187 echo "==> waiting for cloud-init to finish (this is the slow first boot)"
188 echo " serial log: $serial_log"
189 echo " polling sshd on 127.0.0.1:$BUILDER_SSH_PORT every 10s for up to 15 min"
190
191 deadline=$(( $(date +%s) + 900 ))
192 while [ "$(date +%s)" -lt "$deadline" ]; do
193 if ssh -p "$BUILDER_SSH_PORT" \
194 -o StrictHostKeyChecking=accept-new \
195 -o UserKnownHostsFile="$VMS/known_hosts" \
196 -o ConnectTimeout=3 \
197 -o BatchMode=yes \
198 -i "$BUILDER_KEY" \
199 root@127.0.0.1 \
200 'test -f /var/lib/cloud/instance/mountaineer-ready' 2>/dev/null; then
201 echo "==> builder is provisioned and reachable"
202 touch "$PROVISIONED_MARKER"
203 exit 0
204 fi
205 sleep 10
206 printf '.'
207 done
208
209 echo ""
210 echo "error: builder did not come up in 15 minutes" >&2
211 echo " check $serial_log for what went wrong" >&2
212 exit 1
213 fi
214
215 echo "==> builder is running (qemu pid $(cat "$PIDFILE"))"
216 echo " ssh: ssh -i $BUILDER_KEY -p $BUILDER_SSH_PORT root@127.0.0.1"
217 echo " stop: $0 --stop"
218