diff --git a/platform/mellanox/crates/sonic-platform/Cargo.lock b/platform/mellanox/crates/sonic-platform/Cargo.lock new file mode 100644 index 00000000000..7d39201a9bd --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/Cargo.lock @@ -0,0 +1,168 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "sonic-platform" +version = "0.1.0" +dependencies = [ + "anyhow", + "glob", + "regex", + "thiserror", + "tracing", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" diff --git a/platform/mellanox/crates/sonic-platform/Cargo.toml b/platform/mellanox/crates/sonic-platform/Cargo.toml new file mode 100644 index 00000000000..98edf0a5f13 --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sonic-platform" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +thiserror = "1" +tracing = "0.1" + +glob = "0.3" +regex = "1" diff --git a/platform/mellanox/crates/sonic-platform/src/chassis.rs b/platform/mellanox/crates/sonic-platform/src/chassis.rs new file mode 100644 index 00000000000..03c3e6ef221 --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/src/chassis.rs @@ -0,0 +1,232 @@ +use anyhow::{Context, Result}; +use glob::glob; +use std::fs; +use std::path::Path; +use tracing::{debug, info, warn}; + +use crate::fan::{MlnxFan, FanDrawer, Fan}; +use crate::thermal::{MlnxThermal, Thermal}; + +pub struct MlnxChassis { + fans: Vec>, + fan_drawers: Vec, + thermals: Vec>, +} + +impl MlnxChassis { + pub fn new() -> Result { + info!("Initializing Mellanox chassis"); + + let mut chassis = Self { + fans: Vec::new(), + fan_drawers: Vec::new(), + thermals: Vec::new(), + }; + + chassis.discover_hwmon_devices()?; + + info!( + "Mellanox chassis initialized: {} fans, {} thermals", + chassis.fans.len(), + chassis.thermals.len() + ); + + Ok(chassis) + } + + fn discover_hwmon_devices(&mut self) -> Result<()> { + let hwmon_pattern = "/sys/class/hwmon/hwmon*"; + + for entry in glob(hwmon_pattern).context("Failed to read hwmon pattern")? { + match entry { + Ok(path) => { + if let Err(e) = self.process_hwmon_device(&path) { + warn!("Failed to process hwmon device {}: {}", path.display(), e); + } + } + Err(e) => warn!("Failed to read hwmon entry: {}", e), + } + } + + Ok(()) + } + + fn process_hwmon_device(&mut self, hwmon_path: &Path) -> Result<()> { + let name = self.read_hwmon_name(hwmon_path)?; + debug!("Processing hwmon device: {} at {}", name, hwmon_path.display()); + + if name.contains("mlxsw") { + self.discover_mlxsw_sensors(hwmon_path, &name)?; + } else if name.contains("fan") || name.contains("cooling") { + self.discover_fans(hwmon_path, &name)?; + } else { + self.discover_generic_sensors(hwmon_path, &name)?; + } + + Ok(()) + } + + fn read_hwmon_name(&self, hwmon_path: &Path) -> Result { + let name_path = hwmon_path.join("name"); + fs::read_to_string(&name_path) + .context("Failed to read hwmon name") + .map(|s| s.trim().to_string()) + } + + fn discover_mlxsw_sensors(&mut self, hwmon_path: &Path, _name: &str) -> Result<()> { + let mut temp_indices = Vec::new(); + let mut fan_indices = Vec::new(); + let mut pwm_indices = Vec::new(); + + for entry in fs::read_dir(hwmon_path)? { + let entry = entry?; + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + if filename_str.starts_with("temp") && filename_str.ends_with("_input") { + if let Some(idx_str) = filename_str.strip_prefix("temp").and_then(|s| s.strip_suffix("_input")) { + if let Ok(idx) = idx_str.parse::() { + temp_indices.push(idx); + } + } + } else if filename_str.starts_with("fan") && filename_str.ends_with("_input") { + if let Some(idx_str) = filename_str.strip_prefix("fan").and_then(|s| s.strip_suffix("_input")) { + if let Ok(idx) = idx_str.parse::() { + fan_indices.push(idx); + } + } + } else if filename_str.starts_with("pwm") && !filename_str.contains('_') { + if let Some(idx_str) = filename_str.strip_prefix("pwm") { + if let Ok(idx) = idx_str.parse::() { + pwm_indices.push(idx); + } + } + } + } + + temp_indices.sort_unstable(); + fan_indices.sort_unstable(); + pwm_indices.sort_unstable(); + + for temp_idx in temp_indices { + let name = format!("Thermal {}", temp_idx); + let thermal = MlnxThermal::new(name, hwmon_path.to_path_buf(), temp_idx); + self.thermals.push(Box::new(thermal)); + debug!("Added thermal sensor at temp{}", temp_idx); + } + + let num_drawers = (fan_indices.len() + 1) / 2; + let mut drawer_fans: Vec>> = Vec::with_capacity(num_drawers); + for _ in 0..num_drawers { + drawer_fans.push(Vec::new()); + } + + for (i, fan_idx) in fan_indices.iter().enumerate() { + let fan_name = format!("fan{}", fan_idx); + let pwm_idx = pwm_indices.get(i).copied(); + let fan = MlnxFan::new(fan_name, hwmon_path.to_path_buf(), *fan_idx, pwm_idx); + drawer_fans[i / 2].push(Box::new(fan)); + debug!("Added fan at fan{} with pwm{:?}", fan_idx, pwm_idx); + } + + for (drawer_idx, fans) in drawer_fans.into_iter().enumerate() { + if !fans.is_empty() { + let drawer_name = format!("drawer{}", drawer_idx); + let num_fans = fans.len(); + let drawer = FanDrawer::new(drawer_name, fans); + self.fan_drawers.push(drawer); + debug!("Created fan drawer {} with {} fans", drawer_idx, num_fans); + } + } + + Ok(()) + } + + fn discover_fans(&mut self, hwmon_path: &Path, name: &str) -> Result<()> { + let mut fan_indices = Vec::new(); + + for entry in fs::read_dir(hwmon_path)? { + let entry = entry?; + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + if filename_str.starts_with("fan") && filename_str.ends_with("_input") { + if let Some(idx_str) = filename_str.strip_prefix("fan").and_then(|s| s.strip_suffix("_input")) { + if let Ok(idx) = idx_str.parse::() { + fan_indices.push(idx); + } + } + } + } + + fan_indices.sort_unstable(); + + let mut fans: Vec> = Vec::new(); + for fan_idx in fan_indices { + let fan_name = format!("{}_fan{}", name, fan_idx); + let fan = MlnxFan::new(fan_name, hwmon_path.to_path_buf(), fan_idx, None); + fans.push(Box::new(fan)); + debug!("Added fan {} at {}", fan_idx, hwmon_path.display()); + } + + if !fans.is_empty() { + let drawer_name = format!("{}_drawer", name); + let drawer = FanDrawer::new(drawer_name, fans); + self.fan_drawers.push(drawer); + debug!("Created {} fan drawer at {}", name, hwmon_path.display()); + } + + Ok(()) + } + + fn discover_generic_sensors(&mut self, hwmon_path: &Path, name: &str) -> Result<()> { + let mut temp_indices = Vec::new(); + + for entry in fs::read_dir(hwmon_path)? { + let entry = entry?; + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + if filename_str.starts_with("temp") && filename_str.ends_with("_input") { + if let Some(idx_str) = filename_str.strip_prefix("temp").and_then(|s| s.strip_suffix("_input")) { + if let Ok(idx) = idx_str.parse::() { + temp_indices.push(idx); + } + } + } + } + + temp_indices.sort_unstable(); + + for temp_idx in temp_indices { + let thermal_name = format!("{} Thermal {}", name, temp_idx); + let thermal = MlnxThermal::new(thermal_name, hwmon_path.to_path_buf(), temp_idx); + self.thermals.push(Box::new(thermal)); + debug!("Added thermal {} at {}", temp_idx, hwmon_path.display()); + } + + Ok(()) + } + + pub fn get_fans(&self) -> &[Box] { + &self.fans + } + + pub fn get_fan_drawers(&self) -> &[FanDrawer] { + &self.fan_drawers + } + + pub fn get_thermals(&self) -> &[Box] { + &self.thermals + } + + pub fn into_components( + self, + ) -> ( + Vec>, + Vec, + Vec>, + ) { + (self.fans, self.fan_drawers, self.thermals) + } +} diff --git a/platform/mellanox/crates/sonic-platform/src/fan.rs b/platform/mellanox/crates/sonic-platform/src/fan.rs new file mode 100644 index 00000000000..7e25ed5019d --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/src/fan.rs @@ -0,0 +1,350 @@ +use anyhow::{Context, Result}; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU32, Ordering}; + +static BAD_FAN_COUNT: AtomicU32 = AtomicU32::new(0); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedColor { + Green, + Red, + Amber, + Off, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FanDirection { + Intake, + Exhaust, + NotApplicable, +} + +impl fmt::Display for FanDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FanDirection::Intake => write!(f, "intake"), + FanDirection::Exhaust => write!(f, "exhaust"), + FanDirection::NotApplicable => write!(f, "N/A"), + } + } +} + +pub trait Fan: Send + Sync { + fn get_name(&self) -> Result; + + fn get_presence(&self) -> Result; + + fn get_status(&self) -> Result; + + fn get_speed(&self) -> Result; + + fn get_target_speed(&self) -> Result; + + fn is_under_speed(&self) -> Result; + + fn is_over_speed(&self) -> Result; + + fn get_direction(&self) -> Result; + + fn get_model(&self) -> Result; + + fn get_serial(&self) -> Result; + + fn is_replaceable(&self) -> Result; + + fn get_position_in_parent(&self) -> Result; + + fn set_status_led(&self, color: LedColor) -> Result<()>; + + fn get_status_led(&self) -> Result; +} + +#[derive(Debug)] +pub struct FanStatus { + pub presence: bool, + pub under_speed: bool, + pub over_speed: bool, + pub fault_status: bool, +} + +impl FanStatus { + pub fn new() -> Self { + Self { + presence: true, + under_speed: false, + over_speed: false, + fault_status: true, + } + } + + pub fn set_presence(&mut self, presence: bool) -> bool { + if self.presence != presence { + if !presence { + BAD_FAN_COUNT.fetch_add(1, Ordering::SeqCst); + } else if !self.presence { + BAD_FAN_COUNT.fetch_sub(1, Ordering::SeqCst); + } + self.presence = presence; + true + } else { + false + } + } + + pub fn set_under_speed(&mut self, under_speed: bool) -> bool { + if self.under_speed != under_speed { + self.under_speed = under_speed; + true + } else { + false + } + } + + pub fn set_over_speed(&mut self, over_speed: bool) -> bool { + if self.over_speed != over_speed { + self.over_speed = over_speed; + true + } else { + false + } + } + + pub fn set_fault_status(&mut self, fault_status: bool) -> bool { + if self.fault_status != fault_status { + if !fault_status { + BAD_FAN_COUNT.fetch_add(1, Ordering::SeqCst); + } else if !self.fault_status { + BAD_FAN_COUNT.fetch_sub(1, Ordering::SeqCst); + } + self.fault_status = fault_status; + true + } else { + false + } + } + + pub fn is_ok(&self) -> bool { + self.presence && !self.under_speed && !self.over_speed && self.fault_status + } + + pub fn get_bad_fan_count() -> u32 { + BAD_FAN_COUNT.load(Ordering::SeqCst) + } + + pub fn reset_fan_counter() { + BAD_FAN_COUNT.store(0, Ordering::SeqCst); + } +} + +impl Default for FanStatus { + fn default() -> Self { + Self::new() + } +} + +pub struct FanDrawer { + name: String, + fans: Vec>, +} + +impl FanDrawer { + pub fn new(name: String, fans: Vec>) -> Self { + Self { name, fans } + } + + pub fn get_name(&self) -> Result { + Ok(self.name.clone()) + } + + pub fn get_all_fans(&self) -> &[Box] { + &self.fans + } + + pub fn get_presence(&self) -> Result { + Ok(true) + } + + pub fn get_status(&self) -> Result { + for fan in &self.fans { + if !fan.get_status()? { + return Ok(false); + } + } + Ok(true) + } + + pub fn get_model(&self) -> Result { + Ok("Unknown".to_string()) + } + + pub fn get_serial(&self) -> Result { + Ok("Unknown".to_string()) + } + + pub fn is_replaceable(&self) -> Result { + Ok(true) + } + + pub fn set_status_led(&self, _color: LedColor) -> Result<()> { + Ok(()) + } + + pub fn get_status_led(&self) -> Result { + Ok(LedColor::Green) + } +} + +pub struct MlnxFan { + name: String, + hwmon_path: PathBuf, + fan_index: usize, + pwm_index: Option, +} + +impl MlnxFan { + pub fn new(name: String, hwmon_path: PathBuf, fan_index: usize, pwm_index: Option) -> Self { + Self { + name, + hwmon_path, + fan_index, + pwm_index, + } + } + + fn read_sysfs_value(&self, filename: &str) -> Result { + let path = self.hwmon_path.join(filename); + fs::read_to_string(&path) + .with_context(|| format!("Failed to read {}", path.display())) + .map(|s| s.trim().to_string()) + } + + fn read_sysfs_u32(&self, filename: &str) -> Result { + self.read_sysfs_value(filename)? + .parse() + .with_context(|| format!("Failed to parse {} as u32", filename)) + } + + fn write_sysfs_value(&self, filename: &str, value: &str) -> Result<()> { + let path = self.hwmon_path.join(filename); + fs::write(&path, value) + .with_context(|| format!("Failed to write to {}", path.display())) + } + + fn rpm_to_percentage(&self, rpm: u32) -> u32 { + const MAX_RPM: u32 = 25000; + ((rpm as f32 / MAX_RPM as f32) * 100.0).min(100.0) as u32 + } + + fn pwm_to_percentage(&self, pwm: u32) -> u32 { + ((pwm as f32 / 255.0) * 100.0) as u32 + } + + fn percentage_to_pwm(&self, percentage: u32) -> u32 { + ((percentage.min(100) as f32 / 100.0) * 255.0) as u32 + } +} + +impl Fan for MlnxFan { + fn get_name(&self) -> Result { + Ok(self.name.clone()) + } + + fn get_presence(&self) -> Result { + let fault_file = format!("fan{}_fault", self.fan_index); + match self.read_sysfs_u32(&fault_file) { + Ok(0) => Ok(true), + Ok(_) => Ok(false), + Err(_) => Ok(true), + } + } + + fn get_status(&self) -> Result { + let fault_file = format!("fan{}_fault", self.fan_index); + match self.read_sysfs_u32(&fault_file) { + Ok(0) => Ok(true), + Ok(_) => Ok(false), + Err(_) => Ok(true), + } + } + + fn get_speed(&self) -> Result { + let input_file = format!("fan{}_input", self.fan_index); + let rpm = self.read_sysfs_u32(&input_file)?; + Ok(self.rpm_to_percentage(rpm)) + } + + fn get_target_speed(&self) -> Result { + if let Some(pwm_idx) = self.pwm_index { + let pwm_file = format!("pwm{}", pwm_idx); + let pwm = self.read_sysfs_u32(&pwm_file)?; + Ok(self.pwm_to_percentage(pwm)) + } else { + self.get_speed() + } + } + + fn is_under_speed(&self) -> Result { + let speed = self.get_speed()?; + let target = self.get_target_speed()?; + const TOLERANCE: u32 = 20; + + Ok(speed < target.saturating_sub(TOLERANCE)) + } + + fn is_over_speed(&self) -> Result { + let speed = self.get_speed()?; + let target = self.get_target_speed()?; + const TOLERANCE: u32 = 20; + + Ok(speed > target.saturating_add(TOLERANCE)) + } + + fn get_direction(&self) -> Result { + Ok(FanDirection::Intake) + } + + fn get_model(&self) -> Result { + let name_file = self.hwmon_path.join("name"); + match fs::read_to_string(&name_file) { + Ok(name) => Ok(name.trim().to_string()), + Err(_) => Ok("Mellanox Fan".to_string()), + } + } + + fn get_serial(&self) -> Result { + Ok("N/A".to_string()) + } + + fn is_replaceable(&self) -> Result { + Ok(true) + } + + fn get_position_in_parent(&self) -> Result { + Ok(self.fan_index) + } + + fn set_status_led(&self, _color: LedColor) -> Result<()> { + Ok(()) + } + + fn get_status_led(&self) -> Result { + if self.get_status()? { + Ok(LedColor::Green) + } else { + Ok(LedColor::Red) + } + } +} + +pub fn set_fan_speed(hwmon_path: &Path, pwm_index: usize, speed_percentage: u32) -> Result<()> { + let pwm_file = format!("pwm{}", pwm_index); + let pwm_value = ((speed_percentage.min(100) as f32 / 100.0) * 255.0) as u32; + + let path = hwmon_path.join(&pwm_file); + fs::write(&path, pwm_value.to_string()) + .with_context(|| format!("Failed to set fan speed via {}", path.display()))?; + + Ok(()) +} diff --git a/platform/mellanox/crates/sonic-platform/src/lib.rs b/platform/mellanox/crates/sonic-platform/src/lib.rs new file mode 100644 index 00000000000..9d001e66862 --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/src/lib.rs @@ -0,0 +1,47 @@ +pub mod chassis; +pub mod fan; +pub mod thermal; + +pub use chassis::MlnxChassis; +pub use fan::{ + MlnxFan, Fan, FanDirection, FanDrawer, FanStatus, LedColor, set_fan_speed +}; +pub use thermal::{MlnxThermal, Thermal, TemperatureStatus}; + +use anyhow::Result; +use std::fs; + +pub fn detect_platform() -> bool { + is_mellanox_platform() +} + +pub fn is_mellanox_platform() -> bool { + if let Ok(dmi_board_vendor) = fs::read_to_string("/sys/class/dmi/id/board_vendor") { + if dmi_board_vendor.to_lowercase().contains("mellanox") { + return true; + } + } + + if let Ok(dmi_sys_vendor) = fs::read_to_string("/sys/class/dmi/id/sys_vendor") { + if dmi_sys_vendor.to_lowercase().contains("mellanox") + || dmi_sys_vendor.to_lowercase().contains("nvidia") { + return true; + } + } + + if let Ok(entries) = fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + if let Ok(name) = fs::read_to_string(entry.path().join("name")) { + if name.trim().contains("mlxsw") { + return true; + } + } + } + } + + false +} + +pub fn create_chassis() -> Result { + MlnxChassis::new() +} diff --git a/platform/mellanox/crates/sonic-platform/src/mod.rs b/platform/mellanox/crates/sonic-platform/src/mod.rs new file mode 100644 index 00000000000..4e6bff5f890 --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/src/mod.rs @@ -0,0 +1,40 @@ +pub mod chassis; +pub mod fan; +pub mod thermal; + +pub use chassis::MlnxChassis; +pub use fan::MlnxFan; +pub use thermal::MlnxThermal; + +use std::fs; + +pub fn detect_platform() -> bool { + is_mellanox_platform() +} + +pub fn is_mellanox_platform() -> bool { + if let Ok(dmi_board_vendor) = fs::read_to_string("/sys/class/dmi/id/board_vendor") { + if dmi_board_vendor.to_lowercase().contains("mellanox") { + return true; + } + } + + if let Ok(dmi_sys_vendor) = fs::read_to_string("/sys/class/dmi/id/sys_vendor") { + if dmi_sys_vendor.to_lowercase().contains("mellanox") + || dmi_sys_vendor.to_lowercase().contains("nvidia") { + return true; + } + } + + if let Ok(entries) = fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + if let Ok(name) = fs::read_to_string(entry.path().join("name")) { + if name.trim().contains("mlxsw") { + return true; + } + } + } + } + + false +} diff --git a/platform/mellanox/crates/sonic-platform/src/thermal.rs b/platform/mellanox/crates/sonic-platform/src/thermal.rs new file mode 100644 index 00000000000..d2366ef6de0 --- /dev/null +++ b/platform/mellanox/crates/sonic-platform/src/thermal.rs @@ -0,0 +1,207 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::PathBuf; + +pub trait Thermal: Send + Sync { + fn get_name(&self) -> Result; + + fn get_temperature(&self) -> Result; + + fn get_high_threshold(&self) -> Result; + + fn get_low_threshold(&self) -> Result; + + fn get_high_critical_threshold(&self) -> Result; + + fn get_low_critical_threshold(&self) -> Result; + + fn get_minimum_recorded(&self) -> Result; + + fn get_maximum_recorded(&self) -> Result; + + fn is_replaceable(&self) -> Result; + + fn get_position_in_parent(&self) -> Result; +} + +#[derive(Debug)] +pub struct TemperatureStatus { + pub temperature: Option, + pub over_temperature: bool, + pub under_temperature: bool, +} + +impl TemperatureStatus { + pub fn new() -> Self { + Self { + temperature: None, + over_temperature: false, + under_temperature: false, + } + } + + pub fn set_temperature(&mut self, name: &str, new_temp: f32) -> bool { + const TEMPERATURE_DIFF_THRESHOLD: f32 = 10.0; + + if let Some(old_temp) = self.temperature { + let diff = (new_temp - old_temp).abs(); + if diff > TEMPERATURE_DIFF_THRESHOLD { + tracing::warn!( + "Temperature of {} changed too fast: {} -> {}°C", + name, + old_temp, + new_temp + ); + } + } + + let changed = self.temperature.map_or(true, |t| (t - new_temp).abs() > 0.1); + self.temperature = Some(new_temp); + changed + } + + pub fn set_over_temperature(&mut self, temperature: f32, threshold: f32) -> bool { + const NOT_AVAILABLE: f32 = -999.0; + + if (temperature - NOT_AVAILABLE).abs() < 0.1 || (threshold - NOT_AVAILABLE).abs() < 0.1 { + return false; + } + + let new_status = temperature > threshold; + let changed = self.over_temperature != new_status; + self.over_temperature = new_status; + changed + } + + pub fn set_under_temperature(&mut self, temperature: f32, threshold: f32) -> bool { + const NOT_AVAILABLE: f32 = -999.0; + + if (temperature - NOT_AVAILABLE).abs() < 0.1 || (threshold - NOT_AVAILABLE).abs() < 0.1 { + return false; + } + + let new_status = temperature < threshold; + let changed = self.under_temperature != new_status; + self.under_temperature = new_status; + changed + } +} + +impl Default for TemperatureStatus { + fn default() -> Self { + Self::new() + } +} + +pub struct MlnxThermal { + name: String, + hwmon_path: PathBuf, + temp_index: usize, + min_temp: f32, + max_temp: f32, +} + +impl MlnxThermal { + pub fn new(name: String, hwmon_path: PathBuf, temp_index: usize) -> Self { + Self { + name, + hwmon_path, + temp_index, + min_temp: 1000.0, + max_temp: -1000.0, + } + } + + fn read_sysfs_value(&self, filename: &str) -> Result { + let path = self.hwmon_path.join(filename); + fs::read_to_string(&path) + .with_context(|| format!("Failed to read {}", path.display())) + .map(|s| s.trim().to_string()) + } + + fn read_sysfs_temp(&self, filename: &str) -> Result { + let millidegrees: i32 = self.read_sysfs_value(filename)? + .parse() + .with_context(|| format!("Failed to parse {} as temperature", filename))?; + + Ok(millidegrees as f32 / 1000.0) + } + + fn update_min_max(&mut self, temp: f32) { + if temp < self.min_temp { + self.min_temp = temp; + } + if temp > self.max_temp { + self.max_temp = temp; + } + } +} + +impl Thermal for MlnxThermal { + fn get_name(&self) -> Result { + let label_file = format!("temp{}_label", self.temp_index); + match self.read_sysfs_value(&label_file) { + Ok(label) => Ok(label), + Err(_) => Ok(self.name.clone()), + } + } + + fn get_temperature(&self) -> Result { + let input_file = format!("temp{}_input", self.temp_index); + let temp = self.read_sysfs_temp(&input_file)?; + + Ok(temp) + } + + fn get_high_threshold(&self) -> Result { + let max_file = format!("temp{}_max", self.temp_index); + match self.read_sysfs_temp(&max_file) { + Ok(temp) => Ok(temp), + Err(_) => Ok(85.0), + } + } + + fn get_low_threshold(&self) -> Result { + let min_file = format!("temp{}_min", self.temp_index); + match self.read_sysfs_temp(&min_file) { + Ok(temp) => Ok(temp), + Err(_) => Ok(0.0), + } + } + + fn get_high_critical_threshold(&self) -> Result { + let crit_file = format!("temp{}_crit", self.temp_index); + match self.read_sysfs_temp(&crit_file) { + Ok(temp) => Ok(temp), + Err(_) => Ok(100.0), + } + } + + fn get_low_critical_threshold(&self) -> Result { + Ok(-10.0) + } + + fn get_minimum_recorded(&self) -> Result { + let lowest_file = format!("temp{}_lowest", self.temp_index); + match self.read_sysfs_temp(&lowest_file) { + Ok(temp) => Ok(temp), + Err(_) => Ok(self.min_temp), + } + } + + fn get_maximum_recorded(&self) -> Result { + let highest_file = format!("temp{}_highest", self.temp_index); + match self.read_sysfs_temp(&highest_file) { + Ok(temp) => Ok(temp), + Err(_) => Ok(self.max_temp), + } + } + + fn is_replaceable(&self) -> Result { + Ok(false) + } + + fn get_position_in_parent(&self) -> Result { + Ok(self.temp_index) + } +}