Skip to content

feat(bmc-mock): add optional IPMI and SOL simulation - #3542

Merged
poroh merged 1 commit into
NVIDIA:mainfrom
poroh:bmc-mock-ipmi-simulation
Jul 15, 2026
Merged

feat(bmc-mock): add optional IPMI and SOL simulation#3542
poroh merged 1 commit into
NVIDIA:mainfrom
poroh:bmc-mock-ipmi-simulation

Conversation

@poroh

@poroh poroh commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Machine-a-tron currently simulates BMC management through Redfish and optional SSH, but it cannot exercise workflows that depend on IPMI or Serial over LAN. This leaves the SSH console integration path dependent on separately managed simulator infrastructure and prevents local environments from representing an IPMI-capable BMC as one coherent mock.

This PR is the first step toward full IPMI support in machine-a-tron. It establishes the simulator lifecycle and integration foundation; broader IPMI command and behavior coverage will follow separately.

What changed

  • Integrates ipmi_sim into bmc-mock.
  • Allows machine-a-tron to start an independent IPMI/SOL simulator for each supported host BMC.
  • Supports both per-machine BMC mocks and deployments using a shared Redfish BMC-mock instance.
  • Dynamically allocates IPMI LAN, serial, and mock console ports.
  • Advertises the simulated IPMI endpoint through Redfish.
  • Synchronizes Redfish password changes with the corresponding IPMI user.
  • Adds an IPMI-backed mock SOL console for SSH-console integration testing.
  • Makes IPMI simulation opt-in because ipmi_sim and ipmitool are external runtime dependencies.
  • Validates credentials before writing ipmi_sim configuration.
  • Cleans up simulator processes, listener tasks, temporary files, and advertised endpoints when handles are dropped.
  • Updates local test dependencies and SSH-console integration coverage.

Related issues

Part of #3378

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Additional Notes

@poroh
poroh requested a review from a team as a code owner July 15, 2026 06:51
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ead6f2c0-90ce-4007-acfe-868659aeaa62

📥 Commits

Reviewing files that changed from the base of the PR and between ea41c8b and 02db974.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • crates/api-integration-tests/tests/lib.rs
  • crates/bmc-mock/Cargo.toml
  • crates/bmc-mock/src/command_line.rs
  • crates/bmc-mock/src/ipmi_sim.rs
  • crates/bmc-mock/src/lib.rs
  • crates/bmc-mock/src/machine_info.rs
  • crates/bmc-mock/src/main.rs
  • crates/bmc-mock/src/redfish/account_service.rs
  • crates/bmc-mock/src/redfish/manager.rs
  • crates/bmc-mock/src/redfish/manager_network_protocol.rs
🚧 Files skipped from review as they are similar to previous changes (9)
  • crates/bmc-mock/src/command_line.rs
  • crates/bmc-mock/Cargo.toml
  • crates/bmc-mock/src/redfish/manager_network_protocol.rs
  • crates/bmc-mock/src/lib.rs
  • crates/bmc-mock/src/machine_info.rs
  • crates/bmc-mock/src/main.rs
  • crates/bmc-mock/src/redfish/manager.rs
  • crates/bmc-mock/src/ipmi_sim.rs
  • crates/bmc-mock/src/redfish/account_service.rs

Summary by CodeRabbit

  • New Features
    • Added optional IPMI/SOL simulation for IPMI-capable host BMCs, controllable via enable_ipmi_simulation (default off) and --enable-ipmi-simulation.
    • Redfish network protocol now reports the simulated IPMI endpoint port when enabled.
    • Password updates can synchronize asynchronously and fail safely if the external updater errors.
  • Bug Fixes
    • Prevent IPMI simulation when archive-backed router modes are configured.
  • Tests
    • Updated SSH console test utilities to use the shared IPMI simulator implementation.

Walkthrough

Adds optional IPMI/SOL simulation to BMC mocks, synchronizes Redfish administrator passwords with ipmitool, publishes simulator endpoints through Redfish, integrates startup into machine-a-tron, and reuses the simulator in SSH console tests.

