Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8a1fe6e
sled-hardware: find disks through the hardware topology
smklein Oct 23, 2026
ee90985
merge
smklein Oct 23, 2026
6eb6812
Drop incomplete_disks; installinator scans M.2s best-effort, as on main
smklein Oct 28, 2026
3d37f71
Make topology read errors per bay where the bay is known
smklein Oct 28, 2026
6d3fce2
Carry the disk bay read error to poll_device_tree as a Result
smklein Oct 28, 2026
46739e3
Drop a call-site comment that repeats read_bays' docs
smklein Oct 28, 2026
6649ba9
merge
smklein Oct 29, 2026
5d2d2ca
Take one topology snapshot per hardware poll
smklein Oct 28, 2026
df74bfe
Report unreadable disk bays instead of their previous state
smklein Oct 29, 2026
a43217e
Leave the bay location out of its classify errors
smklein Oct 30, 2026
486eb07
Report bays missing from a hardware poll as unreadable
smklein Oct 30, 2026
09c2ecc
merge
smklein Nov 2, 2026
4235d82
topo: don't describe a fixed bay count in module docs
smklein Nov 2, 2026
f06884a
topo: drop sharkfin example from NodeKey docs
smklein Nov 2, 2026
08858aa
sled-hardware: type disk locations and key disk bays by them
smklein Nov 3, 2026
a74fbc3
sled-hardware: report why a bay's device is unusable
smklein Nov 3, 2026
3705a36
Explain why only one NVMe namespace becomes a disk
smklein Nov 3, 2026
9cbad45
Treat an NVMe controller with more than one namespace as unusable
smklein Nov 4, 2026
83fe780
sled-hardware: assemble disk bays from topology records in portable code
smklein Nov 4, 2026
2e84c20
sled-hardware: decide which topology slots are M.2 bays in portable code
smklein Nov 5, 2026
48d693b
sled-hardware: document how oxhc lays out disk bays
smklein Nov 5, 2026
9b52334
sled-hardware: take slot types from libtopo 0.2.1
smklein Nov 5, 2026
9f5fddf
sled-hardware: move topology node records into topo_nodes
smklein Nov 5, 2026
d0533bf
sled-hardware: rename classify_bay to describe_bay
smklein Nov 5, 2026
5ff0ef1
sled-hardware: reword disk_bay module and resolve_bays docs
smklein Nov 5, 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
7 changes: 7 additions & 0 deletions installinator/src/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::ensure;
use sled_agent_types::disk::DiskVariant;
use sled_hardware::ExternalDisks;
Expand Down Expand Up @@ -33,6 +34,12 @@ impl Hardware {
)
.map_err(|err| anyhow!("failed to create HardwareManager: {err}"))?;

// Installing to only the M.2s that happened to be readable would
// leave the other with stale contents.
if let Some(reason) = hardware.incomplete_disks() {
bail!("hardware view of disks is incomplete: {reason}");
}
Comment thread
smklein marked this conversation as resolved.
Resolved
Comment thread
smklein marked this conversation as resolved.
Resolved

let disks: Vec<RawDisk> =
hardware.disks().into_values().map(|disk| disk.into()).collect();

Expand Down
48 changes: 22 additions & 26 deletions sled-agent/config-reconciler/src/disks_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ pub(crate) fn update_properties_from_raw_disk(
}

