diff --git a/hypervisor/cloudhypervisor/args.go b/hypervisor/cloudhypervisor/args.go index db7bd32b..246e2683 100644 --- a/hypervisor/cloudhypervisor/args.go +++ b/hypervisor/cloudhypervisor/args.go @@ -3,7 +3,6 @@ package cloudhypervisor import ( "fmt" "path/filepath" - "runtime" "strconv" "strings" @@ -43,10 +42,8 @@ func buildVMConfig(rec *hypervisor.VMRecord, consoleSockPath string, allowed []i cpu := rec.Config.CPU mem := rec.Config.Memory - maxVCPUs := runtime.NumCPU() - cfg := &chVMConfig{ - CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: maxVCPUs, KVMHyperV: 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}, RNG: chRNG{Src: "/dev/urandom"}, Watchdog: true, diff --git a/hypervisor/utils.go b/hypervisor/utils.go index 08aaa130..df0052c8 100644 --- a/hypervisor/utils.go +++ b/hypervisor/utils.go @@ -18,6 +18,7 @@ import ( "github.com/projecteru2/core/log" "github.com/vishvananda/netns" + "github.com/cocoonstack/cocoon/cgroup" "github.com/cocoonstack/cocoon/lock/flock" "github.com/cocoonstack/cocoon/lock/vmlock" "github.com/cocoonstack/cocoon/types" @@ -48,6 +49,8 @@ const ( // socketReadyPollInterval is the WaitForSocket poll cadence — VMM socket usually appears within a few ms after process start. socketReadyPollInterval = 1 * time.Millisecond + + onlineCPUsPath = "/sys/devices/system/cpu/online" ) // SnapshotFileKind classifies a snapshot file for CloneSnapshotFiles. @@ -269,8 +272,22 @@ func MergeDirInto(src, dst string) error { return nil } +// HostCPUCount returns the host's online CPU count; runtime.NumCPU reports the process affinity mask, which undersizes guests when the control plane is core-pinned. +func HostCPUCount() int { + data, err := os.ReadFile(onlineCPUsPath) + if err != nil { + return runtime.NumCPU() + } + cpus, err := cgroup.ParseCPUList(strings.TrimSpace(string(data))) + if err != nil || len(cpus) == 0 { + return runtime.NumCPU() + } + return len(cpus) +} + +// ValidateHostCPU rejects vCPU counts beyond the host's online cores. func ValidateHostCPU(cpu int) error { - maxCPU := runtime.NumCPU() + maxCPU := HostCPUCount() if cpu > maxCPU { return fmt.Errorf("requested %d vCPUs exceeds host cores (%d)", cpu, maxCPU) } diff --git a/hypervisor/utils_test.go b/hypervisor/utils_test.go index 945c4c37..2f9dfc5a 100644 --- a/hypervisor/utils_test.go +++ b/hypervisor/utils_test.go @@ -3,6 +3,7 @@ package hypervisor import ( "os" "path/filepath" + "runtime" "slices" "strings" "testing" @@ -10,6 +11,15 @@ import ( "github.com/cocoonstack/cocoon/types" ) +// Under a pinned test process (taskset) NumCPU shrinks to the affinity mask while HostCPUCount must keep reporting the machine. +func TestHostCPUCount(t *testing.T) { + got := HostCPUCount() + t.Logf("HostCPUCount=%d runtime.NumCPU=%d", got, runtime.NumCPU()) + if got < runtime.NumCPU() { + t.Fatalf("HostCPUCount() = %d, below runtime.NumCPU() = %d", got, runtime.NumCPU()) + } +} + func TestValidateSnapshotIntegrity(t *testing.T) { dir := t.TempDir() mustWrite := func(name string) {