smol-machines--smolvm
337 行
13 KiB
Bash
可执行文件
337 行
13 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
#
|
|
# Storage Tests (overlay, prune, resize)
|
|
#
|
|
# Part of the smolvm test suite. Run with: ./tests/test_storage.sh
|
|
#
|
|
|
|
source "$(dirname "$0")/common.sh"
|
|
init_smolvm
|
|
|
|
log_info "Pre-flight cleanup: killing orphan processes..."
|
|
kill_orphan_smolvm_processes
|
|
|
|
trap cleanup_machine EXIT
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Storage Tests (overlay, prune, resize)"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
test_machine_overlay_root_active() {
|
|
local vm_name="overlay-active-test-$$"
|
|
|
|
# Clean up any existing
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
|
|
# Create and start VM
|
|
$SMOLVM machine create --name "$vm_name" 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
|
|
|
|
# Check that root is an overlay mount
|
|
local output exit_code=0
|
|
output=$($SMOLVM machine exec --name "$vm_name" -- mount 2>&1) || exit_code=$?
|
|
|
|
# Clean up
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
ensure_data_dir_deleted "$vm_name"
|
|
|
|
[[ $exit_code -eq 0 ]] && [[ "$output" == *"overlay on / type overlay"* ]]
|
|
}
|
|
|
|
test_machine_rootfs_persists_across_reboot() {
|
|
local vm_name="overlay-persist-test-$$"
|
|
|
|
# Clean up any existing
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
|
|
# Create and start VM
|
|
$SMOLVM machine create --name "$vm_name" 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
|
|
|
|
# Write a marker file to the rootfs
|
|
local exit_code=0
|
|
$SMOLVM machine exec --name "$vm_name" -- sh -c "echo persistence-test-ok > /tmp/overlay-test-marker" 2>&1 || exit_code=$?
|
|
if [[ $exit_code -ne 0 ]]; then
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
return 1
|
|
fi
|
|
|
|
# Verify file exists before reboot
|
|
local output
|
|
output=$($SMOLVM machine exec --name "$vm_name" -- cat /tmp/overlay-test-marker 2>&1) || {
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
return 1
|
|
}
|
|
if [[ "$output" != *"persistence-test-ok"* ]]; then
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
return 1
|
|
fi
|
|
|
|
# Stop and restart the VM
|
|
$SMOLVM machine stop --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
|
|
|
|
# Verify the file survived the reboot
|
|
exit_code=0
|
|
output=$($SMOLVM machine exec --name "$vm_name" -- cat /tmp/overlay-test-marker 2>&1) || exit_code=$?
|
|
|
|
# Clean up
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
ensure_data_dir_deleted "$vm_name"
|
|
|
|
[[ $exit_code -eq 0 ]] && [[ "$output" == *"persistence-test-ok"* ]]
|
|
}
|
|
|
|
test_machine_overlay_size() {
|
|
local vm_name="test-vm-overlay-size"
|
|
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
|
|
# Create VM with custom overlay size (4 GiB)
|
|
$SMOLVM machine create --name "$vm_name" --overlay 4 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
# Check the overlay disk size inside the VM via df
|
|
local df_output
|
|
df_output=$($SMOLVM machine exec --name "$vm_name" -- df -m / 2>&1)
|
|
|
|
# Cleanup
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
ensure_data_dir_deleted "$vm_name"
|
|
|
|
# The 4GB overlay should show ~3800-4096 MB total (ext4 overhead)
|
|
# Just verify it's > 3000 MB (not the old 2GB default)
|
|
local total_mb
|
|
total_mb=$(echo "$df_output" | tail -1 | awk '{print $2}')
|
|
[[ "$total_mb" -gt 3000 ]]
|
|
}
|
|
|
|
test_machine_images() {
|
|
# Start default machine and check images command
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
$SMOLVM machine create --net --name default 2>/dev/null || true
|
|
$SMOLVM machine start --name default 2>/dev/null || true
|
|
|
|
local output
|
|
output=$($SMOLVM machine images --name default 2>&1)
|
|
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
|
|
# Should show storage info
|
|
[[ "$output" == *"Storage"* ]] || [[ "$output" == *"storage"* ]]
|
|
}
|
|
|
|
test_machine_prune_dry_run() {
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
$SMOLVM machine create --net --name default 2>/dev/null || true
|
|
$SMOLVM machine start --name default 2>/dev/null || true
|
|
|
|
local output
|
|
output=$($SMOLVM machine prune --name default --dry-run 2>&1)
|
|
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
|
|
# Should complete without error
|
|
[[ $? -eq 0 ]] || [[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]]
|
|
}
|
|
|
|
assert_vm_stays_running() {
|
|
local desc="$1"; shift
|
|
|
|
ensure_machine_running
|
|
|
|
local status
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM not running before '$desc'"; return 1; }
|
|
|
|
"$@" 2>&1 || return 1
|
|
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM stopped after '$desc'"; return 1; }
|
|
}
|
|
|
|
test_images_does_not_stop_running_vm() {
|
|
assert_vm_stays_running "machine images" $SMOLVM machine images --name default
|
|
}
|
|
|
|
test_prune_on_running_vm() {
|
|
ensure_machine_running
|
|
|
|
local status
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM not running before test"; return 1; }
|
|
|
|
# Regular prune should work without stopping the VM
|
|
local output
|
|
output=$($SMOLVM machine prune --name default 2>&1) || true
|
|
[[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]] || {
|
|
echo "unexpected prune output: $output"
|
|
return 1
|
|
}
|
|
|
|
# VM should still be running after prune
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM stopped after prune"; return 1; }
|
|
}
|
|
|
|
test_prune_dry_run_on_running_vm() {
|
|
ensure_machine_running
|
|
|
|
local output
|
|
output=$($SMOLVM machine prune --name default --dry-run 2>&1) || true
|
|
[[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]] || {
|
|
echo "unexpected prune --dry-run output: $output"
|
|
return 1
|
|
}
|
|
|
|
# VM should still be running
|
|
local status
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM stopped after prune --dry-run"; return 1; }
|
|
}
|
|
|
|
test_prune_all_refuses_on_running_vm() {
|
|
ensure_machine_running "true"
|
|
|
|
local status
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM not running before test"; return 1; }
|
|
|
|
# --all should refuse while the VM is running
|
|
local output exit_code=0
|
|
output=$($SMOLVM machine prune --name default --all 2>&1) || exit_code=$?
|
|
[[ $exit_code -ne 0 ]] || { echo "prune --all should have failed on running VM"; return 1; }
|
|
[[ "$output" == *"cannot prune --all while machine"* ]] || {
|
|
echo "unexpected error: $output"
|
|
return 1
|
|
}
|
|
|
|
# VM should still be running
|
|
status=$($SMOLVM machine status 2>&1)
|
|
[[ "$status" == *"running"* ]] || { echo "VM stopped after rejected prune --all"; return 1; }
|
|
}
|
|
|
|
test_prune_all_keeps_in_use_image() {
|
|
# An image-backed machine needs its cached image to restart, so `prune --all`
|
|
# must keep it (and reclaim only unreferenced layers) rather than brick a
|
|
# stopped machine with "image not found" on the next start.
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
$SMOLVM machine create --image alpine --net --name default 2>&1 || return 1
|
|
$SMOLVM machine start 2>&1 || return 1
|
|
|
|
# Verify the image is cached and the VM is reachable
|
|
$SMOLVM machine exec -- true 2>&1 || { echo "VM not reachable"; return 1; }
|
|
|
|
# Stop the VM (prune --all requires it)
|
|
$SMOLVM machine stop 2>&1
|
|
|
|
# Run prune --all: it must keep the in-use image, not remove it.
|
|
local output
|
|
output=$($SMOLVM machine prune --name default --all 2>&1) || {
|
|
echo "prune --all failed: $output"
|
|
$SMOLVM machine delete --name default -f 2>/dev/null
|
|
return 1
|
|
}
|
|
[[ "$output" == *"Kept"* ]] || [[ "$output" == *"image-backed"* ]] || {
|
|
echo "expected prune --all to keep the in-use image, got: $output"
|
|
$SMOLVM machine delete --name default -f 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
# The machine must still restart — its image was kept, not pruned.
|
|
$SMOLVM machine start 2>&1 || {
|
|
echo "FAIL: machine could not restart after prune --all (image was pruned)"
|
|
$SMOLVM machine delete --name default -f 2>/dev/null
|
|
return 1
|
|
}
|
|
$SMOLVM machine exec -- true 2>&1 || {
|
|
echo "FAIL: VM not reachable after prune + restart"
|
|
$SMOLVM machine delete --name default -f 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
}
|
|
|
|
test_storage_resize_and_large_pull() {
|
|
# Force a fresh storage disk by deleting the default VM data directory.
|
|
# This ensures we exercise the template → resize → mount path.
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
local data_dir
|
|
data_dir=$(vm_data_dir "default")
|
|
rm -rf "$data_dir" 2>/dev/null || true
|
|
|
|
# Pull python:3.12 (full image: ~150MB compressed, ~1GB extracted).
|
|
# This exceeds the 512MB template size, so it will fail with ENOSPC
|
|
# if the storage disk was not properly resized from 512MB to 20GB.
|
|
local output exit_code=0
|
|
output=$($SMOLVM machine run --net --image python:3.12 -- python3 -c 'import sys; print(f"python {sys.version_info.major}.{sys.version_info.minor}")' 2>&1) || exit_code=$?
|
|
|
|
echo "$output"
|
|
|
|
# Verify the command succeeded and Python ran
|
|
[[ $exit_code -eq 0 ]] || { echo "Exit code: $exit_code"; return 1; }
|
|
[[ "$output" == *"python 3.12"* ]] || { echo "Expected python 3.12 output"; return 1; }
|
|
}
|
|
|
|
test_storage_mounted_as_ext4() {
|
|
# Verify /dev/vda is actually mounted at /storage as ext4 (not on overlay).
|
|
# This catches the bug where mount_storage_disk() silently fails and
|
|
# /storage is just a directory on the overlay rootfs.
|
|
local output
|
|
output=$($SMOLVM machine run --net -- sh -c '
|
|
mount_line=$(mount | grep "/dev/vda")
|
|
if [ -z "$mount_line" ]; then
|
|
echo "FAIL: /dev/vda not mounted"
|
|
exit 1
|
|
fi
|
|
echo "$mount_line"
|
|
# Verify the filesystem is large (>1GB = properly resized from 512MB template)
|
|
avail_kb=$(df /storage | tail -1 | awk "{print \$4}")
|
|
if [ "$avail_kb" -lt 1048576 ]; then
|
|
echo "FAIL: /storage too small (${avail_kb}KB available, expected >1GB)"
|
|
exit 1
|
|
fi
|
|
echo "PASS: storage mounted and resized"
|
|
' 2>&1)
|
|
|
|
echo "$output"
|
|
[[ "$output" == *"PASS: storage mounted and resized"* ]]
|
|
}
|
|
|
|
|
|
run_test "Overlay: root is overlayfs" test_machine_overlay_root_active || true
|
|
run_test "Overlay: rootfs persists across reboot" test_machine_rootfs_persists_across_reboot || true
|
|
run_test "Overlay: custom size via --overlay" test_machine_overlay_size || true
|
|
run_test "Machine images" test_machine_images || true
|
|
run_test "Machine prune --dry-run" test_machine_prune_dry_run || true
|
|
run_test "Images: does not stop running VM" test_images_does_not_stop_running_vm || true
|
|
run_test "Prune: works on running VM without stopping it" test_prune_on_running_vm || true
|
|
run_test "Prune --dry-run: works on running VM" test_prune_dry_run_on_running_vm || true
|
|
run_test "Prune --all: refuses on running VM" test_prune_all_refuses_on_running_vm || true
|
|
run_test "Prune --all: keeps in-use image, machine still restarts" test_prune_all_keeps_in_use_image || true
|
|
run_test "Storage: resize + large image pull (fresh disk)" test_storage_resize_and_large_pull || true
|
|
run_test "Storage: /dev/vda mounted as ext4 with correct size" test_storage_mounted_as_ext4 || true
|
|
|
|
print_summary "Storage Tests"
|