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