Skip to content

Commit 50bc7e0

Browse files
authored
[wicket] Add sled+switch location to status bar (#3810)
I don't think `Location:` is the right label for this - open to suggestions. Maybe "Connected to:"? Example from madrid: ![wicket-statusbar](https://github.com/oxidecomputer/omicron/assets/1435635/deb20f98-e1b2-4526-ab6f-d66bf34e2427) Fixes #3777
1 parent ab7054c commit 50bc7e0

File tree

11 files changed

+352
-21
lines changed

11 files changed

+352
-21
lines changed

openapi/wicketd.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,31 @@
260260
}
261261
}
262262
},
263+
"/location": {
264+
"get": {
265+
"summary": "Report the identity of the sled and switch we're currently running on /",
266+
"description": "connected to.",
267+
"operationId": "get_location",
268+
"responses": {
269+
"200": {
270+
"description": "successful operation",
271+
"content": {
272+
"application/json": {
273+
"schema": {
274+
"$ref": "#/components/schemas/GetLocationResponse"
275+
}
276+
}
277+
}
278+
},
279+
"4XX": {
280+
"$ref": "#/components/responses/Error"
281+
},
282+
"5XX": {
283+
"$ref": "#/components/responses/Error"
284+
}
285+
}
286+
}
287+
},
263288
"/rack-setup": {
264289
"get": {
265290
"summary": "Query current state of rack setup.",
@@ -1147,6 +1172,48 @@
11471172
}
11481173
]
11491174
},
1175+
"GetLocationResponse": {
1176+
"description": "All the fields of this response are optional, because it's possible we don't know any of them (yet) if MGS has not yet finished discovering its location or (ever) if we're running in a dev environment that doesn't support MGS-location / baseboard mapping.",
1177+
"type": "object",
1178+
"properties": {
1179+
"sled_baseboard": {
1180+
"nullable": true,
1181+
"description": "The baseboard of our sled (where wicketd is running).",
1182+
"allOf": [
1183+
{
1184+
"$ref": "#/components/schemas/Baseboard"
1185+
}
1186+
]
1187+
},
1188+
"sled_id": {
1189+
"nullable": true,
1190+
"description": "The identity of our sled (where wicketd is running).",
1191+
"allOf": [
1192+
{
1193+
"$ref": "#/components/schemas/SpIdentifier"
1194+
}
1195+
]
1196+
},
1197+
"switch_baseboard": {
1198+
"nullable": true,
1199+
"description": "The baseboard of the switch our sled is physically connected to.",
1200+
"allOf": [
1201+
{
1202+
"$ref": "#/components/schemas/Baseboard"
1203+
}
1204+
]
1205+
},
1206+
"switch_id": {
1207+
"nullable": true,
1208+
"description": "The identity of the switch our sled is physically connected to.",
1209+
"allOf": [
1210+
{
1211+
"$ref": "#/components/schemas/SpIdentifier"
1212+
}
1213+
]
1214+
}
1215+
}
1216+
},
11501217
"IpRange": {
11511218
"oneOf": [
11521219
{

wicket/src/events.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use std::fs::File;
1010
use std::time::{Duration, SystemTime};
1111
use wicket_common::update_events::EventReport;
1212
use wicketd_client::types::{
13-
ArtifactId, CurrentRssUserConfig, IgnitionCommand, RackOperationStatus,
14-
RackV1Inventory, SemverVersion,
13+
ArtifactId, CurrentRssUserConfig, GetLocationResponse, IgnitionCommand,
14+
RackOperationStatus, RackV1Inventory, SemverVersion,
1515
};
1616

1717
/// Event report type returned by the get_artifacts_and_event_reports API call.
@@ -41,6 +41,9 @@ pub enum Event {
4141
/// The current state of rack initialization.
4242
RackSetupStatus(Result<RackOperationStatus, String>),
4343

44+
/// The location within the rack where wicketd is running.
45+
WicketdLocation(GetLocationResponse),
46+
4447
/// The tick of a Timer
4548
/// This can be used to draw a frame to the terminal
4649
Tick,

wicket/src/runner.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ impl RunnerCore {
156156
self.state.rack_setup_state = result;
157157
self.screen.draw(&self.state, &mut self.terminal)?;
158158
}
159+
Event::WicketdLocation(location) => {
160+
self.state.wicketd_location = location;
161+
self.screen.draw(&self.state, &mut self.terminal)?;
162+
}
159163
Event::Shutdown => return Ok(true),
160164
}
161165
Ok(false)

wicket/src/state/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ pub use update::{
2323
};
2424

2525
use serde::{Deserialize, Serialize};
26-
use wicketd_client::types::{CurrentRssUserConfig, RackOperationStatus};
26+
use wicketd_client::types::{
27+
CurrentRssUserConfig, GetLocationResponse, RackOperationStatus,
28+
};
2729

2830
/// The global state of wicket
2931
///
@@ -40,6 +42,7 @@ pub struct State {
4042
pub force_update_state: ForceUpdateState,
4143
pub rss_config: Option<CurrentRssUserConfig>,
4244
pub rack_setup_state: Result<RackOperationStatus, String>,
45+
pub wicketd_location: GetLocationResponse,
4346
}
4447

4548
impl State {
@@ -54,6 +57,12 @@ impl State {
5457
force_update_state: ForceUpdateState::default(),
5558
rss_config: None,
5659
rack_setup_state: Err("status not yet polled from wicketd".into()),
60+
wicketd_location: GetLocationResponse {
61+
sled_baseboard: None,
62+
sled_id: None,
63+
switch_baseboard: None,
64+
switch_id: None,
65+
},
5766
}
5867
}
5968
}

wicket/src/ui/main.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
1414
use tui::style::{Modifier, Style};
1515
use tui::text::{Span, Spans};
1616
use tui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph};
17+
use wicketd_client::types::GetLocationResponse;
1718

1819
/// The [`MainScreen`] is the primary UI element of the terminal, covers the
1920
/// entire terminal window/buffer and is visible for all interactions except
@@ -192,9 +193,13 @@ impl MainScreen {
192193
frame: &mut Frame<'_>,
193194
rect: Rect,
194195
) {
196+
let location_spans = location_spans(&state.wicketd_location);
195197
let wicketd_spans = state.service_status.wicketd_liveness().to_spans();
196198
let mgs_spans = state.service_status.mgs_liveness().to_spans();
197-
let mut spans = vec![Span::styled("WICKETD: ", style::service())];
199+
let mut spans = vec![Span::styled("You are here: ", style::service())];
200+
spans.extend_from_slice(&location_spans);
201+
spans.push(Span::styled(" | ", style::divider()));
202+
spans.push(Span::styled("WICKETD: ", style::service()));
198203
spans.extend_from_slice(&wicketd_spans);
199204
spans.push(Span::styled(" | ", style::divider()));
200205
spans.push(Span::styled("MGS: ", style::service()));
@@ -220,6 +225,41 @@ impl MainScreen {
220225
}
221226
}
222227

228+
fn location_spans(location: &GetLocationResponse) -> Vec<Span<'static>> {
229+
// We reuse `style::connected()` and `style::delayed()` in these spans to
230+
// match the wicketd/mgs connection statuses that follow us in the status
231+
// bar.
232+
let mut spans = Vec::new();
233+
if let Some(id) = location.sled_id.as_ref() {
234+
spans.push(Span::styled(
235+
format!("Sled {}", id.slot),
236+
style::connected(),
237+
));
238+
} else if let Some(baseboard) = location.sled_baseboard.as_ref() {
239+
spans.push(Span::styled(
240+
format!("Sled {}", baseboard.identifier()),
241+
style::connected(),
242+
));
243+
} else {
244+
spans.push(Span::styled("Sled UNKNOWN", style::delayed()));
245+
};
246+
spans.push(Span::styled("/", style::divider()));
247+
if let Some(id) = location.switch_id.as_ref() {
248+
spans.push(Span::styled(
249+
format!("Switch {}", id.slot),
250+
style::connected(),
251+
));
252+
} else if let Some(baseboard) = location.switch_baseboard.as_ref() {
253+
spans.push(Span::styled(
254+
format!("Switch {}", baseboard.identifier()),
255+
style::connected(),
256+
));
257+
} else {
258+
spans.push(Span::styled("Switch UNKNOWN", style::delayed()));
259+
};
260+
spans
261+
}
262+
223263
/// The mechanism for selecting panes
224264
pub struct Sidebar {
225265
panes: StatefulList<&'static str>,

wicket/src/wicketd.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use tokio::sync::mpsc::{self, Sender, UnboundedSender};
1111
use tokio::time::{interval, Duration, MissedTickBehavior};
1212
use wicketd_client::types::{
1313
AbortUpdateOptions, ClearUpdateStateOptions, GetInventoryParams,
14-
GetInventoryResponse, IgnitionCommand, SpIdentifier, SpType,
15-
StartUpdateOptions,
14+
GetInventoryResponse, GetLocationResponse, IgnitionCommand, SpIdentifier,
15+
SpType, StartUpdateOptions,
1616
};
1717

1818
use crate::events::EventReportMap;
@@ -114,6 +114,7 @@ impl WicketdManager {
114114
self.poll_artifacts_and_event_reports();
115115
self.poll_rack_setup_config();
116116
self.poll_rack_setup_status();
117+
self.poll_location();
117118

118119
loop {
119120
tokio::select! {
@@ -342,6 +343,61 @@ impl WicketdManager {
342343
});
343344
}
344345

346+
fn poll_location(&self) {
347+
let log = self.log.clone();
348+
let tx = self.events_tx.clone();
349+
let addr = self.wicketd_addr;
350+
tokio::spawn(async move {
351+
let client = create_wicketd_client(&log, addr, WICKETD_TIMEOUT);
352+
let mut ticker = interval(WICKETD_POLL_INTERVAL * 2);
353+
let mut prev = None;
354+
ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
355+
loop {
356+
ticker.tick().await;
357+
// TODO: We should really be using ETAGs here
358+
let location = match client.get_location().await {
359+
Ok(val) => val.into_inner(),
360+
Err(err) => {
361+
warn!(
362+
log,
363+
"Failed to fetch location of wicketd";
364+
"err" => #%err,
365+
);
366+
continue;
367+
}
368+
};
369+
370+
// Only send a new event if the config has changed
371+
if Some(&location) == prev.as_ref() {
372+
continue;
373+
}
374+
prev = Some(location.clone());
375+
376+
// If every field of `location` is filled in, we don't need to
377+
// poll any more - wicketd can't move around while it's running.
378+
// Check this prior to sending the event to avoid an extra
379+
// clone.
380+
let GetLocationResponse {
381+
sled_baseboard,
382+
sled_id,
383+
switch_baseboard,
384+
switch_id,
385+
} = &location;
386+
387+
let location_fully_provided = sled_baseboard.is_some()
388+
&& sled_id.is_some()
389+
&& switch_baseboard.is_some()
390+
&& switch_id.is_some();
391+
392+
let _ = tx.send(Event::WicketdLocation(location));
393+
394+
if location_fully_provided {
395+
break;
396+
}
397+
}
398+
});
399+
}
400+
345401
fn poll_rack_setup_config(&self) {
346402
let log = self.log.clone();
347403
let tx = self.events_tx.clone();

wicketd-client/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ progenitor::generate_api!(
4747
CurrentRssUserConfigInsensitive = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize ] },
4848
CurrentRssUserConfigSensitive = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize ] },
4949
CurrentRssUserConfig = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize ] },
50+
GetLocationResponse = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize ] },
5051
},
5152
replace = {
5253
Duration = std::time::Duration,
@@ -65,3 +66,29 @@ progenitor::generate_api!(
6566

6667
/// A type alias for errors returned by this crate.
6768
pub type ClientError = crate::Error<crate::types::Error>;
69+
70+
impl types::Baseboard {
71+
pub fn identifier(&self) -> &str {
72+
match &self {
73+
Self::Gimlet { identifier, .. } => &identifier,
74+
Self::Pc { identifier, .. } => &identifier,
75+
Self::Unknown => "unknown",
76+
}
77+
}
78+
79+
pub fn model(&self) -> &str {
80+
match self {
81+
Self::Gimlet { model, .. } => &model,
82+
Self::Pc { model, .. } => &model,
83+
Self::Unknown => "unknown",
84+
}
85+
}
86+
87+
pub fn revision(&self) -> i64 {
88+
match self {
89+
Self::Gimlet { revision, .. } => *revision,
90+
Self::Pc { .. } => 0,
91+
Self::Unknown => 0,
92+
}
93+
}
94+
}

wicketd/src/context.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,25 @@ use crate::MgsHandle;
1111
use anyhow::anyhow;
1212
use anyhow::bail;
1313
use anyhow::Result;
14+
use gateway_client::types::SpIdentifier;
1415
use sled_hardware::Baseboard;
1516
use slog::info;
1617
use std::net::Ipv6Addr;
1718
use std::net::SocketAddrV6;
1819
use std::sync::Arc;
1920
use std::sync::Mutex;
21+
use std::sync::OnceLock;
2022

2123
/// Shared state used by API handlers
2224
pub struct ServerContext {
2325
pub mgs_handle: MgsHandle,
2426
pub mgs_client: gateway_client::Client,
2527
pub(crate) log: slog::Logger,
28+
/// Our cached copy of what MGS's `/local/switch-id` endpoint returns; it
29+
/// identifies whether we're connected to switch 0 or 1 and cannot change
30+
/// (plugging us into a different switch would require powering off our sled
31+
/// and physically moving it).
32+
pub(crate) local_switch_id: OnceLock<SpIdentifier>,
2633
pub(crate) bootstrap_peers: BootstrapPeers,
2734
pub(crate) update_tracker: Arc<UpdateTracker>,
2835
pub(crate) baseboard: Option<Baseboard>,
@@ -66,4 +73,35 @@ impl ServerContext {
6673
Ok(ip)
6774
}
6875
}
76+
77+
pub(crate) async fn local_switch_id(&self) -> Option<SpIdentifier> {
78+
// Do we already have it cached from a previous invocation?
79+
if let Some(&switch_id) = self.local_switch_id.get() {
80+
return Some(switch_id);
81+
}
82+
83+
// We don't have a cached switch ID; try to fetch it from MGS. We
84+
// might be racing ourself (if this function is being called multiple
85+
// times concurrently), but that's fine: all invocations can query MGS,
86+
// and only one will succeed in setting the cache.
87+
match self.mgs_client.sp_local_switch_id().await {
88+
Ok(response) => {
89+
let switch_id = response.into_inner();
90+
91+
// Ignore failures on set - that just means we lost the race and
92+
// another concurrent call to us already set it.
93+
_ = self.local_switch_id.set(switch_id);
94+
95+
Some(switch_id)
96+
}
97+
Err(err) => {
98+
slog::warn!(
99+
self.log,
100+
"Failed to fetch local switch ID from MGS";
101+
"err" => #%err,
102+
);
103+
None
104+
}
105+
}
106+
}
69107
}

0 commit comments

Comments
 (0)