Files
diskless-workstation/scripts/extract-artifacts.sh
T
2026-06-14 05:58:55 +00:00

118 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# extract-artifacts.sh <qcow2> <output-dir>
#
# Pulls the diskless deliverables out of the Packer-built disk image:
# vmlinuz -> HTTPS boot server
# initramfs.img -> HTTPS boot server (the fleet-generic image)
# rootfs.erofs -> nvmet target: shared READ-ONLY NVMe/TCP namespace
# (erofs data + dm-verity hash tree appended in-file)
# verity.json -> root hash + hash offset (feeds the kernel cmdline)
# cmdline.txt -> ready-made kernel cmdline fragment for this build
#
# Requires on the build host: libguestfs-tools, erofs-utils (mkfs.erofs),
# cryptsetup (veritysetup), jq.
set -euo pipefail
QCOW2=${1:?usage: extract-artifacts.sh <qcow2> <output-dir> [disable_dot1x]}
OUT=${2:?usage: extract-artifacts.sh <qcow2> <output-dir> [disable_dot1x]}
# TEMPORARY test toggle: when "true", bake rd.dot1x=0 into the cmdline so the
# UKI boots with 802.1X + MACsec disabled (open-port NVMe-oF boot test).
DISABLE_DOT1X=${3:-false}
# NVMe/TCP discovery address baked into the (immutable, signed) UKI cmdline.
# Prefer a per-environment DNS name resolved at boot so one UKI works across
# environments. Defaults to the literal placeholder so build-oci.sh's warning
# fires if it was never set.
NVMET_TRADDR=${NVMET_TRADDR:-NVMET_TRADDR}
for bin in virt-copy-out virt-tar-out virt-ls mkfs.erofs veritysetup jq; do
command -v "$bin" >/dev/null || { echo "ERROR: $bin not found on build host" >&2; exit 1; }
done
mkdir -p "$OUT"
export LIBGUESTFS_BACKEND=direct
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
echo "==> Locating kernel and generic initramfs in $QCOW2"
KERNEL=$(virt-ls -a "$QCOW2" /boot | grep '^vmlinuz-' | grep -v rescue | sort -V | tail -1)
[ -n "$KERNEL" ] || { echo "ERROR: no kernel found in image" >&2; exit 1; }
virt-copy-out -a "$QCOW2" "/boot/$KERNEL" "/boot/initramfs-generic.img" "$OUT/"
mv "$OUT/$KERNEL" "$OUT/vmlinuz"
mv "$OUT/initramfs-generic.img" "$OUT/initramfs.img"
echo "==> Exporting root tree"
virt-tar-out -a "$QCOW2" / "$WORK/rootfs.tar"
mkdir "$WORK/root"
tar -C "$WORK/root" --numeric-owner -xpf "$WORK/rootfs.tar"
rm -rf "$WORK/root/boot/"* "$WORK/rootfs.tar" # diskless hosts don't need /boot
echo "==> Building reproducible erofs image"
# -T0 + fixed UUID => deterministic output => stable dm-verity root hash
# for identical input trees. lz4hc: good ratio, cheap decompress; saves
# fabric bandwidth (and MACsec cycles) on every read.
FIXED_UUID="6c10a7f0-1d2e-4f5a-9b3c-000000000001"
mkfs.erofs -z lz4hc -T 0 -U "$FIXED_UUID" --all-root \
"$WORK/rootfs.erofs" "$WORK/root"
echo "==> Appending dm-verity hash tree (single-file / single-namespace layout)"
DATA_BYTES=$(stat -c %s "$WORK/rootfs.erofs")
# Align hash area to 4 KiB after the data
HASH_OFFSET=$(( (DATA_BYTES + 4095) / 4096 * 4096 ))
VERITY_OUT=$(veritysetup format "$WORK/rootfs.erofs" "$WORK/rootfs.erofs" \
--hash-offset="$HASH_OFFSET" \
--data-block-size=4096 --hash-block-size=4096)
ROOT_HASH=$(printf '%s\n' "$VERITY_OUT" | awk '/Root hash:/ {print $3}')
[ -n "$ROOT_HASH" ] || { echo "ERROR: veritysetup produced no root hash" >&2; exit 1; }
mv "$WORK/rootfs.erofs" "$OUT/rootfs.erofs"
jq -n --arg hash "$ROOT_HASH" --argjson offset "$HASH_OFFSET" \
--arg uuid "$FIXED_UUID" \
'{root_hash: $hash, hash_offset: $offset, erofs_uuid: $uuid,
data_block_size: 4096, hash_block_size: 4096}' \
> "$OUT/verity.json"
# Kernel cmdline fragment for the boot server. Single line: this file is
# consumed verbatim by ukify in build-oci.sh and baked into the signed UKI.
# The device node assumes the root namespace is the host's first/only
# NVMe/TCP namespace; adjust if not. NVMET_TRADDR: set per environment
# *before* the UKI build, since the cmdline is immutable once signed.
DOT1X_FRAGMENT=""
if [ "$DISABLE_DOT1X" = "true" ]; then
echo "==> WARNING: building TEST cmdline with rd.dot1x=0 (802.1X + MACsec DISABLED)" >&2
echo " open-port NVMe-oF boot test only — do NOT ship this UKI." >&2
DOT1X_FRAGMENT="rd.dot1x=0 "
fi
printf '%s' \
"$DOT1X_FRAGMENT" \
"rd.neednet=1 systemd.volatile=overlay " \
"rd.dot1x.nvindex=0x01800001 rd.dot1x.timeout=60 " \
"rd.nvmf.discover=tcp,${NVMET_TRADDR},4420 " \
"root=/dev/mapper/root rootfstype=erofs rootflags=ro " \
"systemd.verity=yes roothash=${ROOT_HASH} " \
"systemd.verity_root_data=/dev/nvme0n1 " \
"systemd.verity_root_hash=/dev/nvme0n1 " \
"systemd.verity_root_options=hash-offset=${HASH_OFFSET}" \
> "$OUT/cmdline.txt"
echo "==> Checksums"
( cd "$OUT" && sha256sum vmlinuz initramfs.img rootfs.erofs | tee SHA256SUMS )
cat <<EOF
Artifacts in $OUT:
vmlinuz, initramfs.img -> publish to the HTTPS boot server (versioned dir,
flip a symlink for atomic rollout/rollback)
rootfs.erofs -> expose as a READ-ONLY nvmet namespace; version
rollout = publish new namespace, repoint hosts
cmdline.txt -> kernel cmdline for this build. The roothash binds
the root fs to the boot artifacts: serve via a
signed UKI or signed GRUB config so Secure Boot
covers it, or the verity guarantee is advisory.
Remember: identity is NOT in these artifacts. Hosts authenticate with the
TPM key at the fleet persistent handle and the cert in the fleet NV index;
the hostnqn is derived from that cert at boot.
EOF