@@ -51,6 +51,49 @@ func newHypervisor(name, cpuCap, cpuAlloc, memCap, memAlloc string) *hv1.Hypervi
5151 }
5252}
5353
54+ // newHypervisorWithCapacityOnly creates a hypervisor with only Capacity set (no EffectiveCapacity).
55+ func newHypervisorWithCapacityOnly (name , cpuCap , memCap string ) * hv1.Hypervisor {
56+ return & hv1.Hypervisor {
57+ ObjectMeta : metav1.ObjectMeta {
58+ Name : name ,
59+ },
60+ Status : hv1.HypervisorStatus {
61+ Capacity : map [hv1.ResourceName ]resource.Quantity {
62+ hv1 .ResourceCPU : resource .MustParse (cpuCap ),
63+ hv1 .ResourceMemory : resource .MustParse (memCap ),
64+ },
65+ Allocation : map [hv1.ResourceName ]resource.Quantity {
66+ hv1 .ResourceCPU : resource .MustParse ("0" ),
67+ hv1 .ResourceMemory : resource .MustParse ("0" ),
68+ },
69+ },
70+ }
71+ }
72+
73+ // newHypervisorWithBothCapacities creates a hypervisor with both Capacity and EffectiveCapacity set.
74+ // EffectiveCapacity is typically >= Capacity due to overcommit ratio.
75+ func newHypervisorWithBothCapacities (name , cpuCap , cpuEffCap , memCap , memEffCap string ) * hv1.Hypervisor {
76+ return & hv1.Hypervisor {
77+ ObjectMeta : metav1.ObjectMeta {
78+ Name : name ,
79+ },
80+ Status : hv1.HypervisorStatus {
81+ Capacity : map [hv1.ResourceName ]resource.Quantity {
82+ hv1 .ResourceCPU : resource .MustParse (cpuCap ),
83+ hv1 .ResourceMemory : resource .MustParse (memCap ),
84+ },
85+ EffectiveCapacity : map [hv1.ResourceName ]resource.Quantity {
86+ hv1 .ResourceCPU : resource .MustParse (cpuEffCap ),
87+ hv1 .ResourceMemory : resource .MustParse (memEffCap ),
88+ },
89+ Allocation : map [hv1.ResourceName ]resource.Quantity {
90+ hv1 .ResourceCPU : resource .MustParse ("0" ),
91+ hv1 .ResourceMemory : resource .MustParse ("0" ),
92+ },
93+ },
94+ }
95+ }
96+
5497func newCommittedReservation (
5598 name , targetHost , observedHost , projectID , flavorName , flavorGroup , cpu , memory string ,
5699 specAllocations map [string ]v1alpha1.CommittedResourceAllocation , // Spec allocations for CR
@@ -440,7 +483,7 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) {
440483 t .Run (tt .name , func (t * testing.T ) {
441484 objects := make ([]client.Object , 0 , len (hypervisors )+ len (tt .reservations ))
442485 for _ , h := range hypervisors {
443- objects = append (objects , h )
486+ objects = append (objects , h . DeepCopy () )
444487 }
445488 for _ , r := range tt .reservations {
446489 objects = append (objects , r )
@@ -469,3 +512,87 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) {
469512 })
470513 }
471514}
515+
516+ func TestFilterHasEnoughCapacity_NilEffectiveCapacityFallback (t * testing.T ) {
517+ scheme := buildTestScheme (t )
518+
519+ tests := []struct {
520+ name string
521+ hypervisors []* hv1.Hypervisor
522+ request api.ExternalSchedulerRequest
523+ expectedHosts []string
524+ filteredHosts []string
525+ }{
526+ {
527+ name : "Hypervisor with nil EffectiveCapacity uses Capacity fallback" ,
528+ hypervisors : []* hv1.Hypervisor {
529+ newHypervisor ("host1" , "16" , "8" , "32Gi" , "16Gi" ), // has EffectiveCapacity: 8 CPU free, 16Gi free
530+ newHypervisorWithCapacityOnly ("host2" , "8" , "16Gi" ), // nil EffectiveCapacity, uses Capacity: 8 CPU free, 16Gi free
531+ newHypervisorWithCapacityOnly ("host3" , "2" , "4Gi" ), // nil EffectiveCapacity, uses Capacity: 2 CPU free (not enough)
532+ newHypervisorWithCapacityOnly ("host4" , "16" , "32Gi" ), // nil EffectiveCapacity, uses Capacity: 16 CPU free, 32Gi free
533+ },
534+ request : newNovaRequest ("instance-123" , "project-A" , "m1.small" , "gp-1" , 4 , "8Gi" , false , []string {"host1" , "host2" , "host3" , "host4" }),
535+ expectedHosts : []string {"host1" , "host2" , "host4" },
536+ filteredHosts : []string {"host3" },
537+ },
538+ {
539+ name : "All hypervisors with nil EffectiveCapacity use Capacity fallback" ,
540+ hypervisors : []* hv1.Hypervisor {
541+ newHypervisorWithCapacityOnly ("host1" , "8" , "16Gi" ),
542+ newHypervisorWithCapacityOnly ("host2" , "4" , "8Gi" ),
543+ },
544+ request : newNovaRequest ("instance-123" , "project-A" , "m1.small" , "gp-1" , 4 , "8Gi" , false , []string {"host1" , "host2" }),
545+ expectedHosts : []string {"host1" , "host2" },
546+ filteredHosts : []string {},
547+ },
548+ {
549+ name : "EffectiveCapacity used when both are set (overcommit scenario)" ,
550+ hypervisors : []* hv1.Hypervisor {
551+ // host1: Capacity=8 CPU, EffectiveCapacity=16 CPU (2x overcommit)
552+ // With Capacity only: 8 free -> passes
553+ // With EffectiveCapacity: 16 free -> passes (more capacity available)
554+ newHypervisorWithBothCapacities ("host1" , "8" , "16" , "16Gi" , "32Gi" ),
555+ // host2: Capacity=4 CPU, EffectiveCapacity=8 CPU (2x overcommit)
556+ // With Capacity only: 4 free -> would be filtered (need 5)
557+ // With EffectiveCapacity: 8 free -> passes
558+ newHypervisorWithBothCapacities ("host2" , "4" , "8" , "8Gi" , "16Gi" ),
559+ // host3: Capacity=4 CPU, EffectiveCapacity=4 CPU (no overcommit)
560+ // Both: 4 free -> filtered (need 5)
561+ newHypervisorWithBothCapacities ("host3" , "4" , "4" , "8Gi" , "8Gi" ),
562+ },
563+ request : newNovaRequest ("instance-123" , "project-A" , "m1.small" , "gp-1" , 5 , "8Gi" , false , []string {"host1" , "host2" , "host3" }),
564+ expectedHosts : []string {"host1" , "host2" },
565+ filteredHosts : []string {"host3" },
566+ },
567+ }
568+
569+ for _ , tt := range tests {
570+ t .Run (tt .name , func (t * testing.T ) {
571+ objects := make ([]client.Object , 0 , len (tt .hypervisors ))
572+ for _ , h := range tt .hypervisors {
573+ objects = append (objects , h .DeepCopy ())
574+ }
575+
576+ step := & FilterHasEnoughCapacity {}
577+ step .Client = fake .NewClientBuilder ().WithScheme (scheme ).WithObjects (objects ... ).Build ()
578+ step .Options = FilterHasEnoughCapacityOpts {LockReserved : false }
579+
580+ result , err := step .Run (slog .Default (), tt .request )
581+ if err != nil {
582+ t .Fatalf ("expected no error, got %v" , err )
583+ }
584+
585+ for _ , host := range tt .expectedHosts {
586+ if _ , ok := result .Activations [host ]; ! ok {
587+ t .Errorf ("expected host %s to be present in activations, but got %+v" , host , result .Activations )
588+ }
589+ }
590+
591+ for _ , host := range tt .filteredHosts {
592+ if _ , ok := result .Activations [host ]; ok {
593+ t .Errorf ("expected host %s to be filtered out" , host )
594+ }
595+ }
596+ })
597+ }
598+ }
0 commit comments