Harden VM delete cleanup and SQLite settings
Multi-VM delete exposed two separate regressions: NAT teardown was still running after stopped VMs had already dropped their tap metadata, and the store was relying on one-off SQLite pragmas instead of configuring every pooled connection. Skip NAT cleanup when the runtime no longer has the network handles needed to identify rules, and move the SQLite profile into the DSN so WAL, busy timeouts, foreign keys, and the other connection-scoped settings apply consistently across the pool. Keep the write mutex in place for concurrent mutations, and update the daemon/store tests to use valid image fixtures now that foreign key enforcement is real. Validated with go test ./... and make build.
This commit is contained in:
parent
08ef706e3f
commit
a14a80fd6b
4 changed files with 228 additions and 37 deletions
|
|
@ -238,6 +238,12 @@ func (natCapability) Cleanup(ctx context.Context, d *Daemon, vm model.VMRecord)
|
|||
if !vm.Spec.NATEnabled {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(vm.Runtime.GuestIP) == "" || strings.TrimSpace(vm.Runtime.TapDevice) == "" {
|
||||
if d.logger != nil {
|
||||
d.logger.Debug("skipping nat cleanup without runtime network handles", append(vmLogAttrs(vm), "guest_ip", vm.Runtime.GuestIP, "tap_device", vm.Runtime.TapDevice)...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return d.ensureNAT(ctx, vm, false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,7 @@ func TestFindVMPrefixResolution(t *testing.T) {
|
|||
testVM("alpine", "image-alpha", "172.16.0.3"),
|
||||
testVM("bravo", "image-alpha", "172.16.0.4"),
|
||||
} {
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
}
|
||||
|
||||
vm, err := d.FindVM(ctx, "alpha")
|
||||
|
|
@ -116,9 +114,7 @@ func TestReconcileStopsStaleRunningVMAndClearsRuntimeHandles(t *testing.T) {
|
|||
vm.Runtime.COWLoop = "/dev/loop11"
|
||||
vm.Runtime.BaseLoop = "/dev/loop10"
|
||||
vm.Runtime.DNSName = ""
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
runner := &scriptedRunner{
|
||||
t: t,
|
||||
|
|
@ -176,9 +172,7 @@ func TestRebuildDNSIncludesOnlyLiveRunningVMs(t *testing.T) {
|
|||
stopped := testVM("stopped", "image-stopped", "172.16.0.23")
|
||||
|
||||
for _, vm := range []model.VMRecord{live, stale, stopped} {
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
}
|
||||
|
||||
server, err := vmdns.New("127.0.0.1:0", nil)
|
||||
|
|
@ -224,9 +218,7 @@ func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) {
|
|||
vm.Runtime.State = model.VMStateRunning
|
||||
vm.Runtime.PID = cmd.Process.Pid
|
||||
vm.Runtime.APISockPath = apiSock
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
d := &Daemon{store: db}
|
||||
tests := []struct {
|
||||
|
|
@ -324,9 +316,7 @@ func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
|||
vm.Runtime.APISockPath = apiSock
|
||||
vm.Runtime.VSockPath = vsockSock
|
||||
vm.Runtime.VSockCID = 10041
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
runner := &scriptedRunner{
|
||||
t: t,
|
||||
|
|
@ -355,9 +345,7 @@ func TestPingVMReturnsFalseForStoppedVM(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
vm := testVM("stopped-ping", "image-stopped", "172.16.0.42")
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
d := &Daemon{store: db}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
|
|
@ -379,9 +367,7 @@ func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
|
|||
vm := testVM("resize", "image-resize", "172.16.0.11")
|
||||
vm.Runtime.WorkDiskPath = workDisk
|
||||
vm.Spec.WorkDiskSizeBytes = 8 * 1024 * 1024 * 1024
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
d := &Daemon{store: db}
|
||||
|
|
@ -464,9 +450,7 @@ func TestSetVMRejectsNonPositiveCPUAndMemory(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
vm := testVM("validate", "image-validate", "172.16.0.13")
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
d := &Daemon{store: db}
|
||||
|
||||
if _, err := d.SetVM(ctx, api.VMSetParams{IDOrName: vm.ID, VCPUCount: ptr(0)}); err == nil || !strings.Contains(err.Error(), "vcpu must be a positive integer") {
|
||||
|
|
@ -590,6 +574,40 @@ func TestCleanupRuntimeRediscoversLiveFirecrackerPID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDeleteStoppedNATVMDoesNotFailWithoutTapDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
vmDir := filepath.Join(t.TempDir(), "stopped-nat-vm")
|
||||
if err := os.MkdirAll(vmDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
vm := testVM("stopped-nat", "image-stopped-nat", "172.16.0.24")
|
||||
vm.Spec.NATEnabled = true
|
||||
vm.Runtime.VMDir = vmDir
|
||||
vm.Runtime.TapDevice = ""
|
||||
vm.State = model.VMStateStopped
|
||||
vm.Runtime.State = model.VMStateStopped
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
d := &Daemon{store: db}
|
||||
deleted, err := d.DeleteVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteVM: %v", err)
|
||||
}
|
||||
if deleted.ID != vm.ID {
|
||||
t.Fatalf("deleted VM = %+v, want %s", deleted, vm.ID)
|
||||
}
|
||||
if _, err := db.GetVMByID(ctx, vm.ID); err == nil {
|
||||
t.Fatal("expected VM record to be deleted")
|
||||
}
|
||||
if _, err := os.Stat(vmDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("vm dir still exists or stat failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopVMFallsBackToForcedCleanupAfterGracefulTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
|
|
@ -615,9 +633,7 @@ func TestStopVMFallsBackToForcedCleanupAfterGracefulTimeout(t *testing.T) {
|
|||
vm.Runtime.State = model.VMStateRunning
|
||||
vm.Runtime.PID = fake.Process.Pid
|
||||
vm.Runtime.APISockPath = apiSock
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
runner := &processKillingRunner{
|
||||
scriptedRunner: &scriptedRunner{
|
||||
|
|
@ -650,9 +666,7 @@ func TestWithVMLockByIDSerializesSameVM(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
vm := testVM("serial", "image-serial", "172.16.0.30")
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
d := &Daemon{store: db}
|
||||
|
||||
firstEntered := make(chan struct{})
|
||||
|
|
@ -710,9 +724,7 @@ func TestWithVMLockByIDAllowsDifferentVMsConcurrently(t *testing.T) {
|
|||
vmA := testVM("alpha-lock", "image-alpha", "172.16.0.31")
|
||||
vmB := testVM("bravo-lock", "image-bravo", "172.16.0.32")
|
||||
for _, vm := range []model.VMRecord{vmA, vmB} {
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
|
||||
}
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
}
|
||||
d := &Daemon{store: db}
|
||||
|
||||
|
|
@ -760,6 +772,19 @@ func openDaemonStore(t *testing.T) *store.Store {
|
|||
return db
|
||||
}
|
||||
|
||||
func upsertDaemonVM(t *testing.T, ctx context.Context, db *store.Store, vm model.VMRecord) {
|
||||
t.Helper()
|
||||
image := testImage(vm.ImageID)
|
||||
image.ID = vm.ImageID
|
||||
image.Name = vm.ImageID
|
||||
if err := db.UpsertImage(ctx, image); err != nil {
|
||||
t.Fatalf("UpsertImage(%s): %v", image.Name, err)
|
||||
}
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testVM(name, imageID, guestIP string) model.VMRecord {
|
||||
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
|
||||
return model.VMRecord{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue