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