Skip to content
Merged
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
10 changes: 10 additions & 0 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use sush_common::jobs::{
SignedJob,
};
use sush_common::keys::{KeyId, SshPublicKey};
use sush_common::targets::SledVersion;
use sush_common::version::VersionInfo;

/// Oxide Support Shell API
Expand Down Expand Up @@ -234,6 +235,15 @@ pub trait SushApi {
query: QueryParams<RoutingParam>,
) -> Result<HttpResponseOk<BaseboardId>, HttpError>;

/// Get the cubby, baseboard, and build of every known sled.
///
/// Unauthenticated, like `/target`.
#[endpoint { method = GET, path = "/versions" }]
async fn versions(
ctx: RequestContext<Self::Context>,
query: QueryParams<RoutingParam>,
) -> Result<HttpResponseOk<Vec<SledVersion>>, HttpError>;

/// Get the version and git commit of the server's build.
///
/// Unauthenticated, like `/target`.
Expand Down
127 changes: 125 additions & 2 deletions client/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use sush_common::jobs::{
SignedJob, job_status_to_json_map,
};
use sush_common::keys::{KeyId, Signature, SshPublicKey};
use sush_common::targets::{MAX_CUBBY, SledVersion};
use sush_common::version::VersionInfo;

use crate::AuthzSigner;
Expand Down Expand Up @@ -119,14 +120,25 @@ impl CommandContext for Cli {

// Build provenance

fn versions(&mut self, client: &VersionInfo, server: Option<&VersionInfo>) {
fn versions(
&mut self,
client: &VersionInfo,
server: Option<&VersionInfo>,
sleds: &[SledVersion],
) {
match self.get_output_format() {
OutputFormat::Json => println!("{}", json!({"client": client, "server": server})),
OutputFormat::Json => println!(
"{}",
json!({"client": client, "server": server, "sleds": sleds})
),
OutputFormat::Text => {
println!("Client:\t{client}");
if let Some(server) = server {
println!("Server:\t{server}");
}
if !sleds.is_empty() {
print!("{}", draw_rack(sleds));
}
}
}
}
Expand Down Expand Up @@ -854,3 +866,114 @@ 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.
fn rack_cell(sled: Option<&SledVersion>) -> String {
match sled {
Some(sled) => {
let build = match &sled.version {
Some(v) => {
let dirty = if v.commit.ends_with("-dirty") {
"+"
} else {
""
};
format!("{} {:.7}{dirty}", v.version, v.commit)
}
None => String::new(),
};
format!(" {:<12.12}{:>14.14} ", sled.baseboard.serial_number, build)
}
None => " ".repeat(CELL),
}
}

/// 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
/// cubby) are listed below the rack.
fn draw_rack(sleds: &[SledVersion]) -> String {
let by_cubby: BTreeMap<u8, &SledVersion> = sleds
.iter()
.filter_map(|sled| sled.cubby.map(|cubby| (cubby, sled)))
.collect();
let row = |row: u8| {
let (left, right) = (2 * row, 2 * row + 1);
format!(
"{left:>3} │{}│{}│ {right}\n",
rack_cell(by_cubby.get(&left).copied()),
rack_cell(by_cubby.get(&right).copied()),
)
};
let bar = "─".repeat(CELL);
let mut out = format!(" ┌{bar}┬{bar}┐\n");
for r in (8..16).rev() {
out.push_str(&row(r));
}
out.push_str(&format!(" ├{bar}┼{bar}┤\n"));
for r in (0..8).rev() {
out.push_str(&row(r));
}
out.push_str(&format!(" └{bar}┴{bar}┘\n"));
for sled in sleds
.iter()
.filter(|sled| sled.cubby.is_none_or(|cubby| cubby > MAX_CUBBY))
{
out.push_str(&format!(" ?? │{}│\n", rack_cell(Some(sled))));
}
out
}

#[cfg(test)]
mod rack {
use std::env;
use std::fs::{read_to_string, write};

use super::*;

fn sled(cubby: u8, serial: &str) -> SledVersion {
SledVersion {
cubby: Some(cubby),
baseboard: BaseboardId {
part_number: "913-0000019".to_string(),
serial_number: serial.to_string(),
},
version: Some(VersionInfo {
version: "0.1.0".to_string(),
commit: "f078e863b17359031de072222bb631270f2d5157".to_string(),
}),
}
}

/// Compare the rack drawing against the snapshot in
/// `tests/output/`, or rewrite it under `EXPECTORATE=overwrite`.
#[test]
fn rack_drawing() {
let mut sleds = vec![
sled(14, "BRM42220030"),
sled(15, "BRM42220036"),
sled(16, "2CN2M459"),
sled(17, "2RGCFG10"),
];
sleds.push(SledVersion {
cubby: None,
..sled(0, "STRAGGLER")
});
sleds.push(sled(32, "MISCUBBIED"));
sleds[2].version = None;
if let Some(version) = &mut sleds[1].version {
version.commit.push_str("-dirty");
}
let drawing = draw_rack(&sleds);
let path = "tests/output/rack.txt";
if env::var("EXPECTORATE").as_deref() == Ok("overwrite") {
write(path, &drawing).unwrap();
} else {
let expected = read_to_string(path).expect("missing snapshot");
assert_eq!(drawing, expected, "rack drawing changed:\n{drawing}");
}
}
}
18 changes: 13 additions & 5 deletions client/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ pub enum ClientCommand {
#[clap(alias = "repl")]
Shell,

/// Report client and server build versions.
/// Report client and sled build versions.
Version,

/// Leave the interactive REPL.
Expand Down Expand Up @@ -669,11 +669,19 @@ impl ClientCommand {
}

(ClientCommand::Version, client) => {
let server = match client {
Some(client) => Some(client.version().send().await?.into_inner()),
None => None,
let (server, sleds) = match client {
Some(client) => (
Some(client.version().send().await?.into_inner()),
client
.versions()
.send()
.await
.map(|sleds| sleds.into_inner())
.unwrap_or_default(),
),
None => (None, Vec::new()),
};
ctx.versions(&VersionInfo::current(), server.as_ref());
ctx.versions(&VersionInfo::current(), server.as_ref(), &sleds);
Ok(())
}

Expand Down
8 changes: 7 additions & 1 deletion client/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use sush_common::jobs::{
Access, JobId, JobOutputStream, JobStatusMap, Session, SessionId, SignedJob,
};
use sush_common::keys::{KeyId, SshPublicKey};
use sush_common::targets::SledVersion;
use sush_common::version::VersionInfo;

use crate::AuthzSigner;
Expand Down Expand Up @@ -97,7 +98,12 @@ pub trait CommandContext: Clone + Send + Sync {
fn pre_parse_hook(&mut self, _command: &str) {}

// Build provenance
fn versions(&mut self, client: &VersionInfo, server: Option<&VersionInfo>);
fn versions(
&mut self,
client: &VersionInfo,
server: Option<&VersionInfo>,
sleds: &[SledVersion],
);

// Session management
fn authz_signer(&self) -> AuthzSigner;
Expand Down
1 change: 1 addition & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ progenitor::generate_api!(
KeyId = sush_common::keys::KeyId,
Signature = sush_common::keys::Signature,
SignedForJobStartRequest = sush_common::jobs::SignedJob,
SledVersion = sush_common::targets::SledVersion,
VersionInfo = sush_common::version::VersionInfo,
},
timeout = 600,
Expand Down
10 changes: 8 additions & 2 deletions client/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use sush_common::jobs::{
Access, JobId, JobOutputStream, JobStatusMap, Session, SessionId, SignedJob,
};
use sush_common::keys::{KeyId, SshPublicKey};
use sush_common::targets::SledVersion;
use sush_common::version::VersionInfo;

use crate::cli::Cli;
Expand Down Expand Up @@ -202,8 +203,13 @@ impl CommandContext for Repl {

// Build provenance

fn versions(&mut self, client: &VersionInfo, server: Option<&VersionInfo>) {
self.cli.versions(client, server)
fn versions(
&mut self,
client: &VersionInfo,
server: Option<&VersionInfo>,
sleds: &[SledVersion],
) {
self.cli.versions(client, server, sleds)
}

// Session management
Expand Down
21 changes: 21 additions & 0 deletions client/tests/output/rack.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
┌────────────────────────────┬────────────────────────────┐
30 │ │ │ 31
28 │ │ │ 29
26 │ │ │ 27
24 │ │ │ 25
22 │ │ │ 23
20 │ │ │ 21
18 │ │ │ 19
16 │ 2CN2M459 │ 2RGCFG10 0.1.0 f078e86 │ 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
└────────────────────────────┴────────────────────────────┘
?? │ STRAGGLER 0.1.0 f078e86 │
?? │ MISCUBBIED 0.1.0 f078e86 │
10 changes: 10 additions & 0 deletions common/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sled_hardware_types::BaseboardId;
use thiserror::Error;

use crate::version::VersionInfo;

/// Baseboards by cubby number, as much of it as is known.
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.
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct SledVersion {
pub cubby: Option<u8>,
pub baseboard: BaseboardId,
pub version: Option<VersionInfo>,
}

/// The sleds a request names.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Target {
Expand Down
17 changes: 16 additions & 1 deletion common/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

//! The build's provenance.

use std::collections::BTreeMap;
use std::fmt;

use borsh::{BorshDeserialize, BorshSerialize};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sled_hardware_types::BaseboardId;

/// The git commit of this build, "-dirty" suffixed when the tree had
/// uncommitted changes.
Expand All @@ -22,7 +25,17 @@ pub const LONG_VERSION: &str = concat!(
);

/// One build's version and commit.
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[derive(
BorshDeserialize,
BorshSerialize,
Clone,
Debug,
Deserialize,
Eq,
JsonSchema,
PartialEq,
Serialize,
)]
pub struct VersionInfo {
/// The package version.
pub version: String,
Expand All @@ -45,3 +58,5 @@ impl fmt::Display for VersionInfo {
write!(f, "{} ({})", self.version, self.commit)
}
}

pub type VersionMap = BTreeMap<BaseboardId, VersionInfo>;
25 changes: 24 additions & 1 deletion server/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Manage authentication, job signature verification, and the session
//! state machine. Does not manage jobs directly.

use std::collections::BTreeSet;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
Expand Down Expand Up @@ -32,7 +33,8 @@ use sush_common::authn::{
use sush_common::jobs::JobOutputStream;
use sush_common::jobs::{Access, JobId, JobStatusMap, Session, SessionId, SignedJob};
use sush_common::keys::{KeyError, KeyId, SshPublicKey};
use sush_common::targets::Cubbies;
use sush_common::targets::{Cubbies, SledVersion};
use sush_common::version::LONG_VERSION;

use crate::error::JobError;
use crate::executor::PathIsolation;
Expand Down Expand Up @@ -127,6 +129,7 @@ impl JobManager {
roots: &[Certificate],
shutdown: CancellationToken,
) -> Result<Self, JobError> {
info!(log, "starting sush"; "version" => LONG_VERSION);
let (tx_req, rx_req) = mpsc::channel(16);
let requests = ReceiverStream::new(rx_req);
let (rx_state, join_state) = StateManager::run(
Expand Down Expand Up @@ -156,6 +159,26 @@ impl JobManager {
&self.own_baseboard
}

/// Every sled known by cubby or by build, sorted by cubby first.
pub fn versions(&self) -> Vec<SledVersion> {
let state = self.state.borrow();
let mut sleds: BTreeSet<&BaseboardId> = state.versions().keys().collect();
sleds.extend(state.cubbies().values());
let mut rows: Vec<SledVersion> = sleds
.into_iter()
.map(|baseboard| SledVersion {
cubby: state
.cubbies()
.iter()
.find_map(|(cubby, b)| (b == baseboard).then_some(*cubby)),
baseboard: baseboard.clone(),
version: state.versions().get(baseboard).cloned(),
})
.collect();
rows.sort_by_key(|row| (row.cubby.is_none(), row.cubby));
rows
}

async fn cert_request(&self, authn: &Identity, request: CertRequest) -> Result<(), JobError> {
self.tx_req
.send(Request::cert(authn.key_id.clone(), request))
Expand Down
Loading