From ad22695adadd636f9ac449c8cff17ab6a4808e61 Mon Sep 17 00:00:00 2001 From: czmDeRepository <56431414+czmDeRepository@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:35:06 +0800 Subject: [PATCH 1/4] fix(cloudhypervisor): disable Windows watchdog Change-Id: I370363cfe432cbdc7d996a1ab62284cf5ebc1732 --- hypervisor/cloudhypervisor/args.go | 12 ++++++++---- hypervisor/cloudhypervisor/args_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/hypervisor/cloudhypervisor/args.go b/hypervisor/cloudhypervisor/args.go index 09a0c0e1..66f93dea 100644 --- a/hypervisor/cloudhypervisor/args.go +++ b/hypervisor/cloudhypervisor/args.go @@ -48,10 +48,14 @@ func buildVMConfig(rec *hypervisor.VMRecord, consoleSockPath string, allowed []i mem := rec.Config.Memory cfg := &chVMConfig{ - CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: hypervisor.HostCPUCount(), KVMHyperV: rec.Config.Windows}, - Memory: chMemory{Size: mem, HugePages: rec.Config.HugePages, Shared: rec.Config.SharedMemory, Mergeable: rec.Config.Mergeable}, - RNG: chRNG{Src: "/dev/urandom"}, - Watchdog: true, + CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: hypervisor.HostCPUCount(), KVMHyperV: rec.Config.Windows}, + Memory: chMemory{Size: mem, HugePages: rec.Config.HugePages, Shared: rec.Config.SharedMemory, Mergeable: rec.Config.Mergeable}, + RNG: chRNG{Src: "/dev/urandom"}, + // Windows stops servicing virtio-watchdog while rebooting. Keeping the + // device armed can reset the VM during driver teardown and leave the guest + // in a SYSTEM_THREAD_EXCEPTION_NOT_HANDLED boot loop. Linux guests keep + // the watchdog for hang recovery. + Watchdog: !rec.Config.Windows, Vsock: &chVsock{CID: hypervisor.VsockGuestCID, Socket: hypervisor.VsockSockPath(rec.RunDir)}, } diff --git a/hypervisor/cloudhypervisor/args_test.go b/hypervisor/cloudhypervisor/args_test.go index 5b1d6ed1..99d28a6d 100644 --- a/hypervisor/cloudhypervisor/args_test.go +++ b/hypervisor/cloudhypervisor/args_test.go @@ -54,6 +54,24 @@ func TestEffectiveDirectIO(t *testing.T) { } } +func TestWatchdogDisabledForWindows(t *testing.T) { + for _, tt := range []struct { + name string + windows bool + want bool + }{ + {name: "linux", want: true}, + {name: "windows", windows: true, want: false}, + } { + t.Run(tt.name, func(t *testing.T) { + rec := &hypervisor.VMRecord{VM: types.VM{Config: types.VMConfig{Config: types.Config{Windows: tt.windows}}}} + if got := buildVMConfig(rec, "", nil).Watchdog; got != tt.want { + t.Fatalf("Watchdog = %v, want %v", got, tt.want) + } + }) + } +} + func TestQcow2OverlayDiskArgs(t *testing.T) { sc := &types.StorageConfig{Path: "/v/overlay.qcow2", Role: types.StorageRoleCOW} got := diskToCLIArg(storageConfigToDisk(sc, 1, 0, false, nil)) From 46cb4960bf02e2bd213f2042e7766c5eaf5a6db0 Mon Sep 17 00:00:00 2001 From: czmDeRepository <56431414+czmDeRepository@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:01:04 +0800 Subject: [PATCH 2/4] feat(cloudhypervisor): make watchdog policy configurable Change-Id: I668e2f9642ae17a192cb92ba7ed77521015a5486 --- cmd/core/vmconfig.go | 7 ++++ cmd/core/vmconfig_test.go | 44 +++++++++++++++++++++++++ cmd/vm/commands.go | 2 ++ cmd/vm/debug.go | 4 ++- cmd/vm/run.go | 2 ++ docs/cli.md | 2 ++ docs/known-issues.md | 2 +- docs/vm.md | 2 +- hypervisor/cloudhypervisor/args.go | 12 +++---- hypervisor/cloudhypervisor/args_test.go | 14 ++++---- types/config.go | 1 + 11 files changed, 74 insertions(+), 18 deletions(-) diff --git a/cmd/core/vmconfig.go b/cmd/core/vmconfig.go index b693d28d..6e60ae9e 100644 --- a/cmd/core/vmconfig.go +++ b/cmd/core/vmconfig.go @@ -32,6 +32,7 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error user, _ := cmd.Flags().GetString("user") password, _ := cmd.Flags().GetString("password") noDirectIO, _ := cmd.Flags().GetBool("no-direct-io") + noWatchdog, _ := cmd.Flags().GetBool("no-watchdog") windows, _ := cmd.Flags().GetBool("windows") sharedMemory, _ := cmd.Flags().GetBool("shared-memory") hugePages, _ := cmd.Flags().GetBool("hugepages") @@ -65,6 +66,7 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error Image: image, Network: network, NoDirectIO: noDirectIO, + NoWatchdog: noWatchdog, Windows: windows, SharedMemory: sharedMemory, HugePages: hugePages, @@ -106,6 +108,10 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (* if cmd.Flags().Changed("no-direct-io") { noDirectIO, _ = cmd.Flags().GetBool("no-direct-io") } + noWatchdog := snapCfg.NoWatchdog + if cmd.Flags().Changed("no-watchdog") { + noWatchdog, _ = cmd.Flags().GetBool("no-watchdog") + } restoreMode, err := restoreModeFromFlags(cmd) if err != nil { @@ -130,6 +136,7 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (* ImageType: snapCfg.ImageType, Network: network, NoDirectIO: noDirectIO, + NoWatchdog: noWatchdog, Windows: snapCfg.Windows, SharedMemory: snapCfg.SharedMemory, HugePages: snapCfg.HugePages, diff --git a/cmd/core/vmconfig_test.go b/cmd/core/vmconfig_test.go index 190aca1b..c987dcb1 100644 --- a/cmd/core/vmconfig_test.go +++ b/cmd/core/vmconfig_test.go @@ -117,6 +117,7 @@ func TestCloneVMConfigKnobFlagsOverrideSnapshot(t *testing.T) { cmd.Flags().Int64("cpu-burst-us", 0, "") cmd.Flags().String("network", "", "") cmd.Flags().Bool("no-direct-io", false, "") + cmd.Flags().Bool("no-watchdog", false, "") cmd.Flags().String("restore-mode", "", "") cmd.Flags().StringArray("data-disk", nil, "") for k, v := range tt.set { @@ -138,3 +139,46 @@ func TestCloneVMConfigKnobFlagsOverrideSnapshot(t *testing.T) { }) } } + +func TestCloneVMConfigWatchdogOverride(t *testing.T) { + snapCfg := types.SnapshotConfig{Config: types.Config{ + CPU: 2, Memory: 1 << 30, Storage: 10 << 30, NoWatchdog: true, + }} + newCommand := func() *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().String("name", "c", "") + cmd.Flags().Int("nics", 0, "") + cmd.Flags().Int("queue-size", 0, "") + cmd.Flags().Int("disk-queue-size", 0, "") + cmd.Flags().Int("cpu-weight", 0, "") + cmd.Flags().Int64("cpu-quota-us", 0, "") + cmd.Flags().Int64("cpu-period-us", 0, "") + cmd.Flags().Int64("cpu-burst-us", 0, "") + cmd.Flags().String("network", "", "") + cmd.Flags().Bool("no-direct-io", false, "") + cmd.Flags().Bool("no-watchdog", false, "") + cmd.Flags().String("restore-mode", "", "") + cmd.Flags().StringArray("data-disk", nil, "") + return cmd + } + + got, err := CloneVMConfigFromFlags(newCommand(), snapCfg) + if err != nil { + t.Fatal(err) + } + if !got.NoWatchdog { + t.Fatal("clone must inherit the snapshot watchdog policy") + } + + cmd := newCommand() + if err := cmd.Flags().Set("no-watchdog", "false"); err != nil { + t.Fatal(err) + } + got, err = CloneVMConfigFromFlags(cmd, snapCfg) + if err != nil { + t.Fatal(err) + } + if got.NoWatchdog { + t.Fatal("an explicit --no-watchdog=false must re-enable the device") + } +} diff --git a/cmd/vm/commands.go b/cmd/vm/commands.go index 0e400f1c..0af7cb53 100644 --- a/cmd/vm/commands.go +++ b/cmd/vm/commands.go @@ -336,6 +336,7 @@ func addVMFlags(cmd *cobra.Command) { cmd.Flags().String("user", "root", "guest username for cloud-init (cloudimg only)") cmd.Flags().String("password", "cocoon", "guest password for cloud-init (cloudimg only)") cmd.Flags().Bool("no-direct-io", false, "disable O_DIRECT on writable disks (use page cache instead; CH only)") + cmd.Flags().Bool("no-watchdog", false, "omit the virtio watchdog device (CH only; use when the guest driver cannot safely handle reboot)") cmd.Flags().Bool("windows", false, "Windows guest (UEFI boot, kvm_hyperv=on, no cidata)") cmd.Flags().Bool("shared-memory", false, "enable CH memory shared=on; required to attach vhost-user-fs later (CH only, fixed for VM lifetime)") cmd.Flags().Bool("hugepages", false, "back guest memory with hugetlbfs (CH only, fixed for VM lifetime); snapshots of such a VM restore via eager copy, never mmap") @@ -356,6 +357,7 @@ func addCloneFlags(cmd *cobra.Command) { cmd.Flags().String("network", "", "CNI conflist name (empty = inherit from source VM)") cmd.Flags().String("bridge", "", "use TAP-on-bridge instead of CNI (value is bridge device, e.g. cni0)") cmd.Flags().Bool("no-direct-io", false, "disable O_DIRECT on writable disks (inherit from snapshot if not set)") + cmd.Flags().Bool("no-watchdog", false, "omit the virtio watchdog device (inherit from snapshot if not set; CH only)") cmd.Flags().String("restore-mode", "", "memory restore mode: copy|ondemand|mmap (CH only; default mmap for plain private-anon snapshots, else copy; hugepages/shared degrade mmap to copy with a warning)") cmd.Flags().Bool("pull", false, "auto-pull base image if not found locally (for cross-node clone)") cmd.Flags().StringArray("data-disk", nil, "create and hot-add an extra data disk to the clone: size=20G[,name=...][,fstype=ext4|none]; repeatable (CH only)") diff --git a/cmd/vm/debug.go b/cmd/vm/debug.go index a03b2a9f..194840d9 100644 --- a/cmd/vm/debug.go +++ b/cmd/vm/debug.go @@ -224,6 +224,8 @@ func printCommonCHArgs(s chDebugSpec) { if s.Balloon > 0 { fmt.Printf(" --balloon size=%dM,deflate_on_oom=on,free_page_reporting=on \\\n", s.Balloon) } - fmt.Print(" --watchdog \\\n") + if !s.VMCfg.NoWatchdog { + fmt.Print(" --watchdog \\\n") + } fmt.Println(" --serial tty --console off") } diff --git a/cmd/vm/run.go b/cmd/vm/run.go index c0836474..7d2a2883 100644 --- a/cmd/vm/run.go +++ b/cmd/vm/run.go @@ -490,6 +490,8 @@ func validateBackendFlags(conf *config.Config, vmCfg *types.VMConfig) error { return fmt.Errorf("--fc and --hugepages are mutually exclusive: Firecracker cannot restore hugetlbfs-backed snapshots") case vmCfg.Mergeable: return fmt.Errorf("--fc and --mergeable are mutually exclusive: Firecracker has no KSM madvise knob") + case vmCfg.NoWatchdog: + return fmt.Errorf("--fc and --no-watchdog are mutually exclusive: Firecracker does not expose a virtio watchdog") } return nil } diff --git a/docs/cli.md b/docs/cli.md index 5942e879..9608dac0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -118,6 +118,7 @@ Applies to `cocoon vm create`, `cocoon vm run`, and `cocoon vm debug`: | `--user` | `root` | Guest username for cloud-init (cloudimg only) | | `--password` | `cocoon` | Guest password for cloud-init (cloudimg only) | | `--no-direct-io` | `false` | Disable O_DIRECT on writable disks (use page cache; CH only, useful for dev/test with few VMs) | +| `--no-watchdog` | `false` | Omit the virtio watchdog device (CH only; use for guests whose watchdog driver cannot safely handle reboot) | | `--data-disk` | empty (repeatable) | Attach an extra data disk: `size=20G[,name=...][,fstype=ext4|none][,mount=/mnt/x][,directio=on|off|auto]`. See [Data Disks](vm.md#data-disks) | | `--windows` | `false` | Windows guest (UEFI boot, kvm_hyperv=on, no cidata) | | `--shared-memory` | `false` | Enable CH `memory shared=on`; required for later `vm fs attach` (CH only, fixed for VM lifetime) | @@ -142,6 +143,7 @@ Applies to `cocoon vm clone`: | `--network` | empty (inherit) | CNI conflist name (empty = inherit from source VM) | | `--bridge` | empty | TAP-on-bridge mode (value is bridge device); mutually exclusive with `--network` | | `--no-direct-io` | `false` (inherit) | Disable O_DIRECT on writable disks (inherit from snapshot if not set) | +| `--no-watchdog` | `false` (inherit) | Omit the virtio watchdog device (inherit from snapshot if not set; pass `--no-watchdog=false` to re-enable) | | `--cpu-weight` / `--cpu-quota-us` / `--cpu-period-us` / `--cpu-burst-us` / `--cpuset-cpus` | `0` / empty (defaults, **not** inherited) | The clone's cgroup CPU policy; a snapshot's knobs record its source VM and are never applied — omit for Guaranteed-at-N defaults | | `--restore-mode` | `mmap` for plain private-anon snapshots, else `copy` | Memory restore mode: `copy`, `ondemand` (UFFD) or `mmap` (CoW map, shares page cache across clones); CH only, non-copy modes require a CH build with matching support — an older CH silently ignores the field and restores by copy; hugepages/shared snapshots degrade `mmap` to `copy` with a warning | | `--pull` | `false` | Auto-pull base image if not found locally (for cross-node clone) | diff --git a/docs/known-issues.md b/docs/known-issues.md index 5a4ae08e..945df2c1 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -169,7 +169,7 @@ Cocoon does not attach a virtio-balloon device to Windows VMs (`--windows`). The virtio-win 0.1.240 did not have this problem because its balloon driver gave up on the first host timeout, allowing Windows to proceed to `ResetSystem` quickly (~14 seconds total shutdown). -**Workaround if balloon is needed**: increase `stop_timeout_seconds` to 180+ and apply the watchdog-pause patch to Cloud Hypervisor (pause the watchdog timer when `vm.power-button` is received). +**Workaround if balloon is needed**: increase `stop_timeout_seconds` to 180+ and apply the watchdog-pause patch to Cloud Hypervisor (pause the watchdog timer when `vm.power-button` is received). For guest-initiated reboot paths whose driver leaves the watchdog armed, create the VM with `--no-watchdog`; this trades device-level hang reset for reboot safety and should be paired with an external liveness policy where automatic recovery is required. ## Installing patched binaries for Windows diff --git a/docs/vm.md b/docs/vm.md index 21563558..e840bb55 100644 --- a/docs/vm.md +++ b/docs/vm.md @@ -89,7 +89,7 @@ With `cgroup_cpus=0-14`, the reserved core 15 has no VM competition and acts as - **Mergeable memory / KSM** (Cloud Hypervisor only): opt-in via `--mergeable` at golden creation; guest memory is madvised `MADV_MERGEABLE` so host KSM can dedup identical pages across VMs — the flag persists through snapshot/clone/restore (it lives in the snapshot's CH config, not the CLI), so build the golden with it or rebuild. cocoon only sets the madvise: enabling and tuning the scanner (`/sys/kernel/mm/ksm/run`, `pages_to_scan`) is the operator's. Excludes `--hugepages`/`--shared-memory` (KSM merges only plain private pages); mmap-cloned siblings already share untouched pages via the page cache, so KSM's gain is dirtied-but-equal and cross-golden pages — measure density on your fleet, and weigh ksmd CPU plus the cross-VM dedup timing side channel in multi-tenant setups - **Disk I/O**: multi-queue virtio-blk; readonly base disks keep host page cache (`direct=off`), writable raw COW and data disks use O_DIRECT (`direct=on`) to avoid host cache buildup and guest flush storms, and qcow2 overlays stay buffered — Cloud Hypervisor applies the disk's `direct` flag to the backing file too, and O_DIRECT there would give every VM its own read of the shared base instead of one page-cache copy - **Balloon**: 25% of memory auto-returned via virtio-balloon with deflate-on-OOM and free-page reporting (VMs with < 256 MiB memory skip balloon) -- **Watchdog**: hardware watchdog enabled by default for automatic guest reset on hang +- **Watchdog**: hardware watchdog enabled by default for automatic guest reset on hang; `--no-watchdog` is an explicit compatibility opt-out for guests whose watchdog driver is unsafe during reboot ## Cloud-init & First Boot diff --git a/hypervisor/cloudhypervisor/args.go b/hypervisor/cloudhypervisor/args.go index 66f93dea..24b64ddb 100644 --- a/hypervisor/cloudhypervisor/args.go +++ b/hypervisor/cloudhypervisor/args.go @@ -48,14 +48,10 @@ func buildVMConfig(rec *hypervisor.VMRecord, consoleSockPath string, allowed []i mem := rec.Config.Memory cfg := &chVMConfig{ - CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: hypervisor.HostCPUCount(), KVMHyperV: rec.Config.Windows}, - Memory: chMemory{Size: mem, HugePages: rec.Config.HugePages, Shared: rec.Config.SharedMemory, Mergeable: rec.Config.Mergeable}, - RNG: chRNG{Src: "/dev/urandom"}, - // Windows stops servicing virtio-watchdog while rebooting. Keeping the - // device armed can reset the VM during driver teardown and leave the guest - // in a SYSTEM_THREAD_EXCEPTION_NOT_HANDLED boot loop. Linux guests keep - // the watchdog for hang recovery. - Watchdog: !rec.Config.Windows, + CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: hypervisor.HostCPUCount(), KVMHyperV: rec.Config.Windows}, + Memory: chMemory{Size: mem, HugePages: rec.Config.HugePages, Shared: rec.Config.SharedMemory, Mergeable: rec.Config.Mergeable}, + RNG: chRNG{Src: "/dev/urandom"}, + Watchdog: !rec.Config.NoWatchdog, Vsock: &chVsock{CID: hypervisor.VsockGuestCID, Socket: hypervisor.VsockSockPath(rec.RunDir)}, } diff --git a/hypervisor/cloudhypervisor/args_test.go b/hypervisor/cloudhypervisor/args_test.go index 99d28a6d..9a739139 100644 --- a/hypervisor/cloudhypervisor/args_test.go +++ b/hypervisor/cloudhypervisor/args_test.go @@ -54,17 +54,17 @@ func TestEffectiveDirectIO(t *testing.T) { } } -func TestWatchdogDisabledForWindows(t *testing.T) { +func TestWatchdogPolicy(t *testing.T) { for _, tt := range []struct { - name string - windows bool - want bool + name string + noWatchdog bool + want bool }{ - {name: "linux", want: true}, - {name: "windows", windows: true, want: false}, + {name: "default enabled", want: true}, + {name: "explicitly disabled", noWatchdog: true, want: false}, } { t.Run(tt.name, func(t *testing.T) { - rec := &hypervisor.VMRecord{VM: types.VM{Config: types.VMConfig{Config: types.Config{Windows: tt.windows}}}} + rec := &hypervisor.VMRecord{VM: types.VM{Config: types.VMConfig{Config: types.Config{NoWatchdog: tt.noWatchdog}}}} if got := buildVMConfig(rec, "", nil).Watchdog; got != tt.want { t.Fatalf("Watchdog = %v, want %v", got, tt.want) } diff --git a/types/config.go b/types/config.go index 92d224a5..6e6bb3cb 100644 --- a/types/config.go +++ b/types/config.go @@ -18,6 +18,7 @@ type Config struct { ImageType string `json:"image_type,omitempty"` // backend type, ImageTypeOCI / ImageTypeCloudImg Network string `json:"network,omitempty"` // CNI conflist name; empty = default NoDirectIO bool `json:"no_direct_io,omitempty"` // disable O_DIRECT on writable disks + NoWatchdog bool `json:"no_watchdog,omitempty"` // omit the virtio watchdog device (guest/driver compatibility) Windows bool `json:"windows,omitempty"` // Windows guest: UEFI boot, kvm_hyperv=on, no cidata // SharedMemory toggles CH memory shared=on (vhost-user-fs prerequisite); fixed at create, persists through clone/restore. SharedMemory bool `json:"shared_memory,omitempty"` From a77091cdc7b1ebd9a5576230d69924612910724c Mon Sep 17 00:00:00 2001 From: czmDeRepository <56431414+czmDeRepository@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:46:51 +0800 Subject: [PATCH 3/4] fix: validate clone backend watchdog policy Change-Id: Ifd73381c80fcdbec2451255e78cc1ee536ba5bac --- cmd/vm/run.go | 5 ++++- cmd/vm/run_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 cmd/vm/run_test.go diff --git a/cmd/vm/run.go b/cmd/vm/run.go index 7d2a2883..6a3a0898 100644 --- a/cmd/vm/run.go +++ b/cmd/vm/run.go @@ -328,6 +328,9 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con if err = vmCfg.Validate(); err != nil { return cloneSetup{}, err } + if err = validateBackendFlags(conf, vmCfg); err != nil { + return cloneSetup{}, err + } // Envelope pins share create's digest-lock window; a record-backed clone's source pin already protects these. releasePins, err := cmdcore.PinEnvelopeBlobs(ctx, conf, cfg.ImageBlobIDs) if err != nil { @@ -476,7 +479,7 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t return ctx, info, hyper, nil } -// validateBackendFlags fast-fails flag combinations the selected backend can never launch; boot-mode-dependent checks live in validateBootCompat. Shared by create and debug so the capability gate list cannot drift. +// validateBackendFlags fast-fails flag combinations the selected backend can never launch; boot-mode-dependent checks live in validateBootCompat. Shared by create, clone, and debug so the capability gate list cannot drift. func validateBackendFlags(conf *config.Config, vmCfg *types.VMConfig) error { if !conf.UseFirecracker { return nil diff --git a/cmd/vm/run_test.go b/cmd/vm/run_test.go new file mode 100644 index 00000000..58670f13 --- /dev/null +++ b/cmd/vm/run_test.go @@ -0,0 +1,30 @@ +package vm + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/cocoonstack/cocoon/config" + "github.com/cocoonstack/cocoon/types" +) + +func TestPrepareCloneRejectsNoWatchdogWithFirecracker(t *testing.T) { + cmd := &cobra.Command{} + addCloneFlags(cmd) + if err := cmd.Flags().Set("name", "clone"); err != nil { + t.Fatal(err) + } + if err := cmd.Flags().Set("no-watchdog", "true"); err != nil { + t.Fatal(err) + } + conf := &config.Config{UseFirecracker: true} + snapshot := types.SnapshotConfig{Config: types.Config{ + CPU: 1, Memory: 512 << 20, Storage: 10 << 30, + }} + _, err := (Handler{}).prepareClone(t.Context(), cmd, conf, nil, snapshot) + if err == nil || !strings.Contains(err.Error(), "--fc and --no-watchdog") { + t.Fatalf("prepareClone() error = %v, want Firecracker incompatibility", err) + } +} From 42f21ef68a7e9c95667482f1ef33e42110f9aade Mon Sep 17 00:00:00 2001 From: czmDeRepository <56431414+czmDeRepository@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:11:39 +0800 Subject: [PATCH 4/4] fix: inherit watchdog policy on clone Change-Id: I4c4e36316cad6cac0a15008390a18014961fd6be --- cmd/core/vmconfig.go | 3 -- cmd/core/vmconfig_test.go | 65 ++++++++------------------------------- cmd/vm/commands.go | 1 - cmd/vm/run_test.go | 30 ------------------ docs/cli.md | 1 - docs/known-issues.md | 4 ++- 6 files changed, 16 insertions(+), 88 deletions(-) delete mode 100644 cmd/vm/run_test.go diff --git a/cmd/core/vmconfig.go b/cmd/core/vmconfig.go index 6e60ae9e..d8f5f73b 100644 --- a/cmd/core/vmconfig.go +++ b/cmd/core/vmconfig.go @@ -109,9 +109,6 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (* noDirectIO, _ = cmd.Flags().GetBool("no-direct-io") } noWatchdog := snapCfg.NoWatchdog - if cmd.Flags().Changed("no-watchdog") { - noWatchdog, _ = cmd.Flags().GetBool("no-watchdog") - } restoreMode, err := restoreModeFromFlags(cmd) if err != nil { diff --git a/cmd/core/vmconfig_test.go b/cmd/core/vmconfig_test.go index c987dcb1..9045ba32 100644 --- a/cmd/core/vmconfig_test.go +++ b/cmd/core/vmconfig_test.go @@ -90,18 +90,20 @@ func TestCloneVMConfigKnobFlagsOverrideSnapshot(t *testing.T) { snapCfg := types.SnapshotConfig{Config: types.Config{ CPU: 2, Memory: 1 << 30, Storage: 10 << 30, CPUWeight: 40, CPUQuotaUs: 200000, CPUBurstUs: 50000, + NoWatchdog: true, }} tests := []struct { - name string - set map[string]string - wantWeight int - wantQuota int64 - wantBurst int64 - wantErr bool + name string + set map[string]string + wantWeight int + wantQuota int64 + wantBurst int64 + wantNoWatchdog bool + wantErr bool }{ - {name: "no flags ignore snapshot knobs", wantWeight: 0, wantQuota: 0, wantBurst: 0}, - {name: "flags set the clone's policy", set: map[string]string{"cpu-weight": "10", "cpu-burst-us": "100000", "cpu-quota-us": "150000"}, wantWeight: 10, wantQuota: 150000, wantBurst: 100000}, + {name: "no flags ignore snapshot knobs", wantWeight: 0, wantQuota: 0, wantBurst: 0, wantNoWatchdog: true}, + {name: "flags set the clone's policy", set: map[string]string{"cpu-weight": "10", "cpu-burst-us": "100000", "cpu-quota-us": "150000"}, wantWeight: 10, wantQuota: 150000, wantBurst: 100000, wantNoWatchdog: true}, {name: "invalid flag rejected", set: map[string]string{"cpu-weight": "20000"}, wantErr: true}, } for _, tt := range tests { @@ -117,7 +119,6 @@ func TestCloneVMConfigKnobFlagsOverrideSnapshot(t *testing.T) { cmd.Flags().Int64("cpu-burst-us", 0, "") cmd.Flags().String("network", "", "") cmd.Flags().Bool("no-direct-io", false, "") - cmd.Flags().Bool("no-watchdog", false, "") cmd.Flags().String("restore-mode", "", "") cmd.Flags().StringArray("data-disk", nil, "") for k, v := range tt.set { @@ -136,49 +137,9 @@ func TestCloneVMConfigKnobFlagsOverrideSnapshot(t *testing.T) { t.Errorf("knobs = %d/%d/%d, want %d/%d/%d", got.CPUWeight, got.CPUQuotaUs, got.CPUBurstUs, tt.wantWeight, tt.wantQuota, tt.wantBurst) } + if got.NoWatchdog != tt.wantNoWatchdog { + t.Errorf("NoWatchdog = %v, want %v", got.NoWatchdog, tt.wantNoWatchdog) + } }) } } - -func TestCloneVMConfigWatchdogOverride(t *testing.T) { - snapCfg := types.SnapshotConfig{Config: types.Config{ - CPU: 2, Memory: 1 << 30, Storage: 10 << 30, NoWatchdog: true, - }} - newCommand := func() *cobra.Command { - cmd := &cobra.Command{} - cmd.Flags().String("name", "c", "") - cmd.Flags().Int("nics", 0, "") - cmd.Flags().Int("queue-size", 0, "") - cmd.Flags().Int("disk-queue-size", 0, "") - cmd.Flags().Int("cpu-weight", 0, "") - cmd.Flags().Int64("cpu-quota-us", 0, "") - cmd.Flags().Int64("cpu-period-us", 0, "") - cmd.Flags().Int64("cpu-burst-us", 0, "") - cmd.Flags().String("network", "", "") - cmd.Flags().Bool("no-direct-io", false, "") - cmd.Flags().Bool("no-watchdog", false, "") - cmd.Flags().String("restore-mode", "", "") - cmd.Flags().StringArray("data-disk", nil, "") - return cmd - } - - got, err := CloneVMConfigFromFlags(newCommand(), snapCfg) - if err != nil { - t.Fatal(err) - } - if !got.NoWatchdog { - t.Fatal("clone must inherit the snapshot watchdog policy") - } - - cmd := newCommand() - if err := cmd.Flags().Set("no-watchdog", "false"); err != nil { - t.Fatal(err) - } - got, err = CloneVMConfigFromFlags(cmd, snapCfg) - if err != nil { - t.Fatal(err) - } - if got.NoWatchdog { - t.Fatal("an explicit --no-watchdog=false must re-enable the device") - } -} diff --git a/cmd/vm/commands.go b/cmd/vm/commands.go index 0af7cb53..76d2cb8a 100644 --- a/cmd/vm/commands.go +++ b/cmd/vm/commands.go @@ -357,7 +357,6 @@ func addCloneFlags(cmd *cobra.Command) { cmd.Flags().String("network", "", "CNI conflist name (empty = inherit from source VM)") cmd.Flags().String("bridge", "", "use TAP-on-bridge instead of CNI (value is bridge device, e.g. cni0)") cmd.Flags().Bool("no-direct-io", false, "disable O_DIRECT on writable disks (inherit from snapshot if not set)") - cmd.Flags().Bool("no-watchdog", false, "omit the virtio watchdog device (inherit from snapshot if not set; CH only)") cmd.Flags().String("restore-mode", "", "memory restore mode: copy|ondemand|mmap (CH only; default mmap for plain private-anon snapshots, else copy; hugepages/shared degrade mmap to copy with a warning)") cmd.Flags().Bool("pull", false, "auto-pull base image if not found locally (for cross-node clone)") cmd.Flags().StringArray("data-disk", nil, "create and hot-add an extra data disk to the clone: size=20G[,name=...][,fstype=ext4|none]; repeatable (CH only)") diff --git a/cmd/vm/run_test.go b/cmd/vm/run_test.go deleted file mode 100644 index 58670f13..00000000 --- a/cmd/vm/run_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package vm - -import ( - "strings" - "testing" - - "github.com/spf13/cobra" - - "github.com/cocoonstack/cocoon/config" - "github.com/cocoonstack/cocoon/types" -) - -func TestPrepareCloneRejectsNoWatchdogWithFirecracker(t *testing.T) { - cmd := &cobra.Command{} - addCloneFlags(cmd) - if err := cmd.Flags().Set("name", "clone"); err != nil { - t.Fatal(err) - } - if err := cmd.Flags().Set("no-watchdog", "true"); err != nil { - t.Fatal(err) - } - conf := &config.Config{UseFirecracker: true} - snapshot := types.SnapshotConfig{Config: types.Config{ - CPU: 1, Memory: 512 << 20, Storage: 10 << 30, - }} - _, err := (Handler{}).prepareClone(t.Context(), cmd, conf, nil, snapshot) - if err == nil || !strings.Contains(err.Error(), "--fc and --no-watchdog") { - t.Fatalf("prepareClone() error = %v, want Firecracker incompatibility", err) - } -} diff --git a/docs/cli.md b/docs/cli.md index 9608dac0..db53ccc5 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -143,7 +143,6 @@ Applies to `cocoon vm clone`: | `--network` | empty (inherit) | CNI conflist name (empty = inherit from source VM) | | `--bridge` | empty | TAP-on-bridge mode (value is bridge device); mutually exclusive with `--network` | | `--no-direct-io` | `false` (inherit) | Disable O_DIRECT on writable disks (inherit from snapshot if not set) | -| `--no-watchdog` | `false` (inherit) | Omit the virtio watchdog device (inherit from snapshot if not set; pass `--no-watchdog=false` to re-enable) | | `--cpu-weight` / `--cpu-quota-us` / `--cpu-period-us` / `--cpu-burst-us` / `--cpuset-cpus` | `0` / empty (defaults, **not** inherited) | The clone's cgroup CPU policy; a snapshot's knobs record its source VM and are never applied — omit for Guaranteed-at-N defaults | | `--restore-mode` | `mmap` for plain private-anon snapshots, else `copy` | Memory restore mode: `copy`, `ondemand` (UFFD) or `mmap` (CoW map, shares page cache across clones); CH only, non-copy modes require a CH build with matching support — an older CH silently ignores the field and restores by copy; hugepages/shared snapshots degrade `mmap` to `copy` with a warning | | `--pull` | `false` | Auto-pull base image if not found locally (for cross-node clone) | diff --git a/docs/known-issues.md b/docs/known-issues.md index 945df2c1..e134354b 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -169,7 +169,9 @@ Cocoon does not attach a virtio-balloon device to Windows VMs (`--windows`). The virtio-win 0.1.240 did not have this problem because its balloon driver gave up on the first host timeout, allowing Windows to proceed to `ResetSystem` quickly (~14 seconds total shutdown). -**Workaround if balloon is needed**: increase `stop_timeout_seconds` to 180+ and apply the watchdog-pause patch to Cloud Hypervisor (pause the watchdog timer when `vm.power-button` is received). For guest-initiated reboot paths whose driver leaves the watchdog armed, create the VM with `--no-watchdog`; this trades device-level hang reset for reboot safety and should be paired with an external liveness policy where automatic recovery is required. +**Workaround if balloon is needed**: increase `stop_timeout_seconds` to 180+ and apply the watchdog-pause patch to Cloud Hypervisor (pause the watchdog timer when `vm.power-button` is received). + +**Workaround for guest-initiated reboot**: if the guest driver leaves the watchdog armed, create the VM with `--no-watchdog`. This trades device-level hang reset for reboot safety and should be paired with an external liveness policy where automatic recovery is required. ## Installing patched binaries for Windows