Skip to content

Onboarding: re-validate the Nova identity and repair stale service IDs (nova-issues#786) - #380

Draft
fwiesel wants to merge 4 commits into
mainfrom
fix-786-stale-service-id
Draft

fwiesel wants to merge 4 commits into
mainfrom
fix-786-stale-service-id

Conversation

@fwiesel

@fwiesel fwiesel commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Fixes the incident class documented in nova-issues#786: after node replacement, hypervisor onboarding can adopt the leftover compute record of the dead agent and store its service ID. When the replacement agent registers, Nova flips the compute node to the new service — the stored ID points at an orphan, the enable step runs against the wrong service, the current compute service stays disabled, and the smoke test loops on No valid host until someone clears status.serviceId and enables the service by hand.

Stacked on #340 — merge #340 first. Until then this PR shows the onboarding SSA migration plus the fix; once #340 merges, the diff shrinks to the three fix commits.

What it does

  1. Add Nova identity selection that refuses stale compute records — openstack.SelectCurrentIdentity picks the authoritative hypervisor/service pair from the exact-hostname matches. Records whose service is not state=up (dead or replaced agents) are never adopted — not even as the only match, not even when they match the stored ID. The stored identity is kept among live duplicates to avoid flapping; unresolvable duplicates are surfaced as an error instead of being guessed.
  2. Onboarding: re-validate the Nova identity and repair stale service IDs — every reconcile of a not-yet-succeeded hypervisor resolves the current identity and repairs the stored IDs when they changed, restarting onboarding at the enable step so the current compute service gets enabled. A service that reports disabled during Testing/Handover routes back to the enable step (the compute/disable_new_services case) instead of burning scheduling rounds. An enable request hitting a vanished service re-resolves. Onboarding=False/Succeeded stays terminal — no Nova calls, no writes.
  3. Onboarding: drop status writes for replaced Hypervisor objects — node replacement deletes and recreates the Hypervisor under the same name with a new UID. The API server does not enforce a UID precondition on subresource applies (verified and pinned by a spec, so a future enforcing release gets noticed), so the apply path checks the observed object's existence and UID client-side and drops the write when the object was replaced. The residual informer-cache race is backstopped by the identity repair from (2).

Verification

  • Failing tests first, then implementation, per commit; all suites green (go test ./...), golangci-lint clean.
  • New specs cover the incident regression (adoption of a down record; stale-ID repair with the enable hitting only the new service), the disabled-service preflight, ambiguity handling, hostname-exact matching, terminal Succeeded, and the cross-replacement apply behavior.
  • Draft because QA validation on a real cluster (cc-d1-qa-de-1) is still pending: replace a node (or stop agent → delete service → restart agent) and confirm onboarding self-heals without the manual workaround.

Not addressed here

The RCA's open question — which request first wrote the stale 6b4fccf9-… onto the new CR — remains unconfirmed; this fix is robust regardless of the answer. Follow-ups: the same UID guard for the eviction controller's status applies, the maintenance controller trusting its cached HypervisorDisabled condition, and the offboarding agent-pod-gate investigation.

Replace PatchHypervisorStatusWithRetry with Status().Apply(). The
Nova ID lookup is inlined into Reconcile and combined with the initial
condition in a single apply, avoiding two applies from the same field
manager in one reconcile cycle which would cause SSA to prune the IDs.
HypervisorID and ServiceID are included in every subsequent apply to
retain ownership.

The status config is built once at the top of Reconcile and threaded
through to all sub-functions (abortOnboarding, initialOnboarding,
smokeTest, completeOnboarding) which mutate it directly. An apply
closure is also passed so each function calls apply() when it has
determined the desired condition, rather than building and applying
the config itself. The flow becomes fully declarative: compute desired
state, then apply once.

Seed only the condition owned by this controller (ConditionTypeOnboarding)
so SSA does not claim ownership over conditions written by other
controllers.
During node replacement, Nova can briefly (or persistently) hold compute
records whose agent is gone. Onboarding must not adopt such a record: the
stored identity goes stale the moment the replacement agent registers,
leaving the new compute service disabled and the smoke test looping on
'No valid host' (nova-issues#786).

SelectCurrentIdentity picks the authoritative hypervisor/service pair from
the exact-hostname matches: records without a live service are skipped,
the stored identity is kept among live duplicates to avoid flapping, and
unresolvable duplicates are surfaced instead of guessed.
Onboarding resolved the hypervisor/service pair exactly once (while the
fields were empty) and then trusted it forever. When a node is replaced,
the leftover record of the dead agent can be adopted before the
replacement registers, and the compute node later flips to the new
service: the stored serviceId then points at an orphan, the enable step
runs against the wrong service, the current service stays disabled and
the smoke test loops on 'No valid host' (nova-issues#786).

Every reconcile of an incompletely onboarded hypervisor now resolves the
current identity and repairs the stored IDs, restarting onboarding at
the enable step. A service that reports disabled during Testing or
Handover routes back to the enable step as well, instead of burning
scheduling rounds on a host that cannot take the test VM. Records
without a live service are never adopted; an enable request that hits a
vanished service re-resolves instead of erroring. Successfully onboarded
hypervisors are terminal and keep their identity untouched.
Node replacement deletes the Hypervisor object and recreates it under
the same name with a new UID. A reconcile that is still in flight then
writes status computed for the old object, and since the API server does
not enforce a UID precondition on subresource applies, the write merges
into the replacement object.

The apply path now checks that the observed object still exists and
still carries the same UID before applying. It is a regular method that
the sub-functions call directly, replacing the closure that was threaded
through them as a parameter. The check reads the informer cache, so a
replacement that is not yet visible can still slip through; the identity
re-validation on the replacement's next reconcile repairs such a write.
The unenforced-UID behavior of subresource applies is pinned by a spec
so a future enforcing release gets noticed.
@fwiesel
fwiesel force-pushed the fix-786-stale-service-id branch from b2f3e90 to e1655f4 Compare October 30, 2026 17:20
@github-actions

Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/controller 76.16% (+1.39%) 👍
github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack 74.88% (+1.96%) 👍
Coverage details

Compared to commit f5172c4 (run #41086905849)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/controller/onboarding_controller.go 66.67% (+5.22%) 258 (+9) 172 (+19) 86 (-10) 👍
github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack/identity.go 100.00% (+100.00%) 15 (+15) 15 (+15) 0 🌟

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files:

  • github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/controller/onboarding_controller_test.go
  • github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack/identity_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant