Skip to content
This repository was archived by the owner on Aug 15, 2026. It is now read-only.

OSAC-3548: Fix IsRestartComplete to wait for host to power on - #83

Merged
openshift-merge-bot[bot] merged 1 commit into
osac-project:mainfrom
mennyaboush:fix/OSAC-3548-requeue-after-restart
Aug 3, 2026
Merged

openshift-merge-bot[bot] merged 1 commit into
osac-project:mainfrom
mennyaboush:fix/OSAC-3548-requeue-after-restart

Conversation

@mennyaboush

@mennyaboush mennyaboush commented Aug 3, 2026 •

Copy link
Copy Markdown

Summary

Fix BMI restart getting permanently stuck in Starting state. The Metal3 IsRestartComplete only checked if the reboot annotation was removed, but the BMH controller removes it as soon as it sends the reboot command — before the host finishes rebooting.

Root Cause

T=0s    Operator adds reboot.metal3.io annotation
T=1s    BMH controller sends reboot to Ironic → removes annotation
T=1s    IsRestartComplete() → annotation gone → "restart complete" ← WRONG
T=2s    Host is shutting down (power off)
T=20s   Host is back online (power on)

The operator declares restart complete while the host is still mid-reboot. In the same reconcile pass, syncBareMetalInstanceStatus sees poweredOn=false and overwrites PowerSynced back to False/Progressing. When RunStrategy is unset (the default), the convergence check is skipped and the operator stops reconciling — leaving PowerSynced stuck.

Fix

Add a poweredOn check to IsRestartComplete: restart is complete only when the annotation is gone AND the host is back online (bmh.Status.PoweredOn=true). This keeps the operator in the reconcileRestartTrigger requeue loop until the host actually finishes rebooting.

return !hasRebootAnnotation && bmh.Status.PoweredOn, nil

Testing

  • Added test: "returns false when annotation is gone but host is not powered on"
  • Existing tests pass (all unit tests green)
  • Validated end-to-end on remote cluster with companion fulfillment-service fix — restart completes in ~36 seconds

Related

Assisted-by: Claude Code noreply@anthropic.com

@openshift-ci-robot

openshift-ci-robot commented Aug 3, 2026 •

Copy link
Copy Markdown

@mennyaboush: This pull request references OSAC-3548 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the bug to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

Fix BMI restart getting permanently stuck in Starting state when RunStrategy is unset (the default for API-created instances).

Root Cause

The convergence check that requeues when host power state doesn't match the desired state (lines 391-407) was inside an if RunStrategy != Unspecified block. When RunStrategy is empty (the default), the entire block was skipped. After a restart, the operator completed the restart trigger reconciliation, saw the host still mid-reboot (power_state: "power off"), set PowerSynced=False/Progressing, and then stopped reconciling — because the convergence check was skipped.

The host eventually came back online, but the operator never ran again to set PowerSynced=True/PowerOn. This left the BMI permanently stuck in Starting state.

Fix

Add a fallback requeue outside the RunStrategy block: if powerStatus.IsTransitioning is true, requeue regardless of RunStrategy. This ensures the operator keeps polling until the host reaches a stable power state.

Validation

  • All unit tests pass (make test)
  • Tested end-to-end on remote cluster (osac-sno) with both this fix and the companion fulfillment-service fix:
  • Restart triggered → PowerSynced=False/Progressing → operator requeued → PowerSynced=True/PowerOn after ~28s → fulfillment controller picked up via feedback Signal → BMI returned to Running with trigger(4/4)

Related

Assisted-by: Claude Code noreply@anthropic.com

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from DanNiESh and carbonin August 3, 2026 11:47
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mennyaboush, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e4ad28a9-9fc5-4c19-acb8-c91b07c4b0bd

📥 Commits

Reviewing files that changed from the base of the PR and between 81db67c and 953177c.

📒 Files selected for processing (2)
  • internal/management/metal3.go
  • internal/management/metal3_test.go

Walkthrough

IsRestartComplete now reports completion only when the reboot annotation is removed and the host is powered on. Tests cover the powered-off case and include a formatting adjustment.

Changes

Restart completion validation

