Merge: Firecracker B0 spike — microVMs boot on tank and morpheus

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-04 21:39:13 -07:00
co-authored by Claude Opus 5
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Install Firecracker + jailer on a fleet node, and prove the node can boot a
# microVM before declaring it ready.
#
# Phase B runs mission workloads in microVMs on the KVM-capable nodes (tank /
# morpheus / architect). gw-04 is itself a VM without nested virtualisation, so
# it has no /dev/kvm and can never host one — KVM availability is a placement
# predicate, not an assumption.
#
# Installing is not the same as working, so this does both and only reports
# success when a VM has actually booted, run our code, and exited. The same
# rule as everywhere else in this repo: a capability that has never been
# observed working has not been shown to work.
#
# Usage:
# scripts/fc-node-setup.sh <ssh-host> [...] # e.g. osobh@tank morpheus
# FC_VERSION=v1.16.1 scripts/fc-node-setup.sh osobh@tank
set -uo pipefail
FC_VERSION="${FC_VERSION:-v1.16.1}"
# Firecracker CI publishes a kernel and an Ubuntu rootfs; both are stand-ins
# for the images we will bake ourselves (one per CLI, per the A6/B4 plan).
CI_BASE="${FC_CI_BASE:-https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.12/x86_64}"
KERNEL="${FC_KERNEL:-vmlinux-6.1.128}"
ROOTFS="${FC_ROOTFS:-ubuntu-24.04.squashfs}"
WORK="${FC_WORK:-/opt/clawmates-fc}"
FAILURES=0
pass() { printf 'PASS %-14s %s\n' "$1" "$2"; }
fail() { printf 'FAIL %-14s %s\n' "$1" "$2"; FAILURES=$((FAILURES + 1)); }
[ $# -ge 1 ] || { echo "usage: $0 <ssh-host> [...]" >&2; exit 2; }
for host in "$@"; do
echo "── $host ────────────────────────────────────────────"
# 1. KVM. Without it there is nothing to install for.
if ! ssh -o ConnectTimeout=10 "$host" 'test -e /dev/kvm' 2>/dev/null; then
fail "$host" "no /dev/kvm — this node cannot host microVMs"
continue
fi
pass "$host" "/dev/kvm present"
# 2. Install, verifying the checksum by hand.
#
# `sha256sum -c` against the published file compares by FILENAME, so saving
# the download under any other name makes it print a warning and exit
# non-zero for a reason that has nothing to do with integrity — a check that
# fails for the wrong reason teaches you to ignore it. Compare the hashes.
installed=$(ssh "$host" "
set -e
sudo mkdir -p '$WORK' && sudo chown \$(id -u):\$(id -g) '$WORK'
cd '$WORK'
if [ \"\$(firecracker --version 2>/dev/null | head -1)\" = 'Firecracker $FC_VERSION' ]; then
echo already; exit 0
fi
curl -sSL -o fc.tgz 'https://github.com/firecracker-microvm/firecracker/releases/download/$FC_VERSION/firecracker-$FC_VERSION-x86_64.tgz'
want=\$(curl -sSL 'https://github.com/firecracker-microvm/firecracker/releases/download/$FC_VERSION/firecracker-$FC_VERSION-x86_64.tgz.sha256.txt' | awk '{print \$1}')
got=\$(sha256sum fc.tgz | awk '{print \$1}')
[ -n \"\$want\" ] && [ \"\$want\" = \"\$got\" ] || { echo \"checksum \$got != \$want\" >&2; exit 1; }
tar xzf fc.tgz
sudo install -m0755 release-$FC_VERSION-x86_64/firecracker-$FC_VERSION-x86_64 /usr/local/bin/firecracker
sudo install -m0755 release-$FC_VERSION-x86_64/jailer-$FC_VERSION-x86_64 /usr/local/bin/jailer
echo installed
" 2>&1) || { fail "$host" "install failed: $installed"; continue; }
pass "$host" "firecracker $FC_VERSION ($installed)"
# 3. Kernel + rootfs, converted to a writable ext4.
ssh "$host" "
set -e
cd '$WORK'
[ -s vmlinux ] || curl -sSL -o vmlinux '$CI_BASE/$KERNEL'
if [ ! -s rootfs.ext4 ]; then
curl -sSL -o rootfs.squashfs '$CI_BASE/$ROOTFS'
command -v unsquashfs >/dev/null || sudo apt-get install -y -qq squashfs-tools >/dev/null 2>&1
rm -rf squashfs-root && unsquashfs -q -d squashfs-root rootfs.squashfs
truncate -s 1G rootfs.ext4 && mkfs.ext4 -q -d squashfs-root rootfs.ext4
rm -rf squashfs-root rootfs.squashfs
fi
" >/dev/null 2>&1 || { fail "$host" "could not stage kernel/rootfs"; continue; }
pass "$host" "kernel + rootfs staged in $WORK"
# 4. Boot one. Installing proves nothing; this is the check that counts.
#
# The guest marker is printed by an init script rather than typed at a shell
# over the serial console — feeding stdin races the shell's startup and
# arrives half-consumed (observed: `# ho FC-GUEST-ALIVE`, the first two
# characters eaten).
boot=$(ssh "$host" "
set -e
cd '$WORK'
sudo mkdir -p /mnt/fcroot
sudo mount -o loop rootfs.ext4 /mnt/fcroot
printf '%s\n' '#!/bin/sh' 'mount -t proc proc /proc 2>/dev/null' \
'echo FC-GUEST-ALIVE kernel=\$(uname -r) cpus=\$(nproc)' 'sync' 'reboot -f' \
| sudo tee /mnt/fcroot/usr/local/bin/fcinit >/dev/null
sudo chmod 0755 /mnt/fcroot/usr/local/bin/fcinit
sudo umount /mnt/fcroot
cat > vm.json <<JSON
{
\"boot-source\": {
\"kernel_image_path\": \"$WORK/vmlinux\",
\"boot_args\": \"console=ttyS0 reboot=k panic=1 pci=off init=/usr/local/bin/fcinit\"
},
\"drives\": [{\"drive_id\":\"rootfs\",\"path_on_host\":\"$WORK/rootfs.ext4\",\"is_root_device\":true,\"is_read_only\":false}],
\"machine-config\": {\"vcpu_count\":2,\"mem_size_mib\":1024,\"smt\":false}
}
JSON
rm -f boot.log
S=\$(date +%s%N)
sudo timeout 30 firecracker --no-api --config-file vm.json > boot.log 2>&1 || true
E=\$(date +%s%N)
echo \"ms=\$(( (E - S) / 1000000 ))\"
grep -a FC-GUEST-ALIVE boot.log || echo MARKER-ABSENT
" 2>&1)
case "$boot" in
*FC-GUEST-ALIVE*) pass "$host" "microVM booted and ran our code ($(printf '%s' "$boot" | grep -o 'ms=[0-9]*'))" ;;
*) fail "$host" "microVM did not boot: $(printf '%s' "$boot" | tail -3 | tr '\n' ' ')" ;;
esac
# 5. Teardown hygiene. Firecracker does NOT unlink its vsock UDS on exit, and
# leaves it owned by whoever ran the VM (root, here) — so a driver running as
# anyone else cannot clean it up. That is the same uid trap that cost this
# codebase four bugs on the mission checkout; the driver must own the socket
# path lifecycle explicitly rather than assume the VM tidies up after itself.
# `pgrep -c` prints its count AND exits 1 when the count is zero, so
# `|| echo 0` appended a second line and the comparison saw "0\n0" — this
# check reported FAIL on a perfectly clean teardown the first time it ran.
leaked=$(ssh "$host" "pgrep -c firecracker 2>/dev/null; true" | head -1 | tr -dc '0-9')
leaked=${leaked:-0}
if [ "$leaked" = "0" ]; then
pass "$host" "no firecracker process survived teardown"
else
fail "$host" "$leaked firecracker process(es) still running after teardown"
fi
done
echo
if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES check(s) failed"
exit 1
fi
echo "all nodes ready"