Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
01f98ba
Embed sush in sled-agent
plotnick Sep 6, 2026
552801f
Log the start of sush tasks
plotnick Sep 9, 2026
1b65c0f
Fix SushConfig::ramdisk_dir doc
plotnick Sep 9, 2026
7f810f4
Fix DDM subnet and cubby map docs
plotnick Sep 9, 2026
d44a838
Document poll_ddmd_for_peer_updates
plotnick Sep 9, 2026
3dc9c68
Use ByteCount for byte counts
plotnick Sep 9, 2026
ae91da0
Return an anyhow::Result from SushHandles::start_api
plotnick Sep 9, 2026
301e9d5
Kill useless GossipInputs doc
plotnick Sep 9, 2026
59f30ff
Wrap long log line
plotnick Sep 9, 2026
4e9c78e
Warn if the config reconciler task goes away
plotnick Sep 9, 2026
5a65227
fix locker link
plotnick Sep 9, 2026
3c7792c
Declaude
plotnick Sep 9, 2026
b1eefa9
better corpus closure
plotnick Sep 9, 2026
4c7ccb1
Fix up applied suggestions
plotnick Sep 9, 2026
ecaf96d
Improve sush module doc
plotnick Sep 9, 2026
6ef1b06
Update sush: `isolated` & `lonely` are now associated functions
plotnick Sep 9, 2026
e50003d
Improve REQUEST_MAX_BODY_BYTES doc
plotnick Sep 9, 2026
48ada88
Reject empty roots at config parsing time
plotnick Sep 9, 2026
e544dcd
Replace the cubby map each round instead of merging
plotnick Sep 9, 2026
898c661
Return a result from AvailableDatasetsReceiver::changed
plotnick Sep 9, 2026
7481505
TODONE: check default_ramdisk_dir
plotnick Sep 9, 2026
3179577
Update sush config doc
plotnick Sep 10, 2026
37e4597
More declauding
plotnick Sep 10, 2026
05b110c
Declaude poll_mgs_for_cubbies doc
plotnick Sep 10, 2026
a2b8a97
Declaude comment
plotnick Sep 10, 2026
b0eef7f
Declaude comment
plotnick Sep 10, 2026
f65febb
Clarify universe dominance rule
plotnick Sep 10, 2026
9e058e7
Point to RFD 620 for locker docs
plotnick Sep 10, 2026
2640954
Clarify encrypted output dir upgrade
plotnick Sep 10, 2026
32741d6
fmt
plotnick Sep 10, 2026
f62cd42
Don't panic!
plotnick Sep 10, 2026
e9ff322
TODO: cleanup poll_ddm_for_peer_updates
plotnick Sep 10, 2026
e3068d5
Merge remote-tracking branch 'github/main' into sush-sled-agent
plotnick Sep 10, 2026
5096283
Bump sush pin
plotnick Sep 10, 2026
c494933
Merge remote-tracking branch 'github/main' into sush-sled-agent
plotnick Sep 11, 2026
fe80b99
Merge branch 'main' into sush-sled-agent
plotnick Nov 4, 2026
e014b50
Choose sush trust anchors by platform identity
plotnick Nov 4, 2026
19d21a1
Trust the production and lab job signing keys
plotnick Nov 5, 2026
40d6e40
Listen only on the bootstrap network
plotnick Nov 6, 2026
f3d856a
Merge branch 'main' into sush-sled-agent
plotnick Nov 6, 2026
289dbec
Update sush to latest main
plotnick Nov 6, 2026
aed0181
Update sush to latest main
plotnick Nov 7, 2026
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
376 changes: 361 additions & 15 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -846,8 +846,8 @@ slog-term = "2.9.1"
smf = "0.2"
socket2 = { version = "0.5", features = ["all"] }
sp-sim = { path = "sp-sim" }
sprockets-tls = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "a233079e04d9688454486c452b694587a28c5257" }
sprockets-tls-test-utils = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "a233079e04d9688454486c452b694587a28c5257" }
sprockets-tls = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "98615b86d51f74ce220198eb16c57932fd24e7cb", default-features = false }
sprockets-tls-test-utils = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "98615b86d51f74ce220198eb16c57932fd24e7cb" }
sqlformat = "0.3.5"
sqlparser = { version = "0.61.0", features = [ "visitor" ] }
static_assertions = "1.1.0"
Expand All @@ -861,6 +861,9 @@ subtle = "2.6.1"
supports-color = "3.0.2"
support-bundle-collection = { path = "support-bundle-collection" }
support-bundle-viewer = "0.1.2"
sush-api = { git = "https://github.com/oxidecomputer/sush", rev = "2ceb9d92ba2d607018d044ce709cc49b94bf040b" }
sush-common = { git = "https://github.com/oxidecomputer/sush", rev = "2ceb9d92ba2d607018d044ce709cc49b94bf040b" }
sush-server = { git = "https://github.com/oxidecomputer/sush", rev = "2ceb9d92ba2d607018d044ce709cc49b94bf040b", features = ["embedded"] }
swrite = "0.1.0"
sync-ptr = "0.1.4"
libsw = { version = "3.5.0", features = ["tokio"] }
Expand Down Expand Up @@ -1160,20 +1163,25 @@ branch = "oxide/omicron"
[patch.crates-io.omicron-workspace-hack]
path = "workspace-hack"

