@@ -131,11 +131,6 @@ pub struct ManagedHostStateSnapshot {
131131 pub host_snapshot : Machine ,
132132 pub dpu_snapshots : Vec < Machine > ,
133133 pub dpa_interface_snapshots : Vec < DpaInterface > ,
134- /// The host's boot interface (MAC + Redfish interface id), captured by
135- /// site-explorer. Populated at read time by `load_object_state` (like
136- /// `dpa_interface_snapshots`); `None` until the host has been explored with
137- /// a fully-resolved boot interface.
138- pub boot_interface : Option < MachineBootInterface > ,
139134 /// If there is an instance provisioned on top of the machine, this holds
140135 /// its state
141136 pub instance : Option < InstanceSnapshot > ,
@@ -213,8 +208,6 @@ impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for ManagedHostStateSnapshot {
213208 host_snapshot,
214209 dpu_snapshots,
215210 dpa_interface_snapshots,
216- // Filled by load_object_state later (see dpa_interface_snapshots).
217- boot_interface : None ,
218211 managed_state,
219212 instance,
220213 rack_health_overrides,
@@ -280,19 +273,38 @@ impl From<ManagedHostStateSnapshotError> for sqlx::Error {
280273/// so it's on the caller to figure out. What this usually means is the
281274/// caller passes `boot_interface_mac: None` to machine_setup, and then
282275/// subsequent logic flows from there (e.g. ::NoDpu handling).
283- fn pick_boot_interface_mac (
276+ fn pick_boot_interface (
284277 interfaces : & [ MachineInterfaceSnapshot ] ,
285- ) -> Option < mac_address :: MacAddress > {
278+ ) -> Option < & MachineInterfaceSnapshot > {
286279 // The primary wins!
287280 if let Some ( primary) = interfaces. iter ( ) . find ( |x| x. primary_interface ) {
288- return Some ( primary. mac_address ) ;
281+ return Some ( primary) ;
289282 }
290283 // ..no primary, so lets try to find *some* interface.
291284 interfaces
292285 . iter ( )
293286 . filter ( |x| x. network_segment_type != Some ( NetworkSegmentType :: Underlay ) )
294287 . min_by_key ( |x| x. mac_address )
295- . map ( |x| x. mac_address )
288+ }
289+
290+ fn pick_boot_interface_mac (
291+ interfaces : & [ MachineInterfaceSnapshot ] ,
292+ ) -> Option < mac_address:: MacAddress > {
293+ pick_boot_interface ( interfaces) . map ( |x| x. mac_address )
294+ }
295+
296+ /// Resolves the boot interface to a fully-populated [`MachineBootInterface`]
297+ /// (MAC + Redfish interface id) from the picked interface's own row. Split out
298+ /// like `pick_boot_interface_mac` so it's unit-testable without a full snapshot.
299+ fn pick_boot_interface_pair (
300+ interfaces : & [ MachineInterfaceSnapshot ] ,
301+ ) -> Option < MachineBootInterface > {
302+ pick_boot_interface ( interfaces) . and_then ( |interface| {
303+ MachineBootInterface :: from_parts (
304+ Some ( interface. mac_address ) ,
305+ interface. boot_interface_id . clone ( ) ,
306+ )
307+ } )
296308}
297309
298310impl ManagedHostStateSnapshot {
@@ -365,6 +377,19 @@ impl ManagedHostStateSnapshot {
365377 pick_boot_interface_mac ( & self . host_snapshot . interfaces )
366378 }
367379
380+ /// Returns the host's boot interface as a fully-populated
381+ /// [`MachineBootInterface`] (MAC + Redfish interface id), derived from the
382+ /// same primary `machine_interface` row that [`Self::boot_interface_mac`]
383+ /// selects.
384+ ///
385+ /// Returns `None` when that row hasn't captured a Redfish interface id yet
386+ /// (e.g. not yet explored, or a zero-DPU host) -- callers then target the MAC
387+ /// alone. Because the MAC and id come from one row, the pair can never name a
388+ /// different interface than `boot_interface_mac`.
389+ pub fn boot_interface ( & self ) -> Option < MachineBootInterface > {
390+ pick_boot_interface_pair ( & self . host_snapshot . interfaces )
391+ }
392+
368393 /// Returns `true` if override report is hw_health, `false` otherwise.
369394 fn merge_override_report_with_hw_health (
370395 output : & mut HealthReport ,
@@ -2321,6 +2346,11 @@ pub struct MachineInterfaceSnapshot {
23212346 pub interface_type : InterfaceType ,
23222347 pub primary_interface : bool ,
23232348 pub mac_address : MacAddress ,
2349+ /// Vendor-native Redfish `EthernetInterface.Id` for this interface, captured
2350+ /// by site-explorer alongside the MAC. Combined with `mac_address` it forms a
2351+ /// [`MachineBootInterface`]; for the `primary_interface` row that pair is the
2352+ /// host's boot device.
2353+ pub boot_interface_id : Option < String > ,
23242354 pub attached_dpu_machine_id : Option < MachineId > ,
23252355 pub domain_id : Option < DomainId > ,
23262356 pub machine_id : Option < MachineId > ,
@@ -2345,6 +2375,7 @@ impl MachineInterfaceSnapshot {
23452375 machine_id : None ,
23462376 segment_id : uuid:: Uuid :: nil ( ) . into ( ) ,
23472377 mac_address,
2378+ boot_interface_id : None ,
23482379 hostname : String :: new ( ) ,
23492380 interface_type : InterfaceType :: Data ,
23502381 primary_interface : true ,
@@ -2643,6 +2674,7 @@ impl<'r> FromRow<'r, PgRow> for MachineInterfaceSnapshot {
26432674 hostname : row. try_get ( "hostname" ) ?,
26442675 interface_type : row. try_get ( "interface_type" ) ?,
26452676 mac_address : row. try_get ( "mac_address" ) ?,
2677+ boot_interface_id : row. try_get ( "boot_interface_id" ) ?,
26462678 primary_interface : row. try_get ( "primary_interface" ) ?,
26472679 created : row. try_get ( "created" ) ?,
26482680 last_dhcp : row. try_get ( "last_dhcp" ) ?,
@@ -3320,6 +3352,38 @@ mod tests {
33203352 ) ;
33213353 }
33223354
3355+ // boot_interface() derives the full pair from the SAME primary row that the
3356+ // MAC selection uses, so the MAC and id can never name different interfaces.
3357+ #[ test]
3358+ fn pick_boot_interface_pair_uses_primary_rows_mac_and_id ( ) {
3359+ let other = build_mock_interface (
3360+ "05:00:00:00:00:01" ,
3361+ false ,
3362+ Some ( NetworkSegmentType :: HostInband ) ,
3363+ ) ;
3364+ let primary = MachineInterfaceSnapshot {
3365+ boot_interface_id : Some ( "NIC.Slot.7-1-1" . to_string ( ) ) ,
3366+ ..build_mock_interface ( "10:00:00:00:00:01" , true , Some ( NetworkSegmentType :: Admin ) )
3367+ } ;
3368+
3369+ assert_eq ! (
3370+ pick_boot_interface_pair( & [ other, primary] ) ,
3371+ Some ( MachineBootInterface {
3372+ mac_address: "10:00:00:00:00:01" . parse( ) . unwrap( ) ,
3373+ interface_id: "NIC.Slot.7-1-1" . to_string( ) ,
3374+ } )
3375+ ) ;
3376+ }
3377+
3378+ // When the primary row hasn't captured a Redfish interface id yet, there's no
3379+ // complete pair -- callers fall back to the MAC alone.
3380+ #[ test]
3381+ fn pick_boot_interface_pair_is_none_without_captured_id ( ) {
3382+ let primary =
3383+ build_mock_interface ( "10:00:00:00:00:01" , true , Some ( NetworkSegmentType :: Admin ) ) ;
3384+ assert_eq ! ( pick_boot_interface_pair( & [ primary] ) , None ) ;
3385+ }
3386+
33233387 // Check the case where only the BMC has been discovered so far (which
33243388 // is common during early ingestion). In this case, there's no valid boot MAC
33253389 // yet; callers fall back to the `::NoDpu` handling downstream.
0 commit comments