From d9ce2e3b380342b3833c6c387c408f3040b37b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Wed, 8 Jul 2026 07:59:07 +0200 Subject: [PATCH] Poll for port/volume status when attached but not yet transitioned After a port is attached to a server, its Neutron status transitions asynchronously from DOWN to ACTIVE. Similarly, a volume transitions from available to in-use after attachment. The cross-controller signaling via serverTo{Port,Volume}MapFunc is susceptible to a race with the controller's own status write. Add defense-in-depth polling: when the port has a device_id but status is still DOWN, or when a volume has attachments but status is not in-use, schedule a re-fetch via WaitingOnOpenStack. This ensures the controller picks up the status transition regardless of whether the cross-controller signal is lost. Fixes #842 --- internal/controllers/port/actuator.go | 15 +++++++++++++++ internal/controllers/volume/actuator.go | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internal/controllers/port/actuator.go b/internal/controllers/port/actuator.go index a602d2d69..aec27a5c2 100644 --- a/internal/controllers/port/actuator.go +++ b/internal/controllers/port/actuator.go @@ -396,6 +396,21 @@ func (actuator portActuator) checkAttachedServer(ctx context.Context, obj orcObj } } + // When the port is attached to a device but still reports DOWN, + // the Neutron status has not yet transitioned to ACTIVE (e.g. + // OVN is still binding the port). serverToPortMapFunc also + // detects this and triggers a reconcile, but it races with the + // port controller's own status write: the controller may + // overwrite Progressing=True with Progressing=False before the + // port becomes ACTIVE. Poll here so we keep Progressing=True + // until the transition completes. + if osResource.Status == PortStatusDown { + log.V(logging.Verbose).Info("port needs reconciliation: attached to server but status is DOWN", + "port", obj.Name, + "status", osResource.Status) + return progress.WaitingOnOpenStack(progress.WaitingOnReady, serverBuildPollingPeriod) + } + return nil } diff --git a/internal/controllers/volume/actuator.go b/internal/controllers/volume/actuator.go index 3b086ccc3..8d3926277 100644 --- a/internal/controllers/volume/actuator.go +++ b/internal/controllers/volume/actuator.go @@ -283,10 +283,31 @@ func handleDescriptionUpdate(updateOpts *volumes.UpdateOpts, resource *resourceS func (actuator volumeActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { return []resourceReconciler{ + actuator.checkAttachmentStatus, actuator.updateResource, }, nil } +func (volumeActuator) checkAttachmentStatus(ctx context.Context, _ orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + + // When the volume has attachments but Cinder still reports + // "available" rather than "in-use", the status transition has + // not completed yet. serverToVolumeMapFunc also detects this + // and triggers a reconcile, but it races with the volume + // controller's own status write: the controller may overwrite + // Progressing=True with Progressing=False before the volume + // becomes in-use. Poll here so we keep Progressing=True until + // the transition completes. + if len(osResource.Attachments) > 0 && osResource.Status != VolumeStatusInUse { + log.V(logging.Verbose).Info("volume needs reconciliation: attached to server but status is not in-use", + "status", osResource.Status) + return progress.WaitingOnOpenStack(progress.WaitingOnReady, volumeAvailablePollingPeriod) + } + + return nil +} + type volumeHelperFactory struct{} var _ helperFactory = volumeHelperFactory{}