Changes

IPMI simulation

Layer / File(s) Summary
Simulator and Redfish integration
crates/bmc-mock/src/ipmi_sim.rs, crates/bmc-mock/src/redfish/*, crates/bmc-mock/Cargo.toml
Adds simulator startup, generated configuration, readiness polling, console handling, credential validation, password synchronization, and Redfish IPMI endpoint reporting.
Configuration and machine startup wiring
crates/bmc-mock/src/{command_line.rs,main.rs,machine_info.rs}, crates/machine-a-tron/config/*, crates/machine-a-tron/src/*, crates/api-integration-tests/tests/lib.rs
Adds the opt-in configuration and CLI flag, validates router incompatibilities, starts simulators for supported machines and backing instances, and preserves disabled test defaults.
SSH console test migration
crates/ssh-console/tests/util/*
Replaces the local simulator implementation with bmc_mock::ipmi_sim and uses its LAN port when constructing mock hosts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MachineATron
  participant BmcMockWrapper
  participant ipmi_sim
  participant BmcState
  participant Redfish
  MachineATron->>BmcMockWrapper: start IPMI simulation
  BmcMockWrapper->>ipmi_sim: start(IpmiSimConfig)
  ipmi_sim->>BmcState: configure password updater and endpoint
  Redfish->>BmcState: patch administrator password
  BmcState->>ipmi_sim: invoke ipmitool password update
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: optional IPMI and SOL simulation added to bmc-mock.
Description check ✅ Passed The description is directly aligned with the PR and accurately reflects the added IPMI/SOL simulation work.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@poroh
poroh requested a review from kensimon July 15, 2026 06:54
@poroh
poroh force-pushed the bmc-mock-ipmi-simulation branch from f436a0d to ea41c8b Compare July 15, 2026 07:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
crates/bmc-mock/src/main.rs (1)

97-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

.expect() safety is an implicit, unenforced cross-branch invariant.

The generated_state.as_ref().expect(...) at Line 111-113 is only safe because the guard at Line 74-78 prevents enable_ipmi_simulation from coexisting with targz. That's correct today, but it's a runtime invariant spanning two independent branches with no compiler enforcement — a future refactor of either check could silently turn this into a panic instead of a clean error return.

Consider returning a Result/graceful error instead of .expect() here, so a broken invariant degrades to an error message rather than a panic.

♻️ Proposed fix to fail gracefully instead of panicking
     let _ipmi_sim_handle = if args.enable_ipmi_simulation {
-        let state = generated_state
-            .as_ref()
-            .expect("archive-backed routers were rejected above");
+        let Some(state) = generated_state.as_ref() else {
+            return Err("--enable-ipmi-simulation requires the default BMC mock state".into());
+        };
         Some(
             bmc_mock::ipmi_sim::start(
                 state,
🤖 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 `@crates/bmc-mock/src/main.rs` around lines 97 - 127, Replace the expect-based
unwrap in the IPMI simulation setup with graceful error propagation when
generated_state is absent. Update the branch using generated_state.as_ref() so
it returns a descriptive error through main’s existing Result flow, while
preserving normal startup for default_host_mock state and the
disabled-simulation path.
🤖 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 `@crates/bmc-mock/src/ipmi_sim.rs`:
- Around line 422-435: Update serve_console to remove the unbounded input Vec
and detect newline termination directly from each read slice, while preserving
the existing echo and prompt behavior. Use the current buffer segment and length
for the newline/carriage-return check, without accumulating data across reads.
- Around line 65-69: The simulator currently requests child termination without
awaiting cleanup, and console session tasks are detached. In
crates/bmc-mock/src/ipmi_sim.rs lines 65-69, replace Drop-only process cleanup
with an explicit async shutdown path that terminates and awaits/reaps the child;
in lines 405-412, track spawned console sessions in a JoinSet and have shutdown
cancel and join all active tasks.

In `@crates/bmc-mock/src/redfish/account_service.rs`:
- Around line 126-145: Restrict the password updater invocation in
update_password to accounts whose typed role is Administrator, leaving
non-administrator Redfish password updates unaffected. Use the account role enum
rather than comparing a string, and add a test confirming non-administrator
updates do not invoke synchronization or alter IPMI credentials.

---

Nitpick comments:
In `@crates/bmc-mock/src/main.rs`:
- Around line 97-127: Replace the expect-based unwrap in the IPMI simulation
setup with graceful error propagation when generated_state is absent. Update the
branch using generated_state.as_ref() so it returns a descriptive error through
main’s existing Result flow, while preserving normal startup for
default_host_mock state and the disabled-simulation path.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 42bdfe9f-48d8-476d-ad55-ae73e428d961

📥 Commits

Reviewing files that changed from the base of the PR and between eb4ea22 and f436a0d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • crates/api-integration-tests/tests/lib.rs
  • crates/bmc-mock/Cargo.toml
  • crates/bmc-mock/src/command_line.rs
  • crates/bmc-mock/src/ipmi_sim.rs
  • crates/bmc-mock/src/lib.rs
  • crates/bmc-mock/src/machine_info.rs
  • crates/bmc-mock/src/main.rs
  • crates/bmc-mock/src/redfish/account_service.rs
  • crates/bmc-mock/src/redfish/manager.rs
  • crates/bmc-mock/src/redfish/manager_network_protocol.rs
  • crates/machine-a-tron/config/mac.toml
  • crates/machine-a-tron/config/mat.toml
  • crates/machine-a-tron/src/bmc_mock_wrapper.rs
  • crates/machine-a-tron/src/config.rs
  • crates/machine-a-tron/src/machine_state_machine.rs
  • crates/ssh-console/tests/util/ipmi_sim.rs
  • crates/ssh-console/tests/util/mod.rs

Comment on lines +65 to +69
impl Drop for IpmiSimHandle {
fn drop(&mut self) {
self.child.start_kill().ok();
self.manager.set_ipmi_endpoint(None);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/bmc-mock/src/ipmi_sim.rs"

echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== targeted lines around Drop and console accept loop =="
sed -n '1,130p' "$FILE" | cat -n
echo
sed -n '360,470p' "$FILE" | cat -n

echo
echo "== searches for shutdown/join/task ownership =="
rg -n "JoinSet|start_kill|wait\(|kill\(|set_ipmi_endpoint|CancellationToken|spawn\(|accept\(\)" "$FILE"

Repository: NVIDIA/infra-controller

Length of output: 13058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/bmc-mock/src/ipmi_sim.rs"

python3 - <<'PY'
from pathlib import Path
p = Path("crates/bmc-mock/src/ipmi_sim.rs")
text = p.read_text()
for needle in ["struct IpmiSimHandle", "impl Drop for IpmiSimHandle", "tokio::spawn(async move", "JoinSet", "kill_on_drop", "wait().await", "start_kill"]:
    idx = text.find(needle)
    print(f"\n### {needle} ###")
    if idx == -1:
        print("not found")
        continue
    start = max(0, text.rfind("\n", 0, idx-300))
    end = min(len(text), text.find("\n", idx+500))
    if end == -1:
        end = min(len(text), idx+1000)
    print(text[start:end])
PY

Repository: NVIDIA/infra-controller

Length of output: 4585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/bmc-mock/src/ipmi_sim.rs"

echo "== start() cleanup branch around child shutdown =="
sed -n '150,210p' "$FILE" | cat -n

echo
echo "== MockConsole tests and shutdown behavior =="
sed -n '470,560p' "$FILE" | cat -n

Repository: NVIDIA/infra-controller

Length of output: 3645


🌐 Web query:

tokio::process::Child kill_on_drop drop reaped wait semantics

💡 Result:

In the tokio::process module, the kill_on_drop(true) configuration and the drop semantics of a Child handle are governed by the following key principles: 1. No Automatic Wait on Drop: When a Child handle is dropped, Tokio does not perform a blocking wait on the child process, even if kill_on_drop(true) is enabled [1][2]. Blocking the current task (or the executor thread) in a destructor is avoided to prevent stalling the runtime [1][2]. 2. kill_on_drop Semantics: When kill_on_drop is set to true, dropping the Child handle triggers a signal (SIGKILL on Unix) to the child process [3][1]. However, because this happens in the destructor, the process is not immediately reaped (i.e., waitpid is not called) [1][4]. This creates a "zombie" process state until the operating system or the Tokio runtime's background reaper processes it [5][1]. 3. Best-Effort Reaping: Tokio implements a best-effort mechanism to reap these orphaned processes [5][2]. This reaping process typically occurs as a side effect when other child processes are spawned or polled on the event loop [2][4]. If no further processes are spawned, there is no strict guarantee on when, or if, the zombie process will be fully reaped, which may lead to an accumulation of zombie processes in extreme cases [5][1][2]. 4. Recommended Best Practice: Because of these limitations, relying solely on drop for cleanup is discouraged for applications requiring strict resource management [5][1]. The authoritative way to ensure a child process is terminated and fully reaped is to manually await its completion using child.wait.await or child.kill.await [5][6][1]. These methods ensure that the process is explicitly signaled and, crucially, that the exit status is collected, preventing the process from lingering as a zombie [5][1][7]. Top Results: [5], [1], [2], [4]

Citations:


Own simulator shutdown and join all background work

  • crates/bmc-mock/src/ipmi_sim.rs#L65-L69: Drop only requests termination; move process cleanup into an explicit async shutdown path so the child is actually awaited and reaped.
  • crates/bmc-mock/src/ipmi_sim.rs#L405-L412: accepted console sessions are spawned-and-forgotten; track them in a JoinSet so teardown cancels and joins active connections.
📍 Affects 1 file
  • crates/bmc-mock/src/ipmi_sim.rs#L65-L69 (this comment)
  • crates/bmc-mock/src/ipmi_sim.rs#L405-L412
🤖 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 `@crates/bmc-mock/src/ipmi_sim.rs` around lines 65 - 69, The simulator
currently requests child termination without awaiting cleanup, and console
session tasks are detached. In crates/bmc-mock/src/ipmi_sim.rs lines 65-69,
replace Drop-only process cleanup with an explicit async shutdown path that
terminates and awaits/reaps the child; in lines 405-412, track spawned console
sessions in a JoinSet and have shutdown cancel and join all active tasks.

Sources: Coding guidelines, Path instructions

Comment on lines +422 to +435
async fn serve_console(mut stream: TcpStream, prompt: &str) -> Result<(), std::io::Error> {
let mut input = Vec::new();
let mut buffer = [0_u8; 32];
loop {
let length = stream.read(&mut buffer).await?;
if length == 0 {
return Ok(());
}
input.extend_from_slice(&buffer[..length]);
stream.write_all(&buffer[..length]).await?;
if input.ends_with(b"\n") || input.ends_with(b"\r") {
input.clear();
stream.write_all(format!("\r\n{prompt}").as_bytes()).await?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the unbounded console input buffer.

input grows until a newline arrives, allowing a stalled or malformed SOL client to consume memory indefinitely. The contents are otherwise unused, so test the current read slice directly.

Proposed fix
 async fn serve_console(mut stream: TcpStream, prompt: &str) -> Result<(), std::io::Error> {
-    let mut input = Vec::new();
     let mut buffer = [0_u8; 32];
     loop {
         let length = stream.read(&mut buffer).await?;
         if length == 0 {
             return Ok(());
         }
-        input.extend_from_slice(&buffer[..length]);
         stream.write_all(&buffer[..length]).await?;
-        if input.ends_with(b"\n") || input.ends_with(b"\r") {
-            input.clear();
+        if buffer[..length].ends_with(b"\n") || buffer[..length].ends_with(b"\r") {
             stream.write_all(format!("\r\n{prompt}").as_bytes()).await?;
         }
     }
 }

As per path instructions, prioritize behavior and resource-lifetime defects in crates/**/*.rs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn serve_console(mut stream: TcpStream, prompt: &str) -> Result<(), std::io::Error> {
let mut input = Vec::new();
let mut buffer = [0_u8; 32];
loop {
let length = stream.read(&mut buffer).await?;
if length == 0 {
return Ok(());
}
input.extend_from_slice(&buffer[..length]);
stream.write_all(&buffer[..length]).await?;
if input.ends_with(b"\n") || input.ends_with(b"\r") {
input.clear();
stream.write_all(format!("\r\n{prompt}").as_bytes()).await?;
}
async fn serve_console(mut stream: TcpStream, prompt: &str) -> Result<(), std::io::Error> {
let mut buffer = [0_u8; 32];
loop {
let length = stream.read(&mut buffer).await?;
if length == 0 {
return Ok(());
}
stream.write_all(&buffer[..length]).await?;
if buffer[..length].ends_with(b"\n") || buffer[..length].ends_with(b"\r") {
stream.write_all(format!("\r\n{prompt}").as_bytes()).await?;
}
}
}
🤖 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 `@crates/bmc-mock/src/ipmi_sim.rs` around lines 422 - 435, Update serve_console
to remove the unbounded input Vec and detect newline termination directly from
each read slice, while preserving the existing echo and prompt behavior. Use the
current buffer segment and length for the newline/carriage-return check, without
accumulating data across reads.

Source: Path instructions

Comment on lines +126 to +145
pub async fn update_password(
&self,
account_id: &str,
password: impl Into<String>,
) -> Result<bool, String> {
let password = password.into();
let account = self.find(account_id);
let Some(account) = account else {
return Ok(false);
};
account.password = password.into();
true
let updater = self
.password_updater
.lock()
.expect("mutex poisoned")
.as_ref()
.and_then(Weak::upgrade);
if let Some(updater) = updater {
updater
.update_password(&account.username, &account.password, &password)
.await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Synchronize only the administrator account.

The updater is invoked for every Redfish account, but IpmiPasswordUpdater always changes IPMI user ID 3—the configured administrator. A non-administrator PATCH can therefore fail unexpectedly or alter unrelated IPMI credentials. Gate synchronization using a typed administrator role and add a non-administrator test.

As per coding guidelines, known finite Rust values must use enums rather than bare strings.
As per path instructions, BMC-facing code requires careful credential handling.

🤖 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 `@crates/bmc-mock/src/redfish/account_service.rs` around lines 126 - 145,
Restrict the password updater invocation in update_password to accounts whose
typed role is Administrator, leaving non-administrator Redfish password updates
unaffected. Use the account role enum rather than comparing a string, and add a
test confirming non-administrator updates do not invoke synchronization or alter
IPMI credentials.

Sources: Coding guidelines, Path instructions

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 259 15 31 78 7 128
machine-validation-runner 532 38 115 183 15 181
machine_validation 532 38 115 183 15 181
machine_validation-aarch64 532 38 115 183 15 181
nvmetal-carbide 532 39 113 184 15 181
TOTAL 2393 168 489 817 67 852

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

Integrate ipmi_sim with bmc-mock and machine-a-tron so simulated BMCs
can expose dynamically allocated IPMI LAN and serial-over-LAN
endpoints.

Keep Redfish and IPMI credentials synchronized, advertise the IPMI
endpoint through Redfish, and support both per-machine and shared
BMC-mock deployments. Make the feature opt-in because ipmi_sim and
ipmitool are external runtime dependencies.

Add IPMI-backed SSH console integration coverage, credential
validation, dynamic port allocation, and cleanup of simulator
processes and listener tasks.

Signed-off-by: Dmitry Porokh <dporokh@nvidia.com>
@poroh
poroh force-pushed the bmc-mock-ipmi-simulation branch from ea41c8b to 02db974 Compare July 15, 2026 18:20
@poroh
poroh enabled auto-merge (squash) July 15, 2026 18:53

@chet chet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for the conflicts. 😬

@poroh
poroh merged commit d42b1eb into NVIDIA:main Jul 15, 2026
59 checks passed
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.

3 participants