Layer / File(s) Summary
Restart state check
internal/management/metal3.go, internal/management/metal3_test.go, internal/controller/baremetalinstance_controller_test.go
IsRestartComplete checks bmh.Status.PoweredOn after annotation removal. Tests verify that a powered-off host is not complete. The controller test adds spacing.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: ajamias, carbonin, danniesh

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Hardcoded-Secrets ✅ Passed The three-file patch adds only restart logic, comments, and test host identifiers; scans found no keys, tokens, passwords, private keys, embedded credentials, or long encoded blobs.
No-Weak-Crypto ✅ Passed The PR changes only restart-state logic and tests. No MD5, SHA1, DES, RC4, Blowfish, ECB, custom crypto, or secret comparison was added.
No-Injection-Vectors ✅ Passed The PR adds only a boolean check, comments, and a unit test; searches found no SQL concatenation, eval/exec, unsafe deserialization, shell execution, or HTML injection.
Container-Privileges ✅ Passed The PR changes only Go logic and tests. The diff adds no container or Kubernetes privilege settings; existing manifests use non-root, dropped capabilities, and allowPrivilegeEscalation: false.
No-Sensitive-Data-In-Logs ✅ Passed The diff adds no logging calls or log fields. It only changes restart-completion logic and tests, and the controller logging-call count is unchanged at 56.
Ai-Attribution ✅ Passed The PR mentions Claude Code and the tip commit includes an Assisted-by: Claude Code <noreply@anthropic.com> trailer; no AI Co-Authored-By trailer is present.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: making IsRestartComplete wait until the host powers on.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/controller/baremetalinstance_controller.go`:
- Around line 409-413: The RunStrategyUnspecified test coverage lacks the
transitioning-power-state case. Extend the relevant controller test to set
PowerStatus.IsTransitioning true and verify the default management recheck
interval, BareMetalInstancePhaseProgressing phase, no SetPowerState call, and
PowerSynced=False with Progressing reason.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3d0c0baf-d4b0-4671-8413-3306976d2ce8

📥 Commits

Reviewing files that changed from the base of the PR and between c085340 and 50749ff.

📒 Files selected for processing (1)
  • internal/controller/baremetalinstance_controller.go

Comment on lines +409 to +413
if powerStatus != nil && powerStatus.IsTransitioning {
bareMetalInstance.Status.Phase = v1alpha1.BareMetalInstancePhaseProgressing
return ctrl.Result{RequeueAfter: r.ManagementRecheckIntervalDuration}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'RunStrategyUnspecified|IsTransitioning|RequeueAfter' \
  internal/controller/baremetalinstance_controller_test.go

Repository: osac-project/bare-metal-fulfillment-operator

Length of output: 14851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the controller segment around the newly added fallback requeue path.
sed -n '360,430p' internal/controller/baremetalinstance_controller.go

echo '--- locate reconcileManagement outline and runStrategy blocks ---'
rg -n -C 4 'RunStrategyUnspecified|RunStrategyHalted|RunStrategyAlways|isTransitioning|RequeueAfter: r.ManagementRecheckIntervalDuration' internal/controller/baremetalinstance_controller.go internal/controller/baremetalinstance_controller_test.go

Repository: osac-project/bare-metal-fulfillment-operator

Length of output: 42167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- relevant test contexts around unspecified RunStrategy ---\n'
sed -n '460,492p' internal/controller/baremetalinstance_controller_test.go

printf '\n--- definitions of reconcileManagement result expectations ---\n'
python3 - <<'PY'
from pathlib import Path
text = Path('internal/controller/baremetalinstance_controller_test.go').read_text()
# Print test headers inside reconcileManagement and syncBareMetalInstanceStatus specs for RunStrategyUnspecified.
lines = text.splitlines()
for start, end in [(300, 571), (840, 925)]:
    for i in range(start, end+1):
        if i <= len(lines) and ('It(' in lines[i-1] or 'BeforeEach' in lines[i-1] or 'Context(' in lines[i-1] or 'Expect(' in lines[i-1]):
            print(f'{i}: {lines[i-1]}')
PY

Repository: osac-project/bare-metal-fulfillment-operator

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- relevant test contexts around unspecified RunStrategy ---'
sed -n '460,492p' internal/controller/baremetalinstance_controller_test.go || true

echo
echo '--- RunStrategyUnspecified tests ---'
rg -n -C 6 'RunStrategyUnspecified' internal/controller/baremetalinstance_controller_test.go || true

Repository: osac-project/bare-metal-fulfillment-operator

Length of output: 2642


Add regression coverage for an unspecified RunStrategy with a transitioning power state.

RunStrategyUnspecified currently covers only the stable offline case that reaches Ready. Add a case with PowerStatus.IsTransitioning == true that asserts DefaultManagementRecheckIntervalDuration, Phase == BareMetalInstancePhaseProgressing, SetPowerState was not called, and PowerSynced == False/Progressing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/baremetalinstance_controller.go` around lines 409 - 413,
The RunStrategyUnspecified test coverage lacks the transitioning-power-state
case. Extend the relevant controller test to set PowerStatus.IsTransitioning
true and verify the default management recheck interval,
BareMetalInstancePhaseProgressing phase, no SetPowerState call, and
PowerSynced=False with Progressing reason.

@mennyaboush
mennyaboush force-pushed the fix/OSAC-3548-requeue-after-restart branch 2 times, most recently from 8bc0459 to 81db67c Compare August 3, 2026 13:20
@mennyaboush mennyaboush changed the title OSAC-3548: Requeue when power is transitioning regardless of RunStrategy OSAC-3548: Fix IsRestartComplete to wait for host to power on Aug 3, 2026
The Metal3 IsRestartComplete only checked whether the reboot
annotation was removed from the BMH. The BMH controller removes
the annotation as soon as it sends the reboot command to Ironic,
before the host finishes rebooting. This caused the operator to
declare restart complete while the host was still mid-reboot,
setting PowerSynced=True then immediately overwriting it with
PowerSynced=False in syncBareMetalInstanceStatus.

Add a poweredOn check: restart is complete only when the annotation
is gone AND the host is back online (bmh.Status.PoweredOn=true).
This keeps the operator in the reconcileRestartTrigger requeue
loop until the host actually finishes rebooting.

Fixes: https://redhat.atlassian.net/browse/OSAC-3548

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: MENNY ABOUSH <maboush@maboush-thinkpadt14gen5.raanaii.csb>
@mennyaboush
mennyaboush force-pushed the fix/OSAC-3548-requeue-after-restart branch from 81db67c to 953177c Compare August 3, 2026 13:35
@openshift-ci

openshift-ci Bot commented Aug 3, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ajamias, mennyaboush

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved label Aug 3, 2026
@openshift-merge-bot
openshift-merge-bot Bot merged commit e8f7f4e into osac-project:main Aug 3, 2026
6 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants