|
4 | 4 |
|
5 | 5 | //! omdb commands related to update status |
6 | 6 |
|
| 7 | +use std::{ |
| 8 | + borrow::Cow, |
| 9 | + collections::{BTreeMap, BTreeSet}, |
| 10 | + iter, |
| 11 | +}; |
| 12 | + |
| 13 | +use super::UpdateStatusArgs; |
7 | 14 | use anyhow::Context; |
8 | 15 | use gateway_types::rot::RotSlot; |
9 | 16 | use nexus_types::internal_api::views::{ |
10 | | - HostPhase1Status, HostPhase2Status, RotBootloaderStatus, RotStatus, |
11 | | - SpStatus, ZoneStatus, |
| 17 | + HostPhase1Status, HostPhase2Status, MgsDrivenUpdateStatus, |
| 18 | + RotBootloaderStatus, RotStatus, SledAgentUpdateStatus, SpStatus, |
| 19 | + TufRepoVersion, UpdateStatus, ZoneStatus, |
12 | 20 | }; |
13 | 21 | use omicron_common::disk::M2Slot; |
14 | 22 | use omicron_uuid_kinds::SledUuid; |
| 23 | +use strum::IntoEnumIterator; |
15 | 24 | use tabled::Tabled; |
16 | 25 |
|
17 | 26 | /// Runs `omdb nexus update-status` |
18 | 27 | pub async fn cmd_nexus_update_status( |
19 | 28 | client: &nexus_lockstep_client::Client, |
| 29 | + args: &UpdateStatusArgs, |
20 | 30 | ) -> Result<(), anyhow::Error> { |
| 31 | + let UpdateStatusArgs { details } = args; |
| 32 | + |
21 | 33 | let status = client |
22 | 34 | .update_status() |
23 | 35 | .await |
24 | 36 | .context("retrieving update status")? |
25 | 37 | .into_inner(); |
26 | 38 |
|
| 39 | + if *details { |
| 40 | + print_status_details(status); |
| 41 | + } else { |
| 42 | + print_status_summary(status); |
| 43 | + } |
| 44 | + |
| 45 | + Ok(()) |
| 46 | +} |
| 47 | + |
| 48 | +#[derive( |
| 49 | + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, strum::EnumIter, |
| 50 | +)] |
| 51 | +enum Component { |
| 52 | + RotBootloader, |
| 53 | + Rot, |
| 54 | + Sp, |
| 55 | + HostPhase1, |
| 56 | + HostPhase2, |
| 57 | + Zone, |
| 58 | +} |
| 59 | + |
| 60 | +#[derive(Debug, Default)] |
| 61 | +struct UpdateStatusSummaryBuilder { |
| 62 | + all_versions: BTreeSet<Cow<'static, str>>, |
| 63 | + counts: BTreeMap<Component, BTreeMap<Cow<'static, str>, usize>>, |
| 64 | +} |
| 65 | + |
| 66 | +impl UpdateStatusSummaryBuilder { |
| 67 | + fn insert(&mut self, component: Component, version: TufRepoVersion) { |
| 68 | + let version = match version { |
| 69 | + TufRepoVersion::Unknown => Cow::Borrowed("unknown"), |
| 70 | + TufRepoVersion::InstallDataset => Cow::Borrowed("install-dataset"), |
| 71 | + TufRepoVersion::Error(_) => Cow::Borrowed("error"), |
| 72 | + TufRepoVersion::Version(v) => Cow::Owned(v.to_string()), |
| 73 | + }; |
| 74 | + |
| 75 | + self.all_versions.insert(version.clone()); |
| 76 | + *self |
| 77 | + .counts |
| 78 | + .entry(component) |
| 79 | + .or_default() |
| 80 | + .entry(version) |
| 81 | + .or_default() += 1; |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +fn print_status_summary(status: UpdateStatus) { |
| 86 | + let mut builder = UpdateStatusSummaryBuilder::default(); |
| 87 | + |
| 88 | + let UpdateStatus { mgs_driven, sleds } = status; |
| 89 | + |
| 90 | + for sled in sleds { |
| 91 | + let SledAgentUpdateStatus { host_phase_2, sled_id: _, zones } = sled; |
| 92 | + |
| 93 | + // Check the boot disk to know which slot to count. |
| 94 | + builder.insert( |
| 95 | + Component::HostPhase2, |
| 96 | + match host_phase_2.boot_disk { |
| 97 | + Ok(M2Slot::A) => host_phase_2.slot_a_version, |
| 98 | + Ok(M2Slot::B) => host_phase_2.slot_b_version, |
| 99 | + Err(err) => TufRepoVersion::Error(err), |
| 100 | + }, |
| 101 | + ); |
| 102 | + for z in zones { |
| 103 | + builder.insert(Component::Zone, z.version); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + for mgs in mgs_driven { |
| 108 | + let MgsDrivenUpdateStatus { |
| 109 | + baseboard_description: _, |
| 110 | + rot_bootloader, |
| 111 | + rot, |
| 112 | + sp, |
| 113 | + host_os_phase_1, |
| 114 | + } = mgs; |
| 115 | + |
| 116 | + // Bootloader and SP always identify the active slot statically. |
| 117 | + builder.insert(Component::RotBootloader, rot_bootloader.stage0_version); |
| 118 | + builder.insert(Component::Sp, sp.slot0_version); |
| 119 | + |
| 120 | + // RoT and host phase 1 need to check the active slot. |
| 121 | + builder.insert( |
| 122 | + Component::Rot, |
| 123 | + match rot.active_slot { |
| 124 | + Some(RotSlot::A) => rot.slot_a_version, |
| 125 | + Some(RotSlot::B) => rot.slot_b_version, |
| 126 | + None => TufRepoVersion::Unknown, |
| 127 | + }, |
| 128 | + ); |
| 129 | + |
| 130 | + match host_os_phase_1 { |
| 131 | + HostPhase1Status::NotASled => (), |
| 132 | + HostPhase1Status::Sled { |
| 133 | + active_slot, |
| 134 | + slot_a_version, |
| 135 | + slot_b_version, |
| 136 | + .. |
| 137 | + } => { |
| 138 | + builder.insert( |
| 139 | + Component::HostPhase1, |
| 140 | + match active_slot { |
| 141 | + Some(M2Slot::A) => slot_a_version, |
| 142 | + Some(M2Slot::B) => slot_b_version, |
| 143 | + None => TufRepoVersion::Unknown, |
| 144 | + }, |
| 145 | + ); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + let rows = Component::iter().map(|c| { |
| 151 | + let name = match c { |
| 152 | + Component::RotBootloader => "RoT bootloader", |
| 153 | + Component::Rot => "RoT", |
| 154 | + Component::Sp => "SP", |
| 155 | + Component::HostPhase1 => "Host OS (phase 1)", |
| 156 | + Component::HostPhase2 => "Host OS (phase 2)", |
| 157 | + Component::Zone => "Zone", |
| 158 | + }; |
| 159 | + let builder = &builder; |
| 160 | + iter::once(Cow::Borrowed(name)).chain(builder.all_versions.iter().map( |
| 161 | + move |v| { |
| 162 | + let count = builder |
| 163 | + .counts |
| 164 | + .get(&c) |
| 165 | + .and_then(|by_version| by_version.get(v)) |
| 166 | + .unwrap_or(&0); |
| 167 | + Cow::Owned(count.to_string()) |
| 168 | + }, |
| 169 | + )) |
| 170 | + }); |
| 171 | + let columns = |
| 172 | + iter::once(Cow::Borrowed("")).chain(builder.all_versions.clone()); |
| 173 | + |
| 174 | + let mut table = tabled::builder::Builder::new(); |
| 175 | + table.push_record(columns); |
| 176 | + for row in rows { |
| 177 | + table.push_record(row); |
| 178 | + } |
| 179 | + let table = table |
| 180 | + .build() |
| 181 | + .with(tabled::settings::Style::psql()) |
| 182 | + .with(tabled::settings::Padding::new(0, 1, 0, 0)) |
| 183 | + .to_string(); |
| 184 | + |
| 185 | + println!("Count of each component type by system version:"); |
| 186 | + println!(); |
| 187 | + println!("{table}"); |
| 188 | + println!(); |
| 189 | + println!("To see each individual component, rerun with `--details`."); |
| 190 | +} |
| 191 | + |
| 192 | +fn print_status_details(status: UpdateStatus) { |
27 | 193 | print_rot_bootloaders( |
28 | 194 | status |
29 | 195 | .mgs_driven |
@@ -62,8 +228,6 @@ pub async fn cmd_nexus_update_status( |
62 | 228 | .iter() |
63 | 229 | .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), |
64 | 230 | ); |
65 | | - |
66 | | - Ok(()) |
67 | 231 | } |
68 | 232 |
|
69 | 233 | fn print_zones(zones: impl Iterator<Item = (SledUuid, Vec<ZoneStatus>)>) { |
|
0 commit comments