From bbd57d8dd29009b1bfa18783a5778535961ba64a Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Fri, 30 Jan 2026 12:13:35 -0300 Subject: [PATCH] Use hostname-safe VM names --- README.md | 12 ++++-- namegen | 2 +- run.sh | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8574b7a..61f6657 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Minimal Firecracker launcher. ## Requirements - Linux host with KVM (`/dev/kvm` access) - `sudo`, `ip`, `curl`, `ssh` +- `dmsetup`, `losetup`, `blockdev` (device-mapper snapshot for rootfs) +- `e2cp`, `e2rm` (writes hostname and resolv.conf into rootfs snapshot) ## Files - `firecracker`: Firecracker binary @@ -21,17 +23,19 @@ Minimal Firecracker launcher. ``` ./run.sh --name calm_otter --vcpu 4 --ram 2048 --home-size 6G ``` -- `--name`: must be unique and match `[a-z0-9][a-z0-9_-]{0,63}`. +- `--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. - `--rootfs`: path to the base rootfs image (default: `./rootfs.ext4`). - `--kernel`: path to the kernel image (default: `./vmlinux`). - `--home-size`: M/G suffixes supported (default: 2G). +- `--var-size`: M/G suffixes supported (default: 2G). ## Storage Layout -- `rootfs.ext4` is mounted read-only as `/` and shared across VMs. -- Each VM gets a writable ext4 disk mounted at `/home`. -- The base image must include an `/etc/fstab` entry for `/dev/vdb` → `/home`. +- `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`. +- The base image must include `/etc/fstab` entries for `/dev/vdb` → `/home` and `/dev/vdc` → `/var`. +- `/run` and `/tmp` should be tmpfs via `/etc/fstab`. ## SSH ``` diff --git a/namegen b/namegen index ed133a8..830632a 100755 --- a/namegen +++ b/namegen @@ -412,4 +412,4 @@ rand() { adjective="${ADJECTIVES[$(rand ${#ADJECTIVES[@]})]}" substantive="${SUBSTANTIVES[$(rand ${#SUBSTANTIVES[@]})]}" - printf '%s_%s' "$adjective" "$substantive" + printf '%s-%s' "$adjective" "$substantive" diff --git a/run.sh b/run.sh index 171635f..685290d 100755 --- a/run.sh +++ b/run.sh @@ -10,12 +10,13 @@ usage() { Usage: ./run.sh [options] Options: - --name VM name (lowercase letters, digits, -, _) + --name VM name (lowercase letters, digits, -) --vcpu vCPU count (default: 2) --ram RAM in MiB (default: 1024) --rootfs Root filesystem image (default: ./rootfs.ext4) --kernel Kernel image (default: ./vmlinux) --home-size Home disk size (e.g. 4G, 10240M) + --var-size Var disk size (e.g. 4G, 10240M) -h, --help Show this help EOF } @@ -40,16 +41,19 @@ CIDR="24" DEFAULT_VCPU=2 DEFAULT_RAM=1024 DEFAULT_HOME_SIZE="2G" +DEFAULT_VAR_SIZE="2G" 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" KERNEL="$DEFAULT_KERNEL" ROOTFS="$DEFAULT_ROOTFS" VM_NAME="" @@ -109,6 +113,10 @@ while [[ $# -gt 0 ]]; do HOME_SIZE="${2:-}" shift 2 ;; + --var-size) + VAR_SIZE="${2:-}" + shift 2 + ;; -h|--help) usage exit 0 @@ -149,6 +157,16 @@ if (( HOME_BYTES > MAX_DISK_BYTES )); then 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" + exit 1 +fi + if [[ -z "$VM_NAME" ]]; then if [[ ! -x "$NAMEGEN" ]]; then log "name generator not found: $NAMEGEN" @@ -167,7 +185,7 @@ if [[ -z "$VM_NAME" ]]; then exit 1 fi -if ! [[ "$VM_NAME" =~ ^[a-z0-9][a-z0-9_-]{0,63}$ ]]; then +if ! [[ "$VM_NAME" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]]; then log "invalid --name value: $VM_NAME" exit 1 fi @@ -213,6 +231,12 @@ 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="" cleanup() { local exit_code=$? @@ -227,6 +251,15 @@ cleanup() { sudo ip link del "$TAP_DEV" 2>/dev/null || true fi rm -f "${API_SOCK:-}" + if [[ -n "${DM_NAME:-}" ]]; then + sudo dmsetup remove "$DM_NAME" 2>/dev/null || true + fi + if [[ -n "${COW_LOOP:-}" ]]; then + sudo losetup -d "$COW_LOOP" 2>/dev/null || true + fi + if [[ -n "${BASE_LOOP:-}" ]]; then + sudo losetup -d "$BASE_LOOP" 2>/dev/null || true + fi if [[ -n "${VM_DIR:-}" && -d "$VM_DIR" ]]; then if [[ "$KEEP_VM_DIR_ON_FAIL" -eq 0 || "$VM_STARTED" -eq 1 || "$CLEANUP_ON_EXIT" -eq 1 ]]; then rm -rf "$VM_DIR" @@ -262,11 +295,57 @@ else fi if ! command -v mkfs.ext4 >/dev/null 2>&1; then - log "mkfs.ext4 required to create home disk" + 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 +fi +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 + +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" +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" +DM_DEV="/dev/mapper/$DM_NAME" + +RESOLV_TMP="$VM_DIR/resolv.conf" +HOSTNAME_TMP="$VM_DIR/hostname" +HOSTS_TMP="$VM_DIR/hosts" +printf 'nameserver %s\n' "$DNS_SERVER" >"$RESOLV_TMP" +printf '%s\n' "$VM_NAME" >"$HOSTNAME_TMP" +printf '127.0.0.1 localhost\n127.0.1.1 %s\n' "$VM_NAME" >"$HOSTS_TMP" +sudo e2rm "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || true +sudo e2rm "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || true +sudo e2rm "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || true +sudo e2cp "$RESOLV_TMP" "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || { + log "failed to write /etc/resolv.conf into rootfs snapshot" + exit 1 +} +sudo e2cp "$HOSTNAME_TMP" "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || { + log "failed to write /etc/hostname into rootfs snapshot" + exit 1 +} +sudo e2cp "$HOSTS_TMP" "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || { + log "failed to write /etc/hosts into rootfs snapshot" + exit 1 +} # Host bridge if ! ip link show "$BR_DEV" >/dev/null 2>&1; then @@ -333,7 +412,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}:${DNS_SERVER}:255.255.255.0::eth0:off 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}" "${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \ -H "Content-Type: application/json" \ @@ -348,9 +427,9 @@ log "attaching root filesystem" -H "Content-Type: application/json" \ -d "{ \"drive_id\": \"rootfs\", - \"path_on_host\": \"$ROOTFS\", + \"path_on_host\": \"$DM_DEV\", \"is_root_device\": true, - \"is_read_only\": true + \"is_read_only\": false }" >/dev/null # Home filesystem @@ -364,6 +443,17 @@ log "attaching home filesystem" \"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 \ @@ -391,6 +481,11 @@ guest_ip=$GUEST_IP tap=$TAP_DEV api_sock=$API_SOCK log=$LOG_FILE +base_loop=$BASE_LOOP +cow_file=$COW_FILE +cow_loop=$COW_LOOP +dm_name=$DM_NAME +dm_dev=$DM_DEV EOF log "vm started successfully"