diff --git a/gateway/src/http_entrypoints.rs b/gateway/src/http_entrypoints.rs index ba972d9107d..c34388b5f74 100644 --- a/gateway/src/http_entrypoints.rs +++ b/gateway/src/http_entrypoints.rs @@ -27,7 +27,6 @@ use dropshot::UntypedBody; use dropshot::WebsocketEndpointResult; use dropshot::WebsocketUpgrade; use futures::TryFutureExt; -use gateway_messages::SpComponent; use gateway_messages::SpError; use gateway_sp_comms::error::CommunicationError; use gateway_sp_comms::HostPhase2Provider; @@ -58,7 +57,6 @@ pub struct SpState { pub revision: u32, pub hubris_archive_id: String, pub base_mac_address: [u8; 6], - pub version: ImageVersion, pub power_state: PowerState, pub rot: RotState, } @@ -76,13 +74,13 @@ pub struct SpState { )] #[serde(tag = "state", rename_all = "snake_case")] pub enum RotState { - // TODO gateway_messages's RotState includes a couple nested structures that - // I've flattened here because they only contain one field each. When those - // structures grow we'll need to expand/change this. Enabled { active: RotSlot, - slot_a: Option, - slot_b: Option, + persistent_boot_preference: RotSlot, + pending_persistent_boot_preference: Option, + transient_boot_preference: Option, + slot_a_sha3_256_digest: Option, + slot_b_sha3_256_digest: Option, }, CommunicationFailed { message: String, @@ -619,6 +617,7 @@ async fn sp_component_get( async fn sp_component_caboose_get( rqctx: RequestContext>, path: Path, + query_params: Query, ) -> Result, HttpError> { const CABOOSE_KEY_GIT_COMMIT: [u8; 4] = *b"GITC"; const CABOOSE_KEY_BOARD: [u8; 4] = *b"BORD"; @@ -628,18 +627,8 @@ async fn sp_component_caboose_get( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); let sp = apictx.mgmt_switch.sp(sp.into())?; - - // At the moment this endpoint only works if the requested component - // is the SP itself; we have no way (yet!) of asking the SP for (e.g.) RoT - // caboose values. + let ComponentCabooseSlot { firmware_slot } = query_params.into_inner(); let component = component_from_str(&component)?; - if component != SpComponent::SP_ITSELF { - return Err(HttpError::from(SpCommsError::from( - CommunicationError::SpError( - SpError::RequestUnsupportedForComponent, - ), - ))); - } let from_utf8 = |key: &[u8], bytes| { // This helper closure is only called with the ascii-printable [u8; 4] @@ -655,18 +644,25 @@ async fn sp_component_caboose_get( }; let git_commit = sp - .get_caboose_value(CABOOSE_KEY_GIT_COMMIT) + .read_component_caboose( + component, + firmware_slot, + CABOOSE_KEY_GIT_COMMIT, + ) .await .map_err(SpCommsError::from)?; let board = sp - .get_caboose_value(CABOOSE_KEY_BOARD) + .read_component_caboose(component, firmware_slot, CABOOSE_KEY_BOARD) .await .map_err(SpCommsError::from)?; let name = sp - .get_caboose_value(CABOOSE_KEY_NAME) + .read_component_caboose(component, firmware_slot, CABOOSE_KEY_NAME) .await .map_err(SpCommsError::from)?; - let version = match sp.get_caboose_value(CABOOSE_KEY_VERSION).await { + let version = match sp + .read_component_caboose(component, firmware_slot, CABOOSE_KEY_VERSION) + .await + { Ok(value) => Some(from_utf8(&CABOOSE_KEY_VERSION, value)?), Err(CommunicationError::SpError(SpError::NoSuchCabooseKey(_))) => None, Err(err) => return Err(SpCommsError::from(err).into()), @@ -838,6 +834,12 @@ pub struct ComponentUpdateIdSlot { pub firmware_slot: u16, } +#[derive(Deserialize, JsonSchema)] +pub struct ComponentCabooseSlot { + /// The firmware slot to for which we want to request caboose information. + pub firmware_slot: u16, +} + #[derive(Deserialize, JsonSchema)] pub struct UpdateAbortBody { /// The ID of the update to abort. diff --git a/gateway/src/http_entrypoints/conversions.rs b/gateway/src/http_entrypoints/conversions.rs index 6a3fe65ed82..1182163bcca 100644 --- a/gateway/src/http_entrypoints/conversions.rs +++ b/gateway/src/http_entrypoints/conversions.rs @@ -124,7 +124,6 @@ impl From for SpState { revision: state.revision, hubris_archive_id: hex::encode(state.hubris_archive_id), base_mac_address: state.base_mac_address, - version: ImageVersion::from(state.version), power_state: PowerState::from(state.power_state), rot: RotState::from(state.rot), } @@ -139,7 +138,6 @@ impl From for SpState { revision: state.revision, hubris_archive_id: hex::encode(state.hubris_archive_id), base_mac_address: state.base_mac_address, - version: ImageVersion { epoch: 0, version: 0 }, // TODO FIXME power_state: PowerState::from(state.power_state), rot: RotState::from(state.rot), } @@ -166,8 +164,19 @@ impl From> let boot_state = state.rot_updates.boot_state; Self::Enabled { active: boot_state.active.into(), - slot_a: boot_state.slot_a.map(Into::into), - slot_b: boot_state.slot_b.map(Into::into), + slot_a_sha3_256_digest: boot_state + .slot_a + .map(|details| hex::encode(details.digest)), + slot_b_sha3_256_digest: boot_state + .slot_b + .map(|details| hex::encode(details.digest)), + // RotState(V1) didn't have the following fields, so we make + // it up as best we can. This RoT version is pre-shipping + // and should only exist on (not updated recently) test + // systems. + persistent_boot_preference: boot_state.active.into(), + pending_persistent_boot_preference: None, + transient_boot_preference: None, } } Err(err) => Self::CommunicationFailed { message: err.to_string() }, @@ -175,7 +184,6 @@ impl From> } } -// TODO FIXME impl From> for RotState { @@ -186,24 +194,24 @@ impl From> >, ) -> Self { match result { - Ok(state) => { - // TODO FIXME: This is wrong for RotStateV2 - Self::Enabled { - active: state.active.into(), - slot_a: state.slot_a_sha3_256_digest.map(|hash| { - RotImageDetails { - digest: hex::encode(hash), - version: ImageVersion { epoch: 0, version: 0 }, - } - }), - slot_b: state.slot_b_sha3_256_digest.map(|hash| { - RotImageDetails { - digest: hex::encode(hash), - version: ImageVersion { epoch: 0, version: 0 }, - } - }), - } - } + Ok(state) => Self::Enabled { + active: state.active.into(), + persistent_boot_preference: state + .persistent_boot_preference + .into(), + pending_persistent_boot_preference: state + .pending_persistent_boot_preference + .map(Into::into), + transient_boot_preference: state + .transient_boot_preference + .map(Into::into), + slot_a_sha3_256_digest: state + .slot_a_sha3_256_digest + .map(hex::encode), + slot_b_sha3_256_digest: state + .slot_b_sha3_256_digest + .map(hex::encode), + }, Err(err) => Self::CommunicationFailed { message: err.to_string() }, } } diff --git a/openapi/gateway.json b/openapi/gateway.json index 2ce0d5ba092..847d1f746dd 100644 --- a/openapi/gateway.json +++ b/openapi/gateway.json @@ -518,6 +518,17 @@ "schema": { "$ref": "#/components/schemas/SpType" } + }, + { + "in": "query", + "name": "firmware_slot", + "description": "The firmware slot to for which we want to request caboose information.", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } } ], "responses": { @@ -1423,25 +1434,6 @@ "verbose" ] }, - "ImageVersion": { - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "version": { - "type": "integer", - "format": "uint32", - "minimum": 0 - } - }, - "required": [ - "epoch", - "version" - ] - }, "InstallinatorImageId": { "type": "object", "properties": { @@ -2079,21 +2071,6 @@ "A2" ] }, - "RotImageDetails": { - "type": "object", - "properties": { - "digest": { - "type": "string" - }, - "version": { - "$ref": "#/components/schemas/ImageVersion" - } - }, - "required": [ - "digest", - "version" - ] - }, "RotSlot": { "oneOf": [ { @@ -2134,31 +2111,43 @@ "active": { "$ref": "#/components/schemas/RotSlot" }, - "slot_a": { + "pending_persistent_boot_preference": { "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/RotImageDetails" + "$ref": "#/components/schemas/RotSlot" } ] }, - "slot_b": { + "persistent_boot_preference": { + "$ref": "#/components/schemas/RotSlot" + }, + "slot_a_sha3_256_digest": { "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/RotImageDetails" - } - ] + "type": "string" + }, + "slot_b_sha3_256_digest": { + "nullable": true, + "type": "string" }, "state": { "type": "string", "enum": [ "enabled" ] + }, + "transient_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] } }, "required": [ "active", + "persistent_boot_preference", "state" ] }, @@ -2642,9 +2631,6 @@ }, "serial_number": { "type": "string" - }, - "version": { - "$ref": "#/components/schemas/ImageVersion" } }, "required": [ @@ -2654,8 +2640,7 @@ "power_state", "revision", "rot", - "serial_number", - "version" + "serial_number" ] }, "SpType": { diff --git a/openapi/wicketd.json b/openapi/wicketd.json index 1e86c95024b..b9da86caf17 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -616,25 +616,6 @@ } ] }, - "ImageVersion": { - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "version": { - "type": "integer", - "format": "uint32", - "minimum": 0 - } - }, - "required": [ - "epoch", - "version" - ] - }, "PowerState": { "description": "See RFD 81.\n\nThis enum only lists power states the SP is able to control; higher power states are controlled by ignition.", "type": "string", @@ -1123,26 +1104,22 @@ "sps" ] }, - "RotImageDetails": { - "type": "object", - "properties": { - "digest": { - "type": "string" - }, - "version": { - "$ref": "#/components/schemas/ImageVersion" - } - }, - "required": [ - "digest", - "version" - ] - }, "RotInventory": { "description": "RoT-related data that isn't already supplied in [`SpState`].", "type": "object", "properties": { - "caboose": { + "active": { + "$ref": "#/components/schemas/RotSlot" + }, + "caboose_a": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_b": { "nullable": true, "allOf": [ { @@ -1150,7 +1127,10 @@ } ] } - } + }, + "required": [ + "active" + ] }, "RotSlot": { "oneOf": [ @@ -1192,31 +1172,43 @@ "active": { "$ref": "#/components/schemas/RotSlot" }, - "slot_a": { + "pending_persistent_boot_preference": { "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/RotImageDetails" + "$ref": "#/components/schemas/RotSlot" } ] }, - "slot_b": { + "persistent_boot_preference": { + "$ref": "#/components/schemas/RotSlot" + }, + "slot_a_sha3_256_digest": { "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/RotImageDetails" - } - ] + "type": "string" + }, + "slot_b_sha3_256_digest": { + "nullable": true, + "type": "string" }, "state": { "type": "string", "enum": [ "enabled" ] + }, + "transient_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] } }, "required": [ "active", + "persistent_boot_preference", "state" ] }, @@ -1511,7 +1503,15 @@ "description": "SP-related data", "type": "object", "properties": { - "caboose": { + "caboose_active": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_inactive": { "nullable": true, "allOf": [ { @@ -1538,7 +1538,12 @@ ] }, "rot": { - "$ref": "#/components/schemas/RotInventory" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotInventory" + } + ] }, "state": { "nullable": true, @@ -1550,8 +1555,7 @@ } }, "required": [ - "id", - "rot" + "id" ] }, "SpState": { @@ -1586,9 +1590,6 @@ }, "serial_number": { "type": "string" - }, - "version": { - "$ref": "#/components/schemas/ImageVersion" } }, "required": [ @@ -1598,8 +1599,7 @@ "power_state", "revision", "rot", - "serial_number", - "version" + "serial_number" ] }, "SpType": { diff --git a/wicket/src/state/inventory.rs b/wicket/src/state/inventory.rs index e116bcfd36c..aa092dfafa0 100644 --- a/wicket/src/state/inventory.rs +++ b/wicket/src/state/inventory.rs @@ -13,8 +13,8 @@ use std::fmt::Display; use std::iter::Iterator; use tui::text::Text; use wicketd_client::types::{ - RackV1Inventory, RotInventory, SpComponentCaboose, SpComponentInfo, - SpIgnition, SpState, SpType, + RackV1Inventory, RotInventory, RotSlot, SpComponentCaboose, + SpComponentInfo, SpIgnition, SpState, SpType, }; pub static ALL_COMPONENT_IDS: Lazy> = Lazy::new(|| { @@ -59,7 +59,8 @@ impl Inventory { let sp = Sp { ignition: sp.ignition, state: sp.state, - caboose: sp.caboose, + caboose_active: sp.caboose_active, + caboose_inactive: sp.caboose_inactive, components: sp.components, rot: sp.rot, }; @@ -104,9 +105,10 @@ impl Inventory { pub struct Sp { ignition: Option, state: Option, - caboose: Option, + caboose_active: Option, + caboose_inactive: Option, components: Option>, - rot: RotInventory, + rot: Option, } // XXX: Eventually a Sled will have a host component. @@ -117,6 +119,10 @@ pub enum Component { Psc(Sp), } +fn version_or_unknown(caboose: Option<&SpComponentCaboose>) -> String { + caboose.and_then(|c| c.version.as_deref()).unwrap_or("UNKNOWN").to_string() +} + impl Component { pub fn sp(&self) -> &Sp { match self { @@ -126,23 +132,28 @@ impl Component { } } - pub fn sp_version(&self) -> String { - self.sp() - .caboose - .as_ref() - .and_then(|caboose| caboose.version.as_deref()) - .unwrap_or("UNKNOWN") - .to_string() + pub fn sp_version_active(&self) -> String { + version_or_unknown(self.sp().caboose_active.as_ref()) + } + + pub fn sp_version_inactive(&self) -> String { + version_or_unknown(self.sp().caboose_inactive.as_ref()) + } + + pub fn rot_active_slot(&self) -> Option { + self.sp().rot.as_ref().map(|rot| rot.active) + } + + pub fn rot_version_a(&self) -> String { + version_or_unknown( + self.sp().rot.as_ref().and_then(|rot| rot.caboose_a.as_ref()), + ) } - pub fn rot_version(&self) -> String { - self.sp() - .rot - .caboose - .as_ref() - .and_then(|caboose| caboose.version.as_deref()) - .unwrap_or("UNKNOWN") - .to_string() + pub fn rot_version_b(&self) -> String { + version_or_unknown( + self.sp().rot.as_ref().and_then(|rot| rot.caboose_b.as_ref()), + ) } } diff --git a/wicket/src/ui/panes/update.rs b/wicket/src/ui/panes/update.rs index cbc2698cacd..83c62eef850 100644 --- a/wicket/src/ui/panes/update.rs +++ b/wicket/src/ui/panes/update.rs @@ -2,6 +2,7 @@ // 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/. +use std::borrow::Cow; use std::collections::BTreeMap; use super::{align_by, help_text, push_text_lines, Control}; @@ -31,7 +32,7 @@ use update_engine::{ExecutionStatus, StepKey}; use wicket_common::update_events::{ EventBuffer, EventReport, StepOutcome, StepStatus, UpdateComponent, }; -use wicketd_client::types::SemverVersion; +use wicketd_client::types::{RotSlot, SemverVersion}; const MAX_COLUMN_WIDTH: u16 = 25; @@ -623,29 +624,29 @@ impl UpdatePane { .map(|(id, states)| { let children: Vec<_> = states .iter() - .map(|(component, s)| { + .flat_map(|(component, s)| { let target_version = artifact_version(id, component, &versions); - let installed_version = - installed_version(id, component, inventory); - let spans = vec![ - Span::styled( - update_component_title(component), - style::selected(), - ), - Span::styled( - installed_version, - style::selected_line(), - ), - Span::styled(target_version, style::selected()), - Span::styled(s.to_string(), s.style()), - ]; - TreeItem::new_leaf(align_by( - 0, - MAX_COLUMN_WIDTH, - self.contents_rect, - spans, - )) + let installed_versions = + all_installed_versions(id, component, inventory); + let contents_rect = self.contents_rect; + installed_versions.into_iter().map(move |v| { + let spans = vec![ + Span::styled(v.title, style::selected()), + Span::styled(v.version, style::selected_line()), + Span::styled( + target_version.clone(), + style::selected(), + ), + Span::styled(s.to_string(), s.style()), + ]; + TreeItem::new_leaf(align_by( + 0, + MAX_COLUMN_WIDTH, + contents_rect, + spans, + )) + }) }) .collect(); TreeItem::new(*id, children) @@ -1058,7 +1059,7 @@ impl UpdatePane { Span::styled("STATUS", header_style), ], )) - .block(block.clone()); + .block(block.clone().title("OVERVIEW (* = active)")); frame.render_widget(headers, self.table_headers_rect); // Need to refresh the items, as their versions/state may have changed @@ -1125,7 +1126,7 @@ impl UpdatePane { .style(header_style), ) .widths(&width_constraints) - .block(block.clone().title("OVERVIEW")); + .block(block.clone().title("OVERVIEW (* = active)")); frame.render_widget(header_table, self.table_headers_rect); // For the selected item, draw the version table. @@ -1133,26 +1134,28 @@ impl UpdatePane { let item_state = &state.update_state.items[&selected]; let version_rows = - item_state.iter().map(|(component, update_state)| { + item_state.iter().flat_map(|(component, update_state)| { let target_version = artifact_version( &state.rack_state.selected, component, versions, ); - let installed_version = installed_version( + let installed_versions = all_installed_versions( &state.rack_state.selected, component, inventory, ); - Row::new(vec![ - Cell::from(update_component_title(component)) - .style(style::selected()), - Cell::from(installed_version).style(style::selected_line()), - Cell::from(target_version).style(style::selected()), - Cell::from(update_state.to_string()) - .style(update_state.style()), - ]) + installed_versions.into_iter().map(move |v| { + Row::new(vec![ + Cell::from(v.title).style(style::selected()), + Cell::from(v.version).style(style::selected_line()), + Cell::from(target_version.clone()) + .style(style::selected()), + Cell::from(update_state.to_string()) + .style(update_state.style()), + ]) + }) }); let version_table = Table::new(version_rows).widths(&width_constraints).block( @@ -1339,7 +1342,7 @@ impl From<&'_ State> for ForceUpdateSelectionState { let artifact_version = artifact_version(&component_id, component, versions); let installed_version = - installed_version(&component_id, component, inventory); + active_installed_version(&component_id, component, inventory); match component { UpdateComponent::Rot => { assert!( @@ -1753,7 +1756,7 @@ enum ComponentUpdateShowHelp { Completed, } -fn installed_version( +fn active_installed_version( id: &ComponentId, update_component: UpdateComponent, inventory: &Inventory, @@ -1762,11 +1765,15 @@ fn installed_version( match update_component { UpdateComponent::Sp => component.map_or_else( || "UNKNOWN".to_string(), - |component| component.sp_version(), + |component| component.sp_version_active(), ), UpdateComponent::Rot => component.map_or_else( || "UNKNOWN".to_string(), - |component| component.rot_version(), + |component| match component.rot_active_slot() { + Some(RotSlot::A) => component.rot_version_a(), + Some(RotSlot::B) => component.rot_version_b(), + None => return "UNKNOWN".to_string(), + }, ), UpdateComponent::Host => { // We currently have no way to tell what version of host software is @@ -1776,6 +1783,80 @@ fn installed_version( } } +struct InstalledVersion { + title: Cow<'static, str>, + version: Cow<'static, str>, +} + +fn all_installed_versions( + id: &ComponentId, + update_component: UpdateComponent, + inventory: &Inventory, +) -> Vec { + let base_title = update_component_title(update_component); + let component = inventory.get_inventory(id); + match update_component { + UpdateComponent::Sp => component.map_or_else( + || { + vec![InstalledVersion { + title: base_title.into(), + version: "UNKNOWN".into(), + }] + }, + |component| { + vec![ + InstalledVersion { + title: format!("{base_title}/0 *").into(), + version: component.sp_version_active().into(), + }, + InstalledVersion { + title: format!("{base_title}/1").into(), + version: component.sp_version_inactive().into(), + }, + ] + }, + ), + UpdateComponent::Rot => component.map_or_else( + || { + vec![InstalledVersion { + title: base_title.into(), + version: "UNKNOWN".into(), + }] + }, + |component| { + let Some(active) = component.rot_active_slot() else { + return vec![InstalledVersion { + title: base_title.into(), + version: "UNKNOWN".into(), + }]; + }; + let (active_a, active_b) = match active { + RotSlot::A => (" *", ""), + RotSlot::B => ("", " *"), + }; + vec![ + InstalledVersion { + title: format!("{base_title}/A{active_a}").into(), + version: component.rot_version_a().into(), + }, + InstalledVersion { + title: format!("{base_title}/B{active_b}").into(), + version: component.rot_version_b().into(), + }, + ] + }, + ), + UpdateComponent::Host => { + // We currently have no way to tell what version of host software is + // installed. + vec![InstalledVersion { + title: base_title.into(), + version: "-----".into(), + }] + } + } +} + fn artifact_version( id: &ComponentId, component: UpdateComponent, @@ -1844,7 +1925,7 @@ impl Control for UpdatePane { [ Constraint::Length(3), Constraint::Length(3), - Constraint::Length(4), + Constraint::Length(6), Constraint::Min(0), Constraint::Length(3), ] diff --git a/wicketd/src/inventory.rs b/wicketd/src/inventory.rs index 4dd2d00e97f..f6bf5c2984b 100644 --- a/wicketd/src/inventory.rs +++ b/wicketd/src/inventory.rs @@ -5,7 +5,8 @@ //! Rack inventory for display by wicket use gateway_client::types::{ - SpComponentCaboose, SpComponentInfo, SpIdentifier, SpIgnition, SpState, + RotSlot, SpComponentCaboose, SpComponentInfo, SpIdentifier, SpIgnition, + SpState, }; use schemars::JsonSchema; use serde::Serialize; @@ -18,8 +19,9 @@ pub struct SpInventory { pub ignition: Option, pub state: Option, pub components: Option>, - pub caboose: Option, - pub rot: RotInventory, + pub caboose_active: Option, + pub caboose_inactive: Option, + pub rot: Option, } impl SpInventory { @@ -32,17 +34,20 @@ impl SpInventory { ignition: None, state: None, components: None, - caboose: None, - rot: RotInventory::default(), + caboose_active: None, + caboose_inactive: None, + rot: None, } } } /// RoT-related data that isn't already supplied in [`SpState`]. -#[derive(Default, Debug, Clone, Serialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(tag = "sp_inventory", rename_all = "snake_case")] pub struct RotInventory { - pub caboose: Option, + pub active: RotSlot, + pub caboose_a: Option, + pub caboose_b: Option, } /// The current state of the v1 Rack as known to wicketd diff --git a/wicketd/src/mgs.rs b/wicketd/src/mgs.rs index 3e5a48e2030..b0906cf7e85 100644 --- a/wicketd/src/mgs.rs +++ b/wicketd/src/mgs.rs @@ -368,7 +368,8 @@ impl MgsManager { .or_insert_with(|| SpInventory::new(sp.id)); entry.state = Some(sp.state); entry.components = sp.components; - entry.caboose = sp.caboose; + entry.caboose_active = sp.caboose_active; + entry.caboose_inactive = sp.caboose_inactive; entry.rot = sp.rot; // Scan any pending waiters and remove this SP from their list; if that diff --git a/wicketd/src/mgs/inventory.rs b/wicketd/src/mgs/inventory.rs index 3e4c54a8aea..5334c08a40e 100644 --- a/wicketd/src/mgs/inventory.rs +++ b/wicketd/src/mgs/inventory.rs @@ -4,6 +4,7 @@ //! Implementation details for polling MGS for rack inventory details. +use gateway_client::types::RotState; use gateway_client::types::SpComponentCaboose; use gateway_client::types::SpComponentInfo; use gateway_client::types::SpIdentifier; @@ -155,8 +156,9 @@ pub(super) struct FetchedSpData { pub(super) id: SpIdentifier, pub(super) state: SpState, pub(super) components: Option>, - pub(super) caboose: Option, - pub(super) rot: RotInventory, + pub(super) caboose_active: Option, + pub(super) caboose_inactive: Option, + pub(super) rot: Option, pub(super) mgs_received: Instant, } @@ -266,8 +268,9 @@ async fn sp_fetching_task( let mut prev_state = None; let mut components = None; - let mut caboose = None; - let mut rot = RotInventory { caboose: None }; + let mut caboose_active = None; + let mut caboose_inactive = None; + let mut rot = None; loop { tokio::select! { @@ -280,8 +283,9 @@ async fn sp_fetching_task( // and recreate `ticker`. prev_state = None; components = None; - caboose = None; - rot = RotInventory { caboose: None }; + caboose_active = None; + caboose_inactive = None; + rot = None; ticker = interval( ignition_presence @@ -310,6 +314,25 @@ async fn sp_fetching_task( }; let mut mgs_received = Instant::now(); + if rot.is_none() || prev_state.as_ref() != Some(&state) { + match &state.rot { + RotState::Enabled { active, .. } => { + rot = Some(RotInventory { + active: *active, + caboose_a: None, + caboose_b: None, + }); + } + RotState::CommunicationFailed { message } => { + warn!( + log, "Failed to get RoT state from SP"; + "message" => message, + ); + rot = None; + } + } + } + // For each of our cached items that require additional MGS requests (SP // components, SP caboose, RoT caboose), only fetch them if either our // state has changed or we previously failed to fetch them after such a @@ -333,12 +356,13 @@ async fn sp_fetching_task( }; } - if prev_state.as_ref() != Some(&state) || caboose.is_none() { - caboose = match mgs_client + if prev_state.as_ref() != Some(&state) || caboose_active.is_none() { + caboose_active = match mgs_client .sp_component_caboose_get( id.type_, id.slot, SpComponent::SP_ITSELF.const_as_str(), + 0, ) .await { @@ -348,7 +372,7 @@ async fn sp_fetching_task( } Err(err) => { warn!( - log, "Failed to get caboose for sp"; + log, "Failed to get caboose for sp (active slot)"; "sp" => ?id, "err" => %err, ); @@ -357,12 +381,13 @@ async fn sp_fetching_task( }; } - if prev_state.as_ref() != Some(&state) || rot.caboose.is_none() { - rot.caboose = match mgs_client + if prev_state.as_ref() != Some(&state) || caboose_inactive.is_none() { + caboose_inactive = match mgs_client .sp_component_caboose_get( id.type_, id.slot, - SpComponent::ROT.const_as_str(), + SpComponent::SP_ITSELF.const_as_str(), + 1, ) .await { @@ -372,7 +397,7 @@ async fn sp_fetching_task( } Err(err) => { warn!( - log, "Failed to get RoT caboose for sp"; + log, "Failed to get caboose for sp (inactive slot)"; "sp" => ?id, "err" => %err, ); @@ -381,11 +406,64 @@ async fn sp_fetching_task( }; } + if let Some(rot) = rot.as_mut() { + if prev_state.as_ref() != Some(&state) || rot.caboose_a.is_none() { + rot.caboose_a = match mgs_client + .sp_component_caboose_get( + id.type_, + id.slot, + SpComponent::ROT.const_as_str(), + 0, + ) + .await + { + Ok(response) => { + mgs_received = Instant::now(); + Some(response.into_inner()) + } + Err(err) => { + warn!( + log, "Failed to get RoT caboose (slot A) for sp"; + "sp" => ?id, + "err" => %err, + ); + None + } + }; + } + + if prev_state.as_ref() != Some(&state) || rot.caboose_b.is_none() { + rot.caboose_b = match mgs_client + .sp_component_caboose_get( + id.type_, + id.slot, + SpComponent::ROT.const_as_str(), + 1, + ) + .await + { + Ok(response) => { + mgs_received = Instant::now(); + Some(response.into_inner()) + } + Err(err) => { + warn!( + log, "Failed to get RoT caboose (slot B) for sp"; + "sp" => ?id, + "err" => %err, + ); + None + } + }; + } + } + let emit = FetchedSpData { id, state, components: components.clone(), - caboose: caboose.clone(), + caboose_active: caboose_active.clone(), + caboose_inactive: caboose_inactive.clone(), rot: rot.clone(), mgs_received, }; diff --git a/wicketd/src/update_tracker.rs b/wicketd/src/update_tracker.rs index f82987b2b6b..f4ded083138 100644 --- a/wicketd/src/update_tracker.rs +++ b/wicketd/src/update_tracker.rs @@ -640,6 +640,11 @@ impl UpdateDriver { let sp_registrar = engine.for_component(UpdateComponent::Sp); + // The SP only has one updateable firmware slot ("the inactive bank"). + // We want to ask about slot 0 (the active slot)'s current version, and + // we are supposed to always pass 0 when updating. + let sp_firmware_slot = 0; + let sp_current_version = sp_registrar .new_step( UpdateStepId::InterrogateSp, @@ -651,6 +656,7 @@ impl UpdateDriver { update_cx.sp.type_, update_cx.sp.slot, SpComponent::SP_ITSELF.const_as_str(), + sp_firmware_slot, ) .await .map_err(|error| { @@ -706,9 +712,6 @@ impl UpdateDriver { .into(); } - // The SP only has one updateable firmware slot ("the - // inactive bank") - we always pass 0. - let sp_firmware_slot = 0; cx.with_nested_engine(|engine| { inner_cx.register_steps( engine,