# Several crates such as crucible and propolis have have a Git dependency on
# Several crates such as crucible and propolis have a Git dependency on
# this repo. Omicron itself depends on these crates, which can lead to two
# copies of these crates in the dependency graph. (As a Git dependency, and as
# a path dependency.) The goal of omicron-uuid-kinds and
# copies of these crates in the dependency graph. (As a Git dependency,
# and as a path dependency.) The goal of omicron-uuid-kinds and
# omicron-generation-kinds is to provide unified registries of UUID and
# generation kinds. Two copies of the same kinds floating around is unnecessary
# and painful.
#
# These directives ensure that whenever we see omicron-uuid-kinds or
# omicron-generation-kinds as a Git dependency, we'll use the path dependency
# version of the crate instead.
# Similarly, sush has a Git dependency on this repo's sled-hardware-types,
# and the BaseboardId in its API must be the same type sled-agent derives
# from HardwareManager::baseboard().
#
# These directives ensure that whenever we see omicron-uuid-kinds,
# omicron-generation-kinds, or sled-hardware-types as a Git dependency,
# we'll use the path dependency version of the crate instead.
#
# See also: uuid-kinds/README.adoc and generation-kinds/README.adoc.
[patch."https://github.com/oxidecomputer/omicron"]
omicron-common = { path = "common" }
omicron-generation-kinds = { path = "generation-kinds" }
omicron-uuid-kinds = { path = "uuid-kinds" }
sled-hardware-types = { path = "sled-hardware/types" }
25 changes: 25 additions & 0 deletions clients/ddm-admin-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use either::Either;
use omicron_common::address::BOOTSTRAP_PREFIX;
use omicron_common::address::BOOTSTRAP_SLED_SUBNET_PREFIX_LENGTH;
use omicron_common::address::DDMD_PORT;
use omicron_common::address::Ipv6Subnet;
use omicron_common::address::SLED_PREFIX_LENGTH;
use oxnet::Ipv6Net;
use sled_hardware_types::underlay::BootstrapInterface;
use slog::Logger;
Expand Down Expand Up @@ -103,6 +105,29 @@ impl Client {
self.inner.enable_stats(request).await.map(|resp| resp.into_inner())
}

/// Returns every non-bootstrap /64 prefix ddmd has learned. These
/// are not all sled subnets: an internal DNS zone is reachable at
/// its own reserved /64, advertised by the sled hosting it, and
/// RFD 63 reserves others.
pub async fn derive_underlay_subnets_from_prefixes(
&self,
) -> Result<
impl Iterator<Item = Ipv6Subnet<SLED_PREFIX_LENGTH>> + use<>,
DdmError,
> {
let prefixes = self.inner.get_prefixes().await?.into_inner();
Ok(prefixes.into_values().flatten().filter_map(|prefix| {
let addr = prefix.destination.addr();
if prefix.destination.width() == SLED_PREFIX_LENGTH
&& addr.segments()[0] != BOOTSTRAP_PREFIX
{
Some(Ipv6Subnet::new(addr))
} else {
None
}
}))
}

