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