diff --git a/faux-mgs/src/main.rs b/faux-mgs/src/main.rs index 48950ec..e841af2 100644 --- a/faux-mgs/src/main.rs +++ b/faux-mgs/src/main.rs @@ -236,20 +236,33 @@ enum Command { long, value_name = "SLOT", requires = "switch_duration", + group = "action", + conflicts_with_all = ["cancel_pending"], help = "set the active slot" )] set: Option, + /// Cancel a pending `set` operation. + /// Used to avoid a reset when recoverying from a failed or abandoned update. #[clap( short, long, - requires = "set", + value_name = "SLOT", + requires = "switch_duration", + group = "action", + conflicts_with_all = ["set"] + )] + cancel_pending: Option, + #[clap( + short, + long, + requires = "action", group = "switch_duration", help = "persist the active slot to non-volatile memory" )] persist: bool, /// Only valid with component "rot": /// Prefer the specified slot on the next soft reset. - #[clap(short, long, requires = "set", group = "switch_duration")] + #[clap(short, long, requires = "action", group = "switch_duration")] transient: bool, }, @@ -1264,7 +1277,13 @@ async fn run_command( ])) } } - Command::ComponentActiveSlot { component, set, persist, transient } => { + Command::ComponentActiveSlot { + component, + set, + persist, + transient, + cancel_pending, + } => { if transient && component != SpComponent::ROT { bail!("The --transient (-t) flag is only allowed for the 'rot' component, not for {component}"); } else if let Some(slot) = set { @@ -1276,6 +1295,20 @@ async fn run_command( "set active slot for {component:?} to {slot}" )])) } + } else if let Some(slot) = cancel_pending { + sp.cancel_pending_component_active_slot( + component, slot, persist, + ) + .await?; + if json { + Ok(Output::Json( + json!({ "ack": "cancel_pending", "slot": slot }), + )) + } else { + Ok(Output::Lines(vec![format!( + "cancel pending active slot for {component:?} as {slot}" + )])) + } } else { let slot = sp.component_active_slot(component).await?; info!(log, "active slot for {component:?}: {slot}"); diff --git a/gateway-messages/src/mgs_to_sp.rs b/gateway-messages/src/mgs_to_sp.rs index 7652c71..5027d22 100644 --- a/gateway-messages/src/mgs_to_sp.rs +++ b/gateway-messages/src/mgs_to_sp.rs @@ -236,6 +236,16 @@ pub enum MgsRequest { GetHostFlashHash { slot: u16, }, + + /// Cancel a pending slot activation for components with multiple slots that + /// have separate `pending` and `active` states. + /// This is useful when recovering from a failed or abandoned update without incurring an + /// additional reset or other activation step. + ComponentCancelPendingActiveSlot { + component: SpComponent, + slot: u16, + persist: bool, + }, } #[derive( diff --git a/gateway-messages/src/sp_impl.rs b/gateway-messages/src/sp_impl.rs index 9bd5e76..5f3707f 100644 --- a/gateway-messages/src/sp_impl.rs +++ b/gateway-messages/src/sp_impl.rs @@ -417,6 +417,13 @@ pub trait SpHandler { fn start_host_flash_hash(&mut self, slot: u16) -> Result<(), SpError>; fn get_host_flash_hash(&mut self, slot: u16) -> Result<[u8; 32], SpError>; + + fn component_cancel_pending_active_slot( + &mut self, + component: SpComponent, + slot: u16, + persist: bool, + ) -> Result<(), SpError>; } /// Handle a single incoming message. @@ -1032,6 +1039,13 @@ fn handle_mgs_request( MgsRequest::GetHostFlashHash { slot } => { handler.get_host_flash_hash(slot).map(SpResponse::HostFlashHash) } + MgsRequest::ComponentCancelPendingActiveSlot { + component, + slot, + persist, + } => handler + .component_cancel_pending_active_slot(component, slot, persist) + .map(|()| SpResponse::ComponentCancelPendingActiveSlotAck), }; let response = match result { @@ -1457,6 +1471,15 @@ mod tests { ) -> Result<[u8; 32], SpError> { unimplemented!() } + + fn component_cancel_pending_active_slot( + &mut self, + _component: SpComponent, + _slot: u16, + _persist: bool, + ) -> Result<(), SpError> { + unimplemented!() + } } #[cfg(feature = "std")] diff --git a/gateway-messages/src/sp_to_mgs.rs b/gateway-messages/src/sp_to_mgs.rs index 17f007f..ddbd37e 100644 --- a/gateway-messages/src/sp_to_mgs.rs +++ b/gateway-messages/src/sp_to_mgs.rs @@ -177,6 +177,9 @@ pub enum SpResponse { /// sha2-256 hash of a flash bank HostFlashHash([u8; 32]), + + /// Cancel a pending slot activation + ComponentCancelPendingActiveSlotAck, } /// Identifier for one of of an SP's KSZ8463 management-network-facing ports. @@ -1295,6 +1298,8 @@ pub enum UpdateError { SignatureNotValidated, VersionNotSupported, InvalidPreferredSlotId, + AlreadyPending, + NonePending, } impl fmt::Display for UpdateError { @@ -1355,10 +1360,13 @@ impl fmt::Display for UpdateError { write!(f, "invalid component for operation") } Self::InvalidPreferredSlotId => { - write!( - f, - "updating a bootloader preferred slot is not permitted" - ) + write!(f, "specified slot ID is not valid for this operation") + } + Self::AlreadyPending => { + write!(f, "pending preference must be canceled first") + } + Self::NonePending => { + write!(f, "no pending preference to cancel") } } } diff --git a/gateway-messages/tests/versioning/mod.rs b/gateway-messages/tests/versioning/mod.rs index ca9fe89..f092264 100644 --- a/gateway-messages/tests/versioning/mod.rs +++ b/gateway-messages/tests/versioning/mod.rs @@ -26,6 +26,7 @@ mod v17; mod v18; mod v19; mod v20; +mod v21; pub fn assert_serialized( expected: &[u8], diff --git a/gateway-messages/tests/versioning/v21.rs b/gateway-messages/tests/versioning/v21.rs new file mode 100644 index 0000000..1f2a459 --- /dev/null +++ b/gateway-messages/tests/versioning/v21.rs @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2023 Oxide Computer Company + +//! The tests in this module check that the serialized form of messages from MGS +//! protocol version 2 have not changed. +//! +//! If a test in this module fails, _do not change the test_! This means you +//! have changed, deleted, or reordered an existing message type or enum +//! variant, and you should revert that change. This will remain true until we +//! bump the `version::MIN` to a value higher than 2, at which point these tests +//! can be removed as we will stop supporting v2. + +use gateway_messages::MgsRequest; +use gateway_messages::SpComponent; +use gateway_messages::SpResponse; +use gateway_messages::UpdateError; + +use super::assert_serialized; + +#[test] +fn mgs_request() { + let request = MgsRequest::ComponentCancelPendingActiveSlot { + component: SpComponent::ROT, + slot: 0x0102, + persist: true, + }; + #[rustfmt::skip] + let expected = &[ + 50, // ComponentCancelPendingActiveSlot + b'r', b'o', b't', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ROT + 2, 1, // slot + 1, // persist = true + ]; + assert_serialized(expected, &request); +} + +#[test] +fn sp_response() { + let response = SpResponse::ComponentCancelPendingActiveSlotAck; + let expected = &[52]; + assert_serialized(expected, &response); +} + +#[test] +fn error_enums() { + let response: [UpdateError; 2] = + [UpdateError::AlreadyPending, UpdateError::NonePending]; + let expected = vec![35, 36]; + assert_serialized(&expected, &response); +} diff --git a/gateway-sp-comms/src/error.rs b/gateway-sp-comms/src/error.rs index 8c45978..3550bb5 100644 --- a/gateway-sp-comms/src/error.rs +++ b/gateway-sp-comms/src/error.rs @@ -116,6 +116,12 @@ pub enum UpdateError { InvalidComponent, #[error("an image was not found")] ImageNotFound, + #[error("cannot update preferred boot image")] + InvalidPreferredSlotId, + #[error("pending preference must be canceled first")] + AlreadyPending, + #[error("no pending preference to cancel")] + NonePending, } #[derive(Debug, thiserror::Error, SlogInlineError)] diff --git a/gateway-sp-comms/src/single_sp.rs b/gateway-sp-comms/src/single_sp.rs index 8ca5573..60d8353 100644 --- a/gateway-sp-comms/src/single_sp.rs +++ b/gateway-sp-comms/src/single_sp.rs @@ -614,6 +614,22 @@ impl SingleSp { }) } + /// Cancel a pending slot activation for a particular component.. + pub async fn cancel_pending_component_active_slot( + &self, + component: SpComponent, + slot: u16, + persist: bool, + ) -> Result<()> { + self.rpc(MgsRequest::ComponentCancelPendingActiveSlot { + component, + slot, + persist, + }) + .await + .and_then(expect_component_cancel_pending_active_slot_ack) + } + /// Request that the status of a component be cleared (e.g., resetting /// counters). pub async fn component_clear_status( diff --git a/gateway-sp-comms/src/sp_response_expect.rs b/gateway-sp-comms/src/sp_response_expect.rs index 5cf4bde..a41834b 100644 --- a/gateway-sp-comms/src/sp_response_expect.rs +++ b/gateway-sp-comms/src/sp_response_expect.rs @@ -129,6 +129,7 @@ expect_data_fn!(BulkIgnitionState(page) -> TlvPage); expect_data_fn!(ComponentDetails(page) -> TlvPage); expect_data_fn!(Inventory(page) -> TlvPage); expect_data_fn!(BulkIgnitionLinkEvents(page) -> TlvPage); +expect_fn!(ComponentCancelPendingActiveSlotAck); pub(crate) fn expect_caboose_value( r: (SocketAddrV6, SpResponse, Vec), diff --git a/wireshark/protofields.lua b/wireshark/protofields.lua index 127c763..418cde1 100644 --- a/wireshark/protofields.lua +++ b/wireshark/protofields.lua @@ -94,6 +94,7 @@ M.mgs_request.names = { [47] = "ReadHostFlash", [48] = "StartHostFlashHash", [49] = "GetHostFlashHash", + [50] = "ComponentCancelPendingActiveSlot", } M.mgs_request.handlers = { [0] = "dissect_discover", @@ -146,6 +147,7 @@ M.mgs_request.handlers = { [47] = "dissect_read_host_flash", [48] = "dissect_start_host_flash_hash", [49] = "dissect_get_host_flash_hash", + [50] = "dissect_component_cancel_pending_active_slot", } M.mgs_request.field = ProtoField.uint8( "mgs.mgs_request", @@ -330,6 +332,7 @@ M.sp_response.names = { [49] = "ReadHostFlash", [50] = "StartHostFlashHashAck", [51] = "HostFlashHash", + [52] = "ComponentCancelPendingActiveSlotAck", } M.sp_response.handlers = { [0] = "dissect_discover", @@ -384,6 +387,7 @@ M.sp_response.handlers = { [49] = "dissect_read_host_flash", [50] = "dissect_start_host_flash_hash_ack", [51] = "dissect_host_flash_hash", + [52] = "dissect_component_cancel_pending_active_slot_ack", } M.sp_response.field = ProtoField.uint8( "mgs.sp_response",