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