@@ -20,6 +20,8 @@ import (
2020 "fmt"
2121 "maps"
2222 "os"
23+ "strconv"
24+ "strings"
2325
2426 corev1 "k8s.io/api/core/v1"
2527 "k8s.io/apimachinery/pkg/api/resource"
@@ -52,6 +54,16 @@ const (
5254 EnableMemoryHotplugThreshold = 1 * 1024 * 1024 * 1024 // 1 Gi (no hotplug for VMs with less than 1Gi)
5355)
5456
57+ const (
58+ // VCPUTopologyDynamicCoresAnnotation annotation indicates "distributed by sockets" or "dynamic cores number" VCPU topology.
59+ VCPUTopologyDynamicCoresAnnotation = "internal.virtualization.deckhouse.io/vcpu-topology-dynamic-cores"
60+
61+ CPUResourcesRequestsFractionAnnotation = "internal.virtualization.deckhouse.io/cpu-resources-requests-fraction"
62+
63+ // CPUMaxCoresPerSocket is a maximum number of cores per socket.
64+ CPUMaxCoresPerSocket = 16
65+ )
66+
5567type KVVMOptions struct {
5668 EnableParavirtualization bool
5769 OsType v1alpha2.OsType
@@ -258,6 +270,17 @@ func (b *KVVM) SetTopologySpreadConstraint(topology []corev1.TopologySpreadConst
258270}
259271
260272func (b * KVVM ) SetCPU (cores int , coreFraction string ) error {
273+ // Support for VMs started with cpu configuration in requests-limits.
274+ // TODO delete this in the future (around 3-4 more versions after enabling cpu hotplug by default).
275+ if b .ResourceExists && isVMRunningWithCPUResources (b .Resource ) {
276+ return b .setCPUNonHotpluggable (cores , coreFraction )
277+ }
278+ return b .setCPUHotpluggable (cores , coreFraction )
279+ }
280+
281+ // setCPUNonHotpluggable translates cpu configuration to requests and limit in KVVM.
282+ // Note: this is a first implementation, cpu hotplug is not compatible with this strategy.
283+ func (b * KVVM ) setCPUNonHotpluggable (cores int , coreFraction string ) error {
261284 domainSpec := & b .Resource .Spec .Template .Spec .Domain
262285 if domainSpec .CPU == nil {
263286 domainSpec .CPU = & virtv1.CPU {}
@@ -266,6 +289,7 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
266289 if err != nil {
267290 return err
268291 }
292+
269293 cpuLimit := GetCPULimit (cores )
270294 if domainSpec .Resources .Requests == nil {
271295 domainSpec .Resources .Requests = make (map [corev1.ResourceName ]resource.Quantity )
@@ -284,6 +308,38 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
284308 return nil
285309}
286310
311+ // setCPUHotpluggable translates cpu configuration to settings in domain.cpu field.
312+ // This field is compatible with memory hotplug.
313+ // Also, remove requests-limits for memory if any.
314+ // Note: we swap cores and sockets to bypass vm-validation webhook.
315+ func (b * KVVM ) setCPUHotpluggable (cores int , coreFraction string ) error {
316+ domainSpec := & b .Resource .Spec .Template .Spec .Domain
317+ if domainSpec .CPU == nil {
318+ domainSpec .CPU = & virtv1.CPU {}
319+ }
320+
321+ fraction , err := GetCPUFraction (coreFraction )
322+ if err != nil {
323+ return err
324+ }
325+ b .SetKVVMIAnnotation (CPUResourcesRequestsFractionAnnotation , strconv .Itoa (fraction ))
326+
327+ socketsNeeded , coresPerSocketNeeded := vm .CalculateCoresAndSockets (cores )
328+ // Use "dynamic cores" hotplug strategy.
329+ // Workaround: swap cores and sockets in domainSpec to bypass vm-validator webhook.
330+ b .SetKVVMIAnnotation (VCPUTopologyDynamicCoresAnnotation , "" )
331+ domainSpec .CPU .Cores = uint32 (socketsNeeded )
332+ domainSpec .CPU .Sockets = uint32 (coresPerSocketNeeded )
333+ domainSpec .CPU .MaxSockets = CPUMaxCoresPerSocket
334+
335+ // Remove CPU limits and requests if set by previous implementation.
336+ res := & b .Resource .Spec .Template .Spec .Domain .Resources
337+ delete (res .Requests , corev1 .ResourceCPU )
338+ delete (res .Limits , corev1 .ResourceCPU )
339+
340+ return nil
341+ }
342+
287343// SetMemory sets memory in kvvm.
288344// There are 2 possibilities to set memory:
289345// 1. Use domain.memory.guest field: it enabled memory hotplugging, but not set resources.limits.
@@ -293,7 +349,7 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
293349func (b * KVVM ) SetMemory (memorySize resource.Quantity ) {
294350 // Support for VMs started with memory size in requests-limits.
295351 // TODO delete this in the future (around 3-4 more versions after enabling memory hotplug by default).
296- if b .ResourceExists && isVMRunningWithMemoryResources (b .Resource ) {
352+ if b .ResourceExists && shouldKeepMemoryNonHotpluggable (b .Resource ) {
297353 b .setMemoryNonHotpluggable (memorySize )
298354 return
299355 }
@@ -349,7 +405,7 @@ func (b *KVVM) setMemoryHotpluggable(memorySize resource.Quantity) {
349405 delete (res .Limits , corev1 .ResourceMemory )
350406}
351407
352- func isVMRunningWithMemoryResources (kvvm * virtv1.VirtualMachine ) bool {
408+ func isVMRunningWithCPUResources (kvvm * virtv1.VirtualMachine ) bool {
353409 if kvvm == nil {
354410 return false
355411 }
@@ -359,10 +415,61 @@ func isVMRunningWithMemoryResources(kvvm *virtv1.VirtualMachine) bool {
359415 }
360416
361417 res := kvvm .Spec .Template .Spec .Domain .Resources
362- _ , hasMemoryRequests := res .Requests [corev1 .ResourceMemory ]
363- _ , hasMemoryLimits := res .Limits [corev1 .ResourceMemory ]
418+ _ , hasCPURequests := res .Requests [corev1 .ResourceCPU ]
419+ _ , hasCPULimits := res .Limits [corev1 .ResourceCPU ]
420+
421+ return hasCPURequests && hasCPULimits
422+ }
423+
424+ func shouldKeepMemoryNonHotpluggable (kvvm * virtv1.VirtualMachine ) bool {
425+ if kvvm == nil {
426+ return false
427+ }
428+
429+ if kvvm .Status .PrintableStatus == virtv1 .VirtualMachineStatusRunning || kvvm .Status .PrintableStatus == virtv1 .VirtualMachineStatusMigrating {
430+ // Running or Migrating machines with memory resources should keep as non-hotpluggable.
431+ // Machines without memory resources should proceed as hotpluggable.
432+ res := kvvm .Spec .Template .Spec .Domain .Resources
433+ _ , hasMemoryRequests := res .Requests [corev1 .ResourceMemory ]
434+ _ , hasMemoryLimits := res .Limits [corev1 .ResourceMemory ]
364435
365- return hasMemoryRequests && hasMemoryLimits
436+ return hasMemoryRequests && hasMemoryLimits
437+ }
438+
439+ // Proceed as hotpluggable if machine is not Running or Migrating.
440+ return false
441+ }
442+
443+ func GetCPUFraction (cpuFraction string ) (int , error ) {
444+ if cpuFraction == "" {
445+ return 100 , nil
446+ }
447+ fraction := intstr .FromString (cpuFraction )
448+ value , _ , err := getIntOrPercentValueSafely (& fraction )
449+ if err != nil {
450+ return 0 , fmt .Errorf ("invalid value for cpu fraction: %w" , err )
451+ }
452+ return value , nil
453+ }
454+
455+ func getIntOrPercentValueSafely (intOrStr * intstr.IntOrString ) (int , bool , error ) {
456+ switch intOrStr .Type {
457+ case intstr .Int :
458+ return intOrStr .IntValue (), false , nil
459+ case intstr .String :
460+ s := intOrStr .StrVal
461+ if ! strings .HasSuffix (s , "%" ) {
462+ return 0 , false , fmt .Errorf ("invalid type: string is not a percentage" )
463+ }
464+ s = strings .TrimSuffix (intOrStr .StrVal , "%" )
465+
466+ v , err := strconv .Atoi (s )
467+ if err != nil {
468+ return 0 , false , fmt .Errorf ("invalid value %q: %w" , intOrStr .StrVal , err )
469+ }
470+ return v , true , nil
471+ }
472+ return 0 , false , fmt .Errorf ("invalid type: neither int nor percentage" )
366473}
367474
368475func GetCPURequest (cores int , coreFraction string ) (* resource.Quantity , error ) {
0 commit comments