1
0
forked from wrenn/wrenn

Prototype with single host server and no admin panel (#2)

Reviewed-on: wrenn/sandbox#2
Co-authored-by: pptx704 <rafeed@omukk.dev>
Co-committed-by: pptx704 <rafeed@omukk.dev>
This commit is contained in:
2026-03-22 21:01:23 +00:00
committed by Rafeed M. Bhuiyan
parent bd78cc068c
commit 32e5a5a715
293 changed files with 46885 additions and 1033 deletions

156
scripts/rootfs-from-container.sh Executable file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env bash
#
# rootfs-from-container.sh — Create a bootable Wrenn rootfs from a Docker container.
#
# Exports a container's filesystem, writes it into an ext4 image, injects
# envd + wrenn-init, and shrinks the image to minimum size.
#
# Usage:
# bash scripts/rootfs-from-container.sh <container> <image_name>
#
# Arguments:
# container — Docker container name or ID to export
# image_name — Directory name under images dir (e.g. "waitlist")
#
# Output:
# ${AGENT_FILES_ROOTDIR}/images/<image_name>/rootfs.ext4
#
# Requires: docker, mkfs.ext4, resize2fs, e2fsck, make (for building envd), curl (for tini download)
# Sudo is used only for mount/umount/copy-into-image operations.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
AGENT_FILES_ROOTDIR="${AGENT_FILES_ROOTDIR:-/var/lib/wrenn}"
AGENT_IMAGES_PATH="${AGENT_FILES_ROOTDIR}/images"
if [ $# -lt 2 ]; then
echo "Usage: $0 <container> <image_name>"
exit 1
fi
CONTAINER="$1"
IMAGE_NAME="$2"
OUTPUT_DIR="${AGENT_IMAGES_PATH}/${IMAGE_NAME}"
OUTPUT_FILE="${OUTPUT_DIR}/rootfs.ext4"
MOUNT_DIR="/tmp/wrenn-rootfs-build"
TAR_FILE="/tmp/wrenn-rootfs-export-${IMAGE_NAME}.tar"
# Verify the container exists.
if ! docker inspect "${CONTAINER}" > /dev/null 2>&1; then
echo "ERROR: Container '${CONTAINER}' not found"
exit 1
fi
# Step 1: Build envd.
echo "==> Building envd..."
cd "${PROJECT_ROOT}"
make build-envd
ENVD_BIN="${PROJECT_ROOT}/builds/envd"
if [ ! -f "${ENVD_BIN}" ]; then
echo "ERROR: envd binary not found at ${ENVD_BIN}"
exit 1
fi
if ! file "${ENVD_BIN}" | grep -q "statically linked"; then
echo "ERROR: envd is not statically linked!"
exit 1
fi
# Step 2: Export container filesystem.
echo "==> Exporting container '${CONTAINER}'..."
docker export "${CONTAINER}" -o "${TAR_FILE}"
cleanup() {
echo "==> Cleaning up..."
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
rmdir "${MOUNT_DIR}" 2>/dev/null || true
rm -f "${TAR_FILE}"
}
trap cleanup EXIT
# Step 3: Create an oversized ext4 image.
# Use 2x the tar size + 256MB headroom for filesystem overhead and injected binaries.
TAR_SIZE_BYTES="$(stat --format=%s "${TAR_FILE}")"
INITIAL_SIZE_MB=$(( (TAR_SIZE_BYTES / 1024 / 1024) * 2 + 256 ))
echo "==> Creating ${INITIAL_SIZE_MB}MB ext4 image (will shrink after populating)..."
sudo mkdir -p "${OUTPUT_DIR}"
sudo dd if=/dev/zero of="${OUTPUT_FILE}" bs=1M count="${INITIAL_SIZE_MB}" status=progress
sudo mkfs.ext4 -F "${OUTPUT_FILE}"
# Step 4: Mount and populate.
echo "==> Mounting image at ${MOUNT_DIR}..."
mkdir -p "${MOUNT_DIR}"
sudo mount -o loop "${OUTPUT_FILE}" "${MOUNT_DIR}"
echo "==> Extracting container filesystem..."
sudo tar xf "${TAR_FILE}" -C "${MOUNT_DIR}"
# Step 5: Inject wrenn guest binaries.
echo "==> Installing envd..."
sudo mkdir -p "${MOUNT_DIR}/usr/local/bin"
sudo cp "${ENVD_BIN}" "${MOUNT_DIR}/usr/local/bin/envd"
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/envd"
echo "==> Installing wrenn-init..."
sudo cp "${PROJECT_ROOT}/images/wrenn-init.sh" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/wrenn-init"
echo "==> Installing tini..."
TINI_BIN=""
# 1. Already in the exported container image?
for p in "${MOUNT_DIR}/usr/bin/tini" "${MOUNT_DIR}/sbin/tini" "${MOUNT_DIR}/usr/local/bin/tini"; do
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
done
# 2. Available on the host?
if [ -z "${TINI_BIN}" ]; then
for p in /usr/bin/tini /usr/local/bin/tini /sbin/tini; do
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
done
fi
# 3. Download from GitHub releases.
if [ -z "${TINI_BIN}" ]; then
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) TINI_ARCH="amd64" ;;
aarch64) TINI_ARCH="arm64" ;;
*) echo "ERROR: Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
TINI_VERSION="v0.19.0"
TINI_URL="https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TINI_ARCH}"
TINI_TMP="/tmp/tini-${TINI_ARCH}"
echo " Downloading tini ${TINI_VERSION} (${TINI_ARCH})..."
curl -fsSL "${TINI_URL}" -o "${TINI_TMP}"
chmod +x "${TINI_TMP}"
TINI_BIN="${TINI_TMP}"
fi
sudo mkdir -p "${MOUNT_DIR}/sbin"
sudo cp "${TINI_BIN}" "${MOUNT_DIR}/sbin/tini"
sudo chmod 755 "${MOUNT_DIR}/sbin/tini"
# Step 6: Verify.
echo ""
echo "==> Installed guest binaries:"
ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init" "${MOUNT_DIR}/sbin/tini"
# Unmount before shrinking.
sudo umount "${MOUNT_DIR}"
rmdir "${MOUNT_DIR}" 2>/dev/null || true
# Step 7: Shrink the image to minimum size.
echo ""
echo "==> Shrinking image..."
sudo e2fsck -fy "${OUTPUT_FILE}"
sudo resize2fs -M "${OUTPUT_FILE}"
# Truncate the file to match the shrunk filesystem.
BLOCK_COUNT="$(sudo dumpe2fs -h "${OUTPUT_FILE}" 2>/dev/null | grep "Block count:" | awk '{print $3}')"
BLOCK_SIZE="$(sudo dumpe2fs -h "${OUTPUT_FILE}" 2>/dev/null | grep "Block size:" | awk '{print $3}')"
FS_SIZE_BYTES=$((BLOCK_COUNT * BLOCK_SIZE))
sudo truncate -s "${FS_SIZE_BYTES}" "${OUTPUT_FILE}"
FINAL_SIZE_MB=$((FS_SIZE_BYTES / 1024 / 1024))
echo ""
echo "==> Done. Rootfs created at: ${OUTPUT_FILE} (${FINAL_SIZE_MB}MB)"

