From 5613074628c160ec6ab6635f87b4ba9d0f5bcf2d Mon Sep 17 00:00:00 2001 From: Dmitry Porokh Date: Mon, 20 Jul 2026 17:35:54 -0700 Subject: [PATCH] fix(machine-a-tron): validate ipmi_sim before startup Signed-off-by: Dmitry Porokh --- crates/bmc-mock/src/ipmi_sim.rs | 71 ++++++++++++++++++++++++++++- crates/machine-a-tron/src/config.rs | 4 ++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/bmc-mock/src/ipmi_sim.rs b/crates/bmc-mock/src/ipmi_sim.rs index 1af8589dfa..439692b194 100644 --- a/crates/bmc-mock/src/ipmi_sim.rs +++ b/crates/bmc-mock/src/ipmi_sim.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use std::ffi::OsStr; use std::io::ErrorKind; use std::net::{IpAddr, SocketAddr, TcpListener, UdpSocket}; use std::os::unix::fs::PermissionsExt; @@ -37,6 +38,7 @@ const READY_TIMEOUT: Duration = Duration::from_secs(5); const READY_POLL_INTERVAL: Duration = Duration::from_millis(50); const PASSWORD_UPDATE_TIMEOUT: Duration = Duration::from_secs(10); pub const STANDARD_IPMI_PORT: u16 = 623; +const IPMI_SIM_EXECUTABLE: &str = "ipmi_sim"; #[derive(Debug, Clone)] pub struct IpmiSimConfig { @@ -90,6 +92,10 @@ impl Drop for IpmiSimHandle { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error( + "IPMI simulation requires {executable}, but it was not found or is not executable in PATH" + )] + ExecutableUnavailable { executable: &'static str }, #[error("the BMC mock has no administrative account")] MissingAdministrativeAccount, #[error("the IPMI simulator {0} contains unsupported characters")] @@ -104,6 +110,30 @@ pub enum Error { AttemptsExhausted(Box), } +pub fn validate_executable() -> Result<(), Error> { + validate_executable_in_path(std::env::var_os("PATH").as_deref()) +} + +fn validate_executable_in_path(path: Option<&OsStr>) -> Result<(), Error> { + let executable_available = path + .into_iter() + .flat_map(std::env::split_paths) + .map(|directory| directory.join(IPMI_SIM_EXECUTABLE)) + .any(|candidate| { + candidate.metadata().is_ok_and(|metadata| { + metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 + }) + }); + + if executable_available { + Ok(()) + } else { + Err(Error::ExecutableUnavailable { + executable: IPMI_SIM_EXECUTABLE, + }) + } +} + struct Reservations { ipmi_sim_lan_socket: UdpSocket, ipmi_sim_serial_listener: TcpListener, @@ -162,7 +192,7 @@ pub async fn start(state: &BmcState, config: IpmiSimConfig) -> Result Result<(), std::i #[cfg(test)] mod tests { - use super::{Error, IpmiEndpoint, MockConsole, stable_guid, validate_credential}; + use std::fs; + use std::os::unix::fs::PermissionsExt; + + use super::{ + Error, IPMI_SIM_EXECUTABLE, IpmiEndpoint, MockConsole, stable_guid, validate_credential, + validate_executable_in_path, + }; #[test] fn endpoint_uses_configured_reachable_port_or_listen_port() { @@ -492,6 +528,37 @@ mod tests { } } + #[test] + fn ipmi_sim_executable_is_required() { + let temp_dir = tempfile::tempdir().unwrap(); + let missing_dir = temp_dir.path().join("missing"); + let non_executable_dir = temp_dir.path().join("non-executable"); + let executable_dir = temp_dir.path().join("executable"); + fs::create_dir_all(&missing_dir).unwrap(); + fs::create_dir_all(&non_executable_dir).unwrap(); + fs::create_dir_all(&executable_dir).unwrap(); + + fs::write(non_executable_dir.join(IPMI_SIM_EXECUTABLE), []).unwrap(); + + let executable = executable_dir.join(IPMI_SIM_EXECUTABLE); + fs::write(&executable, []).unwrap(); + let mut permissions = executable.metadata().unwrap().permissions(); + permissions.set_mode(0o700); + fs::set_permissions(&executable, permissions).unwrap(); + + for (scenario, path, expected) in [ + ("missing executable", missing_dir, false), + ("non-executable file", non_executable_dir, false), + ("executable file", executable_dir, true), + ] { + assert_eq!( + validate_executable_in_path(Some(path.as_os_str())).is_ok(), + expected, + "{scenario}" + ); + } + } + #[test] fn stable_guid_is_stable_and_has_ipmi_length() { assert_eq!(stable_guid("machine-1"), stable_guid("machine-1")); diff --git a/crates/machine-a-tron/src/config.rs b/crates/machine-a-tron/src/config.rs index a47124f5f0..d5592a701e 100644 --- a/crates/machine-a-tron/src/config.rs +++ b/crates/machine-a-tron/src/config.rs @@ -324,6 +324,10 @@ impl MachineATronConfig { ); } + if self.enable_ipmi_simulation { + bmc_mock::ipmi_sim::validate_executable()?; + } + for (rack_id, rack) in &self.racks { eyre::ensure!(!rack_id.as_str().is_empty(), "rack ID cannot be empty"); eyre::ensure!(