Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ resolver = "2"
version = "0.1.0"

[workspace.dependencies]
anstream = "1"
anstyle = "1"
async-recursion = "1"
attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "10952e8d9599b735b85d480af3560a11700e5b64" }
atomicwrites = "0.4"
Expand Down
2 changes: 2 additions & 0 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ path = "src/main.rs"
permslip = ["dep:permission-slip-client", "dep:permission-slip-common"]

[dependencies]
anstream.workspace = true
anstyle.workspace = true
async-recursion.workspace = true
atomicwrites.workspace = true
base64.workspace = true
Expand Down
55 changes: 47 additions & 8 deletions client/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};

use anstream::print;
use anstyle::{AnsiColor, Style};
use atomicwrites::{AtomicFile, OverwriteBehavior};
use bytesize::ByteSize;
use chrono::TimeDelta;
Expand All @@ -32,7 +34,7 @@ use sush_common::jobs::{
SessionSignerNonce, SignedJob, job_status_to_json_map,
};
use sush_common::keys::{KeyId, Signature, SshPublicKey};
use sush_common::targets::{MAX_CUBBY, SledId, SledVersion};
use sush_common::targets::{MAX_CUBBY, SledHealth, SledId, SledVersion};
use sush_common::version::VersionInfo;

use crate::AuthzSigner;
Expand Down Expand Up @@ -1065,7 +1067,8 @@ mod test {
/// Cell text width for one sled in the rack drawing.
const CELL: usize = 28;

/// One sled cell: serial on the left, build on the right.
/// One sled cell: serial on the left, build on the right, colored by
/// health.
fn rack_cell(sled: Option<&SledVersion>) -> String {
match sled {
Some(sled) => {
Expand All @@ -1080,12 +1083,29 @@ fn rack_cell(sled: Option<&SledVersion>) -> String {
}
None => String::new(),
};
format!(" {:<12.12}{:>14.14} ", sled.baseboard.serial_number, build)
let style = health_style(sled);
format!(
"{style} {:<12.12}{:>14.14} {style:#}",
sled.baseboard.serial_number, build
)
}
None => " ".repeat(CELL),
}
}

/// Green gossips with the answering sled, yellow was once known but is
/// out of contact, and red is in the cubby map with no other sign of
/// life. Sleds without health (an old server, a newer state than this
/// build knows) stay unstyled, which renders as nothing.
fn health_style(sled: &SledVersion) -> Style {
match sled.health {
Some(SledHealth::Linked) => AnsiColor::Green.on_default(),
Some(SledHealth::Unlinked) if sled.version.is_some() => AnsiColor::Yellow.on_default(),
Some(SledHealth::Unlinked) => AnsiColor::Red.on_default(),
Some(SledHealth::Unknown) | None => Style::new(),
}
}

/// Draw the rack as wicket does: 16 rows of two cubbies, numbered
/// bottom-to-top and left-to-right per RFD 200, split where the
/// switches and power shelves sit. Sleds known only by build (no
Expand Down Expand Up @@ -1140,11 +1160,10 @@ mod rack {
version: "0.1.0".to_string(),
commit: "f078e863b17359031de072222bb631270f2d5157".to_string(),
}),
health: None,
}
}

