Merge: Firecracker B0 spike — microVMs boot on tank and morpheus
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Executable
+145
@@ -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"
|
||||||
Reference in New Issue
Block a user