diff --git a/internal/scheduling/compute/nova_adapter.go b/internal/scheduling/compute/nova_adapter.go index 035502d3e..fe472bd64 100644 --- a/internal/scheduling/compute/nova_adapter.go +++ b/internal/scheduling/compute/nova_adapter.go @@ -6,6 +6,9 @@ package compute import ( "strings" + hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" + "k8s.io/apimachinery/pkg/api/resource" + api "github.com/cobaltcore-dev/cortex/api/external/nova" "github.com/cobaltcore-dev/cortex/api/v1alpha1" ) @@ -52,11 +55,8 @@ func ToSchedulerRequest( GlobalRequestID: derefString(wire.Context.GlobalRequestID), } - // Flavor sizing. - req.Flavor = FlavorSpec{ - VCPUs: spec.Flavor.Data.VCPUs, - MemoryMB: spec.Flavor.Data.MemoryMB, - } + // Resource sizing. + req.RequestedResources = NewResourceDemand(spec.Flavor.Data.VCPUs, spec.Flavor.Data.MemoryMB) // Hypervisor family. req.HypervisorFamily = toHypervisorFamily(wire) @@ -201,6 +201,19 @@ func firstNonEmpty(a, b string) string { // in-process reservation controllers (which hold a flavor's extra-spec map // directly) call them, so the conventions are understood in exactly one place. +// NewResourceDemand builds a ResourceDemand from Nova's vCPU count and memory in +// mebibytes. Nova's memory_mb field is documented as "megabytes" but its values are +// mebibytes (1 MiB = 2^20 bytes); this converts to the bytes hv.Status and the +// capacity lib use, so the Nova unit convention is applied in exactly one place. +// Resources left at 0 are still written, so callers can rely on the keys being present. +func NewResourceDemand(vcpus, memoryMB uint64) ResourceDemand { + //nolint:gosec // vCPU and memory values are small, non-negative flavor counts. + return ResourceDemand{ + hv1.ResourceCPU: *resource.NewQuantity(int64(vcpus), resource.DecimalSI), + hv1.ResourceMemory: *resource.NewQuantity(int64(memoryMB)*(1<<20), resource.BinarySI), + } +} + // unsupportedCapabilityOps are the Nova extra-spec comparison operators the // capabilities filter does not implement. A capability value containing any of // these means the request uses an advanced match the pipeline can't honor, so diff --git a/internal/scheduling/compute/request.go b/internal/scheduling/compute/request.go index 82ba20889..ac47f21bc 100644 --- a/internal/scheduling/compute/request.go +++ b/internal/scheduling/compute/request.go @@ -20,6 +20,9 @@ package compute import ( "strings" + hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" + "k8s.io/apimachinery/pkg/api/resource" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" @@ -74,16 +77,38 @@ type Capability struct { Value string } -// FlavorSpec is the typed, Nova-agnostic description of the requested flavor. -// It carries no extra-spec map: the adapter resolves every extra spec the -// pipeline needs into the typed fields on this struct and on PlacementRequest. -type FlavorSpec struct { - // MemoryMB is the requested memory per instance. 0 means a zero-memory - // instance to capacity/bin-pack steps (fits anywhere), so the adapter sets it. - MemoryMB uint64 - // VCPUs is the requested vCPUs per instance. 0 means a zero-CPU instance to - // capacity/bin-pack steps (fits anywhere), so the adapter sets it. - VCPUs uint64 +// ResourceDemand is the typed, Nova-agnostic description of the resources a +// placement requests, as a per-resource quantity map keyed by hv1.ResourceName and +// aligned with the units hypervisors report in their status: memory in bytes, CPU +// as a plain count. The adapter resolves the flavor sizing into this map; it carries +// no extra-spec map. +// +// A resource absent from the map is not requested — capacity and bin-pack steps +// treat a missing entry as a zero-sized requirement (fits anywhere). +type ResourceDemand map[hv1.ResourceName]resource.Quantity + +// VCPUs returns the requested vCPU count (0 if unset). +func (d ResourceDemand) VCPUs() int64 { + q := d[hv1.ResourceCPU] + return q.Value() +} + +// MemoryBytes returns the requested memory in bytes (0 if unset). +func (d ResourceDemand) MemoryBytes() int64 { + q := d[hv1.ResourceMemory] + return q.Value() +} + +// Scaled returns a copy of the demand with every quantity multiplied by n. It is +// used to turn a per-instance demand into the total for a multi-instance request. +func (d ResourceDemand) Scaled(n int64) ResourceDemand { + scaled := make(ResourceDemand, len(d)) + for rn, perInstance := range d { + total := perInstance.DeepCopy() + total.Mul(n) + scaled[rn] = total + } + return scaled } // RequestedDestination mirrors the subset of Nova's requested_destination that @@ -203,10 +228,10 @@ type PlacementRequest struct { // means no hardware-version constraint. HardwareVersion string - // Flavor is the requested flavor sizing. A zero FlavorSpec means capacity and - // bin-pack steps see a zero-sized instance (fits anywhere) — the adapter - // always populates it for real placements. - Flavor FlavorSpec + // RequestedResources is the requested instance sizing as a per-resource quantity + // map. A nil or empty map means capacity and bin-pack steps see a zero-sized + // instance (fits anywhere) — the adapter always populates it for real placements. + RequestedResources ResourceDemand // RequiredTraits are trait names the host must have (adapter stripped the // "trait:" prefix and kept the "required" ones). Empty/nil means no required @@ -259,6 +284,15 @@ var _ lib.FilterWeigherPipelineRequest = PlacementRequest{} // GetIntent returns the resolved scheduling intent. func (r PlacementRequest) GetIntent() v1alpha1.SchedulingIntent { return r.Intent } +// TotalResources returns the full per-request resource demand: the per-instance +// RequestedResources scaled by NumInstances. Capacity and bin-pack steps place all +// instances on one host, so they size against this total. NumInstances is used raw +// (not normalized to 1) — the adapter always sets it >= 1. +func (r PlacementRequest) TotalResources() ResourceDemand { + //nolint:gosec // instance count is a small, non-negative count. + return r.RequestedResources.Scaled(int64(r.NumInstances)) +} + // GetHosts returns the compute host names of all candidates. func (r PlacementRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index 50c5bd3bc..e79878add 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -16,7 +16,6 @@ import ( "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib/capacity" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" - "k8s.io/apimachinery/pkg/api/resource" ) type FilterHasEnoughCapacityOpts struct { @@ -200,22 +199,17 @@ func (s *FilterHasEnoughCapacity) Run(traceLog logr.Logger, request compute.Plac usageByHost[hv.Name] = usage } - // A host must fit the full request: flavor resources × NumInstances. We can't spread - // instances across hosts, so all requested instances must land on the same host. - if request.Flavor.VCPUs == 0 { + // A host must fit the full request: requested resources × NumInstances. We can't + // spread instances across hosts, so all requested instances must land on the same host. + if request.RequestedResources.VCPUs() == 0 { return nil, errors.New("flavor has 0 vcpus") } - if request.Flavor.MemoryMB == 0 { + if request.RequestedResources.MemoryBytes() == 0 { return nil, errors.New("flavor has 0 memory") } - // Nova's memory_mb field is named "megabytes" in the API docs, but the values are in - // mebibytes (1 MiB = 2^20 bytes). Multiply by 1<<20 to convert to the bytes used in - // hv.Status. - //nolint:gosec // flavor fields are small, non-negative counts. - required := map[hv1.ResourceName]resource.Quantity{ - "cpu": *resource.NewQuantity(int64(request.Flavor.VCPUs*request.NumInstances), resource.DecimalSI), - "memory": *resource.NewQuantity(int64(request.Flavor.MemoryMB*request.NumInstances*(1<<20)), resource.BinarySI), - } + // RequestedResources holds per-instance quantities in the units hv.Status reports + // (memory in bytes); TotalResources scales by NumInstances for the full request. + required := request.TotalResources() hostsEncountered := make(map[string]struct{}) for host, usage := range usageByHost { @@ -227,8 +221,8 @@ func (s *FilterHasEnoughCapacity) Run(traceLog logr.Logger, request compute.Plac traceLog.V(1).Info( "filtering host due to insufficient capacity", "host", host, - "requested_cpus", request.Flavor.VCPUs, - "requested_memory_mb", request.Flavor.MemoryMB, + "requested_cpus", request.RequestedResources.VCPUs(), + "requested_memory_bytes", request.RequestedResources.MemoryBytes(), "num_instances", request.NumInstances, "available_cpus", freeCPU.String(), "available_memory", freeMemory.String(), @@ -238,9 +232,9 @@ func (s *FilterHasEnoughCapacity) Run(traceLog logr.Logger, request compute.Plac } traceLog.V(1).Info( "host has enough capacity", "host", host, - "requested_cpus", request.Flavor.VCPUs, + "requested_cpus", request.RequestedResources.VCPUs(), "available_cpus", freeCPU.String(), - "requested_memory_mb", request.Flavor.MemoryMB, + "requested_memory_bytes", request.RequestedResources.MemoryBytes(), "available_memory", freeMemory.String(), ) } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index 169f845f7..c85488678 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -260,18 +260,15 @@ func newNovaRequestWithIntent(instanceUUID, projectID, flavorGroup string, vcpus } return compute.PlacementRequest{ - ProjectID: projectID, - InstanceUUID: instanceUUID, - NumInstances: 1, - Intent: intent, - HardwareVersion: flavorGroup, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: uint64(vcpus), //nolint:gosec // test code - MemoryMB: memoryMB, - }, - Hosts: hostList, - Weights: weights, + ProjectID: projectID, + InstanceUUID: instanceUUID, + NumInstances: 1, + Intent: intent, + HardwareVersion: flavorGroup, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(uint64(vcpus), memoryMB), //nolint:gosec // test code + Hosts: hostList, + Weights: weights, } } diff --git a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go index 697701b6a..340cfd2ab 100644 --- a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go +++ b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go @@ -179,12 +179,15 @@ func (s *FilterQuotaEnforcement) Run(traceLog logr.Logger, request compute.Place if numInstances == 0 { numInstances = 1 } - memoryMB := request.Flavor.MemoryMB - vcpus := request.Flavor.VCPUs + // RequestedResources holds per-instance memory in bytes; convert to MiB for the + // liquid RAM math below. + memoryMB := request.RequestedResources.MemoryBytes() / (1 << 20) + vcpus := request.RequestedResources.VCPUs() + instances := int64(numInstances) // MB to GiB: using 1024 as divisor is slightly conservative (overestimates GiB needed). - requestRAM := int64(math.Ceil(float64(memoryMB*numInstances) / 1024.0)) - requestCores := int64(vcpus * numInstances) //nolint:gosec // VCPUs from flavor specs, realistically bounded - requestInstances := int64(numInstances) + requestRAM := int64(math.Ceil(float64(memoryMB*instances) / 1024.0)) + requestCores := vcpus * instances + requestInstances := instances // Step 4: Derive the liquid resource names. resourceRAM := "hw_version_" + hwVersion + "_ram" diff --git a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement_test.go b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement_test.go index bb9b3d856..451b288a7 100644 --- a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement_test.go @@ -47,15 +47,12 @@ func intentFromHint(hints map[string]any) v1alpha1.SchedulingIntent { func makeQuotaEnforcementRequest(projectID, az, hwVersion string, memoryMB, numInstances uint64, hints map[string]any) compute.PlacementRequest { return compute.PlacementRequest{ - ProjectID: projectID, - AvailabilityZone: az, - NumInstances: numInstances, - Intent: intentFromHint(hints), - HardwareVersion: hwVersion, - Flavor: compute.FlavorSpec{ - VCPUs: 4, - MemoryMB: memoryMB, - }, + ProjectID: projectID, + AvailabilityZone: az, + NumInstances: numInstances, + Intent: intentFromHint(hints), + HardwareVersion: hwVersion, + RequestedResources: compute.NewResourceDemand(4, memoryMB), Hosts: []compute.Host{ {ComputeHost: "host1"}, {ComputeHost: "host2"}, diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go index 9f19b47d1..5f860ef6f 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go @@ -13,7 +13,6 @@ import ( "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/api/resource" ) type KVMBinpackStepOpts struct { @@ -85,7 +84,7 @@ func (s *KVMBinpackStep) Run(traceLog logr.Logger, request compute.PlacementRequ } hvsByName[hv.Name] = hv } - vmResources := s.calcVMResources(request) + vmResources := request.TotalResources() for host := range result.Activations { hv, ok := hvsByName[host] @@ -140,20 +139,6 @@ func (s *KVMBinpackStep) Run(traceLog logr.Logger, request compute.PlacementRequ return result, nil } -// calcVMResources calculates the total resource requests for the VM to be scheduled. -func (s *KVMBinpackStep) calcVMResources(req compute.PlacementRequest) map[hv1.ResourceName]resource.Quantity { - resources := make(map[hv1.ResourceName]resource.Quantity) - resourcesMemBytes := int64(req.Flavor.MemoryMB) * (1024 * 1024) //nolint:gosec // memory values are bounded by Nova - resourcesMemBytes *= int64(req.NumInstances) //nolint:gosec // instance count is bounded by Nova - resources[hv1.ResourceMemory] = *resource. - NewQuantity(resourcesMemBytes, resource.BinarySI) - resourcesCPU := int64(req.Flavor.VCPUs) //nolint:gosec // vCPU values are bounded by Nova - resourcesCPU *= int64(req.NumInstances) //nolint:gosec // instance count is bounded by Nova - resources[hv1.ResourceCPU] = *resource. - NewQuantity(resourcesCPU, resource.DecimalSI) - return resources -} - func init() { Index["kvm_binpack"] = func() NovaWeigher { return &KVMBinpackStep{} } } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go index d7df0dc22..61b3b0e11 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go @@ -46,16 +46,13 @@ func newBinpackRequest(memoryMB, vcpus, numInstances uint64, hosts []string) com } return compute.PlacementRequest{ - ProjectID: "project-A", - InstanceUUID: "instance-123", - NumInstances: numInstances, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: vcpus, - MemoryMB: memoryMB, - }, - Hosts: hostList, - Weights: weights, + ProjectID: "project-A", + InstanceUUID: "instance-123", + NumInstances: numInstances, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(vcpus, memoryMB), + Hosts: hostList, + Weights: weights, } } @@ -471,7 +468,7 @@ func TestKVMBinpackStep_Run(t *testing.T) { } } -func TestKVMBinpackStep_calcVMResources(t *testing.T) { +func TestKVMBinpackStep_TotalResources(t *testing.T) { tests := []struct { name string request compute.PlacementRequest @@ -512,8 +509,7 @@ func TestKVMBinpackStep_calcVMResources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - step := &KVMBinpackStep{} - resources := step.calcVMResources(tt.request) + resources := tt.request.TotalResources() memResource, ok := resources[hv1.ResourceMemory] if !ok { diff --git a/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation.go b/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation.go index 2c626b7ad..8d960a761 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation.go @@ -71,7 +71,7 @@ func (s *KVMCommittedResourceReservationStep) Run(traceLog logr.Logger, request } // Flavor memory in bytes for free-capacity check. - flavorMemoryBytes := int64(request.Flavor.MemoryMB) * 1024 * 1024 //nolint:gosec + flavorMemoryBytes := request.RequestedResources.MemoryBytes() var reservations v1alpha1.ReservationList if err := s.Client.List(context.Background(), &reservations); err != nil { diff --git a/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation_test.go index 84a0befe4..6daf77a09 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_committed_resource_reservation_test.go @@ -70,14 +70,12 @@ func newCRRequest(hosts []string) compute.PlacementRequest { weights[h] = 1.0 } return compute.PlacementRequest{ - ProjectID: "project-A", - AvailabilityZone: "qa-de-1a", - ResourceGroup: "group-2101", - Flavor: compute.FlavorSpec{ - MemoryMB: 8 * 1024, - }, - Hosts: hostList, - Weights: weights, + ProjectID: "project-A", + AvailabilityZone: "qa-de-1a", + ResourceGroup: "group-2101", + RequestedResources: compute.NewResourceDemand(0, 8*1024), + Hosts: hostList, + Weights: weights, } } @@ -212,14 +210,12 @@ func TestKVMCommittedResourceReservationStep_Run(t *testing.T) { newCRReservation("res-1", "host1", "project-A", "group-2101", "qa-de-1a", "16Gi"), }, request: compute.PlacementRequest{ - ProjectID: "project-A", - AvailabilityZone: "qa-de-1a", - Intent: api.ReserveForFailoverIntent, - ResourceGroup: "group-2101", - Flavor: compute.FlavorSpec{ - MemoryMB: 4096, - }, - Hosts: []compute.Host{{ComputeHost: "host1"}, {ComputeHost: "host2"}}, + ProjectID: "project-A", + AvailabilityZone: "qa-de-1a", + Intent: api.ReserveForFailoverIntent, + ResourceGroup: "group-2101", + RequestedResources: compute.NewResourceDemand(0, 4096), + Hosts: []compute.Host{{ComputeHost: "host1"}, {ComputeHost: "host2"}}, }, opts: KVMCommittedResourceReservationOpts{ReservationHostWeight: floatPtr(1.0), DefaultHostWeight: floatPtr(0.1)}, expectedWeights: map[string]float64{"host1": 0.0, "host2": 0.0}, diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go index d4de183d6..c47ae5c8c 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go @@ -120,15 +120,12 @@ func newNovaRequest(instanceUUID string, evacuation bool, hosts []string) comput } req := compute.PlacementRequest{ - ProjectID: "project-A", - InstanceUUID: instanceUUID, - NumInstances: 1, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: 4, - MemoryMB: parseMemoryToMB("8Gi"), - }, - Hosts: hostList, + ProjectID: "project-A", + InstanceUUID: instanceUUID, + NumInstances: 1, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(4, parseMemoryToMB("8Gi")), + Hosts: hostList, } if evacuation { req.Intent = api.EvacuateIntent diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go index c2bdf5996..208ca21d5 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go @@ -63,18 +63,15 @@ func newFailoverReservationRequest(resourceGroup string, hosts []string) compute } return compute.PlacementRequest{ - ProjectID: "project-A", - InstanceUUID: "test-instance", - NumInstances: 1, - Intent: api.ReserveForFailoverIntent, - ResourceGroup: resourceGroup, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: 4, - MemoryMB: 8192, - }, - Hosts: hostList, - Weights: weights, + ProjectID: "project-A", + InstanceUUID: "test-instance", + NumInstances: 1, + Intent: api.ReserveForFailoverIntent, + ResourceGroup: resourceGroup, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(4, 8192), + Hosts: hostList, + Weights: weights, } } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go index ec360c361..623be4b0a 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go @@ -40,15 +40,12 @@ func newInstanceGroupRequest(policy string, members, hosts []string) compute.Pla } req := compute.PlacementRequest{ - ProjectID: "project-A", - InstanceUUID: "new-instance-uuid", - NumInstances: 1, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: 4, - MemoryMB: 8192, - }, - Hosts: hostList, + ProjectID: "project-A", + InstanceUUID: "new-instance-uuid", + NumInstances: 1, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(4, 8192), + Hosts: hostList, } // Only set InstanceGroup if policy is provided diff --git a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go index c6902a101..84d628f1e 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go @@ -42,16 +42,13 @@ func newPreferSmallerHostsRequest(hosts []string) compute.PlacementRequest { } return compute.PlacementRequest{ - ProjectID: "project-A", - InstanceUUID: "instance-123", - NumInstances: 1, - HypervisorFamily: compute.HypervisorFamilyKVM, - Flavor: compute.FlavorSpec{ - VCPUs: 4, - MemoryMB: 8192, - }, - Hosts: hostList, - Weights: weights, + ProjectID: "project-A", + InstanceUUID: "instance-123", + NumInstances: 1, + HypervisorFamily: compute.HypervisorFamilyKVM, + RequestedResources: compute.NewResourceDemand(4, 8192), + Hosts: hostList, + Weights: weights, } } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go index 27bc33f39..10124cb00 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go @@ -15,6 +15,7 @@ import ( "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" sched "github.com/cobaltcore-dev/cortex/internal/scheduling/compute" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" + hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "sigs.k8s.io/controller-runtime/pkg/client" @@ -26,7 +27,7 @@ type VMwareBinpackStepOpts struct { // node's resource utilizations after placing the VM. // If a resource is not specified, is ignored in the score calculation // (equivalent to a weight of 0). - ResourceWeights map[corev1.ResourceName]float64 `json:"resourceWeights"` + ResourceWeights map[hv1.ResourceName]float64 `json:"resourceWeights"` } // Validate the options to ensure they are correct before running the weigher. @@ -34,9 +35,9 @@ func (o VMwareBinpackStepOpts) Validate() error { if len(o.ResourceWeights) == 0 { return errors.New("at least one resource weight must be specified") } - supportedResources := []corev1.ResourceName{ - corev1.ResourceMemory, - corev1.ResourceCPU, + supportedResources := []hv1.ResourceName{ + hv1.ResourceMemory, + hv1.ResourceCPU, } for resourceName, value := range o.ResourceWeights { if !slices.Contains(supportedResources, resourceName) { @@ -107,7 +108,7 @@ func (s *VMwareBinpackStep) Run(traceLog logr.Logger, request sched.PlacementReq for _, hostUtilization := range hostUtilizations { utilizationByHost[hostUtilization.ComputeHost] = hostUtilization } - vmResources := s.calcVMResources(request) + vmResources := request.TotalResources() for host := range result.Activations { utilization, ok := utilizationByHost[host] @@ -163,41 +164,27 @@ func (s *VMwareBinpackStep) Run(traceLog logr.Logger, request sched.PlacementReq } // calcHostCapacity calculates the total capacity of the host. -func (s *VMwareBinpackStep) calcHostCapacity(hostUtilization compute.HostUtilization) map[corev1.ResourceName]resource.Quantity { - resources := make(map[corev1.ResourceName]resource.Quantity) +func (s *VMwareBinpackStep) calcHostCapacity(hostUtilization compute.HostUtilization) map[hv1.ResourceName]resource.Quantity { + resources := make(map[hv1.ResourceName]resource.Quantity) capaMemoryBytes := int64(hostUtilization.TotalRAMAllocatableMB) * (1 << 20) - resources[corev1.ResourceMemory] = *resource. + resources[hv1.ResourceMemory] = *resource. NewQuantity(capaMemoryBytes, resource.BinarySI) capaCPU := int64(hostUtilization.TotalVCPUsAllocatable) - resources[corev1.ResourceCPU] = *resource. + resources[hv1.ResourceCPU] = *resource. NewQuantity(capaCPU, resource.DecimalSI) return resources } // calcHostAllocation calculates the total allocated resources on the host. -func (s *VMwareBinpackStep) calcHostAllocation(hostUtilization compute.HostUtilization) map[corev1.ResourceName]resource.Quantity { - resources := make(map[corev1.ResourceName]resource.Quantity) - resources[corev1.ResourceMemory] = *resource. +func (s *VMwareBinpackStep) calcHostAllocation(hostUtilization compute.HostUtilization) map[hv1.ResourceName]resource.Quantity { + resources := make(map[hv1.ResourceName]resource.Quantity) + resources[hv1.ResourceMemory] = *resource. NewQuantity(int64(hostUtilization.RAMUsedMB)*(1<<20), resource.BinarySI) - resources[corev1.ResourceCPU] = *resource. + resources[hv1.ResourceCPU] = *resource. NewQuantity(int64(hostUtilization.VCPUsUsed), resource.DecimalSI) return resources } -// calcVMResources calculates the total resource requests for the VM to be scheduled. -func (s *VMwareBinpackStep) calcVMResources(req sched.PlacementRequest) map[corev1.ResourceName]resource.Quantity { - resources := make(map[corev1.ResourceName]resource.Quantity) - resourcesMemBytes := int64(req.Flavor.MemoryMB) * (1 << 20) //nolint:gosec // memory values are bounded by Nova - resourcesMemBytes *= int64(req.NumInstances) //nolint:gosec // instance count is bounded by Nova - resources[corev1.ResourceMemory] = *resource. - NewQuantity(resourcesMemBytes, resource.BinarySI) - resourcesCPU := int64(req.Flavor.VCPUs) //nolint:gosec // vCPU values are bounded by Nova - resourcesCPU *= int64(req.NumInstances) //nolint:gosec // instance count is bounded by Nova - resources[corev1.ResourceCPU] = *resource. - NewQuantity(resourcesCPU, resource.DecimalSI) - return resources -} - func init() { Index["vmware_binpack"] = func() NovaWeigher { return &VMwareBinpackStep{} } } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go index 7d3226256..1f66cb147 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go @@ -10,7 +10,7 @@ import ( "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" sched "github.com/cobaltcore-dev/cortex/internal/scheduling/compute" - corev1 "k8s.io/api/core/v1" + hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -24,9 +24,9 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "valid opts with memory and cpu", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceMemory: 1.0, - corev1.ResourceCPU: 1.0, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceMemory: 1.0, + hv1.ResourceCPU: 1.0, }, }, wantError: false, @@ -34,8 +34,8 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "valid opts with only memory", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceMemory: 2.0, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceMemory: 2.0, }, }, wantError: false, @@ -43,8 +43,8 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "valid opts with only cpu", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceCPU: 0.5, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceCPU: 0.5, }, }, wantError: false, @@ -52,15 +52,15 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "invalid opts - empty resource weights", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{}, + ResourceWeights: map[hv1.ResourceName]float64{}, }, wantError: true, }, { name: "invalid opts - unsupported resource", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceStorage: 1.0, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceName("storage"): 1.0, }, }, wantError: true, @@ -68,8 +68,8 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "invalid opts - zero weight", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceMemory: 0.0, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceMemory: 0.0, }, }, wantError: true, @@ -77,8 +77,8 @@ func TestVMwareBinpackStepOpts_Validate(t *testing.T) { { name: "invalid opts - negative weight", opts: VMwareBinpackStepOpts{ - ResourceWeights: map[corev1.ResourceName]float64{ - corev1.ResourceCPU: -1.0, + ResourceWeights: map[hv1.ResourceName]float64{ + hv1.ResourceCPU: -1.0, }, }, wantError: true, @@ -142,9 +142,9 @@ func TestVMwareBinpackStep_Run(t *testing.T) { } step := &VMwareBinpackStep{} - step.Options.ResourceWeights = map[corev1.ResourceName]float64{ - corev1.ResourceMemory: 1.0, - corev1.ResourceCPU: 1.0, + step.Options.ResourceWeights = map[hv1.ResourceName]float64{ + hv1.ResourceMemory: 1.0, + hv1.ResourceCPU: 1.0, } step.Client = fake.NewClientBuilder(). WithScheme(scheme). @@ -163,11 +163,8 @@ func TestVMwareBinpackStep_Run(t *testing.T) { { name: "binpack prefers hosts with higher utilization", request: sched.PlacementRequest{ - NumInstances: 1, - Flavor: sched.FlavorSpec{ - MemoryMB: 1000, // 1GB - VCPUs: 1, - }, + NumInstances: 1, + RequestedResources: sched.NewResourceDemand(1, 1000), // 1GB Hosts: []sched.Host{ {ComputeHost: "host1"}, {ComputeHost: "host2"}, @@ -182,11 +179,8 @@ func TestVMwareBinpackStep_Run(t *testing.T) { { name: "handles missing host data gracefully", request: sched.PlacementRequest{ - NumInstances: 1, - Flavor: sched.FlavorSpec{ - MemoryMB: 1000, - VCPUs: 1, - }, + NumInstances: 1, + RequestedResources: sched.NewResourceDemand(1, 1000), Hosts: []sched.Host{ {ComputeHost: "host1"}, {ComputeHost: "unknown_host"}, // No utilization data @@ -230,7 +224,7 @@ func TestVMwareBinpackStep_CalcHostCapacity(t *testing.T) { // Memory capacity: 6000 MiB in bytes expectedMemoryBytes := int64(6000) * (1 << 20) - memoryCapacity := capacity[corev1.ResourceMemory] + memoryCapacity := capacity[hv1.ResourceMemory] if memoryCapacity.Value() != expectedMemoryBytes { t.Errorf("expected memory capacity %d, got %d", expectedMemoryBytes, memoryCapacity.Value()) @@ -238,7 +232,7 @@ func TestVMwareBinpackStep_CalcHostCapacity(t *testing.T) { // CPU capacity: 6 expectedCPU := int64(6) - cpuCapacity := capacity[corev1.ResourceCPU] + cpuCapacity := capacity[hv1.ResourceCPU] if cpuCapacity.Value() != expectedCPU { t.Errorf("expected CPU capacity %d, got %d", expectedCPU, cpuCapacity.Value()) @@ -260,7 +254,7 @@ func TestVMwareBinpackStep_CalcHostAllocation(t *testing.T) { // Memory allocation: 4000 MiB in bytes expectedMemoryBytes := int64(4000) * (1 << 20) - memoryAllocation := allocation[corev1.ResourceMemory] + memoryAllocation := allocation[hv1.ResourceMemory] if memoryAllocation.Value() != expectedMemoryBytes { t.Errorf("expected memory allocation %d, got %d", expectedMemoryBytes, memoryAllocation.Value()) @@ -268,16 +262,14 @@ func TestVMwareBinpackStep_CalcHostAllocation(t *testing.T) { // CPU allocation: 4 expectedCPU := int64(4) - cpuAllocation := allocation[corev1.ResourceCPU] + cpuAllocation := allocation[hv1.ResourceCPU] if cpuAllocation.Value() != expectedCPU { t.Errorf("expected CPU allocation %d, got %d", expectedCPU, cpuAllocation.Value()) } } -func TestVMwareBinpackStep_CalcVMResources(t *testing.T) { - step := &VMwareBinpackStep{} - +func TestVMwareBinpackStep_TotalResources(t *testing.T) { tests := []struct { name string request sched.PlacementRequest @@ -287,11 +279,8 @@ func TestVMwareBinpackStep_CalcVMResources(t *testing.T) { { name: "single instance", request: sched.PlacementRequest{ - NumInstances: 1, - Flavor: sched.FlavorSpec{ - MemoryMB: 2048, - VCPUs: 4, - }, + NumInstances: 1, + RequestedResources: sched.NewResourceDemand(4, 2048), }, expectedMemory: 2048 * (1 << 20), // 2048 MiB in bytes expectedCPU: 4, @@ -299,11 +288,8 @@ func TestVMwareBinpackStep_CalcVMResources(t *testing.T) { { name: "multiple instances", request: sched.PlacementRequest{ - NumInstances: 3, - Flavor: sched.FlavorSpec{ - MemoryMB: 1024, - VCPUs: 2, - }, + NumInstances: 3, + RequestedResources: sched.NewResourceDemand(2, 1024), }, expectedMemory: 3 * 1024 * (1 << 20), // 3 instances * 1024 MiB in bytes expectedCPU: 3 * 2, // 3 instances * 2 vCPUs @@ -312,15 +298,15 @@ func TestVMwareBinpackStep_CalcVMResources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - resources := step.calcVMResources(tt.request) + resources := tt.request.TotalResources() - memoryResources := resources[corev1.ResourceMemory] + memoryResources := resources[hv1.ResourceMemory] if memoryResources.Value() != tt.expectedMemory { t.Errorf("expected memory %d, got %d", tt.expectedMemory, memoryResources.Value()) } - cpuResources := resources[corev1.ResourceCPU] + cpuResources := resources[hv1.ResourceCPU] if cpuResources.Value() != tt.expectedCPU { t.Errorf("expected CPU %d, got %d", tt.expectedCPU, cpuResources.Value()) diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index 4cba8577b..75734b4c8 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -843,10 +843,7 @@ func (c *Reconciler) probeScheduler( ForbiddenTraits: forbiddenTraits, RequestedCapabilities: reservations.HypervisorTypeCapability(flavor.ExtraSpecs, false), AcceleratorsRequested: sched.RequestsAccelerators(flavor.ExtraSpecs), - Flavor: sched.FlavorSpec{ - VCPUs: flavor.VCPUs, - MemoryMB: flavor.MemoryMB, - }, + RequestedResources: sched.NewResourceDemand(flavor.VCPUs, flavor.MemoryMB), Options: scheduling.Options{ ReadOnly: true, AssumeEmptyHosts: ignoreAllocations, diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index 63b247342..54df7a68a 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -331,10 +331,7 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr ForbiddenTraits: forbiddenTraits, RequestedCapabilities: reservations.HypervisorTypeCapability(flavorDetails.ExtraSpecs, false), AcceleratorsRequested: compute.RequestsAccelerators(flavorDetails.ExtraSpecs), - Flavor: compute.FlavorSpec{ - VCPUs: flavorDetails.VCPUs, - MemoryMB: flavorDetails.MemoryMB, - }, + RequestedResources: compute.NewResourceDemand(flavorDetails.VCPUs, flavorDetails.MemoryMB), Options: scheduling.Options{ ReadOnly: false, // mutates state (reservation placement) LockReservations: true, // don't unlock CR reservations; finding a slot, not placing a VM diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index d1a77d98f..98bc46f12 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -125,10 +125,7 @@ func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx contex ForbiddenTraits: forbiddenTraits, RequestedCapabilities: reservations.HypervisorTypeCapability(vm.FlavorExtraSpecs, true), AcceleratorsRequested: compute.RequestsAccelerators(vm.FlavorExtraSpecs), - Flavor: compute.FlavorSpec{ - VCPUs: resSpec.VCPUs, - MemoryMB: resSpec.MemoryMB, - }, + RequestedResources: compute.NewResourceDemand(resSpec.VCPUs, resSpec.MemoryMB), } logger.V(1).Info("scheduling failover reservation", @@ -282,10 +279,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( ForbiddenTraits: forbiddenTraits, RequestedCapabilities: reservations.HypervisorTypeCapability(flavorExtraSpecs, true), AcceleratorsRequested: compute.RequestsAccelerators(flavorExtraSpecs), - Flavor: compute.FlavorSpec{ - VCPUs: vcpus, - MemoryMB: memoryMB, - }, + RequestedResources: compute.NewResourceDemand(vcpus, memoryMB), } logger.V(1).Info("validating VM via scheduler evacuation", diff --git a/internal/scheduling/reservations/failoverhost/pathfinding.go b/internal/scheduling/reservations/failoverhost/pathfinding.go index e61b6fde7..ce533e097 100644 --- a/internal/scheduling/reservations/failoverhost/pathfinding.go +++ b/internal/scheduling/reservations/failoverhost/pathfinding.go @@ -282,10 +282,7 @@ func (f *evacuationPathFinder) fetchSeedCandidates(ctx context.Context, seed See // set (no default needed). RequestedCapabilities: reservations.HypervisorTypeCapability(seed.Flavor.ExtraSpecs, false), AcceleratorsRequested: compute.RequestsAccelerators(seed.Flavor.ExtraSpecs), - Flavor: compute.FlavorSpec{ - VCPUs: seed.Flavor.VCPUs, - MemoryMB: seed.Flavor.MemoryMB, - }, + RequestedResources: compute.NewResourceDemand(seed.Flavor.VCPUs, seed.Flavor.MemoryMB), Options: scheduling.Options{ // Read-only probe: don't write any resources. ReadOnly additionally // requires SkipInflight and SkipCommittedResourceTracking, else the