/// Compare the rack drawing against the snapshot in
/// `tests/output/`, or rewrite it under `EXPECTORATE=overwrite`.
#[test]
fn rack_drawing() {
let mut sleds = vec![
Expand All @@ -1162,10 +1181,30 @@ mod rack {
if let Some(version) = &mut sleds[1].version {
version.commit.push_str("-dirty");
}
let drawing = draw_rack(&sleds);
let path = "tests/output/rack.txt";
check(&draw_rack(&sleds), "tests/output/rack.txt");
}

/// A healthy sled, a silent one, and one that is only a cubby
/// number, pinning the color codes.
#[test]
fn rack_drawing_health() {
let mut sleds = vec![
sled(14, "BRM42220030"),
sled(15, "BRM42220036"),
sled(16, "2CN2M459"),
];
sleds[0].health = Some(SledHealth::Linked);
sleds[1].health = Some(SledHealth::Unlinked);
sleds[2].health = Some(SledHealth::Unlinked);
sleds[2].version = None;
check(&draw_rack(&sleds), "tests/output/rack-health.txt");
}

/// Compare against the snapshot at `path`, or rewrite it under
/// `EXPECTORATE=overwrite`.
fn check(drawing: &str, path: &str) {
if env::var("EXPECTORATE").as_deref() == Ok("overwrite") {
write(path, &drawing).unwrap();
write(path, drawing).unwrap();
} else {
let expected = read_to_string(path).expect("missing snapshot");
assert_eq!(drawing, expected, "rack drawing changed:\n{drawing}");
Expand Down
42 changes: 24 additions & 18 deletions client/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,22 +1151,25 @@ async fn session(
},
Some(client),
) => {
let (session_id, nonce) = if let (Some(session_id), Some(nonce)) = (session_id, nonce) {
(session_id, nonce)
} else {
let (baseboard_id, nonce) = with_login(ctx, client, async || {
Ok((
client.target().send().await?.into_inner(),
client.session_start_nonce().send().await?.into_inner(),
))
})
.await?;
let (session_id, nonce) =
session_create(permslip, permslip_url, &baseboard_id, nonce.nonce).await?;
ctx.session_created(Session::new(session_id), nonce);
(session_id, nonce)
};
session_start(ctx, client, session_id, nonce, wait).await
// Creation already announces the session; don't echo it
// when the start succeeds.
let (session_id, nonce, show) =
if let (Some(session_id), Some(nonce)) = (session_id, nonce) {
(session_id, nonce, true)
} else {
let (baseboard_id, nonce) = with_login(ctx, client, async || {
Ok((
client.target().send().await?.into_inner(),
client.session_start_nonce().send().await?.into_inner(),
))
})
.await?;
let (session_id, nonce) =
session_create(permslip, permslip_url, &baseboard_id, nonce.nonce).await?;
ctx.session_created(Session::new(session_id), nonce);
(session_id, nonce, false)
};
session_start(ctx, client, session_id, nonce, wait, show).await
}

#[cfg(feature = "permslip")]
Expand Down Expand Up @@ -1199,7 +1202,7 @@ async fn session(
} else {
return Err(CommandError::SigningUnavailable);
};
session_start(ctx, client, session_id, nonce, wait).await
session_start(ctx, client, session_id, nonce, wait, true).await
}

(SessionCommand::Allow { key_id, write }, Some(client)) => {
Expand Down Expand Up @@ -1758,6 +1761,7 @@ async fn session_start(
session_id: SessionId,
signer_nonce: SessionSignerNonce,
wait: bool,
show: bool,
) -> Result<(), CommandError> {
let session = Session::new(session_id);
with_login(ctx, client, async || {
Expand All @@ -1771,7 +1775,9 @@ async fn session_start(
})
.await?
.into_inner();
ctx.session_started(session, true);
if show {
ctx.session_started(session, true);
}
Ok(())
}

Expand Down
19 changes: 19 additions & 0 deletions client/tests/output/rack-health.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
┌────────────────────────────┬────────────────────────────┐
30 │ │ │ 31
28 │ │ │ 29
26 │ │ │ 27
24 │ │ │ 25
22 │ │ │ 23
20 │ │ │ 21
18 │ │ │ 19
16 │ 2CN2M459 │ │ 17
├────────────────────────────┼────────────────────────────┤
14 │ BRM42220030 0.1.0 f078e86 │ BRM42220036 0.1.0 f078e86 │ 15
12 │ │ │ 13
10 │ │ │ 11
8 │ │ │ 9
6 │ │ │ 7
4 │ │ │ 5
2 │ │ │ 3
0 │ │ │ 1
└────────────────────────────┴────────────────────────────┘
18 changes: 17 additions & 1 deletion common/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,28 @@ pub type Cubbies = BTreeMap<u8, BaseboardId>;
/// The highest cubby number in a rack.
pub const MAX_CUBBY: u8 = 31;

/// One sled's location and build.
/// One sled's location, build, and health.
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct SledVersion {
pub cubby: Option<u8>,
pub baseboard: BaseboardId,
pub version: Option<VersionInfo>,
#[serde(default)]
pub health: Option<SledHealth>,
}

/// One sled's gossip link health, as the answering sled sees it. A
/// silent death can lag `Linked` at TCP's pace.
#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SledHealth {
/// Attested, holding a live gossip link.
Linked,
/// Known by version or cubby, but no live link.
Unlinked,
/// A state from a build newer than this one.
#[serde(other)]
Unknown,
}

/// The sleds a request names.
Expand Down
Loading