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 std::sync::Arc;
24use std::time::Duration;
25
26use crate::Error;
27use crate::NvBmc;
28use crate::Resource;
29use crate::ResourceSchema;
30use crate::ServiceRoot;
31use crate::core::NavProperty;
32use crate::patch_support::Payload;
33use crate::patch_support::ReadPatchFn;
34use crate::schema::update_service::UpdateService as UpdateServiceSchema;
35use crate::schema::update_service::UpdateServiceSimpleUpdateAction;
36
37use nv_redfish_core::Bmc;
38use nv_redfish_core::DataStream;
39#[cfg(feature = "update-service-deprecated")]
40use nv_redfish_core::EntityTypeRef;
41#[cfg(feature = "update-service-deprecated")]
42use nv_redfish_core::HttpPushUriUpdateRequest;
43use nv_redfish_core::ModificationResponse;
44use nv_redfish_core::MultipartUpdateRequest;
45use nv_redfish_core::UploadReader;
46#[cfg(feature = "update-service-deprecated")]
47use nv_redfish_core::UploadStream;
48use serde_json::Value as JsonValue;
49use software_inventory::SoftwareInventoryCollection;
50
51#[doc(inline)]
52pub use crate::schema::update_service::TransferProtocolType;
53#[doc(inline)]
54pub use crate::schema::update_service::UpdateParametersUpdate as MultipartUpdateParameters;
55#[cfg(feature = "update-service-deprecated")]
56#[doc(inline)]
57pub use crate::schema::update_service::UpdateServiceUpdate;
58#[doc(inline)]
59pub use software_inventory::SoftwareInventory;
60#[doc(inline)]
61pub use software_inventory::Version;
62#[doc(inline)]
63pub use software_inventory::VersionRef;
64
65/// Update service.
66///
67/// Provides functions to access firmware and software inventory, and perform update actions.
68pub struct UpdateService<B: Bmc> {
69    bmc: NvBmc<B>,
70    data: Arc<UpdateServiceSchema>,
71    fw_inventory_read_patch_fn: Option<ReadPatchFn>,
72}
73
74impl<B: Bmc> UpdateService<B> {
75    /// Create a new update service handle.
76    pub(crate) async fn new(
77        bmc: &NvBmc<B>,
78        root: &ServiceRoot<B>,
79    ) -> Result<Option<Self>, Error<B>> {
80        let mut service_patches = Vec::new();
81        if bmc.quirks.bug_missing_update_service_name_field() {
82            service_patches.push(add_default_update_service_name);
83        }
84        let service_patch_fn = (!service_patches.is_empty()).then(|| {
85            Arc::new(move |v| service_patches.iter().fold(v, |acc, f| f(acc))) as ReadPatchFn
86        });
87
88        let mut fw_inventory_patches = Vec::new();
89        if bmc.quirks.fw_inventory_wrong_release_date() {
90            fw_inventory_patches.push(fw_inventory_patch_wrong_release_date);
91        }
92        let fw_inventory_read_patch_fn = (!fw_inventory_patches.is_empty()).then(|| {
93            Arc::new(move |v| fw_inventory_patches.iter().fold(v, |acc, f| f(acc))) as ReadPatchFn
94        });
95
96        if let Some(nav) = &root.root.update_service {
97            if let Some(service_patch_fn) = service_patch_fn {
98                Payload::get(bmc.as_ref(), nav, service_patch_fn.as_ref()).await
99            } else {
100                nav.get(bmc.as_ref()).await.map_err(Error::Bmc)
101            }
102            .map(Some)
103        } else if bmc.quirks.bug_missing_root_nav_properties() {
104            let nav =
105                NavProperty::new_reference(format!("{}/UpdateService", root.odata_id()).into());
106            if let Some(service_patch_fn) = service_patch_fn {
107                Payload::get(bmc.as_ref(), &nav, service_patch_fn.as_ref()).await
108            } else {
109                nav.get(bmc.as_ref()).await.map_err(Error::Bmc)
110            }
111            .map(Some)
112        } else {
113            Ok(None)
114        }
115        .map(|d| {
116            d.map(|data| Self {
117                bmc: bmc.clone(),
118                data,
119                fw_inventory_read_patch_fn,
120            })
121        })
122    }
123
124    /// Get the raw schema data for this update service.
125    ///
126    /// Returns an `Arc` to the underlying schema, allowing cheap cloning
127    /// and sharing of the data.
128    #[must_use]
129    pub fn raw(&self) -> Arc<UpdateServiceSchema> {
130        self.data.clone()
131    }
132
133    /// List all firmware inventory items.
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if:
138    /// - The update service does not have a firmware inventory collection
139    /// - Fetching firmware inventory data fails
140    pub async fn firmware_inventories(
141        &self,
142    ) -> Result<Option<Vec<SoftwareInventory<B>>>, Error<B>> {
143        if let Some(collection_ref) = &self.data.firmware_inventory {
144            SoftwareInventoryCollection::new(
145                &self.bmc,
146                collection_ref,
147                self.fw_inventory_read_patch_fn.clone(),
148            )
149            .await?
150            .members()
151            .await
152            .map(Some)
153        } else {
154            Ok(None)
155        }
156    }
157
158    /// List all software inventory items.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if:
163    /// - The update service does not have a software inventory collection
164    /// - Fetching software inventory data fails
165    pub async fn software_inventories(
166        &self,
167    ) -> Result<Option<Vec<SoftwareInventory<B>>>, Error<B>> {
168        if let Some(collection_ref) = &self.data.software_inventory {
169            let collection = self.bmc.expand_property(collection_ref).await?;
170            let mut items = Vec::new();
171            for item_ref in &collection.members {
172                items.push(SoftwareInventory::new(&self.bmc, item_ref, None).await?);
173            }
174            Ok(Some(items))
175        } else {
176            Ok(None)
177        }
178    }
179
180    /// Perform a simple update with the specified image URI.
181    ///
182    /// This action updates software components by downloading and installing
183    /// a software image from the specified URI.
184    ///
185    /// # Arguments
186    ///
187    /// * `image_uri` - The URI of the software image to install
188    /// * `transfer_protocol` - Optional network protocol to use for retrieving the image
189    /// * `targets` - Optional list of URIs indicating where to apply the update
190    /// * `username` - Optional username for accessing the image URI
191    /// * `password` - Optional password for accessing the image URI
192    /// * `force_update` - Whether to bypass update policies (e.g., allow downgrade)
193    /// * `stage` - Whether to stage the image for later activation instead of immediate
194    ///   installation
195    /// * `local_image` - An indication of whether the service adds the image to the local image
196    ///   store
197    /// * `exclude_targets` - An array of URIs that indicate where not to apply the update image
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if:
202    /// - The update service does not support the `SimpleUpdate` action
203    /// - The action execution fails
204    #[allow(clippy::too_many_arguments)]
205    pub async fn simple_update(
206        &self,
207        image_uri: String,
208        transfer_protocol: Option<TransferProtocolType>,
209        targets: Option<Vec<String>>,
210        username: Option<String>,
211        password: Option<String>,
212        force_update: Option<bool>,
213        stage: Option<bool>,
214        local_image: Option<bool>,
215        exclude_targets: Option<Vec<String>>,
216    ) -> Result<ModificationResponse<()>, Error<B>>
217    where
218        B::Error: nv_redfish_core::ActionError,
219    {
220        let actions = self
221            .data
222            .actions
223            .as_ref()
224            .ok_or(Error::ActionNotAvailable)?;
225
226        actions
227            .simple_update(
228                self.bmc.as_ref(),
229                &UpdateServiceSimpleUpdateAction {
230                    image_uri: Some(image_uri),
231                    transfer_protocol,
232                    targets,
233                    username,
234                    password,
235                    force_update,
236                    stage,
237                    local_image,
238                    exclude_targets,
239                },
240            )
241            .await
242            .map_err(Error::Bmc)
243    }
244
245    /// Start updates that have been previously invoked with an `OperationApplyTime` of
246    /// `OnStartUpdateRequest`.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if:
251    /// - The update service does not support the `StartUpdate` action
252    /// - The action execution fails
253    pub async fn start_update(&self) -> Result<ModificationResponse<()>, Error<B>>
254    where
255        B::Error: nv_redfish_core::ActionError,
256    {
257        let actions = self
258            .data
259            .actions
260            .as_ref()
261            .ok_or(Error::ActionNotAvailable)?;
262
263        actions
264            .start_update(self.bmc.as_ref())
265            .await
266            .map_err(Error::Bmc)
267    }
268
269    /// Update this service with deprecated generated `UpdateServiceUpdate` fields.
270    ///
271    /// Use this for standard `HttpPushUriOptions`, `HttpPushUriTargets`, and
272    /// related busy flags before or after an `HttpPushUri` upload.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the update request fails.
277    #[cfg(feature = "update-service-deprecated")]
278    pub async fn update(
279        &self,
280        update: &UpdateServiceUpdate,
281    ) -> Result<ModificationResponse<Self>, Error<B>> {
282        let response = self
283            .bmc
284            .as_ref()
285            .update::<_, NavProperty<UpdateServiceSchema>>(
286                self.data.odata_id(),
287                self.data.etag(),
288                update,
289            )
290            .await
291            .map_err(Error::Bmc)?;
292
293        match response {
294            ModificationResponse::Entity(nav) => {
295                let data = nav.get(self.bmc.as_ref()).await.map_err(Error::Bmc)?;
296
297                Ok(ModificationResponse::Entity(Self {
298                    bmc: self.bmc.clone(),
299                    data,
300                    fw_inventory_read_patch_fn: self.fw_inventory_read_patch_fn.clone(),
301                }))
302            }
303            ModificationResponse::Task(task) => Ok(ModificationResponse::Task(task)),
304            ModificationResponse::Empty => Ok(ModificationResponse::Empty),
305        }
306    }
307
308    /// Upload a raw binary stream using this service's deprecated `HttpPushUri`.
309    ///
310    /// The stream is sent as `application/octet-stream` without multipart
311    /// `UpdateParameters`.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if `HttpPushUri` is absent or the upload fails.
316    #[cfg(feature = "update-service-deprecated")]
317    pub async fn http_push_uri_update_from_reader<U, R>(
318        &self,
319        update_stream: UploadStream<U>,
320        upload_timeout: Duration,
321    ) -> Result<ModificationResponse<R>, Error<B>>
322    where
323        U: UploadReader,
324        R: Send + Sync + for<'de> serde::Deserialize<'de>,
325    {
326        self.http_push_uri_update(HttpPushUriUpdateRequest {
327            update_stream,
328            upload_timeout,
329        })
330        .await
331    }
332
333    /// Perform a raw binary upload using this service's deprecated `HttpPushUri`.
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if `HttpPushUri` is absent or the upload fails.
338    #[cfg(feature = "update-service-deprecated")]
339    pub async fn http_push_uri_update<U, R>(
340        &self,
341        request: HttpPushUriUpdateRequest<U>,
342    ) -> Result<ModificationResponse<R>, Error<B>>
343    where
344        U: UploadReader,
345        R: Send + Sync + for<'de> serde::Deserialize<'de>,
346    {
347        let http_push_uri = self
348            .data
349            .http_push_uri
350            .as_ref()
351            .ok_or(Error::UpdateServiceHttpPushUriNotAvailable)?;
352
353        self.bmc
354            .as_ref()
355            .http_push_uri_update(http_push_uri, request)
356            .await
357            .map_err(Error::Bmc)
358    }
359
360    /// Upload a named stream using this service's `MultipartHttpPushUri`.
361    ///
362    /// Prefer the generated [`MultipartUpdateParameters`] type. A generic
363    /// payload is accepted for platform fields that are not generated.
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if `MultipartHttpPushUri` is absent or the upload fails.
368    pub async fn multipart_update_from_reader<U, V, R>(
369        &self,
370        update_parameters: &V,
371        update_stream: DataStream<U>,
372        upload_timeout: Duration,
373    ) -> Result<ModificationResponse<R>, Error<B>>
374    where
375        U: UploadReader,
376        V: Send + Sync + serde::Serialize,
377        R: Send + Sync + for<'de> serde::Deserialize<'de>,
378    {
379        self.multipart_update(MultipartUpdateRequest {
380            update_parameters,
381            update_stream,
382            oem_parts: Vec::new(),
383            upload_timeout,
384        })
385        .await
386    }
387
388    /// Perform a multipart upload using this service's `MultipartHttpPushUri`.
389    ///
390    /// Use this method when the request needs optional OEM multipart parts.
391    ///
392    /// # Errors
393    ///
394    /// Returns an error if `MultipartHttpPushUri` is absent or the upload fails.
395    pub async fn multipart_update<U, V, R>(
396        &self,
397        request: MultipartUpdateRequest<'_, U, V>,
398    ) -> Result<ModificationResponse<R>, Error<B>>
399    where
400        U: UploadReader,
401        V: Send + Sync + serde::Serialize,
402        R: Send + Sync + for<'de> serde::Deserialize<'de>,
403    {
404        let multipart_uri = self
405            .data
406            .multipart_http_push_uri
407            .as_ref()
408            .ok_or(Error::UpdateServiceMultipartHttpPushUriNotAvailable)?;
409
410        self.bmc
411            .as_ref()
412            .multipart_update(multipart_uri, request)
413            .await
414            .map_err(Error::Bmc)
415    }
416}
417
418impl<B: Bmc> Resource for UpdateService<B> {
419    fn resource_ref(&self) -> &ResourceSchema {
420        &self.data.as_ref().base
421    }
422}
423
424// `ReleaseDate` is marked as `edm.DateTimeOffset`, but some systems
425// puts "00:00:00Z" as ReleaseDate that is not conform to ABNF of the DateTimeOffset.
426// we delete such fields...
427fn fw_inventory_patch_wrong_release_date(v: JsonValue) -> JsonValue {
428    if let JsonValue::Object(mut obj) = v {
429        if let Some(JsonValue::String(date)) = obj.get("ReleaseDate") {
430            if date == "00:00:00Z" || date == "0000-00-00T00:00:00Z" {
431                obj.remove("ReleaseDate");
432            }
433        }
434        JsonValue::Object(obj)
435    } else {
436        v
437    }
438}
439
440fn add_default_update_service_name(v: JsonValue) -> JsonValue {
441    if let JsonValue::Object(mut obj) = v {
442        obj.entry("Name")
443            .or_insert(JsonValue::String("Unnamed update service".into()));
444        JsonValue::Object(obj)
445    } else {
446        v
447    }
448}