| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! End-to-end host test for a full PLDM firmware-update flow. |
| //! |
| //! This extends the wiring exercised by `base_host.rs` with the FD-initiated |
| //! request path. `FirmwareDevice` owns both MCTP transports directly: when it |
| //! enters update mode it drives the download / verify / apply state machine by |
| //! issuing PLDM requests (`RequestFirmwareData`, `TransferComplete`, |
| //! `VerifyComplete`, `ApplyComplete`) to the Update Agent through its |
| //! `requester_transport`, while UA->FD commands keep flowing through its |
| //! `responder_transport`: |
| //! |
| //! ```text |
| //! UA cmd --> FirmwareDevice.run_terminus (responder_transport) |
| //! | |
| //! v (initiator mode) |
| //! requester_transport --> remote UA (over MCTP) |
| //! ``` |
| //! |
| //! All transports are backed by in-memory MCTP `Server`s and packet queues. |
| |
| use core::cell::{Cell, RefCell}; |
| |
| use mctp::Eid; |
| use mctp_lib::Sender; |
| use openprot_mctp_api::Handle; |
| use openprot_mctp_server::Server; |
| use openprot_pldm_service::firmware_device::{FirmwareDevice, RunTerminusResult}; |
| use openprot_pldm_service::{MctpPldmTransport, PldmServiceError}; |
| use pldm_common::codec::{PldmCodec, PldmCodecWithLifetime}; |
| use pldm_common::message::firmware_update::apply_complete::{ApplyCompleteResponse, ApplyResult}; |
| use pldm_common::message::firmware_update::get_fw_params::FirmwareParameters; |
| use pldm_common::message::firmware_update::get_status::{ |
| GetStatusRequest, GetStatusResponse, ProgressPercent, |
| }; |
| use pldm_common::message::firmware_update::pass_component::PassComponentTableRequest; |
| use pldm_common::message::firmware_update::request_fw_data::{ |
| RequestFirmwareDataRequest, RequestFirmwareDataResponse, MAX_TRANSFER_SIZE, |
| }; |
| use pldm_common::message::firmware_update::request_update::RequestUpdateRequest; |
| use pldm_common::message::firmware_update::transfer_complete::{ |
| TransferCompleteResponse, TransferResult, |
| }; |
| use pldm_common::message::firmware_update::update_component::UpdateComponentRequest; |
| use pldm_common::message::firmware_update::verify_complete::{ |
| VerifyCompleteResponse, VerifyResult, |
| }; |
| use pldm_common::protocol::base::{ |
| PldmBaseCompletionCode, PldmMsgHeader, PldmMsgType, TransferRespFlag, |
| }; |
| use pldm_common::protocol::firmware_update::{ |
| ComponentClassification, ComponentResponseCode, Descriptor, FirmwareDeviceState, FwUpdateCmd, |
| PldmFirmwareString, UpdateOptionFlags, VersionStringType, PLDM_FWUP_IMAGE_SET_VER_STR_MAX_LEN, |
| }; |
| use pldm_common::util::fw_component::FirmwareComponent; |
| use pldm_interface::firmware_device::fd_ops::{ComponentOperation, FdOps, FdOpsError}; |
| |
| mod common; |
| use common::{transfer, BufferSender, DirectClientWithPump, FD_EID, TIMEOUT_MILLIS, UA_EID}; |
| |
| const PLDM_MSG_TYPE: u8 = 0x01; |
| |
| /// Total firmware image size (bytes) advertised in `UpdateComponent`. |
| const IMAGE_SIZE: u32 = 1024; |
| |
| // --------------------------------------------------------------------------- |
| // Fake firmware-device operations. |
| // --------------------------------------------------------------------------- |
| |
| struct MockFdOps { |
| component_accepted: Cell<bool>, |
| download_bytes_received: Cell<usize>, |
| verified: Cell<bool>, |
| applied: Cell<bool>, |
| } |
| |
| impl FdOps for MockFdOps { |
| fn get_device_identifiers( |
| &self, |
| _device_identifiers: &mut [Descriptor], |
| ) -> Result<usize, FdOpsError> { |
| Ok(0) |
| } |
| |
| fn get_firmware_parms( |
| &self, |
| firmware_params: &mut FirmwareParameters, |
| ) -> Result<(), FdOpsError> { |
| *firmware_params = FirmwareParameters::default(); |
| Ok(()) |
| } |
| |
| fn get_xfer_size(&self, ua_transfer_size: usize) -> Result<usize, FdOpsError> { |
| Ok(ua_transfer_size.min(MAX_TRANSFER_SIZE)) |
| } |
| |
| fn handle_component( |
| &self, |
| _component: &FirmwareComponent, |
| _fw_params: &FirmwareParameters, |
| _op: ComponentOperation, |
| ) -> Result<ComponentResponseCode, FdOpsError> { |
| self.component_accepted.set(true); |
| Ok(ComponentResponseCode::CompCanBeUpdated) |
| } |
| |
| fn query_download_offset_and_length( |
| &self, |
| _component: &FirmwareComponent, |
| ) -> Result<(usize, usize), FdOpsError> { |
| Ok((0, IMAGE_SIZE as usize)) |
| } |
| |
| fn download_fw_data( |
| &self, |
| _offset: usize, |
| data: &[u8], |
| _component: &FirmwareComponent, |
| ) -> Result<TransferResult, FdOpsError> { |
| self.download_bytes_received |
| .set(self.download_bytes_received.get() + data.len()); |
| Ok(TransferResult::TransferSuccess) |
| } |
| |
| fn is_download_complete(&self, _component: &FirmwareComponent) -> bool { |
| self.download_bytes_received.get() >= IMAGE_SIZE as usize |
| } |
| |
| fn query_download_progress( |
| &self, |
| _component: &FirmwareComponent, |
| progress_percent: &mut ProgressPercent, |
| ) -> Result<(), FdOpsError> { |
| let pct = (self.download_bytes_received.get() * 100 / IMAGE_SIZE as usize) as u8; |
| progress_percent |
| .set_value(pct.min(100)) |
| .map_err(|_| FdOpsError::FwDownloadError)?; |
| Ok(()) |
| } |
| |
| fn verify( |
| &self, |
| _component: &FirmwareComponent, |
| _progress_percent: &mut ProgressPercent, |
| ) -> Result<VerifyResult, FdOpsError> { |
| // Leave `progress_percent` at its default (NOT_SUPPORTED), which the FD |
| // treats as "done" so the VerifyComplete request is issued immediately. |
| self.verified.set(true); |
| Ok(VerifyResult::VerifySuccess) |
| } |
| |
| fn apply( |
| &self, |
| _component: &FirmwareComponent, |
| _progress_percent: &mut ProgressPercent, |
| ) -> Result<ApplyResult, FdOpsError> { |
| self.applied.set(true); |
| Ok(ApplyResult::ApplySuccess) |
| } |
| |
| fn activate( |
| &self, |
| _self_contained_activation: u8, |
| _estimated_time: &mut u16, |
| ) -> Result<u8, FdOpsError> { |
| Ok(0) |
| } |
| |
| fn cancel_update_component(&self, _component: &FirmwareComponent) -> Result<(), FdOpsError> { |
| Ok(()) |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Helpers |
| // --------------------------------------------------------------------------- |
| |
| /// Build a fixed-size PLDM firmware version string. |
| fn fw_string(s: &str) -> PldmFirmwareString { |
| let bytes = s.as_bytes(); |
| assert!(bytes.len() <= PLDM_FWUP_IMAGE_SET_VER_STR_MAX_LEN); |
| let mut str_data = [0u8; PLDM_FWUP_IMAGE_SET_VER_STR_MAX_LEN]; |
| str_data[..bytes.len()].copy_from_slice(bytes); |
| PldmFirmwareString { |
| str_type: VersionStringType::Ascii as u8, |
| str_len: bytes.len() as u8, |
| str_data, |
| } |
| } |
| |
| /// Act as the remote Update Agent for a single FD-initiated PLDM request. |
| /// |
| /// Reads one request off `ua_server`'s PLDM listener, produces the matching |
| /// success response (returning firmware bytes for `RequestFirmwareData`), and |
| /// sends it back to the firmware device. |
| fn serve_ua_fw_request<S: Sender, const N: usize>( |
| ua_server: &RefCell<Server<S, N>>, |
| listener: Handle, |
| ) { |
| let mut req = [0u8; 1024]; |
| let meta = match ua_server.borrow_mut().try_recv(listener, &mut req) { |
| Some(m) => m, |
| None => return, |
| }; |
| let payload = &req[..meta.payload_size]; |
| |
| let header = PldmMsgHeader::<[u8; 3]>::decode(payload).expect("decode FD request header"); |
| let instance_id = header.instance_id(); |
| let cmd = header.cmd_code(); |
| |
| let success = PldmBaseCompletionCode::Success as u8; |
| let mut resp = [0u8; 1024]; |
| let resp_len = match FwUpdateCmd::try_from(cmd) { |
| Ok(FwUpdateCmd::RequestFirmwareData) => { |
| let fw_req = |
| RequestFirmwareDataRequest::decode(payload).expect("decode RequestFirmwareData"); |
| let length = fw_req.length as usize; |
| assert!(length <= MAX_TRANSFER_SIZE, "requested chunk exceeds MTU"); |
| let data = [0xA5u8; MAX_TRANSFER_SIZE]; |
| let resp_msg = RequestFirmwareDataResponse::new(instance_id, success, &data[..length]); |
| PldmCodecWithLifetime::encode(&resp_msg, &mut resp) |
| .expect("encode RequestFirmwareData response") |
| } |
| Ok(FwUpdateCmd::TransferComplete) => TransferCompleteResponse::new(instance_id, success) |
| .encode(&mut resp) |
| .expect("encode TransferComplete response"), |
| Ok(FwUpdateCmd::VerifyComplete) => VerifyCompleteResponse::new(instance_id, success) |
| .encode(&mut resp) |
| .expect("encode VerifyComplete response"), |
| Ok(FwUpdateCmd::ApplyComplete) => ApplyCompleteResponse::new(instance_id, success) |
| .encode(&mut resp) |
| .expect("encode ApplyComplete response"), |
| _ => panic!("unexpected FD-initiated request: cmd={cmd:#x}"), |
| }; |
| |
| ua_server |
| .borrow_mut() |
| .send( |
| None, |
| PLDM_MSG_TYPE, |
| Some(meta.remote_eid), |
| Some(meta.msg_tag), |
| false, |
| &resp[..resp_len], |
| ) |
| .expect("send FD-initiated request response"); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Test |
| // --------------------------------------------------------------------------- |
| |
| #[test] |
| fn firmware_update_full_flow_via_requester() { |
| let fd_ops = MockFdOps { |
| component_accepted: Cell::new(false), |
| download_bytes_received: Cell::new(0), |
| verified: Cell::new(false), |
| applied: Cell::new(false), |
| }; |
| |
| // In-memory MCTP endpoints and packet queues. |
| let ua_to_fd_packets = RefCell::new(Vec::new()); |
| let ua_sender = BufferSender { |
| packets: &ua_to_fd_packets, |
| }; |
| let ua_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(UA_EID), 0, ua_sender)); |
| |
| let fd_to_ua_packets = RefCell::new(Vec::new()); |
| let fd_sender = BufferSender { |
| packets: &fd_to_ua_packets, |
| }; |
| let fd_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(FD_EID), 0, fd_sender)); |
| |
| // Persistent UA-side listener for FD-initiated PLDM requests. Registered up |
| // front so inbound requests route to it regardless of delivery ordering. |
| let ua_fw_listener = ua_server |
| .borrow_mut() |
| .listener(PLDM_MSG_TYPE) |
| .expect("register UA PLDM listener"); |
| |
| // Requester transport: forwards FD-initiated requests to the remote UA. |
| // Its MCTP client sends from `fd_server` (EID 42) to the UA and, before |
| // each recv, pumps packets across and lets the UA answer the request. |
| let requester_client = DirectClientWithPump::new(&fd_server, || { |
| // Deliver the FD-originated request to the UA. |
| transfer(&fd_to_ua_packets, &mut ua_server.borrow_mut()); |
| fd_to_ua_packets.borrow_mut().clear(); |
| // UA answers the request. |
| serve_ua_fw_request(&ua_server, ua_fw_listener); |
| // Deliver the UA response back to the FD. |
| transfer(&ua_to_fd_packets, &mut fd_server.borrow_mut()); |
| ua_to_fd_packets.borrow_mut().clear(); |
| }); |
| let requester_transport = MctpPldmTransport::new(requester_client); |
| |
| // Responder transport: receives UA->FD commands on `fd_server` and hands |
| // them to the FD. Its pre-recv pump delivers queued UA->FD packets into |
| // `fd_server`. |
| let responder_client = DirectClientWithPump::new(&fd_server, || { |
| transfer(&ua_to_fd_packets, &mut fd_server.borrow_mut()); |
| ua_to_fd_packets.borrow_mut().clear(); |
| }); |
| let responder_transport = MctpPldmTransport::new(responder_client); |
| |
| let fd = RefCell::new(FirmwareDevice::init( |
| &fd_ops, |
| &pldm_interface::config::PLDM_PROTOCOL_CAPABILITIES, |
| responder_transport, |
| requester_transport, |
| )); |
| let fd_buf = RefCell::new([0u8; 1024]); |
| |
| // Run one full UA->FD->UA command round-trip and return the PLDM response |
| // payload (without the MCTP framing byte). |
| let ua_transact = |req_pldm: &[u8]| -> Vec<u8> { |
| let req_handle = ua_server |
| .borrow_mut() |
| .req(FD_EID) |
| .expect("allocate UA request handle to FD"); |
| ua_server |
| .borrow_mut() |
| .send(Some(req_handle), PLDM_MSG_TYPE, None, None, false, req_pldm) |
| .expect("send UA command"); |
| |
| // Drive the FD until its inbound queue drains; this also drives any |
| // FD-initiated requests to the UA through `requester_transport`. A |
| // terminating timeout means "done", not a failure. |
| match fd.borrow_mut().run_terminus( |
| UA_EID, |
| &mut fd_buf.borrow_mut()[..], |
| TIMEOUT_MILLIS, |
| TIMEOUT_MILLIS, |
| ) { |
| RunTerminusResult::Completed => {} |
| RunTerminusResult::StoppedByError(PldmServiceError::Mctp(e)) if e.is_timeout() => {} |
| RunTerminusResult::StoppedByError(e) => panic!("firmware device failed: {e:?}"), |
| } |
| |
| transfer(&fd_to_ua_packets, &mut ua_server.borrow_mut()); |
| fd_to_ua_packets.borrow_mut().clear(); |
| |
| let mut resp = [0u8; 1024]; |
| let meta = ua_server |
| .borrow_mut() |
| .try_recv(req_handle, &mut resp) |
| .expect("UA response should be available"); |
| let out = resp[..meta.payload_size].to_vec(); |
| let _ = ua_server.borrow_mut().unbind(req_handle); |
| out |
| }; |
| |
| let mut buf = [0u8; 1024]; |
| let comp_ver = fw_string("v1.0"); |
| let mut instance_id = 0u8; |
| |
| // ---- RequestUpdate: move FD from Idle -> LearnComponents ---- |
| let req_update = RequestUpdateRequest::new( |
| instance_id, |
| PldmMsgType::Request, |
| IMAGE_SIZE, // max_transfer_size |
| 1, // num_of_comp |
| 1, // max_outstanding_transfer_req |
| 0, // pkg_data_len |
| &comp_ver, |
| ); |
| let len = req_update.encode(&mut buf).expect("encode RequestUpdate"); |
| let resp = ua_transact(&buf[..len]); |
| assert_eq!( |
| resp[3], 0, |
| "RequestUpdate completion code should be success" |
| ); |
| |
| // ---- PassComponentTable (Start+End): move to ReadyXfer ---- |
| instance_id += 1; |
| let pass_comp = PassComponentTableRequest::new( |
| instance_id, |
| PldmMsgType::Request, |
| TransferRespFlag::StartAndEnd, |
| ComponentClassification::Firmware, |
| 0x0001, // comp_identifier |
| 0, // comp_classification_index |
| 0, // comp_comparison_stamp |
| &comp_ver, |
| ); |
| let len = pass_comp |
| .encode(&mut buf) |
| .expect("encode PassComponentTable"); |
| let resp = ua_transact(&buf[..len]); |
| assert_eq!( |
| resp[3], 0, |
| "PassComponentTable completion code should be success" |
| ); |
| assert!( |
| fd_ops.component_accepted.get(), |
| "FD should have accepted the passed component" |
| ); |
| |
| // ---- UpdateComponent: enter Download and issue the first RequestFirmwareData ---- |
| instance_id += 1; |
| let update_comp = UpdateComponentRequest::new( |
| instance_id, |
| PldmMsgType::Request, |
| ComponentClassification::Firmware, |
| 0x0001, // comp_identifier |
| 0, // comp_classification_index |
| 0, // comp_comparison_stamp |
| IMAGE_SIZE, // comp_image_size |
| UpdateOptionFlags(0), |
| &comp_ver, |
| ); |
| let len = update_comp |
| .encode(&mut buf) |
| .expect("encode UpdateComponent"); |
| let resp = ua_transact(&buf[..len]); |
| assert_eq!( |
| resp[3], 0, |
| "UpdateComponent completion code should be success" |
| ); |
| |
| // Because `FirmwareDevice` now owns both MCTP transports directly, |
| // `run_terminus` autonomously drives the entire download / verify / |
| // apply state machine to completion within the same call that |
| // processed `UpdateComponent` above (issuing RequestFirmwareData, |
| // TransferComplete, VerifyComplete, and ApplyComplete to the UA via |
| // `requester_transport` in a tight loop, with nothing to interrupt it |
| // since no other UA command is pending). A single GetStatus check |
| // afterward should therefore already observe the FD back in ReadyXfer. |
| instance_id += 1; |
| let mut b = [0u8; 1024]; |
| let gs = GetStatusRequest::new(instance_id, PldmMsgType::Request); |
| let n = gs.encode(&mut b).expect("encode GetStatus"); |
| let resp = ua_transact(&b[..n]); |
| let status = GetStatusResponse::decode(&resp).expect("decode GetStatusResponse"); |
| assert_eq!( |
| status.completion_code, 0, |
| "GetStatus completion should be success" |
| ); |
| assert_eq!( |
| status.current_state, |
| FirmwareDeviceState::ReadyXfer as u8, |
| "FD should have returned to ReadyXfer once the update completed" |
| ); |
| |
| // ---- Final assertions: the whole FD-driven flow ran end to end ---- |
| assert_eq!( |
| fd_ops.download_bytes_received.get(), |
| IMAGE_SIZE as usize, |
| "the full firmware image should have been downloaded" |
| ); |
| assert!(fd_ops.verified.get(), "firmware should have been verified"); |
| assert!(fd_ops.applied.get(), "firmware should have been applied"); |
| |
| println!( |
| "Firmware update host test completed: downloaded {} bytes, verified={}, applied={}", |
| fd_ops.download_bytes_received.get(), |
| fd_ops.verified.get(), |
| fd_ops.applied.get() |
| ); |
| } |