diff --git a/src/ami.rs b/src/ami.rs index 7a7f845ca..dc58a1d99 100644 --- a/src/ami.rs +++ b/src/ami.rs @@ -1221,6 +1221,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/dell.rs b/src/dell.rs index b4f38ae7d..714bcb4bc 100644 --- a/src/dell.rs +++ b/src/dell.rs @@ -1445,6 +1445,54 @@ impl Redfish for Bmc { Ok(()) }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { + if servers.is_empty() { + return Ok(()); + } + + if self.is_lockdown().await? { + return Err(RedfishError::Lockdown); + } + + let mut attrs = HashMap::from([("NTPConfigGroup.1.NTPEnable", "Enabled")]); + const NTP_KEYS: [&str; 3] = [ + "NTPConfigGroup.1.NTP1", + "NTPConfigGroup.1.NTP2", + "NTPConfigGroup.1.NTP3", + ]; + for (i, key) in NTP_KEYS.into_iter().enumerate() { + // blank unused slots so the set is authoritative + attrs.insert(key, servers.get(i).map_or("", String::as_str)); + } + + // Try standard path first + let body = HashMap::from([("Attributes", attrs)]); + let manager_id = self.s.manager_id(); + let standard_url = format!("Managers/{manager_id}/Attributes"); + match self.s.client.patch(&standard_url, &body).await { + Ok(_) => return Ok(()), + Err(RedfishError::HTTPErrorCode { + status_code: StatusCode::NOT_FOUND, + .. + }) => { + tracing::info!( + "Managers/Attributes not found, using OEM DellAttributes path for NTP server config" + ); + } + Err(e) => return Err(e), + } + + // Fallback to OEM DellAttributes path + let oem_url = format!("Managers/{manager_id}/Oem/Dell/DellAttributes/{manager_id}"); + self.s.client.patch(&oem_url, body).await?; + Ok(()) + }) + } } impl Bmc { diff --git a/src/delta_powershelf.rs b/src/delta_powershelf.rs index 428dd1caf..0ffefe109 100644 --- a/src/delta_powershelf.rs +++ b/src/delta_powershelf.rs @@ -857,6 +857,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/hpe.rs b/src/hpe.rs index e2b54d21c..af77ca2ed 100644 --- a/src/hpe.rs +++ b/src/hpe.rs @@ -1206,6 +1206,33 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { + if servers.is_empty() { + return Ok(()); + } + + // iLO supports at most 2 static NTP servers; extra entries are ignored. + if servers.len() > 2 { + tracing::warn!( + "iLO supports at most 2 static NTP servers; ignoring {} extra entries", + servers.len() - 2, + ); + } + let static_ntp_servers = vec![ + servers.first().cloned().unwrap_or_default(), + servers.get(1).cloned().unwrap_or_default(), + ]; + + let url = format!("Managers/{}/DateTime", self.s.manager_id()); + let body = HashMap::from([("StaticNTPServers", static_ntp_servers)]); + self.s.client.patch(&url, body).await.map(|_resp| ()) + }) + } } impl Bmc { diff --git a/src/lenovo.rs b/src/lenovo.rs index 3bdf95451..0799c41da 100644 --- a/src/lenovo.rs +++ b/src/lenovo.rs @@ -1239,6 +1239,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/lib.rs b/src/lib.rs index 64278b7d7..183459b0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -673,6 +673,12 @@ pub trait Redfish: Send + Sync + 'static { // Sets the timezone to UTC // Only applicable to Dells fn set_utc_timezone<'a>(&'a self) -> RedfishFuture<'a, Result<(), RedfishError>>; + + // Sets the NTP servers + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> RedfishFuture<'a, Result<(), RedfishError>>; } #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] diff --git a/src/liteon_powershelf.rs b/src/liteon_powershelf.rs index e73097622..f7cb13416 100644 --- a/src/liteon_powershelf.rs +++ b/src/liteon_powershelf.rs @@ -882,6 +882,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/nvidia_dpu.rs b/src/nvidia_dpu.rs index d2dd4008a..d0920229a 100644 --- a/src/nvidia_dpu.rs +++ b/src/nvidia_dpu.rs @@ -1101,6 +1101,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/nvidia_gbswitch.rs b/src/nvidia_gbswitch.rs index 07906eb55..057950c70 100644 --- a/src/nvidia_gbswitch.rs +++ b/src/nvidia_gbswitch.rs @@ -1075,6 +1075,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/nvidia_gbx00.rs b/src/nvidia_gbx00.rs index 0d02aca2b..bbd0b8372 100644 --- a/src/nvidia_gbx00.rs +++ b/src/nvidia_gbx00.rs @@ -1337,6 +1337,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/nvidia_gh200.rs b/src/nvidia_gh200.rs index 8a70a35e0..35f8d883c 100644 --- a/src/nvidia_gh200.rs +++ b/src/nvidia_gh200.rs @@ -1023,6 +1023,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/nvidia_viking.rs b/src/nvidia_viking.rs index 796ea1e15..fc58034ff 100644 --- a/src/nvidia_viking.rs +++ b/src/nvidia_viking.rs @@ -1276,6 +1276,13 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.s.set_manager_ntp_servers(servers).await }) + } } impl Bmc { diff --git a/src/standard.rs b/src/standard.rs index 25bb5cb2c..2bdc53a60 100644 --- a/src/standard.rs +++ b/src/standard.rs @@ -1250,6 +1250,13 @@ impl Redfish for RedfishStandard { Ok(()) }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { self.set_manager_ntp_servers(servers).await }) + } } impl RedfishStandard { @@ -1684,6 +1691,31 @@ impl RedfishStandard { Ok(body) } + /// Set NTP servers via the standard ManagerNetworkProtocol resource. + pub async fn set_manager_ntp_servers(&self, servers: &[String]) -> Result<(), RedfishError> { + if servers.is_empty() { + return Ok(()); + } + + let url = format!("Managers/{}/NetworkProtocol", self.manager_id()); + let ntp_servers = HashMap::from([( + "NTP", + json!({ + "NTPServers": servers, + "ProtocolEnabled": true, + }), + )]); + + if matches!( + self.vendor, + Some(RedfishVendor::AMI | RedfishVendor::LenovoAMI) + ) { + self.client.patch_with_if_match(&url, ntp_servers).await + } else { + self.client.patch(&url, ntp_servers).await.map(|_resp| ()) + } + } + pub async fn reset_manager( &self, reset_type: ManagerResetType, diff --git a/src/supermicro.rs b/src/supermicro.rs index 73854b4e8..122818aa3 100644 --- a/src/supermicro.rs +++ b/src/supermicro.rs @@ -1175,6 +1175,18 @@ impl Redfish for Bmc { fn set_utc_timezone<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { Box::pin(async move { self.s.set_utc_timezone().await }) } + + fn set_ntp_servers<'a>( + &'a self, + servers: &'a [String], + ) -> crate::RedfishFuture<'a, Result<(), RedfishError>> { + Box::pin(async move { + if self.get_syslockdown().await? { + return Err(RedfishError::Lockdown); + } + self.s.set_manager_ntp_servers(servers).await + }) + } } impl Bmc {