Skip to main content

nv_redfish/update_service/
mod.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
16//! Update Service entities and collections.
17//!
18//! This module provides types for working with Redfish UpdateService resources
19//! and their sub-resources like firmware and software inventory.
20
21mod software_inventory;
22
23use crate::core::NavProperty;
24use crate::patch_support::Payload;
25use crate::patch_support::ReadPatchFn;
26use crate::schema::redfish::update_service::UpdateService as UpdateServiceSchema;
27use crate::schema::redfish::update_service::UpdateServiceSimpleUpdateAction;
28use crate::Error;
29use crate::NvBmc;
30use crate::Resource;
31use crate::ResourceSchema;
32use crate::ServiceRoot;
33use nv_redfish_core::Bmc;
34use serde_json::Value as JsonValue;
35use software_inventory::SoftwareInventoryCollection;
36use std::sync::Arc;
37
38#[doc(inline)]
39pub use crate::schema::redfish::update_service::TransferProtocolType;
40#[doc(inline)]
41pub use software_inventory::SoftwareInventory;
42#[doc(inline)]
43pub use software_inventory::Version;
44#[doc(inline)]
45pub use software_inventory::VersionRef;
46
47/// Update service.
48///
49/// Provides functions to access firmware and software inventory, and perform update actions.
50pub struct UpdateService<B: Bmc> {
51    bmc: NvBmc<B>,
52    data: Arc<UpdateServiceSchema>,
53    fw_inventory_read_patch_fn: Option<ReadPatchFn>,
54}
55
56impl<B: Bmc> UpdateService<B> {
57    /// Create a new update service handle.
58    pub(crate) async fn new(
59        bmc: &NvBmc<B>,
60        root: &ServiceRoot<B>,
61    ) -> Result<Option<Self>, Error<B>> {
62        let mut service_patches = Vec::new();
63        if root.bug_missing_update_service_name_field() {
64            service_patches.push(add_default_update_service_name);
65        }
66        let service_patch_fn = (!service_patches.is_empty()).then(|| {
67            Arc::new(move |v| service_patches.iter().fold(v, |acc, f| f(acc))) as ReadPatchFn
68        });
69
70        let mut fw_inventory_patches = Vec::new();
71        if root.fw_inventory_wrong_release_date() {
72            fw_inventory_patches.push(fw_inventory_patch_wrong_release_date);
73        }
74        let fw_inventory_read_patch_fn = (!fw_inventory_patches.is_empty()).then(|| {
75            Arc::new(move |v| fw_inventory_patches.iter().fold(v, |acc, f| f(acc))) as ReadPatchFn
76        });
77
78        if let Some(nav) = &root.root.update_service {
79            if let Some(service_patch_fn) = service_patch_fn {
80                Payload::get(bmc.as_ref(), nav, service_patch_fn.as_ref()).await
81            } else {
82                nav.get(bmc.as_ref()).await.map_err(Error::Bmc)
83            }
84            .map(Some)
85        } else if root.bug_missing_root_nav_properties() {
86            let nav =
87                NavProperty::new_reference(format!("{}/UpdateService", root.odata_id()).into());
88            if let Some(service_patch_fn) = service_patch_fn {
89                Payload::get(bmc.as_ref(), &nav, service_patch_fn.as_ref()).await
90            } else {
91                nav.get(bmc.as_ref()).await.map_err(Error::Bmc)
92            }
93            .map(Some)
94        } else {
95            Ok(None)
96        }
97        .map(|d| {
98            d.map(|data| Self {
99                bmc: bmc.clone(),
100                data,
101                fw_inventory_read_patch_fn,
102            })
103        })
104    }
105
106    /// Get the raw schema data for this update service.
107    ///
108    /// Returns an `Arc` to the underlying schema, allowing cheap cloning
109    /// and sharing of the data.
110    #[must_use]
111    pub fn raw(&self) -> Arc<UpdateServiceSchema> {
112        self.data.clone()
113    }
114
115    /// List all firmware inventory items.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if:
120    /// - The update service does not have a firmware inventory collection
121    /// - Fetching firmware inventory data fails
122    pub async fn firmware_inventories(
123        &self,
124    ) -> Result<Option<Vec<SoftwareInventory<B>>>, Error<B>> {
125        if let Some(collection_ref) = &self.data.firmware_inventory {
126            SoftwareInventoryCollection::new(
127                &self.bmc,
128                collection_ref,
129                self.fw_inventory_read_patch_fn.clone(),
130            )
131            .await?
132            .members()
133            .await
134            .map(Some)
135        } else {
136            Ok(None)
137        }
138    }
139
140    /// List all software inventory items.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if:
145    /// - The update service does not have a software inventory collection
146    /// - Fetching software inventory data fails
147    pub async fn software_inventories(
148        &self,
149    ) -> Result<Option<Vec<SoftwareInventory<B>>>, Error<B>> {
150        if let Some(collection_ref) = &self.data.software_inventory {
151            let collection = self.bmc.expand_property(collection_ref).await?;
152            let mut items = Vec::new();
153            for item_ref in &collection.members {
154                items.push(SoftwareInventory::new(&self.bmc, item_ref, None).await?);
155            }
156            Ok(Some(items))
157        } else {
158            Ok(None)
159        }
160    }
161
162    /// Perform a simple update with the specified image URI.
163    ///
164    /// This action updates software components by downloading and installing
165    /// a software image from the specified URI.
166    ///
167    /// # Arguments
168    ///
169    /// * `image_uri` - The URI of the software image to install
170    /// * `transfer_protocol` - Optional network protocol to use for retrieving the image
171    /// * `targets` - Optional list of URIs indicating where to apply the update
172    /// * `username` - Optional username for accessing the image URI
173    /// * `password` - Optional password for accessing the image URI
174    /// * `force_update` - Whether to bypass update policies (e.g., allow downgrade)
175    /// * `stage` - Whether to stage the image for later activation instead of immediate installation
176    /// * `local_image` - An indication of whether the service adds the image to the local image store
177    /// * `exclude_targets` - An array of URIs that indicate where not to apply the update image
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if:
182    /// - The update service does not support the `SimpleUpdate` action
183    /// - The action execution fails
184    #[allow(clippy::too_many_arguments)]
185    pub async fn simple_update(
186        &self,
187        image_uri: String,
188        transfer_protocol: Option<TransferProtocolType>,
189        targets: Option<Vec<String>>,
190        username: Option<String>,
191        password: Option<String>,
192        force_update: Option<bool>,
193        stage: Option<bool>,
194        local_image: Option<bool>,
195        exclude_targets: Option<Vec<String>>,
196    ) -> Result<(), Error<B>>
197    where
198        B::Error: nv_redfish_core::ActionError,
199    {
200        let actions = self
201            .data
202            .actions
203            .as_ref()
204            .ok_or(Error::ActionNotAvailable)?;
205
206        actions
207            .simple_update(
208                self.bmc.as_ref(),
209                &UpdateServiceSimpleUpdateAction {
210                    image_uri: Some(image_uri),
211                    transfer_protocol,
212                    targets,
213                    username,
214                    password,
215                    force_update,
216                    stage,
217                    local_image,
218                    exclude_targets,
219                },
220            )
221            .await
222            .map_err(Error::Bmc)?;
223
224        Ok(())
225    }
226
227    /// Start updates that have been previously invoked with an `OperationApplyTime` of `OnStartUpdateRequest`.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if:
232    /// - The update service does not support the `StartUpdate` action
233    /// - The action execution fails
234    pub async fn start_update(&self) -> Result<(), Error<B>>
235    where
236        B::Error: nv_redfish_core::ActionError,
237    {
238        let actions = self
239            .data
240            .actions
241            .as_ref()
242            .ok_or(Error::ActionNotAvailable)?;
243
244        actions
245            .start_update(self.bmc.as_ref())
246            .await
247            .map_err(Error::Bmc)?;
248
249        Ok(())
250    }
251}
252
253impl<B: Bmc> Resource for UpdateService<B> {
254    fn resource_ref(&self) -> &ResourceSchema {
255        &self.data.as_ref().base
256    }
257}
258
259// `ReleaseDate` is marked as `edm.DateTimeOffset`, but some systems
260// puts "00:00:00Z" as ReleaseDate that is not conform to ABNF of the DateTimeOffset.
261// we delete such fields...
262fn fw_inventory_patch_wrong_release_date(v: JsonValue) -> JsonValue {
263    if let JsonValue::Object(mut obj) = v {
264        if let Some(JsonValue::String(date)) = obj.get("ReleaseDate") {
265            if date == "00:00:00Z" {
266                obj.remove("ReleaseDate");
267            }
268        }
269        JsonValue::Object(obj)
270    } else {
271        v
272    }
273}
274
275fn add_default_update_service_name(v: JsonValue) -> JsonValue {
276    if let JsonValue::Object(mut obj) = v {
277        obj.entry("Name")
278            .or_insert(JsonValue::String("Unnamed update service".into()));
279        JsonValue::Object(obj)
280    } else {
281        v
282    }
283}