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