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