Move the supported systemd path to two services: an owner-user bangerd for orchestration and a narrow root helper for bridge/tap, NAT/resolver, dm/loop, and Firecracker ownership. This removes repeated sudo from daily vm and image flows without leaving the general daemon running as root. Add install metadata, system install/status/restart/uninstall commands, and a system-owned runtime layout. Keep user SSH/config material in the owner home, lock file_sync to the owner home, and move daemon known_hosts handling out of the old root-owned control path. Route privileged lifecycle steps through typed privilegedOps calls, harden the two systemd units, and rewrite smoke plus docs around the supported service model. Verified with make build, make test, make lint, and make smoke on the supported systemd host path.
468 lines
22 KiB
Bash
468 lines
22 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# scripts/smoke.sh — end-to-end smoke suite for banger's supported
|
|
# two-service systemd model.
|
|
#
|
|
# Installs instrumented binaries as temporary bangerd.service +
|
|
# bangerd-root.service, drives real Firecracker/KVM scenarios, collects
|
|
# covdata from both services plus the CLI, then purges the smoke-owned
|
|
# install on exit.
|
|
#
|
|
# Because the supported path is global host state, smoke refuses to
|
|
# overwrite a pre-existing non-smoke install. If a prior smoke crashed,
|
|
# rerun `make smoke-clean` or `make smoke`; the smoke marker lets the
|
|
# harness purge only its own stale install safely.
|
|
#
|
|
# Scratch files live under $BANGER_SMOKE_XDG_DIR (historic name kept for
|
|
# make-compat). Service state uses the real supported system paths and is
|
|
# purged by the smoke cleanup path.
|
|
|
|
set -euo pipefail
|
|
|
|
log() { printf '[smoke] %s\n' "$*" >&2; }
|
|
die() { printf '[smoke] FAIL: %s\n' "$*" >&2; exit 1; }
|
|
|
|
wait_for_ssh() {
|
|
local vm="$1"
|
|
local deadline=$(( $(date +%s) + 60 ))
|
|
while (( $(date +%s) < deadline )); do
|
|
if "$BANGER" vm ssh "$vm" -- true >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
: "${BANGER_SMOKE_BIN_DIR:?must point at the instrumented binary dir, set by make smoke}"
|
|
: "${BANGER_SMOKE_COVER_DIR:?must point at the coverage dir, set by make smoke}"
|
|
: "${BANGER_SMOKE_XDG_DIR:?must point at the smoke scratch root, set by make smoke}"
|
|
|
|
BANGER="$BANGER_SMOKE_BIN_DIR/banger"
|
|
BANGERD="$BANGER_SMOKE_BIN_DIR/bangerd"
|
|
VSOCK_AGENT="$BANGER_SMOKE_BIN_DIR/banger-vsock-agent"
|
|
|
|
for bin in "$BANGER" "$BANGERD" "$VSOCK_AGENT"; do
|
|
[[ -x "$bin" ]] || die "binary missing or not executable: $bin"
|
|
done
|
|
|
|
scratch_root="$BANGER_SMOKE_XDG_DIR"
|
|
runtime_dir=
|
|
smoke_owner="$(id -un)"
|
|
smoke_marker='/etc/banger/.smoke-owned'
|
|
service_cover_dir='/var/lib/banger'
|
|
owner_service='bangerd.service'
|
|
root_service='bangerd-root.service'
|
|
|
|
mkdir -p "$BANGER_SMOKE_COVER_DIR"
|
|
rm -rf "$scratch_root"
|
|
mkdir -p "$scratch_root"
|
|
runtime_dir="$(mktemp -d "$scratch_root/runtime-XXXXXX")"
|
|
|
|
# The CLI binary itself is instrumented, so keep its covdata local.
|
|
export GOCOVERDIR="$BANGER_SMOKE_COVER_DIR"
|
|
|
|
cleanup_export_vm() {
|
|
"$BANGER" vm delete smoke-export >/dev/null 2>&1 || true
|
|
}
|
|
|
|
cleanup_prune() {
|
|
"$BANGER" vm delete smoke-prune-running >/dev/null 2>&1 || true
|
|
"$BANGER" vm delete smoke-prune-stopped >/dev/null 2>&1 || true
|
|
}
|
|
|
|
collect_service_coverage() {
|
|
local uid gid
|
|
uid="$(id -u)"
|
|
gid="$(id -g)"
|
|
sudo bash -lc '
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
dst="$1"
|
|
uid="$2"
|
|
gid="$3"
|
|
src="$4"
|
|
for file in "$src"/covmeta.* "$src"/covcounters.*; do
|
|
base="${file##*/}"
|
|
cp "$file" "$dst/$base"
|
|
chown "$uid:$gid" "$dst/$base"
|
|
chmod 0644 "$dst/$base"
|
|
done
|
|
' bash "$BANGER_SMOKE_COVER_DIR" "$uid" "$gid" "$service_cover_dir"
|
|
}
|
|
|
|
stop_services_for_coverage() {
|
|
sudo systemctl stop "$owner_service" "$root_service" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
sudo_banger() {
|
|
sudo env GOCOVERDIR="$BANGER_SMOKE_COVER_DIR" "$@"
|
|
}
|
|
|
|
cleanup() {
|
|
set +e
|
|
for vm in \
|
|
smoke-lifecycle smoke-set smoke-restart smoke-kill smoke-ports smoke-fc \
|
|
smoke-basecommit smoke-nat smoke-nocnat; do
|
|
"$BANGER" vm delete "$vm" >/dev/null 2>&1 || true
|
|
done
|
|
cleanup_export_vm
|
|
cleanup_prune
|
|
stop_services_for_coverage
|
|
collect_service_coverage
|
|
sudo_banger "$BANGER" system uninstall --purge >/dev/null 2>&1 || true
|
|
rm -rf "$scratch_root"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if sudo test -f /etc/banger/install.toml; then
|
|
if sudo test -f "$smoke_marker"; then
|
|
log 'found stale smoke-owned install; purging it first'
|
|
sudo_banger "$BANGER" system uninstall --purge >/dev/null 2>&1 || true
|
|
else
|
|
die 'banger is already installed on this host; supported-path smoke refuses to overwrite a non-smoke install'
|
|
fi
|
|
fi
|
|
|
|
log 'installing smoke-owned services'
|
|
sudo env \
|
|
GOCOVERDIR="$BANGER_SMOKE_COVER_DIR" \
|
|
BANGER_SYSTEM_GOCOVERDIR="$service_cover_dir" \
|
|
BANGER_ROOT_HELPER_GOCOVERDIR="$service_cover_dir" \
|
|
"$BANGER" system install --owner "$smoke_owner" >/dev/null \
|
|
|| die 'system install failed'
|
|
sudo touch "$smoke_marker"
|
|
|
|
status_out="$("$BANGER" system status)" || die 'system status failed after install'
|
|
grep -q 'active: active' <<<"$status_out" || die "owner daemon not active after install: $status_out"
|
|
grep -q 'helper_active: active' <<<"$status_out" || die "root helper not active after install: $status_out"
|
|
|
|
log 'doctor: checking host readiness'
|
|
if ! "$BANGER" doctor; then
|
|
die 'doctor reported failures; fix the host before running smoke'
|
|
fi
|
|
|
|
log 'system restart: services should come back cleanly'
|
|
sudo_banger "$BANGER" system restart >/dev/null || die 'system restart failed'
|
|
status_out="$("$BANGER" system status)" || die 'system status failed after restart'
|
|
grep -q 'active: active' <<<"$status_out" || die "owner daemon not active after restart: $status_out"
|
|
grep -q 'helper_active: active' <<<"$status_out" || die "root helper not active after restart: $status_out"
|
|
|
|
# --- bare vm run ------------------------------------------------------
|
|
log "bare vm run: create + start + ssh + exec 'echo smoke-bare-ok' + --rm"
|
|
bare_out="$("$BANGER" vm run --rm -- echo smoke-bare-ok)" || die "bare vm run exit $?"
|
|
grep -q 'smoke-bare-ok' <<<"$bare_out" || die "bare vm run stdout missing marker: $bare_out"
|
|
|
|
# --- workspace vm run -------------------------------------------------
|
|
log 'workspace vm run: preparing a throwaway git repo'
|
|
repodir="$runtime_dir/fake-repo"
|
|
mkdir -p "$repodir"
|
|
(
|
|
cd "$repodir"
|
|
git init -q -b main
|
|
git config commit.gpgsign false
|
|
git config user.name smoke
|
|
git config user.email smoke@smoke
|
|
echo 'smoke-workspace-marker' > smoke-file.txt
|
|
git add .
|
|
git commit -q -m init
|
|
)
|
|
|
|
log "workspace vm run: create + start + workspace prepare + cat guest file + --rm"
|
|
ws_out="$("$BANGER" vm run --rm "$repodir" -- cat /root/repo/smoke-file.txt)" || die "workspace vm run exit $?"
|
|
grep -q 'smoke-workspace-marker' <<<"$ws_out" || die "workspace vm run didn't ship smoke-file.txt: $ws_out"
|
|
|
|
# --- command exit-code propagation ------------------------------------
|
|
log 'exit-code propagation: guest `sh -c "exit 42"` must produce rc=42'
|
|
set +e
|
|
"$BANGER" vm run --rm -- sh -c 'exit 42'
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 42 ]] || die "exit-code propagation: got rc=$rc, want 42"
|
|
|
|
# --- workspace dry-run (no VM) ----------------------------------------
|
|
log 'workspace dry-run: list tracked files without creating a VM'
|
|
dry_out="$("$BANGER" vm run --dry-run "$repodir")" || die "dry-run exit $?"
|
|
grep -q 'smoke-file.txt' <<<"$dry_out" || die "dry-run didn't list smoke-file.txt: $dry_out"
|
|
grep -q 'mode: tracked only' <<<"$dry_out" || die "dry-run mode line missing or wrong: $dry_out"
|
|
|
|
# --- workspace --include-untracked -----------------------------------
|
|
log 'workspace --include-untracked: opt-in ships files outside the git index'
|
|
echo 'untracked-marker' > "$repodir/smoke-untracked.txt"
|
|
inc_out="$("$BANGER" vm run --rm --include-untracked "$repodir" -- cat /root/repo/smoke-untracked.txt)" || die "include-untracked vm run exit $?"
|
|
grep -q 'untracked-marker' <<<"$inc_out" || die "--include-untracked didn't ship the untracked file: $inc_out"
|
|
rm -f "$repodir/smoke-untracked.txt"
|
|
|
|
# --- workspace export round-trip --------------------------------------
|
|
log 'workspace export: create + prepare + guest edit + export + assert marker'
|
|
"$BANGER" vm create --name smoke-export --image debian-bookworm >/dev/null \
|
|
|| die "export: vm create exit $?"
|
|
"$BANGER" vm workspace prepare smoke-export "$repodir" >/dev/null \
|
|
|| die "export: workspace prepare exit $?"
|
|
"$BANGER" vm ssh smoke-export -- sh -c 'echo guest-edit > /root/repo/new-guest-file.txt' \
|
|
|| die "export: guest-side file write exit $?"
|
|
export_patch="$runtime_dir/smoke-export.diff"
|
|
"$BANGER" vm workspace export smoke-export --output "$export_patch" \
|
|
|| die "export: workspace export exit $?"
|
|
[[ -s "$export_patch" ]] || die "export: patch file empty at $export_patch"
|
|
grep -q 'new-guest-file.txt' "$export_patch" \
|
|
|| die "export: patch missing new-guest-file.txt marker (head: $(head -c 400 "$export_patch"))"
|
|
cleanup_export_vm
|
|
|
|
# --- concurrent vm runs -----------------------------------------------
|
|
log 'concurrent vm runs: two --rm invocations must both succeed'
|
|
tmpA="$runtime_dir/concurrent-a.out"
|
|
tmpB="$runtime_dir/concurrent-b.out"
|
|
"$BANGER" vm run --rm -- echo smoke-concurrent-a > "$tmpA" 2>&1 &
|
|
pidA=$!
|
|
"$BANGER" vm run --rm -- echo smoke-concurrent-b > "$tmpB" 2>&1 &
|
|
pidB=$!
|
|
wait "$pidA" || die "concurrent VM A exited non-zero: $(cat "$tmpA")"
|
|
wait "$pidB" || die "concurrent VM B exited non-zero: $(cat "$tmpB")"
|
|
grep -q 'smoke-concurrent-a' "$tmpA" || die "concurrent VM A missing marker: $(cat "$tmpA")"
|
|
grep -q 'smoke-concurrent-b' "$tmpB" || die "concurrent VM B missing marker: $(cat "$tmpB")"
|
|
|
|
# --- vm lifecycle (create → stop → start → delete) --------------------
|
|
log 'vm lifecycle: explicit create / stop / start / ssh / delete'
|
|
lifecycle_name=smoke-lifecycle
|
|
"$BANGER" vm create --name "$lifecycle_name" >/dev/null || die "vm create $lifecycle_name failed"
|
|
show_out="$("$BANGER" vm show "$lifecycle_name")" || die "vm show after create failed"
|
|
grep -q '"state": "running"' <<<"$show_out" || die "post-create state not running: $show_out"
|
|
|
|
wait_for_ssh "$lifecycle_name" || die 'vm lifecycle: ssh did not come up after create'
|
|
ssh_out="$("$BANGER" vm ssh "$lifecycle_name" -- echo hello-1)" || die "vm ssh #1 failed"
|
|
grep -q 'hello-1' <<<"$ssh_out" || die "vm ssh #1 missing marker: $ssh_out"
|
|
|
|
"$BANGER" vm stop "$lifecycle_name" >/dev/null || die "vm stop failed"
|
|
show_out="$("$BANGER" vm show "$lifecycle_name")" || die "vm show after stop failed"
|
|
grep -q '"state": "stopped"' <<<"$show_out" || die "post-stop state not stopped: $show_out"
|
|
|
|
"$BANGER" vm start "$lifecycle_name" >/dev/null || die "vm start (from stopped) failed"
|
|
show_out="$("$BANGER" vm show "$lifecycle_name")" || die "vm show after start failed"
|
|
grep -q '"state": "running"' <<<"$show_out" || die "post-start state not running: $show_out"
|
|
|
|
wait_for_ssh "$lifecycle_name" || die 'vm lifecycle: ssh did not come up after restart'
|
|
ssh_out="$("$BANGER" vm ssh "$lifecycle_name" -- echo hello-2)" || die "vm ssh #2 (post-restart) failed"
|
|
grep -q 'hello-2' <<<"$ssh_out" || die "vm ssh #2 missing marker: $ssh_out"
|
|
|
|
"$BANGER" vm delete "$lifecycle_name" >/dev/null || die "vm delete failed"
|
|
set +e
|
|
"$BANGER" vm show "$lifecycle_name" >/dev/null 2>&1
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -ne 0 ]] || die "vm show still finds $lifecycle_name after delete"
|
|
|
|
# --- vm set reconfiguration (vcpu change + restart) -------------------
|
|
log 'vm set: create --vcpu 2 → stop → set --vcpu 4 → restart → nproc=4'
|
|
"$BANGER" vm create --name smoke-set --vcpu 2 >/dev/null || die 'vm set: create failed'
|
|
wait_for_ssh smoke-set || die 'vm set: initial ssh did not come up'
|
|
|
|
set +e
|
|
nproc_before="$("$BANGER" vm ssh smoke-set -- nproc 2>/dev/null)"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 0 ]] || die "vm set: initial nproc ssh exit $rc"
|
|
[[ "$(printf '%s' "$nproc_before" | tr -d '[:space:]')" == "2" ]] \
|
|
|| die "vm set: initial nproc got '$nproc_before', want 2"
|
|
|
|
"$BANGER" vm stop smoke-set >/dev/null || die 'vm set: stop failed'
|
|
"$BANGER" vm set smoke-set --vcpu 4 >/dev/null || die 'vm set: reconfigure failed'
|
|
"$BANGER" vm start smoke-set >/dev/null || die 'vm set: restart failed'
|
|
wait_for_ssh smoke-set || die 'vm set: post-reconfig ssh did not come up'
|
|
|
|
set +e
|
|
nproc_after="$("$BANGER" vm ssh smoke-set -- nproc 2>/dev/null)"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 0 ]] || die "vm set: post-reconfig nproc ssh exit $rc"
|
|
[[ "$(printf '%s' "$nproc_after" | tr -d '[:space:]')" == "4" ]] \
|
|
|| die "vm set: post-reconfig nproc got '$nproc_after', want 4 (spec change didn't land)"
|
|
|
|
"$BANGER" vm delete smoke-set >/dev/null || die 'vm set: delete failed'
|
|
|
|
# --- vm restart (dedicated verb) --------------------------------------
|
|
log 'vm restart: boot_id must change across the verb'
|
|
"$BANGER" vm create --name smoke-restart >/dev/null || die 'vm restart: create failed'
|
|
wait_for_ssh smoke-restart || die 'vm restart: initial ssh never came up'
|
|
boot_before="$("$BANGER" vm ssh smoke-restart -- cat /proc/sys/kernel/random/boot_id | tr -d '[:space:]')"
|
|
[[ -n "$boot_before" ]] || die 'vm restart: could not read initial boot_id'
|
|
|
|
"$BANGER" vm restart smoke-restart >/dev/null || die 'vm restart: verb failed'
|
|
wait_for_ssh smoke-restart || die 'vm restart: ssh did not come up after restart'
|
|
boot_after="$("$BANGER" vm ssh smoke-restart -- cat /proc/sys/kernel/random/boot_id | tr -d '[:space:]')"
|
|
[[ -n "$boot_after" ]] || die 'vm restart: could not read post-restart boot_id'
|
|
[[ "$boot_before" != "$boot_after" ]] \
|
|
|| die "vm restart: boot_id unchanged ($boot_before); verb didn't actually reboot the guest"
|
|
|
|
"$BANGER" vm delete smoke-restart >/dev/null || die 'vm restart: delete failed'
|
|
|
|
# --- vm kill (--signal KILL, forceful path) ---------------------------
|
|
log 'vm kill --signal KILL: forceful terminate, state=stopped, no leaked dm device'
|
|
"$BANGER" vm create --name smoke-kill >/dev/null || die 'vm kill: create failed'
|
|
dm_name="$("$BANGER" vm show smoke-kill 2>/dev/null | awk -F'"' '/"dm_dev"|fc-rootfs-/ {for(i=1;i<=NF;i++) if($i~/^fc-rootfs-/) print $i}' | head -1 || true)"
|
|
"$BANGER" vm kill --signal KILL smoke-kill >/dev/null || die 'vm kill: verb failed'
|
|
show_out="$("$BANGER" vm show smoke-kill)" || die 'vm kill: show after kill failed'
|
|
grep -q '"state": "stopped"' <<<"$show_out" || die "vm kill: post-kill state not stopped: $show_out"
|
|
if [[ -n "$dm_name" ]]; then
|
|
if sudo -n dmsetup ls 2>/dev/null | awk '{print $1}' | grep -qx "$dm_name"; then
|
|
die "vm kill: dm device $dm_name still mapped (cleanup didn't run)"
|
|
fi
|
|
fi
|
|
"$BANGER" vm delete smoke-kill >/dev/null || die 'vm kill: delete failed'
|
|
|
|
# --- vm prune (-f) ----------------------------------------------------
|
|
log 'vm prune -f: removes stopped VMs, preserves running ones'
|
|
"$BANGER" vm create --name smoke-prune-running >/dev/null || die 'vm prune: create running failed'
|
|
"$BANGER" vm create --name smoke-prune-stopped >/dev/null || die 'vm prune: create stopped failed'
|
|
"$BANGER" vm stop smoke-prune-stopped >/dev/null || die 'vm prune: stop the stopped one failed'
|
|
|
|
"$BANGER" vm prune -f >/dev/null || die 'vm prune: verb failed'
|
|
|
|
"$BANGER" vm show smoke-prune-running >/dev/null 2>&1 || die 'vm prune: running VM was deleted (regression!)'
|
|
if "$BANGER" vm show smoke-prune-stopped >/dev/null 2>&1; then
|
|
die 'vm prune: stopped VM survived prune'
|
|
fi
|
|
|
|
"$BANGER" vm delete smoke-prune-running >/dev/null || die 'vm prune: cleanup delete failed'
|
|
|
|
# --- vm ports ---------------------------------------------------------
|
|
log 'vm ports: sshd :22 visible from host, endpoint uses the VM DNS name'
|
|
"$BANGER" vm create --name smoke-ports >/dev/null || die 'vm ports: create failed'
|
|
wait_for_ssh smoke-ports || die 'vm ports: ssh did not come up'
|
|
|
|
ports_out="$("$BANGER" vm ports smoke-ports 2>&1)" \
|
|
|| die "vm ports: verb failed: $ports_out"
|
|
grep -q 'smoke-ports.vm:22' <<<"$ports_out" \
|
|
|| die "vm ports: expected 'smoke-ports.vm:22' in output; got: $ports_out"
|
|
grep -q 'sshd' <<<"$ports_out" \
|
|
|| die "vm ports: expected process 'sshd' in output; got: $ports_out"
|
|
|
|
"$BANGER" vm delete smoke-ports >/dev/null || die 'vm ports: delete failed'
|
|
|
|
# --- workspace prepare --mode full_copy -------------------------------
|
|
log 'workspace prepare --mode full_copy: alternate transfer path still delivers'
|
|
"$BANGER" vm create --name smoke-fc >/dev/null || die 'workspace fc: create failed'
|
|
"$BANGER" vm workspace prepare smoke-fc "$repodir" --mode full_copy >/dev/null \
|
|
|| die 'workspace fc: prepare --mode full_copy failed'
|
|
fc_out="$("$BANGER" vm ssh smoke-fc -- cat /root/repo/smoke-file.txt)" \
|
|
|| die 'workspace fc: guest read failed'
|
|
grep -q 'smoke-workspace-marker' <<<"$fc_out" \
|
|
|| die "workspace fc: marker missing in full_copy workspace: $fc_out"
|
|
|
|
"$BANGER" vm delete smoke-fc >/dev/null || die 'workspace fc: delete failed'
|
|
|
|
# --- workspace export --base-commit (committed guest delta) -----------
|
|
log 'workspace export --base-commit: guest-side commits captured in patch'
|
|
"$BANGER" vm create --name smoke-basecommit >/dev/null || die 'export base: create failed'
|
|
"$BANGER" vm workspace prepare smoke-basecommit "$repodir" >/dev/null \
|
|
|| die 'export base: prepare failed'
|
|
|
|
base_sha="$("$BANGER" vm ssh smoke-basecommit -- sh -c 'cd /root/repo && git rev-parse HEAD' | tr -d '[:space:]')"
|
|
[[ "${#base_sha}" -eq 40 ]] || die "export base: bad base sha: $base_sha"
|
|
|
|
"$BANGER" vm ssh smoke-basecommit -- sh -c "cd /root/repo && git -c user.email=smoke@smoke -c user.name=smoke checkout -b smoke-branch >/dev/null 2>&1 && echo committed-marker > smoke-committed.txt && git add smoke-committed.txt && git -c user.email=smoke@smoke -c user.name=smoke commit -q -m 'guest side'" \
|
|
|| die 'export base: guest-side commit failed'
|
|
|
|
plain_patch="$runtime_dir/smoke-plain.diff"
|
|
"$BANGER" vm workspace export smoke-basecommit --output "$plain_patch" \
|
|
|| die 'export base: plain export failed'
|
|
if [[ -f "$plain_patch" ]] && grep -q 'smoke-committed.txt' "$plain_patch"; then
|
|
die 'export base: plain export unexpectedly captured the guest-side commit'
|
|
fi
|
|
|
|
base_patch="$runtime_dir/smoke-base.diff"
|
|
"$BANGER" vm workspace export smoke-basecommit --base-commit "$base_sha" --output "$base_patch" \
|
|
|| die 'export base: --base-commit export failed'
|
|
[[ -s "$base_patch" ]] || die 'export base: patch file empty'
|
|
grep -q 'smoke-committed.txt' "$base_patch" \
|
|
|| die "export base: --base-commit patch missing committed marker (head: $(head -c 400 "$base_patch"))"
|
|
|
|
"$BANGER" vm delete smoke-basecommit >/dev/null || die 'export base: delete failed'
|
|
|
|
# --- ssh-config install / uninstall (HOME-isolated) -------------------
|
|
log 'ssh-config --install / --uninstall: idempotent, survives round-trip'
|
|
fake_home="$scratch_root/fake-home"
|
|
mkdir -p "$fake_home/.ssh"
|
|
printf 'Host myserver\n HostName example.invalid\n' > "$fake_home/.ssh/config"
|
|
|
|
(
|
|
export HOME="$fake_home"
|
|
"$BANGER" ssh-config --install >/dev/null || die 'ssh-config: install failed'
|
|
grep -q '^Include ' "$fake_home/.ssh/config" \
|
|
|| die "ssh-config: install didn't add Include line to ~/.ssh/config"
|
|
grep -q '^Host myserver' "$fake_home/.ssh/config" \
|
|
|| die 'ssh-config: install clobbered pre-existing content (!!)'
|
|
|
|
"$BANGER" ssh-config --install >/dev/null || die 'ssh-config: second install failed'
|
|
include_count="$(grep -c '^Include .*banger' "$fake_home/.ssh/config")"
|
|
[[ "$include_count" == "1" ]] \
|
|
|| die "ssh-config: install not idempotent (Include appeared $include_count times)"
|
|
|
|
"$BANGER" ssh-config --uninstall >/dev/null || die 'ssh-config: uninstall failed'
|
|
if grep -q '^Include .*banger' "$fake_home/.ssh/config"; then
|
|
die 'ssh-config: uninstall left the Include line behind'
|
|
fi
|
|
grep -q '^Host myserver' "$fake_home/.ssh/config" \
|
|
|| die 'ssh-config: uninstall nuked user content (!!)'
|
|
)
|
|
|
|
# --- NAT rule installation (per-VM MASQUERADE) ------------------------
|
|
log 'NAT: --nat installs a per-VM MASQUERADE rule; no --nat means no rule'
|
|
if ! sudo -n iptables -t nat -S POSTROUTING >/dev/null 2>&1; then
|
|
log 'NAT: skipping — passwordless sudo iptables unavailable'
|
|
else
|
|
"$BANGER" vm create --name smoke-nat --nat >/dev/null || die 'NAT: create --nat failed'
|
|
"$BANGER" vm create --name smoke-nocnat >/dev/null || die 'NAT: control create failed'
|
|
|
|
nat_ip="$("$BANGER" vm show smoke-nat 2>/dev/null | awk -F'"' '/"guest_ip"/ {print $4}')"
|
|
ctl_ip="$("$BANGER" vm show smoke-nocnat 2>/dev/null | awk -F'"' '/"guest_ip"/ {print $4}')"
|
|
[[ -n "$nat_ip" && -n "$ctl_ip" ]] || die "NAT: couldn't read guest IPs (nat='$nat_ip', ctl='$ctl_ip')"
|
|
|
|
postrouting="$(sudo -n iptables -t nat -S POSTROUTING 2>/dev/null || true)"
|
|
grep -q -- "-s $nat_ip/32.*-j MASQUERADE" <<<"$postrouting" \
|
|
|| die "NAT: --nat VM has no POSTROUTING MASQUERADE rule for $nat_ip; got:"$'\n'"$postrouting"
|
|
if grep -q -- "-s $ctl_ip/32.*-j MASQUERADE" <<<"$postrouting"; then
|
|
die "NAT: control VM unexpectedly has a MASQUERADE rule for $ctl_ip"
|
|
fi
|
|
|
|
"$BANGER" vm stop smoke-nat >/dev/null || die 'NAT: stop --nat VM failed'
|
|
"$BANGER" vm start smoke-nat >/dev/null || die 'NAT: restart --nat VM failed'
|
|
postrouting="$(sudo -n iptables -t nat -S POSTROUTING 2>/dev/null || true)"
|
|
rule_count="$(grep -c -- "-s $nat_ip/32.*-j MASQUERADE" <<<"$postrouting" || true)"
|
|
[[ "$rule_count" == "1" ]] \
|
|
|| die "NAT: MASQUERADE rule count for $nat_ip = $rule_count after restart, want 1"
|
|
|
|
"$BANGER" vm delete smoke-nat >/dev/null || die 'NAT: delete --nat VM failed'
|
|
"$BANGER" vm delete smoke-nocnat >/dev/null || die 'NAT: delete control VM failed'
|
|
postrouting="$(sudo -n iptables -t nat -S POSTROUTING 2>/dev/null || true)"
|
|
if grep -q -- "-s $nat_ip/32.*-j MASQUERADE" <<<"$postrouting"; then
|
|
die "NAT: delete left a MASQUERADE rule behind for $nat_ip"
|
|
fi
|
|
fi
|
|
|
|
# --- invalid spec rejection + no artifact leak ------------------------
|
|
log 'invalid spec rejection: --vcpu 0 must fail and leave no VM behind'
|
|
pre_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
|
set +e
|
|
"$BANGER" vm run --rm --vcpu 0 -- echo unused >/dev/null 2>&1
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -ne 0 ]] || die 'invalid spec: vm run succeeded despite --vcpu 0'
|
|
post_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
|
[[ "$pre_vms" == "$post_vms" ]] || die "invalid spec leaked a VM row: pre=$pre_vms, post=$post_vms"
|
|
|
|
# --- invalid name rejection ------------------------------------------
|
|
log 'invalid name rejection: uppercase / space / dot / leading-hyphen must all fail'
|
|
pre_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
|
for bad in 'MyBox' 'my box' 'box.vm' '-box'; do
|
|
set +e
|
|
"$BANGER" vm create --name "$bad" --no-start >/dev/null 2>&1
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -ne 0 ]] || die "invalid name: vm create accepted '$bad'"
|
|
done
|
|
post_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
|
[[ "$pre_vms" == "$post_vms" ]] \
|
|
|| die "invalid name leaked VM row(s): pre=$pre_vms, post=$post_vms"
|
|
|
|
log 'all scenarios passed'
|