Skip to main content

nv_redfish/chassis/
item.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::hardware_id::HardwareIdRef;
17use crate::hardware_id::Manufacturer as HardwareIdManufacturer;
18use crate::hardware_id::Model as HardwareIdModel;
19use crate::hardware_id::PartNumber as HardwareIdPartNumber;
20use crate::hardware_id::SerialNumber as HardwareIdSerialNumber;
21use crate::patch_support::JsonValue;
22use crate::patch_support::Payload;
23use crate::patch_support::ReadPatchFn;
24use crate::schema::redfish::chassis::Chassis as ChassisSchema;
25use crate::Error;
26use crate::NvBmc;
27use crate::Resource;
28use crate::ResourceSchema;
29use crate::ServiceRoot;
30use nv_redfish_core::bmc::Bmc;
31use nv_redfish_core::NavProperty;
32use std::sync::Arc;
33
34#[cfg(feature = "assembly")]
35use crate::assembly::Assembly;
36#[cfg(feature = "network-adapters")]
37use crate::chassis::NetworkAdapter;
38#[cfg(feature = "network-adapters")]
39use crate::chassis::NetworkAdapterCollection;
40#[cfg(feature = "power")]
41use crate::chassis::Power;
42#[cfg(feature = "power-supplies")]
43use crate::chassis::PowerSupply;
44#[cfg(feature = "thermal")]
45use crate::chassis::Thermal;
46#[cfg(feature = "log-services")]
47use crate::log_service::LogService;
48#[cfg(feature = "oem-nvidia-baseboard")]
49use crate::oem::nvidia::baseboard::NvidiaCbcChassis;
50#[cfg(feature = "pcie-devices")]
51use crate::pcie_device::PcieDeviceCollection;
52#[cfg(feature = "sensors")]
53use crate::schema::redfish::sensor::Sensor as SchemaSensor;
54#[cfg(feature = "sensors")]
55use crate::sensor::extract_environment_sensors;
56#[cfg(feature = "sensors")]
57use crate::sensor::SensorRef;
58#[cfg(feature = "oem-nvidia-baseboard")]
59use std::convert::identity;
60
61#[doc(hidden)]
62pub enum ChassisTag {}
63
64/// Chassis manufacturer.
65pub type Manufacturer<T> = HardwareIdManufacturer<T, ChassisTag>;
66
67/// Chassis model.
68pub type Model<T> = HardwareIdModel<T, ChassisTag>;
69
70/// Chassis part number.
71pub type PartNumber<T> = HardwareIdPartNumber<T, ChassisTag>;
72
73/// Chassis serial number.
74pub type SerialNumber<T> = HardwareIdSerialNumber<T, ChassisTag>;
75
76pub struct Config {
77    read_patch_fn: Option<ReadPatchFn>,
78}
79
80impl Config {
81    pub fn new<B: Bmc>(root: &ServiceRoot<B>) -> Self {
82        let mut patches = Vec::new();
83        if root.bug_invalid_contained_by_fields() {
84            patches.push(remove_invalid_contained_by_fields as fn(JsonValue) -> JsonValue);
85        }
86        if root.bug_missing_chassis_type_field() {
87            patches.push(add_default_chassis_type);
88        }
89        if root.bug_missing_chassis_name_field() {
90            patches.push(add_default_chassis_name);
91        }
92        let read_patch_fn = (!patches.is_empty())
93            .then(|| Arc::new(move |v| patches.iter().fold(v, |acc, f| f(acc))) as ReadPatchFn);
94        Self { read_patch_fn }
95    }
96}
97
98/// Represents a chassis in the BMC.
99///
100/// Provides access to chassis information and sub-resources such as power supplies.
101pub struct Chassis<B: Bmc> {
102    #[allow(dead_code)] // used if any feature enabled.
103    bmc: NvBmc<B>,
104    data: Arc<ChassisSchema>,
105    #[allow(dead_code)] // used when assembly feature enabled.
106    config: Arc<Config>,
107}
108
109impl<B: Bmc> Chassis<B> {
110    /// Create a new chassis handle.
111    pub(crate) async fn new(
112        bmc: &NvBmc<B>,
113        nav: &NavProperty<ChassisSchema>,
114        config: Arc<Config>,
115    ) -> Result<Self, Error<B>> {
116        if let Some(read_patch_fn) = &config.read_patch_fn {
117            Payload::get(bmc.as_ref(), nav, read_patch_fn.as_ref()).await
118        } else {
119            nav.get(bmc.as_ref()).await.map_err(Error::Bmc)
120        }
121        .map(|data| Self {
122            bmc: bmc.clone(),
123            data,
124            config,
125        })
126    }
127
128    /// Get the raw schema data for this chassis.
129    ///
130    /// Returns an `Arc` to the underlying schema, allowing cheap cloning
131    /// and sharing of the data.
132    #[must_use]
133    pub fn raw(&self) -> Arc<ChassisSchema> {
134        self.data.clone()
135    }
136
137    /// Get hardware identifier of the network adpater.
138    #[must_use]
139    pub fn hardware_id(&self) -> HardwareIdRef<'_, ChassisTag> {
140        HardwareIdRef {
141            manufacturer: self
142                .data
143                .manufacturer
144                .as_ref()
145                .and_then(Option::as_deref)
146                .map(Manufacturer::new),
147            model: self
148                .data
149                .model
150                .as_ref()
151                .and_then(Option::as_deref)
152                .map(Model::new),
153            part_number: self
154                .data
155                .part_number
156                .as_ref()
157                .and_then(Option::as_deref)
158                .map(PartNumber::new),
159            serial_number: self
160                .data
161                .serial_number
162                .as_ref()
163                .and_then(Option::as_deref)
164                .map(SerialNumber::new),
165        }
166    }
167
168    /// Get assembly of this chassis
169    ///
170    /// Returns `Ok(None)` when the assembly link is absent.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if fetching assembly data fails.
175    #[cfg(feature = "assembly")]
176    pub async fn assembly(&self) -> Result<Option<Assembly<B>>, Error<B>> {
177        if let Some(assembly_ref) = &self.data.assembly {
178            Assembly::new(&self.bmc, assembly_ref).await.map(Some)
179        } else {
180            Ok(None)
181        }
182    }
183
184    /// Get power supplies from this chassis.
185    ///
186    /// Attempts to fetch power supplies from `PowerSubsystem` (modern API)
187    /// with fallback to Power resource (deprecated API).
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if fetching power supply data fails.
192    #[cfg(feature = "power-supplies")]
193    pub async fn power_supplies(&self) -> Result<Vec<PowerSupply<B>>, Error<B>> {
194        if let Some(ps) = &self.data.power_subsystem {
195            let ps = ps.get(self.bmc.as_ref()).await.map_err(Error::Bmc)?;
196            if let Some(supplies) = &ps.power_supplies {
197                let supplies = &self.bmc.expand_property(supplies).await?.members;
198                let mut power_supplies = Vec::with_capacity(supplies.len());
199                for power_supply in supplies {
200                    power_supplies.push(PowerSupply::new(&self.bmc, power_supply).await?);
201                }
202                return Ok(power_supplies);
203            }
204        }
205
206        Ok(Vec::new())
207    }
208
209    /// Get legacy Power resource (for older BMCs).
210    ///
211    /// Returns the deprecated `Chassis/Power` resource if available.
212    /// For modern BMCs, prefer using direct sensor links via `HasSensors`
213    /// or the modern `PowerSubsystem` API.
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if fetching power data fails.
218    #[cfg(feature = "power")]
219    pub async fn power(&self) -> Result<Option<Power<B>>, Error<B>> {
220        if let Some(power_ref) = &self.data.power {
221            Ok(Some(Power::new(&self.bmc, power_ref).await?))
222        } else {
223            Ok(None)
224        }
225    }
226
227    /// Get legacy Thermal resource (for older BMCs).
228    ///
229    /// Returns the deprecated `Chassis/Thermal` resource if available.
230    /// For modern BMCs, prefer using direct sensor links via `HasSensors`
231    /// or the modern `ThermalSubsystem` API.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if fetching thermal data fails.
236    #[cfg(feature = "thermal")]
237    pub async fn thermal(&self) -> Result<Option<Thermal<B>>, Error<B>> {
238        if let Some(thermal_ref) = &self.data.thermal {
239            Thermal::new(&self.bmc, thermal_ref).await.map(Some)
240        } else {
241            Ok(None)
242        }
243    }
244
245    /// Get network adapter resources
246    ///
247    /// Returns the `Chassis/NetworkAdapter` resources if available, and `Ok(None)` when
248    /// the network adapters link is absent.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if fetching network adapters data fails.
253    #[cfg(feature = "network-adapters")]
254    pub async fn network_adapters(&self) -> Result<Option<Vec<NetworkAdapter<B>>>, Error<B>> {
255        if let Some(network_adapters_collection_ref) = &self.data.network_adapters {
256            NetworkAdapterCollection::new(&self.bmc, network_adapters_collection_ref)
257                .await?
258                .members()
259                .await
260                .map(Some)
261        } else {
262            Ok(None)
263        }
264    }
265
266    /// Get log services for this chassis.
267    ///
268    /// Returns `Ok(None)` when the log services link is absent.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if fetching log service data fails.
273    #[cfg(feature = "log-services")]
274    pub async fn log_services(&self) -> Result<Option<Vec<LogService<B>>>, Error<B>> {
275        if let Some(log_services_ref) = &self.data.log_services {
276            let log_services_collection = log_services_ref
277                .get(self.bmc.as_ref())
278                .await
279                .map_err(Error::Bmc)?;
280
281            let mut log_services = Vec::new();
282            for m in &log_services_collection.members {
283                log_services.push(LogService::new(&self.bmc, m).await?);
284            }
285
286            Ok(Some(log_services))
287        } else {
288            Ok(None)
289        }
290    }
291
292    /// Get the environment sensors for this chassis.
293    ///
294    /// Returns a vector of `Sensor<B>` obtained from environment metrics, if available.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if get of environment metrics failed.
299    #[cfg(feature = "sensors")]
300    pub async fn environment_sensors(&self) -> Result<Vec<SensorRef<B>>, Error<B>> {
301        let sensor_refs = if let Some(env_ref) = &self.data.environment_metrics {
302            extract_environment_sensors(env_ref, self.bmc.as_ref()).await?
303        } else {
304            Vec::new()
305        };
306
307        Ok(sensor_refs
308            .into_iter()
309            .map(|r| SensorRef::new(self.bmc.clone(), r))
310            .collect())
311    }
312
313    /// Get the sensors collection for this chassis.
314    ///
315    /// Returns all available sensors associated with the chassis, and `Ok(None)`
316    /// when the sensors link is absent.
317    ///
318    /// # Errors
319    ///
320    /// Returns an error if fetching sensors data fails.
321    #[cfg(feature = "sensors")]
322    pub async fn sensors(&self) -> Result<Option<Vec<SensorRef<B>>>, Error<B>> {
323        if let Some(sensors_collection) = &self.data.sensors {
324            let sc = sensors_collection
325                .get(self.bmc.as_ref())
326                .await
327                .map_err(Error::Bmc)?;
328            let mut sensor_data = Vec::with_capacity(sc.members.len());
329            for sensor in &sc.members {
330                sensor_data.push(SensorRef::new(
331                    self.bmc.clone(),
332                    NavProperty::<SchemaSensor>::new_reference(sensor.id().clone()),
333                ));
334            }
335            Ok(Some(sensor_data))
336        } else {
337            Ok(None)
338        }
339    }
340
341    /// Get `PCIe` devices for this computer system.
342    ///
343    /// Returns `Ok(None)` when the `PCIeDevices` link is absent.
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if fetching `PCIe` devices data fails.
348    #[cfg(feature = "pcie-devices")]
349    pub async fn pcie_devices(&self) -> Result<Option<PcieDeviceCollection<B>>, crate::Error<B>> {
350        if let Some(p) = &self.data.pcie_devices {
351            PcieDeviceCollection::new(&self.bmc, p).await.map(Some)
352        } else {
353            Ok(None)
354        }
355    }
356
357    /// NVIDIA Bluefield OEM extension
358    ///
359    /// Returns `Ok(None)` when the chassis does not include NVIDIA OEM extension data.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if NVIDIA OEM data parsing fails.
364    #[cfg(feature = "oem-nvidia-baseboard")]
365    pub fn oem_nvidia_baseboard_cbc(&self) -> Result<Option<NvidiaCbcChassis<B>>, Error<B>> {
366        self.data
367            .base
368            .base
369            .oem
370            .as_ref()
371            .map(NvidiaCbcChassis::new)
372            .transpose()
373            .map(|v| v.and_then(identity))
374    }
375}
376
377impl<B: Bmc> Resource for Chassis<B> {
378    fn resource_ref(&self) -> &ResourceSchema {
379        &self.data.as_ref().base
380    }
381}
382
383fn remove_invalid_contained_by_fields(mut v: JsonValue) -> JsonValue {
384    if let JsonValue::Object(ref mut obj) = v {
385        if let Some(JsonValue::Object(ref mut links_obj)) = obj.get_mut("Links") {
386            if let Some(JsonValue::Object(ref mut contained_by_obj)) =
387                links_obj.get_mut("ContainedBy")
388            {
389                contained_by_obj.retain(|k, _| k == "@odata.id");
390            }
391        }
392    }
393    v
394}
395
396fn add_default_chassis_type(v: JsonValue) -> JsonValue {
397    if let JsonValue::Object(mut obj) = v {
398        obj.entry("ChassisType")
399            .or_insert(JsonValue::String("Other".into()));
400        JsonValue::Object(obj)
401    } else {
402        v
403    }
404}
405
406fn add_default_chassis_name(v: JsonValue) -> JsonValue {
407    if let JsonValue::Object(mut obj) = v {
408        obj.entry("Name")
409            .or_insert(JsonValue::String("Unnamed chassis".into()));
410        JsonValue::Object(obj)
411    } else {
412        v
413    }
414}