/// Returns the addresses of connected sleds.
///
/// Note: These sleds have not yet been verified.
Expand Down
2 changes: 2 additions & 0 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ pub const BOOTSTRAP_AGENT_RACK_INIT_PORT: u16 = 12346;
pub const BOOTSTORE_PORT: u16 = 12347;
pub const REPO_DEPOT_PORT: u16 = 12348;
pub const TRUST_QUORUM_PORT: u16 = 12349;
pub const SUSH_API_PORT: u16 = 12350;
pub const SUSH_GOSSIP_PORT: u16 = 12351;

pub const BOOTSTRAP_AGENT_LOCKSTEP_PORT: u16 = 8080;

Expand Down
2 changes: 1 addition & 1 deletion dev-tools/ls-apis/tests/api_dependencies.out
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Management Gateway Service (client: gateway-client)
consumed by: lldpd (lldp/lldpd) via 1 path
consumed by: mgd (maghemite/mgd) via 1 path
consumed by: omicron-nexus (omicron/nexus) via 6 paths
consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path
consumed by: omicron-sled-agent (omicron/sled-agent) via 2 paths
consumed by: wicketd (omicron/wicketd) via 3 paths

Wicketd Installinator (client: installinator-client)
Expand Down
7 changes: 6 additions & 1 deletion sled-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dropshot.workspace = true
flate2.workspace = true
flume.workspace = true
futures.workspace = true
gateway-client.workspace = true
gateway-types.workspace = true
glob.workspace = true
hex.workspace = true
http.workspace = true
Expand Down Expand Up @@ -106,8 +108,11 @@ slog-dtrace.workspace = true
slog-error-chain.workspace = true
slog-term.workspace = true
smf.workspace = true
sprockets-tls.workspace = true
sprockets-tls = { workspace = true, features = ["ipcc"] }
strum.workspace = true
sush-api.workspace = true
sush-common.workspace = true
sush-server.workspace = true
tar.workspace = true
thiserror.workspace = true
tofino.workspace = true
Expand Down
26 changes: 26 additions & 0 deletions sled-agent/config-reconciler/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use sled_storage::nested_dataset::NestedDatasetConfig;
use sled_storage::nested_dataset::NestedDatasetListOptions;
use sled_storage::nested_dataset::NestedDatasetLocation;
use slog::Logger;
use slog::warn;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::OnceLock;
Expand Down Expand Up @@ -645,6 +646,31 @@ impl AvailableDatasetsReceiver {
}
}

/// Wait until the set of available datasets may have changed.
///
/// Returns immediately if the set has changed since this receiver last
/// observed it. The test variants' datasets never change, so for them this
/// never returns.
pub async fn changed(&mut self, log: &Logger) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

fn changed(...) -> Result<..., ...>

instead? The caller can then decide what to do with the RecvError.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent suggestion, thank you; applied in 898c661.

match &mut self.inner {
AvailableDatasetsReceiverInner::Real(receiver) => {
if receiver.changed().await.is_err() {
warn!(
log,
"config reconciler task is gone; the set of available \
datasets will never change again",
);
std::future::pending().await
}
}
#[cfg(feature = "testing")]
AvailableDatasetsReceiverInner::FakeTempDir { .. }
| AvailableDatasetsReceiverInner::FakeStatic(_) => {
std::future::pending().await
}
}
}