View File

233
scripts/test-host.sh Executable file
View File

@ -0,0 +1,233 @@
#!/usr/bin/env bash
#
# test-host.sh — Integration test for the Wrenn host agent.
#
# Prerequisites:
# - Host agent running: sudo ./builds/wrenn-agent
# - Firecracker installed at /usr/local/bin/firecracker
# - Kernel at /var/lib/wrenn/kernels/vmlinux
# - Base rootfs at /var/lib/wrenn/images/minimal.ext4 (with envd + wrenn-init baked in)
#
# Usage:
# ./scripts/test-host.sh [agent_url]
#
# The agent URL defaults to http://localhost:50051.
set -euo pipefail
AGENT="${1:-http://localhost:50051}"
BASE="/hostagent.v1.HostAgentService"
SANDBOX_ID=""
# Colors for output.
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
pass() { echo -e "${GREEN}PASS${NC}: $1"; }
fail() { echo -e "${RED}FAIL${NC}: $1"; exit 1; }
info() { echo -e "${YELLOW}----${NC}: $1"; }
rpc() {
local method="$1"
local body="$2"
curl -s -X POST \
-H "Content-Type: application/json" \
"${AGENT}${BASE}/${method}" \
-d "${body}"
}
# ──────────────────────────────────────────────────
# Test 1: List sandboxes (should be empty)
# ──────────────────────────────────────────────────
info "Test 1: List sandboxes (expect empty)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q '"sandboxes"' || echo "${RESULT}" | grep -q '{}' && \
pass "ListSandboxes returned" || \
fail "ListSandboxes failed"
# ──────────────────────────────────────────────────
# Test 2: Create a sandbox
# ──────────────────────────────────────────────────
info "Test 2: Create a sandbox"
RESULT=$(rpc "CreateSandbox" '{
"template": "minimal",
"vcpus": 1,
"memoryMb": 512,
"timeoutSec": 300
}')
echo " Response: ${RESULT}"
SANDBOX_ID=$(echo "${RESULT}" | python3 -c "import sys,json; print(json.load(sys.stdin)['sandboxId'])" 2>/dev/null) || \
fail "CreateSandbox did not return sandboxId"
echo " Sandbox ID: ${SANDBOX_ID}"
pass "Sandbox created: ${SANDBOX_ID}"
# ──────────────────────────────────────────────────
# Test 3: List sandboxes (should have one)
# ──────────────────────────────────────────────────
info "Test 3: List sandboxes (expect one)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
pass "Sandbox ${SANDBOX_ID} found in list" || \
fail "Sandbox not found in list"
# ──────────────────────────────────────────────────
# Test 4: Execute a command
# ──────────────────────────────────────────────────
info "Test 4: Execute 'echo hello world'"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo hello world\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
# stdout is base64-encoded in Connect RPC JSON.
STDOUT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
print(base64.b64decode(r['stdout']).decode().strip())
" 2>/dev/null) || fail "Exec did not return stdout"
[ "${STDOUT}" = "hello world" ] && \
pass "Exec returned correct output: '${STDOUT}'" || \
fail "Expected 'hello world', got '${STDOUT}'"
# ──────────────────────────────────────────────────
# Test 5: Execute a multi-line command
# ──────────────────────────────────────────────────
info "Test 5: Execute multi-line command"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo line1; echo line2; echo line3\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
LINE_COUNT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
lines = base64.b64decode(r['stdout']).decode().strip().split('\n')
print(len(lines))
" 2>/dev/null)
[ "${LINE_COUNT}" = "3" ] && \
pass "Multi-line output: ${LINE_COUNT} lines" || \
fail "Expected 3 lines, got ${LINE_COUNT}"
# ──────────────────────────────────────────────────
# Test 6: Pause the sandbox
# ──────────────────────────────────────────────────
info "Test 6: Pause sandbox"
RESULT=$(rpc "PauseSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
# Verify status is paused.
LIST=$(rpc "ListSandboxes" '{}')
echo "${LIST}" | grep -q '"paused"' && \
pass "Sandbox paused" || \
fail "Sandbox not in paused state"
# ──────────────────────────────────────────────────
# Test 7: Exec should fail while paused
# ──────────────────────────────────────────────────
info "Test 7: Exec while paused (expect error)"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/echo\",
\"args\": [\"should fail\"]
}")
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -qi "not running\|error\|code" && \
pass "Exec correctly rejected while paused" || \
fail "Exec should have failed while paused"
# ──────────────────────────────────────────────────
# Test 8: Resume the sandbox
# ──────────────────────────────────────────────────
info "Test 8: Resume sandbox"
RESULT=$(rpc "ResumeSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
# Verify status is running.
LIST=$(rpc "ListSandboxes" '{}')
echo "${LIST}" | grep -q '"running"' && \
pass "Sandbox resumed" || \
fail "Sandbox not in running state"
# ──────────────────────────────────────────────────
# Test 9: Exec after resume
# ──────────────────────────────────────────────────
info "Test 9: Exec after resume"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo resumed ok\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
STDOUT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
print(base64.b64decode(r['stdout']).decode().strip())
" 2>/dev/null) || fail "Exec after resume failed"
[ "${STDOUT}" = "resumed ok" ] && \
pass "Exec after resume works: '${STDOUT}'" || \
fail "Expected 'resumed ok', got '${STDOUT}'"
# ──────────────────────────────────────────────────
# Test 10: Destroy the sandbox
# ──────────────────────────────────────────────────
info "Test 10: Destroy sandbox"
RESULT=$(rpc "DestroySandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
pass "Sandbox destroyed"
# ──────────────────────────────────────────────────
# Test 11: List sandboxes (should be empty again)
# ──────────────────────────────────────────────────
info "Test 11: List sandboxes (expect empty)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
fail "Destroyed sandbox still in list" || \
pass "Sandbox list is clean"
# ──────────────────────────────────────────────────
# Test 12: Destroy non-existent sandbox (expect error)
# ──────────────────────────────────────────────────
info "Test 12: Destroy non-existent sandbox (expect error)"
RESULT=$(rpc "DestroySandbox" '{"sandboxId": "sb-nonexist"}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -qi "not found\|error\|code" && \
pass "Correctly rejected non-existent sandbox" || \
fail "Should have returned error for non-existent sandbox"
echo ""
echo -e "${GREEN}All tests passed!${NC}"

105
scripts/update-debug-rootfs.sh Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env bash
#
# update-debug-rootfs.sh — Build envd and inject it (plus wrenn-init) into the debug rootfs.
#
# This script:
# 1. Builds a fresh envd static binary via make
# 2. Mounts the rootfs image
# 3. Copies envd and wrenn-init into the image
# 4. Unmounts cleanly
#
# Usage:
# bash scripts/update-debug-rootfs.sh [rootfs_path]
#
# Defaults to /var/lib/wrenn/images/minimal/rootfs.ext4
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ROOTFS="${1:-/var/lib/wrenn/images/minimal/rootfs.ext4}"
MOUNT_DIR="/tmp/wrenn-rootfs-update"
if [ ! -f "${ROOTFS}" ]; then
echo "ERROR: Rootfs not found at ${ROOTFS}"
exit 1
fi
# Step 1: Build envd.
echo "==> Building envd..."
cd "${PROJECT_ROOT}"
make build-envd
ENVD_BIN="${PROJECT_ROOT}/builds/envd"
if [ ! -f "${ENVD_BIN}" ]; then
echo "ERROR: envd binary not found at ${ENVD_BIN}"
exit 1
fi
# Verify it's statically linked.
if ! file "${ENVD_BIN}" | grep -q "statically linked"; then
echo "ERROR: envd is not statically linked!"
exit 1
fi
# Step 2: Mount the rootfs.
echo "==> Mounting rootfs at ${MOUNT_DIR}..."
mkdir -p "${MOUNT_DIR}"
sudo mount -o loop "${ROOTFS}" "${MOUNT_DIR}"
cleanup() {
echo "==> Unmounting rootfs..."
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
rmdir "${MOUNT_DIR}" 2>/dev/null || true
}
trap cleanup EXIT
# Step 3: Copy files into rootfs.
echo "==> Installing envd..."
sudo mkdir -p "${MOUNT_DIR}/usr/local/bin"
sudo cp "${ENVD_BIN}" "${MOUNT_DIR}/usr/local/bin/envd"
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/envd"
echo "==> Installing wrenn-init..."
sudo cp "${PROJECT_ROOT}/images/wrenn-init.sh" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/wrenn-init"
echo "==> Installing tini..."
TINI_BIN=""
# 1. Already in the rootfs?
for p in "${MOUNT_DIR}/usr/bin/tini" "${MOUNT_DIR}/sbin/tini" "${MOUNT_DIR}/usr/local/bin/tini"; do
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
done
# 2. Available on the host?
if [ -z "${TINI_BIN}" ]; then
for p in /usr/bin/tini /usr/local/bin/tini /sbin/tini; do
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
done
fi
# 3. Download from GitHub releases.
if [ -z "${TINI_BIN}" ]; then
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) TINI_ARCH="amd64" ;;
aarch64) TINI_ARCH="arm64" ;;
*) echo "ERROR: Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
TINI_VERSION="v0.19.0"
TINI_URL="https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TINI_ARCH}"
TINI_TMP="/tmp/tini-${TINI_ARCH}"
echo " Downloading tini ${TINI_VERSION} (${TINI_ARCH})..."
curl -fsSL "${TINI_URL}" -o "${TINI_TMP}"
chmod +x "${TINI_TMP}"
TINI_BIN="${TINI_TMP}"
fi
sudo mkdir -p "${MOUNT_DIR}/sbin"
sudo cp "${TINI_BIN}" "${MOUNT_DIR}/sbin/tini"
sudo chmod 755 "${MOUNT_DIR}/sbin/tini"
# Step 4: Verify.
echo ""
echo "==> Installed files:"
ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init" "${MOUNT_DIR}/sbin/tini"
echo ""
echo "==> Done. Rootfs updated: ${ROOTFS}"