banger/internal/firecracker/client_test.go
Thales Maciel 60294e8c90
Fix VM lifecycle issues behind verify.sh
Make the Firecracker and bangerd processes outlive short-lived CLI request contexts so vm create no longer kills the VMM or daemon as soon as the RPC returns.

Fix fresh-VM SSH by flattening the seeded /root work disk when the copied home tree lands under a nested root/ directory, and write a guest sshd override to keep root pubkey auth explicit while debugging.

Harden teardown and smoke diagnostics: verify.sh now reports early Firecracker exit and delete failures directly, while dm snapshot cleanup tolerates already-gone handles and retries busy mapper removal long enough for Firecracker to release the device.

Validation: go test ./..., make build, bash -n verify.sh, direct SSH against a fresh VM, and a live ./verify.sh run that now completes with [verify] ok.
2026-03-17 14:43:09 -03:00

114 lines
3.5 KiB
Go

package firecracker
import (
"bytes"
"log/slog"
"strings"
"testing"
)
func TestBuildConfig(t *testing.T) {
cfg := buildConfig(MachineConfig{
VMID: "vm-1",
SocketPath: "/tmp/fc.sock",
LogPath: "/tmp/fc.log",
MetricsPath: "/tmp/fc.metrics",
KernelImagePath: "/kernel",
InitrdPath: "/initrd",
KernelArgs: "console=ttyS0",
RootDrivePath: "/dev/mapper/root",
WorkDrivePath: "/var/lib/banger/root.ext4",
TapDevice: "tap-fc-1",
VCPUCount: 4,
MemoryMiB: 2048,
})
if cfg.SocketPath != "/tmp/fc.sock" {
t.Fatalf("socket path = %q", cfg.SocketPath)
}
if cfg.LogPath != "/tmp/fc.log" || cfg.MetricsPath != "/tmp/fc.metrics" {
t.Fatalf("unexpected log or metrics path: %+v", cfg)
}
if cfg.KernelImagePath != "/kernel" || cfg.InitrdPath != "/initrd" {
t.Fatalf("unexpected kernel paths: %+v", cfg)
}
if len(cfg.Drives) != 2 {
t.Fatalf("drive count = %d, want 2", len(cfg.Drives))
}
if cfg.Drives[0].DriveID == nil || *cfg.Drives[0].DriveID != "work" {
t.Fatalf("work drive id = %v", cfg.Drives[0].DriveID)
}
if cfg.Drives[1].DriveID == nil || *cfg.Drives[1].DriveID != "rootfs" {
t.Fatalf("root drive id = %v", cfg.Drives[1].DriveID)
}
if len(cfg.NetworkInterfaces) != 1 {
t.Fatalf("interface count = %d, want 1", len(cfg.NetworkInterfaces))
}
if got := cfg.NetworkInterfaces[0].StaticConfiguration.HostDevName; got != "tap-fc-1" {
t.Fatalf("host dev name = %q", got)
}
if cfg.MachineCfg.VcpuCount == nil || *cfg.MachineCfg.VcpuCount != 4 {
t.Fatalf("vcpu = %v", cfg.MachineCfg.VcpuCount)
}
if cfg.MachineCfg.MemSizeMib == nil || *cfg.MachineCfg.MemSizeMib != 2048 {
t.Fatalf("memory = %v", cfg.MachineCfg.MemSizeMib)
}
if cfg.MachineCfg.Smt == nil || *cfg.MachineCfg.Smt {
t.Fatalf("smt = %v, want false", cfg.MachineCfg.Smt)
}
}
func TestBuildProcessRunnerUsesSudoWrapper(t *testing.T) {
cmd := buildProcessRunner(MachineConfig{
BinaryPath: "/repo/firecracker",
SocketPath: "/tmp/fc.sock",
VMID: "vm-1",
}, nil)
if cmd.Path != "/usr/bin/sudo" && cmd.Path != "sudo" {
t.Fatalf("command path = %q", cmd.Path)
}
if len(cmd.Args) != 5 {
t.Fatalf("args = %v", cmd.Args)
}
if cmd.Args[1] != "-n" || cmd.Args[2] != "sh" || cmd.Args[3] != "-c" {
t.Fatalf("args = %v", cmd.Args)
}
if want := "umask 000 && exec '/repo/firecracker' --api-sock '/tmp/fc.sock' --id 'vm-1'"; cmd.Args[4] != want {
t.Fatalf("script = %q, want %q", cmd.Args[4], want)
}
if cmd.Cancel != nil {
t.Fatal("process runner should not be tied to a request context")
}
}
func TestSDKLoggerBridgeEmitsStructuredDebugLogs(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
entry := newLogger(logger)
entry.WithField("vm_id", "vm-1").Info("sdk ready")
output := buf.String()
if !strings.Contains(output, `"component":"firecracker_sdk"`) {
t.Fatalf("output = %q, want firecracker_sdk component", output)
}
if !strings.Contains(output, `"vm_id":"vm-1"`) {
t.Fatalf("output = %q, want vm_id field", output)
}
if !strings.Contains(output, `"msg":"sdk ready"`) {
t.Fatalf("output = %q, want sdk message", output)
}
}
func TestSDKLoggerBridgeSuppressesDebugAtInfoLevel(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
entry := newLogger(logger)
entry.Info("sdk hidden at info")
if buf.Len() != 0 {
t.Fatalf("expected info-level logger to suppress sdk debug chatter, got %q", buf.String())
}
}