pub fn all_mounted_debug_datasets(&self) -> Vec<PathInPool> {
match &self.inner {
AvailableDatasetsReceiverInner::Real(receiver) => {
Expand Down
20 changes: 19 additions & 1 deletion sled-agent/src/bootstrap/bootstore_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::server::StartError;
use bootstore::schemes::v0 as bootstore;
use camino::Utf8PathBuf;
use omicron_common::address::BOOTSTORE_PORT;
use omicron_common::address::SUSH_GOSSIP_PORT;
use omicron_common::address::TRUST_QUORUM_PORT;
use omicron_ddm_admin_client::Client as DdmAdminClient;
use sled_hardware_types::Baseboard;
Expand All @@ -22,6 +23,7 @@ use std::collections::BTreeSet;
use std::net::Ipv6Addr;
use std::net::SocketAddrV6;
use std::time::Duration;
use tokio::sync::watch;

const BOOTSTORE_FSM_STATE_FILE: &str = "bootstore-fsm-state.json";
const BOOTSTORE_NETWORK_CONFIG_FILE: &str = "bootstore-network-config.json";
Expand Down Expand Up @@ -75,10 +77,14 @@ fn bootstore_network_config_paths(
Ok(paths)
}

pub async fn poll_ddmd_for_bootstore_and_tq_peer_update(
/// Poll ddmd for the set of peer sleds on the bootstrap network, and
/// publish it to the three consumers that each need it at their own
/// port: the bootstore, the trust quorum node, and sush gossip.
Comment thread
plotnick marked this conversation as resolved.
Resolved
pub async fn poll_ddmd_for_peer_updates(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that this has a more generic name, perhaps it deserves a comment explaining what the "peer updates" it polls for (i.e. the bootstore data, trust quorum, and sush gossip proto)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, hmm...i kinda wonder if this really still belongs in a module called bootstore_setup, now that it does two other things that are not actually directly related to setting up the bootstore. arguably, that maybe should have been done when the TQ stuff was stuffed in here, though. and, maybe disentangling it from the surrounding code is more effort than it's really worth going to just because i complained about the module's name!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not disagree, but think it should be done as a follow-up in conjunction with someone that has (much) more sled agent experience than I do. Documented in d44a838, thanks.

log: Logger,
bootstore_node_handle: bootstore::NodeHandle,
trust_quorum_handle: trust_quorum::NodeTaskHandle,
sush_gossip_tx: watch::Sender<BTreeSet<SocketAddrV6>>,
) {
let mut current_bootstore_peers: BTreeSet<SocketAddrV6> = BTreeSet::new();
let mut current_tq_peers: BTreeSet<SocketAddrV6> = BTreeSet::new();
Expand Down Expand Up @@ -137,6 +143,18 @@ pub async fn poll_ddmd_for_bootstore_and_tq_peer_update(
return;
}
}
// Inform sush gossip of all known peer addresses.
let sush_peers: BTreeSet<_> = addrs
.iter()
.map(|ip| SocketAddrV6::new(*ip, SUSH_GOSSIP_PORT, 0, 0))
.collect();
sush_gossip_tx.send_if_modified(|current| {
let changed = *current != sush_peers;
if changed {
*current = sush_peers;
}
changed
});
Comment thread
hawkw marked this conversation as resolved.
Resolved
}
Err(err) => {
warn!(
Expand Down
97 changes: 97 additions & 0 deletions sled-agent/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,70 @@ pub struct Config {
/// root certificates and whether to use local certificate chain or
/// one over IPCC
pub sprockets: SprocketsConfig,

/// Settings for the Support Shell server (RFD 620). If this is absent, no
/// Support Shell server runs on this sled.
#[serde(default)]
pub sush: Option<SushConfig>,
}

/// Configuration for the Support Shell (`sush`) server that runs in the global
/// zone. See RFD 620.
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SushConfig {
/// PEM files holding the trusted root certificates for job requests,
/// one certificate per file. A job whose signature does not chain to
/// one of these is refused, so an empty list means no job could ever
/// run, and config parsing rejects it. To run a sled with no Support
/// Shell server, omit the `[sush]` section instead.
Comment thread
plotnick marked this conversation as resolved.
Resolved
#[serde(deserialize_with = "SushConfig::nonempty_roots")]
pub roots: Vec<Utf8PathBuf>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take it or leave it suggestion: alternatively, this could be

pub root: Utf8PathBuf,
pub additional_roots: Vec<Utf8PathBuf>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a nice way to make the bad case unrepresentable, but I think failing at parse time is good enough for now.


/// Where to record job output before an encrypted dataset is available.
/// Must be on a ramdisk so that it is never persisted unencrypted.
#[serde(default = "SushConfig::default_ramdisk_dir")]
pub ramdisk_dir: Utf8PathBuf,

/// Maximum output a single job may record before an encrypted dataset is
/// available. The default is small because the ramdisk is global zone
/// memory.
#[serde(default = "SushConfig::default_ramdisk_max_output_mb")]
pub ramdisk_max_output_mb: u32,

/// Maximum output a single job may record once an encrypted dataset is
/// available.
#[serde(default = "SushConfig::default_max_output_mb")]
pub max_output_mb: u32,
Comment thread
hawkw marked this conversation as resolved.
Resolved
}

impl SushConfig {
// TODO: check that this is the right default. `/var/run/oxide` is tmpfs
// and is created during bootstrap, but it exists to hold ZFS key files
// (see `illumos_utils::zfs::KEYPATH_ROOT`), not bulk job output.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this TODO deserve to become TO-done before this merges?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I am hoping this very review will settle the question!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What properties are you looking for? That the path will exist and be mounted and be a tmpfs? I think that's done by the filesystem/minimal svc:

https://github.com/oxidecomputer/illumos-gate/blob/0a8e320ff3c6ef999a764d0d07f26c92f6205ccf/usr/src/cmd/svc/milestone/fs-minimal#L66-L69

which our images are built with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much, that is exactly what I was looking for! Noted in 7481505.

fn default_ramdisk_dir() -> Utf8PathBuf {
"/var/run/oxide/sush".into()
}

fn default_ramdisk_max_output_mb() -> u32 {
64
}

fn default_max_output_mb() -> u32 {
10 * 1024
}

fn nonempty_roots<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<Utf8PathBuf>, D::Error> {
let roots = Vec::deserialize(deserializer)?;
if roots.is_empty() {
return Err(serde::de::Error::custom(
"sush needs at least one root certificate to run",
));
}
Ok(roots)
}
}

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -210,4 +274,37 @@ mod test {
}
assert!(configs_seen > 0, "No sled-agent configs found");
}

#[test]
fn test_sush_config() {
// Only `roots` is required. The rest have defaults.
let sush: SushConfig =
toml::from_str(r#"roots = ["/pkg/sush-root.pem"]"#).unwrap();
assert_eq!(sush.roots, vec![Utf8PathBuf::from("/pkg/sush-root.pem")]);
assert_eq!(sush.ramdisk_dir, SushConfig::default_ramdisk_dir());
assert_eq!(
sush.ramdisk_max_output_mb,
SushConfig::default_ramdisk_max_output_mb()
);
assert_eq!(sush.max_output_mb, SushConfig::default_max_output_mb());

let sush: SushConfig = toml::from_str(
r#"
roots = ["/pkg/sush-root.pem"]
ramdisk_dir = "/var/run/sush"
ramdisk_max_output_mb = 8
max_output_mb = 128
"#,
)
.unwrap();
assert_eq!(sush.ramdisk_dir, "/var/run/sush");
assert_eq!(sush.ramdisk_max_output_mb, 8);
assert_eq!(sush.max_output_mb, 128);

// No roots would mean no jobs can ever run, so reject at parse time.
let err = toml::from_str::<SushConfig>("roots = []").unwrap_err();
assert!(
err.to_string().contains("needs at least one root certificate")
);
}
}
1 change: 1 addition & 0 deletions sled-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub mod server;
pub mod services;
mod sled_agent;
mod support_bundle;
pub mod sush;
mod swap_device;
mod vmm_reservoir;
mod zone_bundle;
Expand Down
Loading
Loading