// The only properties we expect to change are the firmware metadata and
// the chassis location (which may be learned after the disk was adopted).
// Update those and check again; if they're still not equal, something
// weird is going on. At least log a warning.
// the chassis location, which is re-read from the hardware topology on
// every poll. Update those and check again; if they're still not equal,
// something weird is going on. At least log a warning.
let mut disk = disk.clone();
disk.update_mutable_properties(raw_disk);
if *raw_disk == RawDisk::from(disk.clone()) {
Expand Down Expand Up @@ -68,14 +68,12 @@ mod tests {
use sled_hardware::PooledDisk;
use sled_hardware::UnparsedDisk;

// A disk adopted before its chassis location was known must pick up the
// location once the raw disk reports it, and be considered unchanged
// afterwards.
// A change to a disk's chassis location reported by the hardware
// topology must be copied onto the adopted disk, which is then
// considered unchanged.
#[test]
fn location_learned_after_adoption_is_propagated() {
let logctx = dev::test_setup_log(
"location_learned_after_adoption_is_propagated",
);
fn location_change_is_propagated() {
let logctx = dev::test_setup_log("location_change_is_propagated");

let identity = DiskIdentity {
vendor: "test".into(),
Expand All @@ -91,32 +89,30 @@ mod tests {
pcie_slot: 0,
identity: identity.clone(),
is_boot_disk: false,
location: None,
location: "N2".to_string(),
partitions: vec![],
zpool_name: ZpoolName::External(ExternalZpoolUuid::new_v4()),
firmware: firmware.clone(),
});

// The raw disk matches what was adopted, except that it now carries a
// chassis location.
let raw_disk = RawDisk::Real(
UnparsedDisk::new(
"/test-devfs".into(),
None,
0,
DiskVariant::U2,
identity,
false,
firmware,
)
.with_location(Some("N3".to_string())),
);
// The raw disk matches what was adopted, except for its chassis
// location.
let raw_disk = RawDisk::Real(UnparsedDisk::new(
"/test-devfs".into(),
None,
0,
DiskVariant::U2,
identity,
false,
firmware,
"N3".to_string(),
));

let updated = assert_matches!(
update_properties_from_raw_disk(&adopted, &raw_disk, &logctx.log),
MaybeUpdatedDisk::Updated(disk) => disk
);
assert_eq!(updated.location(), Some("N3"));
assert_eq!(updated.location(), "N3");
assert_eq!(RawDisk::from(updated.clone()), raw_disk);

// A second pass sees nothing left to update.
Expand Down
3 changes: 2 additions & 1 deletion sled-agent/config-reconciler/src/internal_disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,7 @@ mod tests {
pcie_slot: raw_disk.pcie_slot(),
identity: raw_disk.identity().clone(),
is_boot_disk: raw_disk.is_boot_disk(),
location: raw_disk.location().map(str::to_string),
location: raw_disk.location().to_string(),
partitions: vec![],
zpool_name: ZpoolName::Internal(InternalZpoolUuid::new_v4()),
firmware: raw_disk.firmware().clone(),
Expand Down Expand Up @@ -1026,6 +1026,7 @@ mod tests {
},
false,
DiskFirmware::new(0, None, false, 1, vec![]),
sled_storage::disk::synthetic_disk_location(variant, 0),
))
}

Expand Down
6 changes: 4 additions & 2 deletions sled-agent/config-reconciler/src/raw_disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ impl RawDisksSender {
identity: disk.identity().clone(),
variant: disk.variant(),
pcie_slot: disk.pcie_slot(),
location: disk.location().map(str::to_string),
location: Some(disk.location().to_string()),
active_firmware_slot: firmware.active_slot(),
next_active_firmware_slot: firmware.next_active_slot(),
number_of_firmware_slots: firmware.number_of_slots(),
Expand Down Expand Up @@ -191,6 +191,7 @@ mod tests {
let variant = DiskVariant::U2;
let firmware =
DiskFirmware::new(0, None, false, 255, Vec::new());
let location = "N0".to_string();

match kind {
ArbitraryDiskKind::Real => {
Expand All @@ -202,6 +203,7 @@ mod tests {
identity,
is_boot_disk,
firmware,
location,
))
}
ArbitraryDiskKind::Synthetic => {
Expand All @@ -211,7 +213,7 @@ mod tests {
variant,
pcie_slot: slot,
firmware,
location: "N0".to_string(),
location,
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ mod tests {
pcie_slot: raw_disk.pcie_slot(),
identity: raw_disk.identity().clone(),
is_boot_disk: raw_disk.is_boot_disk(),
location: raw_disk.location().map(str::to_string),
location: raw_disk.location().to_string(),
partitions: vec![],
zpool_name: ZpoolName::new_external(pool_id),
firmware: raw_disk.firmware().clone(),
Expand Down Expand Up @@ -840,6 +840,7 @@ mod tests {
},
false,
DiskFirmware::new(0, None, false, 1, vec![]),
sled_storage::disk::synthetic_disk_location(variant, 0),
))
}

Expand Down
43 changes: 20 additions & 23 deletions sled-hardware/src/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,11 @@ pub struct UnparsedDisk {
identity: DiskIdentity,
is_boot_disk: bool,
firmware: DiskFirmware,
#[serde(default)]
location: Option<String>,
location: String,
}

impl UnparsedDisk {
#[expect(clippy::too_many_arguments)]
pub fn new(
devfs_path: Utf8PathBuf,
dev_path: Option<Utf8PathBuf>,
Expand All @@ -230,6 +230,7 @@ impl UnparsedDisk {
identity: DiskIdentity,
is_boot_disk: bool,
firmware: DiskFirmware,
location: String,
) -> Self {
Self {
paths: DiskPaths { devfs_path, dev_path },
Expand All @@ -238,16 +239,10 @@ impl UnparsedDisk {
identity,
is_boot_disk,
firmware,
location: None,
location,
}
}

/// Records where this disk sits in the chassis. See [`Self::location`].
pub fn with_location(mut self, location: Option<String>) -> Self {
self.location = location;
self
}

pub fn paths(&self) -> &DiskPaths {
&self.paths
}
Expand All @@ -268,16 +263,19 @@ impl UnparsedDisk {
self.is_boot_disk
}

/// The PCIe physical slot number of the bridge above this disk.
/// The PCIe physical slot number of the bridge behind this disk's bay.
///
/// This is the `physical-slot#` property of the parent `pcieb` device.
/// It identifies the disk's position in the board's PCIe topology and is
/// board-specific: the same U.2 bay has a different number on Gimlet and
/// Cosmo. It is not the location label printed on the chassis.
/// This is the `binding/slot` property of the bay in the hardware
/// topology, the same number the `pcieb` device reports as
/// `physical-slot#`. It identifies the bay's position in the board's PCIe
/// topology and is board-specific: the same U.2 bay has a different
/// number on Gimlet and Cosmo. It is not the location label printed on
/// the chassis.
///
/// Although **thus far** it has been stable, this value may also change
/// in subsequent host OS versions. For stability, the value of "location"
/// should be preferred.
/// in subsequent host OS versions. It is kept as the key inventory uses
/// for a disk and to join a disk to its firmware. For anything else,
/// prefer [`Self::location`].
pub fn pcie_slot(&self) -> i64 {
self.pcie_slot
}
Expand All @@ -286,12 +284,11 @@ impl UnparsedDisk {
/// hardware topology: "N5" for a U.2 bay, "M.2 East" for a boot device.
///
/// This is the operator-facing position, matching what is printed on the
/// sled, and is the value to show someone who has to find the drive. It
/// comes from libtopo and is best-effort: `None` means the topology had
/// no label for the disk or could not be read. A missing location never
/// prevents the disk from being used.
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
/// sled, and is the value to show someone who has to find the drive. The
/// topology is where sled-hardware finds disks in the first place, so
/// every disk has one.
pub fn location(&self) -> &str {
&self.location
}

pub fn firmware(&self) -> &DiskFirmware {
Expand Down Expand Up @@ -326,7 +323,7 @@ pub struct PooledDisk {
pub zpool_name: ZpoolName,
pub firmware: DiskFirmware,
/// See [`UnparsedDisk::location`].
pub location: Option<String>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In summary, this is the "goal" of this PR. The location information we're reading from libtopo should be non-optional.

pub location: String,
}

impl PooledDisk {
Expand Down
Loading
Loading