diff --git a/cmd/vm/lifecycle.go b/cmd/vm/lifecycle.go index 37ba541..5b0316c 100644 --- a/cmd/vm/lifecycle.go +++ b/cmd/vm/lifecycle.go @@ -238,6 +238,9 @@ func launch(cmd *cobra.Command, dir string, r *record) error { if err := requireCNIVNCPassword(r.Netns != "", r.VNCDisp, r.VNCPass); err != nil { return err } + if err := recoverNet(cmd, r); err != nil { + return fmt.Errorf("recover network: %w", err) + } if isHostAMD() { // macOS reads MSRs an AMD host lacks; without kvm.ignore_msrs KVM injects #GP (best-effort, host-global) if err := os.WriteFile("/sys/module/kvm/parameters/ignore_msrs", []byte("1\n"), 0o600); err != nil { diff --git a/cmd/vm/net_linux.go b/cmd/vm/net_linux.go index c6b976d..9bcfd68 100644 --- a/cmd/vm/net_linux.go +++ b/cmd/vm/net_linux.go @@ -59,6 +59,11 @@ func newProvider(cmd *cobra.Command, r *record) (network.Network, error) { return nil, fmt.Errorf("unknown --net mode %q (want user|tap|cni|bridge)", r.NetMode) } +// netVMConfig pins CPU=1 so NetNumQueues yields a single-queue TAP matching QEMU's single-queue -netdev tap,ifname= +func netVMConfig(r *record) *types.VMConfig { + return &types.VMConfig{CPU: 1, Name: r.Name} +} + // provisionNet auto-creates a TAP via cocoon — the SAME forwarding plane as cocoon's CH/FC VMs. func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err error) { provider, err := newProvider(cmd, r) @@ -66,8 +71,7 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er return "", "", "", err } ctx := cliutil.CommandContext(cmd) - // CPU=1 => NetNumQueues yields a single-queue TAP matching QEMU's single-queue -netdev tap,ifname= - vmCfg := &types.VMConfig{CPU: 1, Name: r.Name} + vmCfg := netVMConfig(r) nsPath, err := provider.Prepare(ctx, r.VMID, vmCfg) if err != nil { return "", "", "", fmt.Errorf("prepare network: %w", err) @@ -92,6 +96,34 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er return cfgs[0].TAP, nsPath, mac, nil } +func recoverNet(cmd *cobra.Command, r *record) error { + if !r.TapOwned { + return nil + } + provider, err := newProvider(cmd, r) + if err != nil { + return err + } + ctx := cliutil.CommandContext(cmd) + expected := []*types.NetworkConfig{{TAP: r.Tap, MAC: r.MAC}} + verifyErr := provider.Verify(ctx, r.VMID, expected) + if verifyErr == nil { + return nil + } + log.WithFunc("cmd.vm.recoverNet").Warnf(ctx, "rebuild network for %s: %v", r.VMID, verifyErr) + vmCfg := netVMConfig(r) + nsPath, err := provider.Prepare(ctx, r.VMID, vmCfg) + if err != nil { + return err + } + cfgs, err := provider.Add(ctx, r.VMID, vmCfg, network.AddRecover(expected)...) + if err != nil { + return err + } + r.Tap, r.Netns = cfgs[0].TAP, nsPath + return nil +} + func teardownNet(ctx context.Context, cmd *cobra.Command, r *record) error { if !r.TapOwned { return nil diff --git a/cmd/vm/net_linux_test.go b/cmd/vm/net_linux_test.go index a04d8c6..4a0afe9 100644 --- a/cmd/vm/net_linux_test.go +++ b/cmd/vm/net_linux_test.go @@ -71,3 +71,27 @@ func TestRMRetainsStateWhenNetworkTeardownFails(t *testing.T) { t.Errorf("VM state was removed after teardown failure: %v", err) } } + +func TestStartChecksOwnedNetworkBeforeQEMU(t *testing.T) { + stateDir := t.TempDir() + cmd := newLifecycleTestCommand(t, stateDir) + dir, err := home.VMDir(cmd, "macos-demo") + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + r := &record{ + Name: "macos-demo", VMID: "TESTVMID", Disk: filepath.Join(dir, "disk.qcow2"), + NetMode: "invalid", TapOwned: true, + } + if err := saveRec(dir, r); err != nil { + t.Fatal(err) + } + + err = Start(cmd, []string{"macos-demo"}) + if err == nil || !strings.Contains(err.Error(), "recover network: unknown --net mode") { + t.Fatalf("Start error = %v, want the network recovery failure before qemu launch", err) + } +} diff --git a/cmd/vm/net_other.go b/cmd/vm/net_other.go index 0300911..0fc9dcc 100644 --- a/cmd/vm/net_other.go +++ b/cmd/vm/net_other.go @@ -15,6 +15,8 @@ func provisionNet(_ *cobra.Command, _ *record) (tap, netns, mac string, err erro return "", "", "", fmt.Errorf("auto-create TAP / CNI / bridge networking requires Linux (running on %s); pass a pre-created --tap or use --net user", runtime.GOOS) } +func recoverNet(_ *cobra.Command, _ *record) error { return nil } + func teardownNet(_ context.Context, _ *cobra.Command, _ *record) error { return nil } func toggleNet(_ *cobra.Command, _ *record, _ bool) {} diff --git a/go.mod b/go.mod index 2ab1871..7d8a932 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cocoonstack/cocoon-macos go 1.27.0 require ( - github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796 + github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 github.com/gofrs/flock v0.13.0 github.com/opencontainers/image-spec v1.1.1 github.com/projecteru2/core v0.1.3 diff --git a/go.sum b/go.sum index 939c0f8..2015474 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILM github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA= github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw= -github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796 h1:P3b28cTPRfbwKgCBj5swqJhwXYdnEtIC8xrKS/PAARM= -github.com/cocoonstack/cocoon v0.6.9-0.20260923034234-4a3ed8c35796/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= +github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 h1:RJe3v2i/sP1BymsYlJx8+0DqnTRxExvYyi1rslV7tvg= +github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA=