From 3cf33d1e0aaacf967e27f28f1f18354fdc84b46e Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Sun, 15 Mar 2026 19:36:54 -0300 Subject: [PATCH] Streamline VM overlays and rootfs packages Move the default guest package list into a repo manifest and record a hash beside built rootfs images so run/make-rootfs can warn when the docker-ready image is stale. Switch the Firecracker launch path to a single sparse root overlay per VM instead of separate /home and /var disks, so many VMs can share the same base image while still installing packages under /var and working from /root. Keep older images bootable by masking stale home.mount and var.mount units at boot, and scrub those obsolete fstab entries when customize.sh rebuilds an image. Verified with bash -n on the updated scripts; no live VM boot was run in this environment. --- README.md | 25 ++++++--- customize.sh | 41 ++++++++++---- interactive.sh | 48 +---------------- make-rootfs.sh | 5 ++ packages.apt | 7 +++ packages.sh | 115 ++++++++++++++++++++++++++++++++++++++++ restore.sh | 28 ++-------- run.sh | 141 ++++++++----------------------------------------- 8 files changed, 206 insertions(+), 204 deletions(-) mode change 100644 => 100755 make-rootfs.sh create mode 100644 packages.apt create mode 100644 packages.sh diff --git a/README.md b/README.md index f79659f..5b5a7b3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Minimal Firecracker launcher. - `wtf/root/lib/modules/6.8.0-94-generic/`: guest kernel modules - `rootfs.ext4`: guest root filesystem (base image if present) - `rootfs-docker.ext4`: docker-ready guest rootfs (built via `make-rootfs.sh`) +- `packages.apt`: apt packages baked into rebuilt guest images - `id_ed25519`: SSH key for `root` - `mapdns`: local DNS mapping CLI used to publish `.vm` → guest IP records @@ -25,22 +26,21 @@ Minimal Firecracker launcher. ## Run Options ``` -./run.sh --name calm_otter --vcpu 4 --ram 2048 --home-size 6G +./run.sh --name calm-otter --vcpu 4 --ram 2048 --overlay-size 12G ``` - `--name`: must be unique and match `[a-z0-9][a-z0-9-]{0,63}`. - `--vcpu`: defaults to 2, max 16. - `--ram`: MiB, defaults to 1024, max 32768. +- `--overlay-size`: writable dm-snapshot size for VM changes under `/`, including `/root` and `/var` (default: 8G). - `--rootfs`: path to the rootfs image (default: `./rootfs-docker.ext4`). - `--kernel`: path to the kernel image (default: `./wtf/root/boot/vmlinux-6.8.0-94-generic`). - `--initrd`: path to the initrd image (default: `./wtf/root/boot/initrd.img-6.8.0-94-generic`). -- `--home-size`: M/G suffixes supported (default: 2G). -- `--var-size`: M/G suffixes supported (default: 2G). ## Storage Layout - `rootfs.ext4` is used as the read-only origin for a per-VM device-mapper snapshot mounted as `/`. -- Each VM gets writable ext4 disks mounted at `/home` and `/var`. -- `run.sh` seeds those `/home` and `/var` disks from the rootfs snapshot before boot so the guest sees the base image contents there on first boot. -- The base image must include `/etc/fstab` entries for `/dev/vdb` → `/home` and `/dev/vdc` → `/var`. +- Each VM gets one sparse writable overlay file (`cow.ext4`) that stores its changes on top of the shared base image. +- `/root` and `/var` live inside that per-VM overlay, so VMs can install packages without copying separate disks per VM. +- `run.sh` masks stale `home.mount` and `var.mount` units at boot so older images with `/dev/vdb` and `/dev/vdc` entries in `/etc/fstab` still boot. - `/run` and `/tmp` should be tmpfs via `/etc/fstab`. ## SSH @@ -84,6 +84,9 @@ preloaded so Docker works out of the box. Pass the base rootfs as a positional argument; the output defaults to `docker-` in the same directory unless you pass `--out`. +Base guest packages come from `./packages.apt`. Edit that file to bake tools +like `vim` and `tmux` into rebuilt images. + ``` ./customize.sh ./rootfs.ext4 --size 6G --docker ``` @@ -114,6 +117,16 @@ invoke `make-rootfs.sh` to build it. `make-rootfs.sh` chooses the first available base image: - `./rootfs.ext4` +If `./packages.apt` changes after `rootfs-docker.ext4` is built, `run.sh` will +warn and keep using the existing image. `make-rootfs.sh` will also warn and +exit without rebuilding while the image already exists. + +To rebuild after package changes: +``` +rm -f ./rootfs-docker.ext4 ./rootfs-docker.ext4.packages.sha256 +./make-rootfs.sh +``` + ## Interactive Customization To create a writable copy and customize it manually over SSH (no automatic package/config changes), use: diff --git a/customize.sh b/customize.sh index ea1ea5e..91a868a 100755 --- a/customize.sh +++ b/customize.sh @@ -31,6 +31,7 @@ parse_size() { DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$DIR/dns.sh" +source "$DIR/packages.sh" STATE="$DIR/state" VM_ROOT="$STATE/vms" mkdir -p "$VM_ROOT" @@ -52,6 +53,7 @@ OUT_ROOTFS="" SIZE_SPEC="" INSTALL_DOCKER=0 MODULES_DIR="$DIR/wtf/root/lib/modules/6.8.0-94-generic" +PACKAGES_FILE="$(banger_packages_file)" while [[ $# -gt 0 ]]; do case "$1" in --out) @@ -136,6 +138,25 @@ if ! command -v jq >/dev/null 2>&1; then log "jq required" exit 1 fi +if ! command -v sha256sum >/dev/null 2>&1; then + log "sha256sum required to record package manifest metadata" + exit 1 +fi +if [[ ! -f "$PACKAGES_FILE" ]]; then + log "package manifest not found: $PACKAGES_FILE" + exit 1 +fi + +APT_PACKAGES=() +if ! banger_packages_read_array APT_PACKAGES "$PACKAGES_FILE"; then + log "package manifest is empty: $PACKAGES_FILE" + exit 1 +fi +if ! PACKAGES_HASH="$(printf '%s\n' "${APT_PACKAGES[@]}" | banger_packages_hash_stream)"; then + log "failed to hash package manifest: $PACKAGES_FILE" + exit 1 +fi +printf -v APT_PACKAGES_ESCAPED '%q ' "${APT_PACKAGES[@]}" log "copying base rootfs to $OUT_ROOTFS" cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS" @@ -223,7 +244,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ "smt": false }' >/dev/null -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME}" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" INITRD_JSON="" if [[ -n "$INITRD" ]]; then @@ -286,13 +307,19 @@ log "enabling NAT for customization" sudo -E ./nat.sh up "$VM_TAG" >/dev/null log "waiting for SSH" +SSH_READY=0 for _ in $(seq 1 60); do if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ "root@${GUEST_IP}" "true" >/dev/null 2>&1; then + SSH_READY=1 break fi sleep 1 done +if [[ "$SSH_READY" -ne 1 ]]; then + log "ssh did not become ready on $GUEST_IP" + exit 1 +fi log "configuring guest" ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ @@ -300,13 +327,8 @@ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf echo \"$VM_NAME\" > /etc/hostname printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts -mkdir -p /home /var -if ! grep -q '^/dev/vdb ' /etc/fstab; then - echo '/dev/vdb /home ext4 defaults 0 2' >> /etc/fstab -fi -if ! grep -q '^/dev/vdc ' /etc/fstab; then - echo '/dev/vdc /var ext4 defaults 0 2' >> /etc/fstab -fi +touch /etc/fstab +sed -i '\|^/dev/vdb[[:space:]]\+/home[[:space:]]|d; \|^/dev/vdc[[:space:]]\+/var[[:space:]]|d' /etc/fstab if ! grep -q '^tmpfs /run ' /etc/fstab; then echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab fi @@ -315,7 +337,7 @@ if ! grep -q '^tmpfs /tmp ' /etc/fstab; then fi apt-get update DEBIAN_FRONTEND=noninteractive apt-get -y upgrade -DEBIAN_FRONTEND=noninteractive apt-get -y install git less tree ca-certificates curl +DEBIAN_FRONTEND=noninteractive apt-get -y install ${APT_PACKAGES_ESCAPED} if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then @@ -362,4 +384,5 @@ for _ in $(seq 1 200); do fi sleep 0.05 done +banger_write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH" log "done" diff --git a/interactive.sh b/interactive.sh index dc04315..b5d9b66 100755 --- a/interactive.sh +++ b/interactive.sh @@ -7,7 +7,7 @@ log() { usage() { cat <<'EOF' -Usage: ./interactive.sh [--out ] [--size ] [--home-size ] [--var-size ] +Usage: ./interactive.sh [--out ] [--size ] Creates a writable copy of the base rootfs and boots a VM so you can customize it manually over SSH. No automatic package/config changes @@ -45,14 +45,10 @@ BR_DEV="br-fc" BR_IP="172.16.0.1" CIDR="24" DNS_SERVER="1.1.1.1" -DEFAULT_HOME_SIZE="2G" -DEFAULT_VAR_SIZE="2G" BASE_ROOTFS="" OUT_ROOTFS="" SIZE_SPEC="" -HOME_SIZE="$DEFAULT_HOME_SIZE" -VAR_SIZE="$DEFAULT_VAR_SIZE" while [[ $# -gt 0 ]]; do case "$1" in --out) @@ -63,14 +59,6 @@ while [[ $# -gt 0 ]]; do SIZE_SPEC="${2:-}" shift 2 ;; - --home-size) - HOME_SIZE="${2:-}" - shift 2 - ;; - --var-size) - VAR_SIZE="${2:-}" - shift 2 - ;; -h|--help) usage exit 0 @@ -115,9 +103,6 @@ if [[ -e "$OUT_ROOTFS" ]]; then exit 1 fi -HOME_BYTES="$(parse_size "$HOME_SIZE")" || { log "invalid --home-size value: $HOME_SIZE"; exit 1; } -VAR_BYTES="$(parse_size "$VAR_SIZE")" || { log "invalid --var-size value: $VAR_SIZE"; exit 1; } - log "copying base rootfs to $OUT_ROOTFS" cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS" @@ -143,8 +128,6 @@ mkdir -p "$VM_DIR" API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock" LOG_FILE="$VM_DIR/firecracker.log" TAP_DEV="tap-fc-$VM_TAG" -HOME_PATH="$VM_DIR/home.ext4" -VAR_PATH="$VM_DIR/var.ext4" DNS_NAME="" # Allocate guest IP @@ -185,15 +168,6 @@ sudo ip link set "$TAP_DEV" master "$BR_DEV" sudo ip link set "$TAP_DEV" up sudo ip link set "$BR_DEV" up -if ! command -v mkfs.ext4 >/dev/null 2>&1; then - log "mkfs.ext4 required to create home/var disks" - exit 1 -fi -truncate -s "$HOME_BYTES" "$HOME_PATH" -mkfs.ext4 -F "$HOME_PATH" >/dev/null -truncate -s "$VAR_BYTES" "$VAR_PATH" -mkfs.ext4 -F "$VAR_PATH" >/dev/null - log "starting firecracker process" rm -f "$API_SOCK" nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 & @@ -215,7 +189,7 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ "smt": false }' >/dev/null -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME}" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \ -H "Content-Type: application/json" \ @@ -234,24 +208,6 @@ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \ \"is_read_only\": false }" >/dev/null -sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/home \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"home\", - \"path_on_host\": \"$HOME_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - -sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/var \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"var\", - \"path_on_host\": \"$VAR_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \ -H "Content-Type: application/json" \ -d "{ diff --git a/make-rootfs.sh b/make-rootfs.sh old mode 100644 new mode 100755 index 3d7b3a4..1f50635 --- a/make-rootfs.sh +++ b/make-rootfs.sh @@ -18,6 +18,7 @@ EOF } DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DIR/packages.sh" OUT_ROOTFS="$DIR/rootfs-docker.ext4" SIZE_SPEC="6G" BASE_ROOTFS="" @@ -45,6 +46,10 @@ while [[ $# -gt 0 ]]; do done if [[ -f "$OUT_ROOTFS" ]]; then + OUT_ROOTFS_WARNING="$(banger_rootfs_manifest_warning "$OUT_ROOTFS" || true)" + if [[ -n "$OUT_ROOTFS_WARNING" ]]; then + log "warning: $OUT_ROOTFS_WARNING" + fi log "already exists: $OUT_ROOTFS" exit 0 fi diff --git a/packages.apt b/packages.apt new file mode 100644 index 0000000..c2e1533 --- /dev/null +++ b/packages.apt @@ -0,0 +1,7 @@ +git +less +tree +ca-certificates +curl +vim +tmux diff --git a/packages.sh b/packages.sh new file mode 100644 index 0000000..25af4c7 --- /dev/null +++ b/packages.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +BANGER_PACKAGES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BANGER_APT_PACKAGES_FILE="${BANGER_APT_PACKAGES_FILE:-$BANGER_PACKAGES_DIR/packages.apt}" + +banger_packages_file() { + printf '%s' "$BANGER_APT_PACKAGES_FILE" +} + +banger_packages_normalized_lines() { + local packages_file="${1:-$BANGER_APT_PACKAGES_FILE}" + + [[ -f "$packages_file" ]] || return 1 + awk ' + { + sub(/\r$/, "") + sub(/[[:space:]]*#.*$/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + if ($0 != "") print + } + ' "$packages_file" +} + +banger_packages_read_array() { + local -n out="$1" + local packages_file="${2:-$BANGER_APT_PACKAGES_FILE}" + + mapfile -t out < <(banger_packages_normalized_lines "$packages_file") + (( ${#out[@]} > 0 )) +} + +banger_packages_hash_stream() { + command -v sha256sum >/dev/null 2>&1 || return 1 + sha256sum | awk '{print $1}' +} + +banger_packages_manifest_hash() { + local packages_file="${1:-$BANGER_APT_PACKAGES_FILE}" + + [[ -f "$packages_file" ]] || return 1 + banger_packages_normalized_lines "$packages_file" | banger_packages_hash_stream +} + +banger_rootfs_manifest_metadata_path() { + local rootfs_path="$1" + printf '%s.packages.sha256' "$rootfs_path" +} + +banger_rootfs_manifest_recorded_hash() { + local rootfs_path="$1" + local metadata_file recorded_hash + + metadata_file="$(banger_rootfs_manifest_metadata_path "$rootfs_path")" + [[ -f "$metadata_file" ]] || return 1 + + recorded_hash="$(head -n 1 "$metadata_file" | tr -d '[:space:]')" + [[ -n "$recorded_hash" ]] || return 1 + printf '%s' "$recorded_hash" +} + +banger_write_rootfs_manifest_metadata() { + local rootfs_path="$1" + local manifest_hash="$2" + local metadata_file + + metadata_file="$(banger_rootfs_manifest_metadata_path "$rootfs_path")" + printf '%s\n' "$manifest_hash" > "$metadata_file" +} + +banger_rootfs_manifest_status() { + local rootfs_path="$1" + local current_hash recorded_hash + + if [[ ! -f "$rootfs_path" ]]; then + printf '%s' "missing-rootfs" + return 0 + fi + + if ! current_hash="$(banger_packages_manifest_hash)"; then + printf '%s' "unknown" + return 0 + fi + + if ! recorded_hash="$(banger_rootfs_manifest_recorded_hash "$rootfs_path")"; then + printf '%s' "missing-metadata" + return 0 + fi + + if [[ "$recorded_hash" == "$current_hash" ]]; then + printf '%s' "fresh" + else + printf '%s' "stale" + fi +} + +banger_rootfs_manifest_warning() { + local rootfs_path="$1" + local status + + status="$(banger_rootfs_manifest_status "$rootfs_path")" + case "$status" in + stale) + printf '%s was built with an older package manifest; rebuild it explicitly to pick up package changes' "$rootfs_path" + ;; + missing-metadata) + printf '%s has no package manifest metadata; rebuild it explicitly to pick up package changes' "$rootfs_path" + ;; + unknown) + printf 'unable to compare %s against %s; install sha256sum and verify the package manifest manually' "$rootfs_path" "$BANGER_APT_PACKAGES_FILE" + ;; + *) + return 1 + ;; + esac +} diff --git a/restore.sh b/restore.sh index 6bdf297..dfddbd0 100755 --- a/restore.sh +++ b/restore.sh @@ -76,8 +76,6 @@ VM_NAME="$(jq -r '.meta.name // empty' "$VM_JSON")" PID="$(jq -r '.meta.pid // empty' "$VM_JSON")" ROOTFS="$(jq -r '.meta.rootfs // empty' "$VM_JSON")" KERNEL="$(jq -r '.meta.kernel // empty' "$VM_JSON")" -HOME_PATH="$(jq -r '.meta.home_path // empty' "$VM_JSON")" -VAR_PATH="$(jq -r '.meta.var_path // empty' "$VM_JSON")" TAP_DEV="$(jq -r '.meta.tap // empty' "$VM_JSON")" API_SOCK="$(jq -r '.meta.api_sock // empty' "$VM_JSON")" LOG_FILE="$(jq -r '.meta.log // empty' "$VM_JSON")" @@ -90,11 +88,11 @@ COW_LOOP_OLD="$(jq -r '.meta.cow_loop // empty' "$VM_JSON")" INITRD_PATH="$(jq -r '.config["boot-source"].initrd_path // empty' "$VM_JSON")" DNS_NAME="$(banger_dns_name "$VM_NAME")" -if [[ -z "$ROOTFS" || -z "$KERNEL" || -z "$HOME_PATH" || -z "$VAR_PATH" || -z "$API_SOCK" || -z "$TAP_DEV" || -z "$GUEST_IP" || -z "$DM_NAME" || -z "$COW_FILE" ]]; then +if [[ -z "$ROOTFS" || -z "$KERNEL" || -z "$API_SOCK" || -z "$TAP_DEV" || -z "$GUEST_IP" || -z "$DM_NAME" || -z "$COW_FILE" ]]; then log "vm.json missing required fields" exit 1 fi -if [[ ! -f "$ROOTFS" || ! -f "$KERNEL" || ! -f "$HOME_PATH" || ! -f "$VAR_PATH" || ! -f "$COW_FILE" || ! -f "$FC_BIN" ]]; then +if [[ ! -f "$ROOTFS" || ! -f "$KERNEL" || ! -f "$COW_FILE" || ! -f "$FC_BIN" ]]; then log "missing disk/kernel file(s)" exit 1 fi @@ -203,9 +201,9 @@ log "configuring machine" -d "$(jq -c '.config["machine-config"]' "$VM_JSON")" >/dev/null boot_args="$(jq -r '.config["boot-source"].boot_args // empty' "$VM_JSON")" -boot_args="$(printf '%s' "$boot_args" | sed -E 's/(^| )hostname=[^ ]+//g; s/(^| )ip=[^ ]+//g' | awk '{$1=$1; print}')" +boot_args="$(printf '%s' "$boot_args" | sed -E 's/(^| )hostname=[^ ]+//g; s/(^| )ip=[^ ]+//g; s/(^| )systemd\.mask=home\.mount//g; s/(^| )systemd\.mask=var\.mount//g' | awk '{$1=$1; print}')" boot_args="$boot_args ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off:${DNS_SERVER}" -boot_args="$boot_args hostname=$VM_NAME" +boot_args="$boot_args hostname=$VM_NAME systemd.mask=home.mount systemd.mask=var.mount" INITRD_JSON="" if [[ -n "$INITRD_PATH" ]]; then INITRD_JSON=", \"initrd_path\": \"$INITRD_PATH\"" @@ -229,24 +227,6 @@ log "attaching drives" \"is_read_only\": false }" >/dev/null -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/home \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"home\", - \"path_on_host\": \"$HOME_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/var \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"var\", - \"path_on_host\": \"$VAR_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - log "configuring network interface" /usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \ -H "Content-Type: application/json" \ diff --git a/run.sh b/run.sh index 45e84b4..28b0711 100755 --- a/run.sh +++ b/run.sh @@ -13,11 +13,10 @@ Options: --name VM name (lowercase letters, digits, -) --vcpu vCPU count (default: 2) --ram RAM in MiB (default: 1024) - --rootfs Root filesystem image (default: ./rootfs.ext4) + --overlay-size Writable overlay size (e.g. 8G, 16384M) + --rootfs Root filesystem image (default: ./rootfs-docker.ext4) --kernel Kernel image (default: ./vmlinux) --initrd Initrd image (optional) - --home-size Home disk size (e.g. 4G, 10240M) - --var-size Var disk size (e.g. 4G, 10240M) -h, --help Show this help EOF } @@ -26,6 +25,7 @@ log "starting" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$DIR/dns.sh" +source "$DIR/packages.sh" STATE="$DIR/state" VM_ROOT="$STATE/vms" mkdir -p "$VM_ROOT" @@ -43,20 +43,17 @@ CIDR="24" DEFAULT_VCPU=2 DEFAULT_RAM=1024 -DEFAULT_HOME_SIZE="2G" -DEFAULT_VAR_SIZE="2G" +DEFAULT_OVERLAY_SIZE="8G" MIN_VCPU=1 MAX_VCPU=16 MIN_RAM=256 MAX_RAM=32768 MAX_DISK_BYTES=$((128 * 1024 * 1024 * 1024)) DNS_SERVER="1.1.1.1" -COW_SIZE="2G" VCPU_COUNT="$DEFAULT_VCPU" RAM_MIB="$DEFAULT_RAM" -HOME_SIZE="$DEFAULT_HOME_SIZE" -VAR_SIZE="$DEFAULT_VAR_SIZE" +OVERLAY_SIZE="$DEFAULT_OVERLAY_SIZE" KERNEL="$DEFAULT_KERNEL" ROOTFS="$DEFAULT_ROOTFS" INITRD="$DEFAULT_INITRD" @@ -105,6 +102,10 @@ while [[ $# -gt 0 ]]; do RAM_MIB="${2:-}" shift 2 ;; + --overlay-size) + OVERLAY_SIZE="${2:-}" + shift 2 + ;; --rootfs) ROOTFS="${2:-}" shift 2 @@ -117,14 +118,6 @@ while [[ $# -gt 0 ]]; do INITRD="${2:-}" shift 2 ;; - --home-size) - HOME_SIZE="${2:-}" - shift 2 - ;; - --var-size) - VAR_SIZE="${2:-}" - shift 2 - ;; -h|--help) usage exit 0 @@ -155,23 +148,12 @@ if (( RAM_MIB < MIN_RAM || RAM_MIB > MAX_RAM )); then exit 1 fi -HOME_BYTES="" -if ! HOME_BYTES="$(parse_disk_size "$HOME_SIZE")"; then - log "invalid --home-size value: $HOME_SIZE" +if ! OVERLAY_BYTES="$(parse_disk_size "$OVERLAY_SIZE")"; then + log "invalid --overlay-size value: $OVERLAY_SIZE" exit 1 fi -if (( HOME_BYTES > MAX_DISK_BYTES )); then - log "home-size exceeds max of $((MAX_DISK_BYTES / 1024 / 1024 / 1024))G" - exit 1 -fi - -VAR_BYTES="" -if ! VAR_BYTES="$(parse_disk_size "$VAR_SIZE")"; then - log "invalid --var-size value: $VAR_SIZE" - exit 1 -fi -if (( VAR_BYTES > MAX_DISK_BYTES )); then - log "var-size exceeds max of $((MAX_DISK_BYTES / 1024 / 1024 / 1024))G" +if (( OVERLAY_BYTES > MAX_DISK_BYTES )); then + log "overlay-size exceeds max of $((MAX_DISK_BYTES / 1024 / 1024 / 1024))G" exit 1 fi @@ -207,6 +189,12 @@ if [[ ! -f "$ROOTFS" ]]; then log "rootfs not found: $ROOTFS" exit 1 fi +if [[ "$ROOTFS" == "$DEFAULT_ROOTFS" ]]; then + ROOTFS_WARNING="$(banger_rootfs_manifest_warning "$ROOTFS" || true)" + if [[ -n "$ROOTFS_WARNING" ]]; then + log "warning: $ROOTFS_WARNING" + fi +fi if [[ ! -f "$KERNEL" ]]; then log "kernel not found: $KERNEL" exit 1 @@ -248,49 +236,13 @@ sudo -v VM_STARTED=0 CLEANUP_ON_EXIT=0 KEEP_VM_DIR_ON_FAIL=1 -HOME_PATH="$VM_DIR/home.ext4" -VAR_PATH="$VM_DIR/var.ext4" COW_FILE="$VM_DIR/cow.ext4" BASE_LOOP="" COW_LOOP="" DM_NAME="fc-rootfs-$VM_TAG" DM_DEV="" -SEED_ROOT_MNT="$VM_DIR/mnt-root" -SEED_HOME_MNT="$VM_DIR/mnt-home" -SEED_VAR_MNT="$VM_DIR/mnt-var" DNS_NAME="" -unmount_if_needed() { - local mount_path="$1" - [[ -n "$mount_path" ]] || return 0 - sudo umount "$mount_path" 2>/dev/null || true -} - -seed_volume_from_rootfs() { - local source_dir="$1" - local target_image="$2" - local target_mount="$3" - local label="$4" - - if [[ ! -d "$source_dir" ]]; then - log "source directory missing in rootfs snapshot: $label" - exit 1 - fi - - mkdir -p "$target_mount" - sudo mount "$target_image" "$target_mount" - sudo cp -a "$source_dir/." "$target_mount/" - sudo umount "$target_mount" -} - -populate_data_disks() { - mkdir -p "$SEED_ROOT_MNT" "$SEED_HOME_MNT" "$SEED_VAR_MNT" - sudo mount -o ro "$DM_DEV" "$SEED_ROOT_MNT" - seed_volume_from_rootfs "$SEED_ROOT_MNT/home" "$HOME_PATH" "$SEED_HOME_MNT" "/home" - seed_volume_from_rootfs "$SEED_ROOT_MNT/var" "$VAR_PATH" "$SEED_VAR_MNT" "/var" - sudo umount "$SEED_ROOT_MNT" -} - cleanup() { local exit_code=$? if [[ "$VM_STARTED" -eq 1 && "$CLEANUP_ON_EXIT" -eq 0 ]]; then @@ -304,9 +256,6 @@ cleanup() { sudo ip link del "$TAP_DEV" 2>/dev/null || true fi rm -f "${API_SOCK:-}" - unmount_if_needed "${SEED_VAR_MNT:-}" - unmount_if_needed "${SEED_HOME_MNT:-}" - unmount_if_needed "${SEED_ROOT_MNT:-}" banger_dns_remove_record_name "${DNS_NAME:-}" if [[ -n "${DM_NAME:-}" ]]; then sudo dmsetup remove "$DM_NAME" 2>/dev/null || true @@ -351,15 +300,6 @@ else log "setcap not available; firecracker may need root to open TAP" fi -if ! command -v mkfs.ext4 >/dev/null 2>&1; then - log "mkfs.ext4 required to create home/var disks" - exit 1 -fi -truncate -s "$HOME_BYTES" "$HOME_PATH" -mkfs.ext4 -F "$HOME_PATH" >/dev/null -truncate -s "$VAR_BYTES" "$VAR_PATH" -mkfs.ext4 -F "$VAR_PATH" >/dev/null - if ! command -v dmsetup >/dev/null 2>&1 || ! command -v losetup >/dev/null 2>&1 || ! command -v blockdev >/dev/null 2>&1; then log "dmsetup, losetup, and blockdev are required for rootfs snapshots" exit 1 @@ -368,23 +308,13 @@ if ! command -v e2cp >/dev/null 2>&1 || ! command -v e2rm >/dev/null 2>&1; then log "e2cp and e2rm are required to set hostname and resolv.conf" exit 1 fi -if ! command -v mount >/dev/null 2>&1 || ! command -v umount >/dev/null 2>&1; then - log "mount and umount are required to populate home/var disks" - exit 1 -fi if ! command -v jq >/dev/null 2>&1; then log "jq is required to persist VM metadata" exit 1 fi -COW_BYTES="$(parse_disk_size "$COW_SIZE")" -if [[ -z "$COW_BYTES" ]]; then - log "invalid COW size: $COW_SIZE" - exit 1 -fi - BASE_LOOP="$(sudo losetup -f --show --read-only "$ROOTFS")" -truncate -s "$COW_BYTES" "$COW_FILE" +truncate -s "$OVERLAY_BYTES" "$COW_FILE" COW_LOOP="$(sudo losetup -f --show "$COW_FILE")" SECTORS="$(sudo blockdev --getsz "$BASE_LOOP")" sudo dmsetup create "$DM_NAME" --table "0 $SECTORS snapshot $BASE_LOOP $COW_LOOP P 8" @@ -412,9 +342,6 @@ sudo e2cp "$HOSTS_TMP" "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || { exit 1 } -log "populating /home and /var disks from rootfs snapshot" -populate_data_disks - # Host bridge if ! ip link show "$BR_DEV" >/dev/null 2>&1; then log "creating host bridge $BR_DEV ($BR_IP/$CIDR)" @@ -480,7 +407,7 @@ log "configuring machine" # Boot source log "configuring boot source" -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off:${DNS_SERVER} hostname=${VM_NAME}" +KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount" INITRD_JSON="" if [[ -n "$INITRD" ]]; then @@ -505,28 +432,6 @@ log "attaching root filesystem" \"is_read_only\": false }" >/dev/null -# Home filesystem -log "attaching home filesystem" -"${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/drives/home \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"home\", - \"path_on_host\": \"$HOME_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - -# Var filesystem -log "attaching var filesystem" -"${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/drives/var \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"var\", - \"path_on_host\": \"$VAR_PATH\", - \"is_root_device\": false, - \"is_read_only\": false - }" >/dev/null - # Network interface log "configuring network interface" "${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \ @@ -556,8 +461,6 @@ jq -n \ --arg log "$LOG_FILE" \ --arg rootfs "$ROOTFS" \ --arg kernel "$KERNEL" \ - --arg home_path "$HOME_PATH" \ - --arg var_path "$VAR_PATH" \ --arg base_loop "$BASE_LOOP" \ --arg cow_file "$COW_FILE" \ --arg cow_loop "$COW_LOOP" \ @@ -565,7 +468,7 @@ jq -n \ --arg dm_dev "$DM_DEV" \ --arg dns_name "$DNS_NAME" \ --argjson config "$VM_CONFIG_JSON" \ - '{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel,home_path:$home_path,var_path:$var_path,base_loop:$base_loop,cow_file:$cow_file,cow_loop:$cow_loop,dm_name:$dm_name,dm_dev:$dm_dev,dns_name:$dns_name},config:$config}' \ + '{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel,base_loop:$base_loop,cow_file:$cow_file,cow_loop:$cow_loop,dm_name:$dm_name,dm_dev:$dm_dev,dns_name:$dns_name},config:$config}' \ > "$VM_DIR/vm.json" VM_STARTED=1