From 71501aa7001ac9ba76665925d3ecfb7a26fa33ae Mon Sep 17 00:00:00 2001 From: Marcel <156897072+mblos@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:07:57 +0100 Subject: [PATCH 1/3] fix: introduce ValidateFailoverReservationIntent to silence noisy filter_external_customer errors on failover validation Signed-off-by: Marcel <156897072+mblos@users.noreply.github.com> --- api/external/nova/messages.go | 7 +++++++ .../filters/filter_external_customer.go | 1 + .../filters/filter_external_customer_test.go | 21 +++++++++++++++++++ .../filters/filter_has_enough_capacity.go | 2 +- .../failover/reservation_scheduling.go | 2 +- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/api/external/nova/messages.go b/api/external/nova/messages.go index 33ed4c66f..1a2d3573f 100644 --- a/api/external/nova/messages.go +++ b/api/external/nova/messages.go @@ -241,6 +241,10 @@ const ( ReserveForCommittedResourceIntent v1alpha1.SchedulingIntent = "reserve_for_committed_resource" // CapacityProbeIntent indicates that the request is a synthetic capacity probe (not a real VM placement). CapacityProbeIntent v1alpha1.SchedulingIntent = "capacity_probe" + // ValidateFailoverReservationIntent indicates that cortex is validating whether a VM + // can use a specific failover reservation host. Uses EvacuateIntent semantics in + // filter_has_enough_capacity to unlock reservation resources for the VM. + ValidateFailoverReservationIntent v1alpha1.SchedulingIntent = "validate_failover_reservation" // HintKeyResourceGroup is the scheduler hint key used to pass the resource group // (e.g., flavor group name) for failover reservation scheduling. @@ -289,6 +293,9 @@ func (req ExternalSchedulerRequest) GetIntent() (v1alpha1.SchedulingIntent, erro // Used by cortex capacity probe controller case "capacity_probe": return CapacityProbeIntent, nil + // Used by cortex failover reservation controller (host validation check) + case "validate_failover_reservation": + return ValidateFailoverReservationIntent, nil default: return CreateIntent, nil } diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer.go b/internal/scheduling/nova/plugins/filters/filter_external_customer.go index 19a412797..90c4b6609 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer.go @@ -43,6 +43,7 @@ func (s *FilterExternalCustomerStep) Run(traceLog logr.Logger, request api.Exter if intent, err := request.GetIntent(); err == nil && slices.Contains([]v1alpha1.SchedulingIntent{ api.ReserveForFailoverIntent, api.ReuseFailoverReservationIntent, + api.ValidateFailoverReservationIntent, api.CapacityProbeIntent, }, intent) { traceLog.V(1).Info("skipping external customer filter for non-placement intent", "intent", intent) diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go index 4d2ebe784..318616e12 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go @@ -380,6 +380,27 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) { expectedHosts: []string{"host1", "host3"}, filteredHosts: []string{}, }, + { + name: "ValidateFailoverReservationIntent - skips filter, all hosts pass (no domain_name hint)", + opts: FilterExternalCustomerStepOpts{ + CustomerDomainNamePrefixes: []string{"ext-"}, + }, + request: api.ExternalSchedulerRequest{ + Spec: api.NovaObject[api.NovaSpec]{ + Data: api.NovaSpec{ + SchedulerHints: map[string]any{ + "_nova_check_type": string(api.ValidateFailoverReservationIntent), + }, + }, + }, + Hosts: []api.ExternalSchedulerHost{ + {ComputeHost: "host1"}, + {ComputeHost: "host3"}, + }, + }, + expectedHosts: []string{"host1", "host3"}, + filteredHosts: []string{}, + }, { name: "ReserveForCommittedResourceIntent - domain filter applies (no domain_name hint → all hosts pass)", opts: FilterExternalCustomerStepOpts{ 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 308e3a185..631f839fb 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -187,7 +187,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog logr.Logger, request api.External intent, err := request.GetIntent() if err == nil { switch intent { - case api.EvacuateIntent: + case api.EvacuateIntent, api.ValidateFailoverReservationIntent: if reservation.Status.FailoverReservation != nil { if _, contained := reservation.Status.FailoverReservation.Allocations[request.Spec.Data.InstanceUUID]; contained { traceLog.V(1).Info("unlocking resources reserved by failover reservation for VM in allocations (evacuation)", diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index 2cae7105a..81140002d 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -257,7 +257,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( IgnoreHosts: []string{vm.CurrentHypervisor}, Pipeline: inferFailoverPipeline(flavorExtraSpecs), AvailabilityZone: vm.AvailabilityZone, - SchedulerHints: map[string]any{"_nova_check_type": string(api.EvacuateIntent)}, + SchedulerHints: map[string]any{"_nova_check_type": string(api.ValidateFailoverReservationIntent)}, ImageProperties: imagePropertiesFromFlavor(flavorExtraSpecs), } From 488c4df249ff7dad653a924c03a229eb9eaff7b6 Mon Sep 17 00:00:00 2001 From: Marcel <156897072+mblos@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:16:18 +0100 Subject: [PATCH 2/3] fix quota Signed-off-by: Marcel <156897072+mblos@users.noreply.github.com> --- .../scheduling/nova/plugins/filters/filter_quota_enforcement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go index 86d56e405..7433f8837 100644 --- a/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go +++ b/internal/scheduling/nova/plugins/filters/filter_quota_enforcement.go @@ -92,7 +92,7 @@ func (s *FilterQuotaEnforcement) Run(traceLog logr.Logger, request api.ExternalS traceLog.V(1).Info("skipping quota enforcement for non-consuming intent", "intent", intent) QuotaEnforcementMetricsSingleton.RecordDecision(mode, "accept_skipped_intent_migration", "", "", "") return result, nil - case api.ReserveForFailoverIntent, api.ReuseFailoverReservationIntent: + case api.ReserveForFailoverIntent, api.ReuseFailoverReservationIntent, api.ValidateFailoverReservationIntent: traceLog.V(1).Info("skipping quota enforcement for failover reservation intent") QuotaEnforcementMetricsSingleton.RecordDecision(mode, "accept_skipped_intent_internal", "", "", "") return result, nil From f56a0978cf2515a853fc54719fb192fed549e1d4 Mon Sep 17 00:00:00 2001 From: Marcel <156897072+mblos@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:22:59 +0100 Subject: [PATCH 3/3] fix e2e test Signed-off-by: Marcel <156897072+mblos@users.noreply.github.com> --- internal/scheduling/nova/e2e_checks.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/scheduling/nova/e2e_checks.go b/internal/scheduling/nova/e2e_checks.go index d8dc57d92..5bf59e8bf 100644 --- a/internal/scheduling/nova/e2e_checks.go +++ b/internal/scheduling/nova/e2e_checks.go @@ -340,6 +340,26 @@ func randomRequest(dc datacenter, seed int) api.ExternalSchedulerRequest { Hosts: hosts, Weights: weights, } + // Synthesize image properties from flavor extra specs so filter_image_properties + // can determine the hypervisor type. Mirrors imagePropertiesFromFlavor in reservation_scheduling.go. + switch strings.ToLower(extraSpecs["capabilities:hypervisor_type"]) { + case "qemu", "ch": + request.Spec.Data.Image = api.NovaObject[api.NovaImageMeta]{ + Data: api.NovaImageMeta{ + Properties: api.NovaObject[map[string]any]{ + Data: map[string]any{"img_hv_type": string(api.NovaImageMetaHVTypeKVM)}, + }, + }, + } + case "vmware vcenter server": + request.Spec.Data.Image = api.NovaObject[api.NovaImageMeta]{ + Data: api.NovaImageMeta{ + Properties: api.NovaObject[map[string]any]{ + Data: map[string]any{"hypervisor_type": string(api.NovaImageMetaHVTypeVMware)}, + }, + }, + } + } // Force to use the pipeline with all filters enabled for kvm flavors. hvType, err := request.GetHypervisorType() if err != nil {