diff --git a/crates/admin-cli/src/expected_machines/add/args.rs b/crates/admin-cli/src/expected_machines/add/args.rs index d85bffc1a7..6036242410 100644 --- a/crates/admin-cli/src/expected_machines/add/args.rs +++ b/crates/admin-cli/src/expected_machines/add/args.rs @@ -21,7 +21,7 @@ use carbide_utils::has_duplicates; use carbide_uuid::rack::RackId; use clap::Parser; use mac_address::MacAddress; -use rpc::forge::{BmcIpAllocationType, DpuMode, ExpectedHostNic}; +use rpc::forge::{BmcIpAllocationType, ExpectedHostNic, HostDpuPolicy}; use serde::{Deserialize, Serialize}; use crate::errors::{CarbideCliError, CarbideCliResult}; @@ -49,7 +49,7 @@ Pre-allocate a static BMC IP (site-explorer path, like expected switches): Add a host whose DPU should be treated as a plain NIC: $ nico-admin-cli expected-machine add --bmc-mac-address 00:11:22:33:44:55 \ --bmc-username admin --bmc-password mypassword --chassis-serial-number sample_serial-1 \ - --dpu-mode nic-mode + --dpu-policy use-as-nic Retain the BMC's auto-allocated DHCP address as a static one (never expires): $ nico-admin-cli expected-machine add --bmc-mac-address 00:11:22:33:44:55 \ @@ -165,12 +165,13 @@ pub struct Args { pub bmc_retain_credentials: Option, #[clap( - long = "dpu-mode", - value_name = "DPU_MODE", + long = "dpu-policy", + visible_alias = "dpu-mode", + value_name = "DPU_POLICY", value_enum, - help = "Per-host DPU operating mode. `dpu-mode` (default): DPUs are managed by NICo; `nic-mode`: DPU hardware present but treated as a plain NIC; `no-dpu`: no DPU hardware at all. Unset defers to the site-wide `[site_explorer] dpu_mode` setting (which itself falls back to `dpu-mode` when not set)." + help = "Per-host DPU policy. `manage` (default): inherit the site policy, which defaults to managing DPUs; `use-as-nic`: configure DPU hardware as plain NICs; `ignore`: do not configure or attach DPU hardware. Unset defers to the site-wide `[site_explorer] dpu_policy` setting. The legacy `--dpu-mode` flag and values remain accepted." )] - pub dpu_mode: Option, + pub dpu_policy: Option, #[clap( long = "bmc-ip-allocation", @@ -229,7 +230,7 @@ impl TryFrom for rpc::forge::ExpectedMachine { is_dpf_enabled: value.dpf_enabled, bmc_ip_address: value.bmc_ip_address.map(|ip| ip.to_string()), bmc_retain_credentials: value.bmc_retain_credentials, - dpu_mode: value.dpu_mode.map(|m| m as i32), + dpu_mode: value.dpu_policy.map(|policy| policy as i32), bmc_ip_allocation: value.bmc_ip_allocation.map(|m| m as i32), host_lifecycle_profile: value.disable_lockdown.map(|dl| { rpc::forge::HostLifecycleProfile { diff --git a/crates/admin-cli/src/expected_machines/common.rs b/crates/admin-cli/src/expected_machines/common.rs index 2b5055820a..ffa03643ee 100644 --- a/crates/admin-cli/src/expected_machines/common.rs +++ b/crates/admin-cli/src/expected_machines/common.rs @@ -45,11 +45,11 @@ pub struct ExpectedMachineJson { pub bmc_ip_address: Option, #[serde(default)] pub bmc_retain_credentials: Option, - /// Per-host DPU operating mode. None == defer to the site-wide - /// `[site_explorer] dpu_mode` setting (falls back to `DpuMode::DpuMode` - /// if that's also unset). - #[serde(default)] - pub dpu_mode: Option, + /// Per-host DPU policy. None == defer to the site-wide + /// `[site_explorer] dpu_policy` setting (falls back to `Manage` if that's + /// also unset). The legacy `dpu_mode` field and values remain accepted. + #[serde(default, alias = "dpu_mode")] + pub dpu_policy: Option, /// Per-host control over how this BMC's IP is assigned and retained. None == /// the server default (`Auto`), which resolves to `fixed` when a /// `bmc_ip_address` is set and `retained` when it isn't. @@ -75,3 +75,49 @@ pub struct _ExpectedMachineMetadata { pub description: Option, pub labels: HashMap>, } + +#[cfg(test)] +mod tests { + use carbide_test_support::Outcome::*; + use carbide_test_support::scenarios; + + use super::*; + + #[test] + fn expected_machine_json_deserializes_new_and_legacy_dpu_policy() { + scenarios!( + run = |policy_json| { + let json = format!( + r#"{{ + "bmc_mac_address": "AA:BB:CC:DD:EE:FF", + "bmc_username": "root", + "bmc_password": "pass", + "chassis_serial_number": "SN-1" + {policy_json} + }}"#, + ); + serde_json::from_str::(&json) + .map(|machine| machine.dpu_policy) + .map_err(drop) + }; + "policy omitted" { + "" => Yields(None), + } + + "canonical field and value" { + r#", "dpu_policy": "use_as_nic""# => + Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), + } + + "legacy field and config value" { + r#", "dpu_mode": "nic_mode""# => + Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), + } + + "legacy field and generated Rust value" { + r#", "dpu_mode": "NicMode""# => + Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), + } + ); + } +} diff --git a/crates/admin-cli/src/expected_machines/patch/args.rs b/crates/admin-cli/src/expected_machines/patch/args.rs index e97272fbe4..5beecab60b 100644 --- a/crates/admin-cli/src/expected_machines/patch/args.rs +++ b/crates/admin-cli/src/expected_machines/patch/args.rs @@ -19,7 +19,7 @@ use carbide_utils::has_duplicates; use carbide_uuid::rack::RackId; use clap::{ArgGroup, Parser}; use mac_address::MacAddress; -use rpc::forge::{BmcIpAllocationType, DpuMode}; +use rpc::forge::{BmcIpAllocationType, HostDpuPolicy}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -47,7 +47,7 @@ use crate::errors::CarbideCliError; "fallback_dpu_serial_numbers", "sku_id", "bmc_ip_address", -"dpu_mode", +"dpu_policy", "bmc_ip_allocation", "dpf_enabled", "host_nics", @@ -67,9 +67,9 @@ Rotate the BMC credentials (username and password must be set together): $ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \ --bmc-username admin --bmc-password mynewpassword -Change the per-host DPU mode: +Change the per-host DPU policy: $ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \ - --dpu-mode no-dpu + --dpu-policy ignore Retain the BMC's auto-allocated DHCP address as a static one (never expires): $ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \ @@ -185,13 +185,14 @@ pub struct Args { pub bmc_retain_credentials: Option, #[clap( - long = "dpu-mode", - value_name = "DPU_MODE", + long = "dpu-policy", + visible_alias = "dpu-mode", + value_name = "DPU_POLICY", value_enum, group = "group", - help = "Per-host DPU operating mode. `dpu-mode` (default): DPUs are managed by NICo; `nic-mode`: DPU hardware present but treated as a plain NIC; `no-dpu`: no DPU hardware at all. Unset preserves the existing per-host value." + help = "Per-host DPU policy. `manage`: inherit the site policy, which defaults to managing DPUs; `use-as-nic`: configure DPU hardware as plain NICs; `ignore`: do not configure or attach DPU hardware. Unset preserves the existing per-host value. The legacy `--dpu-mode` flag and values remain accepted." )] - pub dpu_mode: Option, + pub dpu_policy: Option, #[clap( long = "bmc-ip-allocation", @@ -241,11 +242,11 @@ impl Args { && self.rack_id.is_none() && self.dpf_enabled.is_none() && self.bmc_ip_address.is_none() - && self.dpu_mode.is_none() + && self.dpu_policy.is_none() && self.bmc_ip_allocation.is_none() && self.host_nics.is_none() { - return Err(CarbideCliError::GenericError("One of the following options must be specified: bmc-username and bmc-password or chassis-serial-number or fallback-dpu-serial-number or bmc-ip-address or dpu-mode or bmc-ip-allocation or dpf-enabled or host_nics".to_string())); + return Err(CarbideCliError::GenericError("One of the following options must be specified: bmc-username and bmc-password or chassis-serial-number or fallback-dpu-serial-number or bmc-ip-address or dpu-policy or bmc-ip-allocation or dpf-enabled or host_nics".to_string())); } if self .fallback_dpu_serial_numbers diff --git a/crates/admin-cli/src/expected_machines/patch/mod.rs b/crates/admin-cli/src/expected_machines/patch/mod.rs index 6dd47b671e..3066176c5f 100644 --- a/crates/admin-cli/src/expected_machines/patch/mod.rs +++ b/crates/admin-cli/src/expected_machines/patch/mod.rs @@ -50,7 +50,7 @@ impl Run for Args { self.dpf_enabled, self.bmc_ip_address, self.bmc_retain_credentials, - self.dpu_mode, + self.dpu_policy, self.bmc_ip_allocation, self.disable_lockdown .map(|dl| ::rpc::forge::HostLifecycleProfile { diff --git a/crates/admin-cli/src/expected_machines/show/cmd.rs b/crates/admin-cli/src/expected_machines/show/cmd.rs index e07046719e..fcd4ae5a9e 100644 --- a/crates/admin-cli/src/expected_machines/show/cmd.rs +++ b/crates/admin-cli/src/expected_machines/show/cmd.rs @@ -135,7 +135,7 @@ async fn convert_and_print_into_nice_table( "Pause On Ingestion", "DPF Enabled", "Disable Lockdown", - "DPU Mode", + "DPU Policy", ]); for expected_machine in &expected_machines.expected_machines { @@ -157,16 +157,7 @@ async fn convert_and_print_into_nice_table( let labels = crate::metadata::fmt_labels_as_kv_pairs(expected_machine.metadata.as_ref()); - // None on the wire == the DB default (`DpuMode`); fall back to that - // rather than `Unspecified` so `to_possible_value()` produces the - // same kebab-case string the `--dpu-mode` CLI flag accepts. - let dpu_mode_display = expected_machine - .dpu_mode - .and_then(|i| ::rpc::forge::DpuMode::try_from(i).ok()) - .unwrap_or(::rpc::forge::DpuMode::DpuMode) - .to_possible_value() - .map(|pv| pv.get_name().to_owned()) - .unwrap_or_default(); + let dpu_policy_display = dpu_policy_display(expected_machine.dpu_mode); table.add_row(row![ expected_machine.chassis_serial_number, @@ -200,7 +191,7 @@ async fn convert_and_print_into_nice_table( .and_then(|hlp| hlp.disable_lockdown) .unwrap_or_default() .to_string(), - dpu_mode_display, + dpu_policy_display, ]); } @@ -208,3 +199,53 @@ async fn convert_and_print_into_nice_table( Ok(()) } + +/// Formats the wire-compatible `DpuMode` field using the canonical host-policy +/// vocabulary. Both an absent value and the protobuf sentinel mean `Manage`. +fn dpu_policy_display(dpu_mode: Option) -> String { + dpu_mode + .and_then(|value| ::rpc::forge::HostDpuPolicy::try_from(value).ok()) + .filter(|policy| *policy != ::rpc::forge::HostDpuPolicy::Unspecified) + .unwrap_or(::rpc::forge::HostDpuPolicy::Manage) + .to_possible_value() + .map(|value| value.get_name().to_owned()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use carbide_test_support::value_scenarios; + use rpc::forge::HostDpuPolicy; + + use super::dpu_policy_display; + + #[test] + fn dpu_policy_display_uses_canonical_policy_vocabulary() { + value_scenarios!( + run = dpu_policy_display; + "missing value defaults to manage" { + None => "manage".to_string(), + } + + "protobuf sentinel defaults to manage" { + Some(HostDpuPolicy::Unspecified as i32) => "manage".to_string(), + } + + "manage" { + Some(HostDpuPolicy::Manage as i32) => "manage".to_string(), + } + + "use as NIC" { + Some(HostDpuPolicy::UseAsNic as i32) => "use-as-nic".to_string(), + } + + "ignore" { + Some(HostDpuPolicy::Ignore as i32) => "ignore".to_string(), + } + + "unknown value defaults to manage" { + Some(i32::MAX) => "manage".to_string(), + } + ); + } +} diff --git a/crates/admin-cli/src/expected_machines/tests.rs b/crates/admin-cli/src/expected_machines/tests.rs index ca052dc387..2faa65cbde 100644 --- a/crates/admin-cli/src/expected_machines/tests.rs +++ b/crates/admin-cli/src/expected_machines/tests.rs @@ -471,12 +471,10 @@ fn validate_patch_all_fields() { } } -// parse_add_without_dpu_mode ensures the flag is optional and defaults to -// unset; downstream, unset is treated as "defer to the site-wide -// `[site_explorer] dpu_mode` setting" (which itself falls back to -// `DpuMode::DpuMode` when not set). +// The DPU policy flag is optional. Downstream, unset defers to the site-wide +// `[site_explorer] dpu_policy` setting and ultimately defaults to `Manage`. #[test] -fn parse_add_without_dpu_mode() { +fn parse_add_without_dpu_policy() { let cmd = Cmd::try_parse_from([ "expected-machine", "add", @@ -489,33 +487,31 @@ fn parse_add_without_dpu_mode() { "--chassis-serial-number", "SN12345", ]) - .expect("should parse without --dpu-mode"); + .expect("should parse without --dpu-policy"); match cmd { Cmd::Add(args) => { - assert!(args.dpu_mode.is_none(), "--dpu-mode should be optional"); + assert!(args.dpu_policy.is_none(), "--dpu-policy should be optional"); } _ => panic!("expected Add variant"), } } -// `--dpu-mode ` parses to the matching DpuMode variant on both `add` -// (alongside the required credential/chassis args) and `patch` (where flipping -// dpu_mode on a single host is the whole point). The closure pulls dpu_mode off -// whichever variant parsed; each row pins the parsed `Some(variant)`. +// Both the canonical `--dpu-policy` vocabulary and the legacy `--dpu-mode` +// vocabulary parse to the matching policy on `add` and `patch`. #[test] -fn parse_dpu_mode_to_its_variant() { +fn parse_dpu_policy_to_its_variant() { scenarios!( run = |argv| { Cmd::try_parse_from(argv.iter().copied()) .map(|cmd| match cmd { - Cmd::Add(args) => args.dpu_mode, - Cmd::Patch(args) => args.dpu_mode, + Cmd::Add(args) => args.dpu_policy, + Cmd::Patch(args) => args.dpu_policy, _ => panic!("expected Add or Patch variant"), }) .map_err(drop) }; - "add --dpu-mode nic-mode" { + "add --dpu-policy use-as-nic" { &[ "expected-machine", "add", @@ -527,12 +523,12 @@ fn parse_dpu_mode_to_its_variant() { "secret", "--chassis-serial-number", "SN12345", - "--dpu-mode", - "nic-mode", - ][..] => Yields(Some(rpc::forge::DpuMode::NicMode)), + "--dpu-policy", + "use-as-nic", + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), } - "add --dpu-mode no-dpu" { + "add --dpu-policy ignore" { &[ "expected-machine", "add", @@ -544,12 +540,29 @@ fn parse_dpu_mode_to_its_variant() { "secret", "--chassis-serial-number", "SN12345", - "--dpu-mode", - "no-dpu", - ][..] => Yields(Some(rpc::forge::DpuMode::NoDpu)), + "--dpu-policy", + "ignore", + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::Ignore)), + } + + "add --dpu-policy manage" { + &[ + "expected-machine", + "add", + "--bmc-mac-address", + "1a:2b:3c:4d:5e:6f", + "--bmc-username", + "admin", + "--bmc-password", + "secret", + "--chassis-serial-number", + "SN12345", + "--dpu-policy", + "manage", + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::Manage)), } - "add --dpu-mode dpu-mode" { + "legacy add --dpu-mode nic-mode" { &[ "expected-machine", "add", @@ -562,11 +575,11 @@ fn parse_dpu_mode_to_its_variant() { "--chassis-serial-number", "SN12345", "--dpu-mode", - "dpu-mode", - ][..] => Yields(Some(rpc::forge::DpuMode::DpuMode)), + "nic-mode", + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), } - "patch --dpu-mode nic-mode" { + "legacy patch --dpu-mode nic-mode" { &[ "expected-machine", "patch", @@ -574,10 +587,10 @@ fn parse_dpu_mode_to_its_variant() { "1a:2b:3c:4d:5e:6f", "--dpu-mode", "nic-mode", - ][..] => Yields(Some(rpc::forge::DpuMode::NicMode)), + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::UseAsNic)), } - "patch --dpu-mode no-dpu" { + "legacy patch --dpu-mode no-dpu" { &[ "expected-machine", "patch", @@ -585,10 +598,10 @@ fn parse_dpu_mode_to_its_variant() { "1a:2b:3c:4d:5e:6f", "--dpu-mode", "no-dpu", - ][..] => Yields(Some(rpc::forge::DpuMode::NoDpu)), + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::Ignore)), } - "patch --dpu-mode dpu-mode" { + "legacy patch --dpu-mode dpu-mode" { &[ "expected-machine", "patch", @@ -596,15 +609,45 @@ fn parse_dpu_mode_to_its_variant() { "1a:2b:3c:4d:5e:6f", "--dpu-mode", "dpu-mode", - ][..] => Yields(Some(rpc::forge::DpuMode::DpuMode)), + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::Manage)), + } + + "legacy patch --dpu-mode unspecified" { + &[ + "expected-machine", + "patch", + "--bmc-mac-address", + "1a:2b:3c:4d:5e:6f", + "--dpu-mode", + "unspecified", + ][..] => Yields(Some(rpc::forge::HostDpuPolicy::Unspecified)), } ); } -// parse_add_rejects_invalid_dpu_mode ensures clap rejects values that -// don't match the enum. +// The protobuf sentinel remains accepted for backwards compatibility, but it +// is not part of the canonical three-value policy vocabulary shown to users. +#[test] +fn dpu_policy_help_only_lists_policy_values() { + let mut command = Cmd::command(); + let add = command.find_subcommand_mut("add").unwrap(); + let dpu_policy = add + .get_arguments() + .find(|argument| argument.get_id() == "dpu_policy") + .unwrap(); + let visible_values = dpu_policy + .get_possible_values() + .into_iter() + .filter(|value| !value.is_hide_set()) + .map(|value| value.get_name().to_owned()) + .collect::>(); + + assert_eq!(visible_values, ["manage", "use-as-nic", "ignore"]); +} + +// Clap rejects policy values that do not match the enum. #[test] -fn parse_add_rejects_invalid_dpu_mode() { +fn parse_add_rejects_invalid_dpu_policy() { let result = Cmd::try_parse_from([ "expected-machine", "add", @@ -616,36 +659,35 @@ fn parse_add_rejects_invalid_dpu_mode() { "secret", "--chassis-serial-number", "SN12345", - "--dpu-mode", + "--dpu-policy", "garbage", ]); assert!( result.is_err(), - "clap should reject --dpu-mode with an invalid value" + "clap should reject --dpu-policy with an invalid value" ); } -// validate_patch_with_dpu_mode_only ensures `patch --dpu-mode nic-mode` +// `patch --dpu-policy use-as-nic` // alone (no other patchable fields) satisfies clap's ArgGroup and the -// `Args::validate()` "at least one field" check. The whole point of this -// patch is "flip dpu_mode", so it must work without dummy companion args. +// `Args::validate()` "at least one field" check. #[test] -fn validate_patch_with_dpu_mode_only() { +fn validate_patch_with_dpu_policy_only() { let cmd = Cmd::try_parse_from([ "expected-machine", "patch", "--bmc-mac-address", "00:00:00:00:00:00", - "--dpu-mode", - "nic-mode", + "--dpu-policy", + "use-as-nic", ]) - .expect("patch --dpu-mode alone should parse (ArgGroup)"); + .expect("patch --dpu-policy alone should parse (ArgGroup)"); match cmd { Cmd::Patch(args) => { assert!( args.validate().is_ok(), - "patch --dpu-mode alone should validate" + "patch --dpu-policy alone should validate" ); } _ => panic!("expected Patch variant"), diff --git a/crates/admin-cli/src/expected_machines/update/mod.rs b/crates/admin-cli/src/expected_machines/update/mod.rs index 508b9ccf8c..c00e8082c8 100644 --- a/crates/admin-cli/src/expected_machines/update/mod.rs +++ b/crates/admin-cli/src/expected_machines/update/mod.rs @@ -68,7 +68,7 @@ impl Run for Args { expected_machine.dpf_enabled, expected_machine.bmc_ip_address, expected_machine.bmc_retain_credentials, - expected_machine.dpu_mode, + expected_machine.dpu_policy, expected_machine.bmc_ip_allocation, expected_machine.host_lifecycle_profile.map(|hlp| { ::rpc::forge::HostLifecycleProfile { diff --git a/crates/admin-cli/src/rpc.rs b/crates/admin-cli/src/rpc.rs index 43a3fb7d52..4a0a545d0b 100644 --- a/crates/admin-cli/src/rpc.rs +++ b/crates/admin-cli/src/rpc.rs @@ -828,7 +828,7 @@ impl ApiClient { dpf_enabled: Option, bmc_ip_address: Option, bmc_retain_credentials: Option, - dpu_mode: Option<::rpc::forge::DpuMode>, + dpu_policy: Option<::rpc::forge::HostDpuPolicy>, bmc_ip_allocation: Option<::rpc::forge::BmcIpAllocationType>, host_lifecycle_profile: Option<::rpc::forge::HostLifecycleProfile>, host_nics: Option, @@ -915,7 +915,9 @@ impl ApiClient { bmc_ip_address: bmc_ip_address.or(expected_machine.bmc_ip_address), bmc_retain_credentials: bmc_retain_credentials .or(expected_machine.bmc_retain_credentials), - dpu_mode: dpu_mode.map(|m| m as i32).or(expected_machine.dpu_mode), + dpu_mode: dpu_policy + .map(|policy| policy as i32) + .or(expected_machine.dpu_mode), // Use the flag value if given, else preserve the stored per-host // value (patch semantics). bmc_ip_allocation: bmc_ip_allocation @@ -957,7 +959,7 @@ impl ApiClient { is_dpf_enabled: machine.dpf_enabled, bmc_ip_address: machine.bmc_ip_address, bmc_retain_credentials: machine.bmc_retain_credentials, - dpu_mode: machine.dpu_mode.map(|m| m as i32), + dpu_mode: machine.dpu_policy.map(|policy| policy as i32), bmc_ip_allocation: machine.bmc_ip_allocation.map(|m| m as i32), host_lifecycle_profile: machine.host_lifecycle_profile.map(|hlp| { ::rpc::forge::HostLifecycleProfile { diff --git a/crates/admin-cli/src/site_explorer/mlx_devices/cmd.rs b/crates/admin-cli/src/site_explorer/mlx_devices/cmd.rs index 2b15542245..d24973ba00 100644 --- a/crates/admin-cli/src/site_explorer/mlx_devices/cmd.rs +++ b/crates/admin-cli/src/site_explorer/mlx_devices/cmd.rs @@ -16,7 +16,7 @@ */ use ::rpc::admin_cli::OutputFormat; -use ::rpc::site_explorer::{ExploredMlxDevice, MlxDeviceKind, NicMode}; +use ::rpc::site_explorer::{BlueFieldOperatingMode, ExploredMlxDevice, MlxDeviceKind}; use prettytable::{Row, Table}; use serde::Serialize; @@ -111,7 +111,7 @@ impl From for MlxDeviceRow { /// below we err toward surfacing a device rather than hiding it. fn operating_as_nic(device: &ExploredMlxDevice) -> bool { match device.nic_mode { - Some(mode) => mode == NicMode::Nic as i32, + Some(mode) => mode == BlueFieldOperatingMode::Nic as i32, None => { device.device_kind == MlxDeviceKind::Bf3NicMode as i32 || device.device_kind == MlxDeviceKind::Bf3SuperNic as i32 @@ -132,9 +132,9 @@ fn kind_label(device_kind: i32) -> String { } fn nic_mode_label(nic_mode: Option) -> Option { - nic_mode.and_then(|mode| match NicMode::try_from(mode) { - Ok(NicMode::Nic) => Some("NIC".to_string()), - Ok(NicMode::Dpu) => Some("DPU".to_string()), + nic_mode.and_then(|mode| match BlueFieldOperatingMode::try_from(mode) { + Ok(BlueFieldOperatingMode::Nic) => Some("NIC".to_string()), + Ok(BlueFieldOperatingMode::Dpu) => Some("DPU".to_string()), Err(_) => None, }) } @@ -197,26 +197,26 @@ mod tests { struct Case { name: &'static str, device_kind: MlxDeviceKind, - nic_mode: Option, + nic_mode: Option, expect: bool, } let cases = [ Case { name: "dpu sku flipped into nic mode", device_kind: MlxDeviceKind::Bf3DpuMode, - nic_mode: Some(NicMode::Nic), + nic_mode: Some(BlueFieldOperatingMode::Nic), expect: true, }, Case { name: "dpu sku running as a dpu", device_kind: MlxDeviceKind::Bf3DpuMode, - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), expect: false, }, Case { name: "supernic sku flipped into dpu mode", device_kind: MlxDeviceKind::Bf3NicMode, - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), expect: false, }, Case { diff --git a/crates/api-core/src/cfg/README.md b/crates/api-core/src/cfg/README.md index dd6800ea7e..c3eb1dddb1 100644 --- a/crates/api-core/src/cfg/README.md +++ b/crates/api-core/src/cfg/README.md @@ -310,7 +310,7 @@ flows. | `create_switches` | `bool` | `true` | Auto-create Switch state machines for explored switches with a matching `expected_switches` record. | | `switches_created_per_run` | `u64` | `9` | Max switches created per run. | | `explore_mode` | `SiteExplorerExploreMode` | `NvRedfish` | Redfish backend: `libredfish`, `nv-redfish`, or `compare-result`. | -| `dpu_mode` | `Option` | — | Site-wide DPU operating mode. When set, applies to every host that doesn't declare a per-host `ExpectedMachine.dpu_mode` override. | +| `dpu_policy` | `Option` | — | Site-wide policy for DPU hardware: `manage`, `use_as_nic`, or `ignore`. Per-host `use_as_nic` and `ignore` override it; per-host `manage` inherits it for backward compatibility. When omitted, the site default is `manage`. The legacy `dpu_mode` field and `dpu_mode` / `nic_mode` / `no_dpu` values remain accepted during deserialization. | ### `StateControllerConfig` diff --git a/crates/api-core/src/cfg/file.rs b/crates/api-core/src/cfg/file.rs index 2d657ebc07..8359046114 100644 --- a/crates/api-core/src/cfg/file.rs +++ b/crates/api-core/src/cfg/file.rs @@ -578,7 +578,7 @@ pub struct CarbideConfig { /// (disconnected / air-gapped) infrastructure manager for racks of GB200/GB300/VR144. /// Only set this if using NICo site controller with Rack Manager to manage GB200/300/VR144. /// It will change site controller behavior significantly in the following ways, etc.: - /// 1. skip dpu management and use dpus in nic mode (set the site-wide `[site_explorer] dpu_mode = "nic_mode"`, or per-host `ExpectedMachine.dpu_mode`) + /// 1. skip DPU management and use DPUs as NICs (set the site-wide `[site_explorer] dpu_policy = "use_as_nic"`, or per-host `ExpectedMachine.dpu_policy`) /// a. no dpu bfb upgrade and host power cycle /// b. no firmware upgrade and host power cycle /// c. no hbn deployment (no ecmp, etc) @@ -3021,7 +3021,7 @@ mod tests { use health_report::HealthAlertClassification; use libmlx::variables::value::MlxValueType; use libredfish::model::service_root::RedfishVendor; - use model::expected_machine::DpuMode; + use model::expected_machine::HostDpuPolicy; use model::network_segment::NetworkDefinitionSegmentType; use model::resource_pool; @@ -3467,7 +3467,7 @@ mod tests { create_switches: Arc::new(true.into()), switches_created_per_run: 9, rotate_switch_nvos_credentials: Arc::new(false.into()), - dpu_mode: None, + dpu_policy: None, explore_mode: SiteExplorerExploreMode::NvRedfish, } ); @@ -3675,7 +3675,7 @@ mod tests { create_switches: Arc::new(true.into()), switches_created_per_run: 9, rotate_switch_nvos_credentials: Arc::new(false.into()), - dpu_mode: None, + dpu_policy: None, explore_mode: SiteExplorerExploreMode::NvRedfish, } ); @@ -4018,7 +4018,7 @@ mod tests { create_switches: Arc::new(true.into()), switches_created_per_run: 9, rotate_switch_nvos_credentials: Arc::new(false.into()), - dpu_mode: None, + dpu_policy: None, explore_mode: SiteExplorerExploreMode::NvRedfish, } ); @@ -4208,34 +4208,34 @@ mod tests { Ok(()) } - /// Verifies the `[site_explorer] dpu_mode = ...` setting parses - /// correctly for every named variant. When unset (the default), - /// `site_explorer.dpu_mode` is `None` and hosts resolve to - /// `DpuMode::DpuMode`. + /// Verifies the canonical `[site_explorer] dpu_policy = ...` setting and + /// legacy `dpu_mode` spelling parse correctly. When unset, hosts ultimately + /// resolve to `HostDpuPolicy::Manage`. #[test] - fn site_explorer_dpu_mode_parses_and_defaults_to_none() { + fn site_explorer_dpu_policy_parses_and_defaults_to_none() { let config: CarbideConfig = Figment::new() .merge(Toml::file(format!("{TEST_DATA_DIR}/min_config.toml"))) .extract() .unwrap(); - assert_eq!(config.site_explorer.dpu_mode, None); - - for (toml_value, expected) in [ - ("dpu_mode", DpuMode::DpuMode), - ("nic_mode", DpuMode::NicMode), - ("no_dpu", DpuMode::NoDpu), + assert_eq!(config.site_explorer.dpu_policy, None); + + for (toml_setting, expected) in [ + ("dpu_policy = \"manage\"", HostDpuPolicy::Manage), + ("dpu_policy = \"use_as_nic\"", HostDpuPolicy::UseAsNic), + ("dpu_policy = \"ignore\"", HostDpuPolicy::Ignore), + ("dpu_mode = \"dpu_mode\"", HostDpuPolicy::Manage), + ("dpu_mode = \"nic_mode\"", HostDpuPolicy::UseAsNic), + ("dpu_mode = \"no_dpu\"", HostDpuPolicy::Ignore), ] { let config: CarbideConfig = Figment::new() .merge(Toml::file(format!("{TEST_DATA_DIR}/min_config.toml"))) - .merge(Toml::string(&format!( - "[site_explorer]\ndpu_mode = \"{toml_value}\"\n" - ))) + .merge(Toml::string(&format!("[site_explorer]\n{toml_setting}\n"))) .extract() .unwrap(); assert_eq!( - config.site_explorer.dpu_mode, + config.site_explorer.dpu_policy, Some(expected), - "[site_explorer] dpu_mode = {toml_value:?} should parse to {expected:?}", + "[site_explorer] {toml_setting} should parse to {expected:?}", ); } } diff --git a/crates/api-core/src/handlers/bmc_endpoint_explorer.rs b/crates/api-core/src/handlers/bmc_endpoint_explorer.rs index a430721ab6..58c99699b9 100644 --- a/crates/api-core/src/handlers/bmc_endpoint_explorer.rs +++ b/crates/api-core/src/handlers/bmc_endpoint_explorer.rs @@ -30,7 +30,7 @@ use model::machine::machine_search_config::MachineSearchConfig; use model::machine::{LoadSnapshotOptions, MachineInterfaceSnapshot}; use model::machine_boot_interface::MachineBootInterface; use model::predicted_machine_interface::PredictedMachineInterface; -use model::site_explorer::{NicMode, PreingestionState}; +use model::site_explorer::{BlueFieldOperatingMode, PreingestionState}; use sqlx::PgConnection; use tokio::net::lookup_host; use tonic::{Request, Response, Status}; @@ -782,10 +782,11 @@ pub(crate) async fn copy_bfb_to_dpu_rshim( // BFB preingestion flow will work its way through the states, and then // wait for the ARM OS to come up, which it never will. Waiting will // eventually, time out (SLA), and then the host will mark as failed. - if dpu_endpoint.report.nic_mode() == Some(NicMode::Nic) { + if dpu_endpoint.report.bluefield_operating_mode() == Some(BlueFieldOperatingMode::Nic) { return Err(CarbideError::InvalidArgument(format!( "Cannot trigger BFB recovery: DPU {dpu_ip} is in NIC mode. \ - Update the host's `ExpectedMachine.dpu_mode` to `DpuMode` \ + Ensure the host's resolved DPU policy is `Manage` \ + (update `ExpectedMachine.dpu_policy` and the site policy as needed) \ and wait for site-explorer to reconcile the DPU back to \ DPU mode before retrying.", )) diff --git a/crates/api-core/src/test_support/fixture_config.rs b/crates/api-core/src/test_support/fixture_config.rs index 6ad44abf6d..186633daa4 100644 --- a/crates/api-core/src/test_support/fixture_config.rs +++ b/crates/api-core/src/test_support/fixture_config.rs @@ -21,7 +21,7 @@ use carbide_utils::test_support::certs::create_random_self_signed_cert; use model::expected_machine::ExpectedMachineData; use model::hardware_info::TpmEkCertificate; use model::machine::ManagedHostState; -use model::site_explorer::NicMode; +use model::site_explorer::BlueFieldOperatingMode; use model::test_support::managed_host::REQUIRED_IB_GUIDS; use model::test_support::{DpuConfig, HardwareInfoTemplate, ManagedHostConfig}; @@ -53,7 +53,7 @@ impl FixtureDefault for DpuConfig { last_exploration_error: None, override_hosts_uefi_device_path: None, hardware_info_template: HardwareInfoTemplate::Default, - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), } } } diff --git a/crates/api-core/src/tests/common/api_fixtures/mod.rs b/crates/api-core/src/tests/common/api_fixtures/mod.rs index 5a04a00f5f..dde19af091 100644 --- a/crates/api-core/src/tests/common/api_fixtures/mod.rs +++ b/crates/api-core/src/tests/common/api_fixtures/mod.rs @@ -1151,7 +1151,7 @@ pub async fn create_test_env(db_pool: sqlx::PgPool) -> TestEnv { /// `create_test_env` with the fixture admin + host-inband site prefixes /// routable and the host-inband network segment created -- the standard -/// setup for zero-DPU / NicMode ingestion tests. +/// setup for zero-DPU / `UseAsNic` ingestion tests. pub async fn create_test_env_with_host_inband(db_pool: sqlx::PgPool) -> TestEnv { let env = create_test_env_with_overrides( db_pool, @@ -1728,7 +1728,7 @@ pub async fn create_test_env_with_overrides( create_switches: Arc::new(true.into()), switches_created_per_run: 1, rotate_switch_nvos_credentials: Arc::new(false.into()), - dpu_mode: None, + dpu_policy: None, // Tests use MockEndpointExplorer. So this doesn't affect anything. explore_mode: SiteExplorerExploreMode::NvRedfish, }, diff --git a/crates/api-core/src/tests/common/api_fixtures/site_explorer.rs b/crates/api-core/src/tests/common/api_fixtures/site_explorer.rs index 65b36d5ab3..10b2684473 100644 --- a/crates/api-core/src/tests/common/api_fixtures/site_explorer.rs +++ b/crates/api-core/src/tests/common/api_fixtures/site_explorer.rs @@ -1499,10 +1499,10 @@ pub async fn register_expected_machine( data.dpf_enabled = default_dpf_enabled; } // For fixtures that intentionally create zero-DPU hosts (no DpuConfigs), - // declare them as `NoDpu` so site-explorer accepts them. Tests that - // explicitly set `dpu_mode` via `expected_machine_data` are left alone. - if config.dpus.is_empty() && data.dpu_mode == model::expected_machine::DpuMode::DpuMode { - data.dpu_mode = model::expected_machine::DpuMode::NoDpu; + // declare them as `Ignore` so site-explorer accepts them. Explicit + // non-`Manage` policies in `expected_machine_data` are left alone. + if config.dpus.is_empty() && data.dpu_policy == model::expected_machine::HostDpuPolicy::Manage { + data.dpu_policy = model::expected_machine::HostDpuPolicy::Ignore; } let em = ExpectedMachine { diff --git a/crates/api-core/src/tests/expected_machine.rs b/crates/api-core/src/tests/expected_machine.rs index 7bce993bc9..46c82dc4dc 100644 --- a/crates/api-core/src/tests/expected_machine.rs +++ b/crates/api-core/src/tests/expected_machine.rs @@ -2709,29 +2709,27 @@ async fn test_declared_primary_survives_dhcp_arrival_order( Ok(()) } -/// Simple test to have some round-trip coverage for `ExpectedMachine.dpu_mode` -/// to make sure a `NicMode` setting makes it from the API to the DB and back -/// correctly. Verifies: -/// - The RPC carrying `Some(DpuMode::NicMode)` persists. -/// - The re-read RPC response replies `dpu_mode = Some(NicMode)` back -/// - Other `dpu_mode` values do the same. +/// Non-default host DPU policies round-trip from the API through the database. #[crate::sqlx_test] -async fn test_dpu_mode_round_trip_for_non_default_values( +async fn test_dpu_policy_round_trip_for_non_default_values( pool: sqlx::PgPool, ) -> Result<(), Box> { let env = create_test_env(pool).await; - for (idx, mode) in [rpc::forge::DpuMode::NicMode, rpc::forge::DpuMode::NoDpu] - .iter() - .enumerate() + for (idx, policy) in [ + rpc::forge::HostDpuPolicy::UseAsNic, + rpc::forge::HostDpuPolicy::Ignore, + ] + .iter() + .enumerate() { let mac = format!("5A:5B:5C:5D:5E:{idx:02X}"); let request = rpc::forge::ExpectedMachine { bmc_mac_address: mac.clone(), bmc_username: "ADMIN".into(), bmc_password: "PASS".into(), - chassis_serial_number: format!("EM-DPU-MODE-{idx}"), - dpu_mode: Some(*mode as i32), + chassis_serial_number: format!("EM-DPU-POLICY-{idx}"), + dpu_mode: Some(*policy as i32), ..Default::default() }; @@ -2750,20 +2748,18 @@ async fn test_dpu_mode_round_trip_for_non_default_values( assert_eq!( retrieved.dpu_mode, - Some(*mode as i32), - "DPU mode {mode:?} should survive DB round-trip unchanged" + Some(*policy as i32), + "DPU policy {policy:?} should survive DB round-trip unchanged" ); } Ok(()) } -/// Also have some "round trip" coverage for the dpu_mode default case, -/// when the operator didn't set `dpu_mode` on the wire. In this case, -/// we should persist the Postgrs default (`DpuMode::DpuMode`) and return -/// `None` on the wire (so old clients see the same thing they sent). +/// The default host DPU policy is omitted on the wire, preserving existing +/// clients' absent-field behavior. #[crate::sqlx_test] -async fn test_dpu_mode_default_value_omitted_on_wire( +async fn test_dpu_policy_default_value_omitted_on_wire( pool: sqlx::PgPool, ) -> Result<(), Box> { let env = create_test_env(pool).await; @@ -2791,16 +2787,16 @@ async fn test_dpu_mode_default_value_omitted_on_wire( assert_eq!( retrieved.dpu_mode, None, - "default DpuMode should not be emitted on the wire for stable round-trips" + "default HostDpuPolicy should not be emitted on the wire for stable round-trips" ); Ok(()) } -/// Verify the update RPC (for update/patch flows) actually flips -/// `dpu_mode` as expected. +/// Verify the update RPC (for update/patch flows) actually changes +/// `dpu_policy` as expected. #[crate::sqlx_test] -async fn test_update_changes_dpu_mode( +async fn test_update_changes_dpu_policy( pool: sqlx::PgPool, ) -> Result<(), Box> { let env = create_test_env(pool).await; @@ -2819,14 +2815,14 @@ async fn test_update_changes_dpu_mode( .add_expected_machine(tonic::Request::new(base.clone())) .await?; - for mode in [ - rpc::forge::DpuMode::NicMode, - rpc::forge::DpuMode::NoDpu, - rpc::forge::DpuMode::DpuMode, + for policy in [ + rpc::forge::HostDpuPolicy::UseAsNic, + rpc::forge::HostDpuPolicy::Ignore, + rpc::forge::HostDpuPolicy::Manage, ] { env.api .update_expected_machine(tonic::Request::new(rpc::forge::ExpectedMachine { - dpu_mode: Some(mode as i32), + dpu_mode: Some(policy as i32), ..base.clone() })) .await?; @@ -2840,16 +2836,16 @@ async fn test_update_changes_dpu_mode( .await? .into_inner(); - // DpuMode is the column default and the wire-default; the model + // Manage is the column default and the wire-default; the model // collapses it to `None` on the way out (see `From // for rpc::forge::ExpectedMachine`), so compare accordingly. - let expected_wire = match mode { - rpc::forge::DpuMode::DpuMode | rpc::forge::DpuMode::Unspecified => None, + let expected_wire = match policy { + rpc::forge::HostDpuPolicy::Manage | rpc::forge::HostDpuPolicy::Unspecified => None, other => Some(other as i32), }; assert_eq!( retrieved.dpu_mode, expected_wire, - "update to {mode:?} should persist and round-trip on the wire" + "update to {policy:?} should persist and round-trip on the wire" ); } diff --git a/crates/api-core/src/tests/machine_interfaces.rs b/crates/api-core/src/tests/machine_interfaces.rs index cb16808d68..ab5da8b02a 100644 --- a/crates/api-core/src/tests/machine_interfaces.rs +++ b/crates/api-core/src/tests/machine_interfaces.rs @@ -329,7 +329,7 @@ async fn reconcile_admin_addresses_errors_without_any_primary_admin_interface( Ok(()) } -/// A DpuMode host that boots from an integrated NIC has a HostInband primary and +/// A managed-DPU host that boots from an integrated NIC has a HostInband primary and /// no primary *admin* interface -- its DPU admin links are present but dormant. /// Reconcile must treat that as valid (clean up the dormant links), not as the /// "no primary admin interface" error. diff --git a/crates/api-core/src/tests/preingestion_dpu_nic_mode.rs b/crates/api-core/src/tests/preingestion_dpu_nic_mode.rs index 383224c961..f38db6712f 100644 --- a/crates/api-core/src/tests/preingestion_dpu_nic_mode.rs +++ b/crates/api-core/src/tests/preingestion_dpu_nic_mode.rs @@ -29,8 +29,8 @@ use std::net::{IpAddr, Ipv4Addr}; use carbide_preingestion_manager::PreingestionManager; use model::site_explorer::{ - Chassis, ComputerSystem, ComputerSystemAttributes, EndpointExplorationReport, EndpointType, - Inventory, NicMode, PowerState, PreingestionState, Service, + BlueFieldOperatingMode, Chassis, ComputerSystem, ComputerSystemAttributes, + EndpointExplorationReport, EndpointType, Inventory, PowerState, PreingestionState, Service, }; use model::test_support::DpuConfig; use rpc::forge::forge_server::Forge; @@ -39,7 +39,7 @@ use tonic::Code; use crate::test_support::fixture_config::FixtureDefault as _; use crate::tests::common; -fn dpu_report(nic_mode: NicMode) -> EndpointExplorationReport { +fn dpu_report(nic_mode: BlueFieldOperatingMode) -> EndpointExplorationReport { DpuConfig { nic_mode: Some(nic_mode), ..DpuConfig::default() @@ -127,15 +127,21 @@ fn build_preingestion_manager(env: &common::api_fixtures::TestEnv) -> Preingesti ) } -/// Seed a NicMode DPU + a host BMC with `pause_remediation = true`. The host +/// Seed a DPU reporting NIC operating mode plus a host BMC with +/// `pause_remediation = true`. The host /// row's flag is the primary marker that the skip's cleanup ran, /// since `set_pause_remediation(host_bmc_ip, false)` is the only thing the /// skip does to that row. async fn seed_nic_mode_pair(env: &common::api_fixtures::TestEnv) { let mut txn = env.pool.begin().await.unwrap(); - db::explored_endpoints::insert(DPU_IP, &dpu_report(NicMode::Nic), false, &mut txn) - .await - .unwrap(); + db::explored_endpoints::insert( + DPU_IP, + &dpu_report(BlueFieldOperatingMode::Nic), + false, + &mut txn, + ) + .await + .unwrap(); db::explored_endpoints::insert(HOST_BMC_IP, &host_bmc_report(), false, &mut txn) .await .unwrap(); @@ -267,7 +273,7 @@ async fn test_preingestion_nic_mode_does_not_skip_bfb_copy_in_progress( /// inserted `BfbRecoveryNeeded` for a NIC-mode DPU into `Complete` on its /// next tick, so accepting the request would be a silent no-op for the /// operator. The handler must reject up front with an actionable message -/// pointing at `ExpectedMachine.dpu_mode`. +/// pointing at `ExpectedMachine.dpu_policy`. #[crate::sqlx_test] async fn test_copy_bfb_to_dpu_rshim_rejects_nic_mode_dpu( pool: sqlx::PgPool, @@ -275,7 +281,13 @@ async fn test_copy_bfb_to_dpu_rshim_rejects_nic_mode_dpu( let env = common::api_fixtures::create_test_env(pool.clone()).await; let mut txn = pool.begin().await?; - db::explored_endpoints::insert(DPU_IP, &dpu_report(NicMode::Nic), false, &mut txn).await?; + db::explored_endpoints::insert( + DPU_IP, + &dpu_report(BlueFieldOperatingMode::Nic), + false, + &mut txn, + ) + .await?; txn.commit().await?; let status = env @@ -300,8 +312,8 @@ async fn test_copy_bfb_to_dpu_rshim_rejects_nic_mode_dpu( "error should name NIC mode; got: {msg}", ); assert!( - msg.contains("ExpectedMachine.dpu_mode"), - "error should point at the operator-facing knob (`ExpectedMachine.dpu_mode`); got: {msg}", + msg.contains("ExpectedMachine.dpu_policy"), + "error should point at the operator-facing knob (`ExpectedMachine.dpu_policy`); got: {msg}", ); Ok(()) diff --git a/crates/api-core/src/tests/sku.rs b/crates/api-core/src/tests/sku.rs index a9188e83d7..d14d5fafba 100644 --- a/crates/api-core/src/tests/sku.rs +++ b/crates/api-core/src/tests/sku.rs @@ -822,7 +822,7 @@ pub mod tests { dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -918,7 +918,7 @@ pub mod tests { dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -989,7 +989,7 @@ pub mod tests { dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -1077,7 +1077,7 @@ pub mod tests { dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -1516,7 +1516,7 @@ pub mod tests { dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, diff --git a/crates/api-core/tests/integration/explored_mlx_devices.rs b/crates/api-core/tests/integration/explored_mlx_devices.rs index 15a4f290c3..1cca68af71 100644 --- a/crates/api-core/tests/integration/explored_mlx_devices.rs +++ b/crates/api-core/tests/integration/explored_mlx_devices.rs @@ -19,8 +19,8 @@ use std::str::FromStr; use carbide_test_harness::prelude::*; use model::site_explorer::{ - Chassis, ComputerSystem, ComputerSystemAttributes, EndpointExplorationReport, EndpointType, - NicMode, PCIeDevice, + BlueFieldOperatingMode, Chassis, ComputerSystem, ComputerSystemAttributes, + EndpointExplorationReport, EndpointType, PCIeDevice, }; fn pcie(part: &str, fw: &str, serial: &str, id: &str) -> PCIeDevice { @@ -69,7 +69,7 @@ async fn test_find_explored_mlx_devices(pool: PgPool) -> Result<(), Box Result<(), Box Databa .bind(machine.data.dpf_enabled) .bind(machine.data.bmc_ip_address) .bind(machine.data.bmc_retain_credentials) - .bind(machine.data.dpu_mode) + .bind(machine.data.dpu_policy) .bind(machine.data.bmc_ip_allocation) .bind( (!machine.data.host_lifecycle_profile.is_empty()) diff --git a/crates/api-db/src/expected_machine/tests.rs b/crates/api-db/src/expected_machine/tests.rs index 715f0535b1..ba7b438528 100644 --- a/crates/api-db/src/expected_machine/tests.rs +++ b/crates/api-db/src/expected_machine/tests.rs @@ -115,7 +115,7 @@ async fn test_duplicate_fail_create(pool: sqlx::PgPool) -> Result<(), Box bool { - matches!(self, DpuMode::DpuMode) - } - - /// Resolve a host's effective DPU mode from the (optional) per-host - /// `ExpectedMachine.dpu_mode` value and the (optional) site-wide - /// `[site_explorer] dpu_mode` setting. Notes: +impl HostDpuPolicy { + /// Resolve per-host and site-wide declarations into a concrete policy. /// - /// - An explicit per-host `NicMode` or `NoDpu` always wins. - /// - Per-host `DpuMode` (the default variant) or no `ExpectedMachine` - /// at all == defer to the site-wide setting. - /// - Site-wide `NicMode` or `NoDpu` then wins. - /// - Site-wide `DpuMode` or missing == fall back to the absolute - /// default of `DpuMode::DpuMode`. - pub fn resolve(expected_mode: Option, site_dpu_mode: Option) -> DpuMode { - match expected_mode { - Some(DpuMode::NicMode) => DpuMode::NicMode, - Some(DpuMode::NoDpu) => DpuMode::NoDpu, - // `DpuMode` (default) or missing == defer to site-wide setting. - _ => match site_dpu_mode { - Some(DpuMode::NicMode) => DpuMode::NicMode, - Some(DpuMode::NoDpu) => DpuMode::NoDpu, - // Site-wide `DpuMode` or missing == absolute default. - _ => DpuMode::DpuMode, - }, + /// Per-host [`HostDpuPolicy::UseAsNic`] and [`HostDpuPolicy::Ignore`] + /// override the site. Per-host [`HostDpuPolicy::Manage`] or a missing + /// declaration inherits the site. A missing site declaration resolves to + /// [`HostDpuPolicy::Manage`]. + pub fn resolve(per_host_policy: Option, site_policy: Option) -> Self { + match per_host_policy { + Some(Self::UseAsNic) => Self::UseAsNic, + Some(Self::Ignore) => Self::Ignore, + Some(Self::Manage) | None => site_policy.unwrap_or_default(), } } + + /// Whether this policy expects NICo to discover and manage the host's DPUs. + pub fn expects_managed_dpus(self) -> bool { + matches!(self, Self::Manage) + } } /// Controls how a BMC's IP address is assigned and whether it is retained. @@ -234,12 +226,11 @@ pub struct ExpectedMachineData { /// factory-default credentials in Vault as-is. #[serde(default)] pub bmc_retain_credentials: Option, - /// Per-host DPU operating mode (default `DpuMode::DpuMode` for - /// backward compat). See `DpuMode` for semantics. Operators set - /// this to `NicMode` when a physically-present DPU should be treated - /// as a plain NIC, or to `NoDpu` when there's no DPU hardware at all. - #[serde(default)] - pub dpu_mode: DpuMode, + /// Per-host DPU policy. The default [`HostDpuPolicy::Manage`] inherits the + /// site policy. The legacy `dpu_mode` field and its values remain accepted + /// during deserialization. + #[serde(default, alias = "dpu_mode")] + pub dpu_policy: HostDpuPolicy, /// Per-host control over how this BMC's IP is assigned and retained. /// Defaults to `BmcIpAllocationType::Auto`, which infers `Fixed` from a /// configured `bmc_ip_address` and otherwise `Retained` (pins an @@ -322,7 +313,7 @@ impl<'r> FromRow<'r, PgRow> for ExpectedMachine { dpf_enabled: row.try_get("dpf_enabled")?, bmc_ip_address: row.try_get("bmc_ip_address")?, bmc_retain_credentials: row.try_get("bmc_retain_credentials")?, - dpu_mode: row.try_get("dpu_mode")?, + dpu_policy: row.try_get("dpu_mode")?, bmc_ip_allocation: row.try_get("bmc_ip_allocation")?, host_lifecycle_profile: row .try_get::, _>("host_lifecycle_profile") @@ -357,106 +348,254 @@ pub struct UnexpectedMachine { #[cfg(test)] mod tests { use carbide_test_support::Outcome::*; - use carbide_test_support::scenarios; + use carbide_test_support::{Check, check_values, scenarios, value_scenarios}; use super::*; - /// Nothing set anywhere -- the host falls back to the absolute - /// default, `DpuMode::DpuMode`. #[test] - fn resolve_no_expected_mode_no_site_setting_returns_dpu_mode() { - assert_eq!(DpuMode::resolve(None, None), DpuMode::DpuMode); - } + fn host_dpu_policy_resolves_legacy_declarations() { + struct Declarations { + per_host: Option, + site: Option, + } - /// Explicit per-host `DpuMode` is indistinguishable from "not set" - /// in the storage type (the default variant), so it also defers to - /// the site-wide setting. - #[test] - fn resolve_explicit_per_host_dpu_mode_defers_to_site_setting() { - assert_eq!( - DpuMode::resolve(Some(DpuMode::DpuMode), None), - DpuMode::DpuMode - ); - assert_eq!( - DpuMode::resolve(Some(DpuMode::DpuMode), Some(DpuMode::NicMode)), - DpuMode::NicMode + check_values( + [ + Check { + scenario: "unset host, unset site", + input: Declarations { + per_host: None, + site: None, + }, + expect: HostDpuPolicy::Manage, + }, + Check { + scenario: "unset host, managed site", + input: Declarations { + per_host: None, + site: Some(HostDpuPolicy::Manage), + }, + expect: HostDpuPolicy::Manage, + }, + Check { + scenario: "unset host, NIC-mode site", + input: Declarations { + per_host: None, + site: Some(HostDpuPolicy::UseAsNic), + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "unset host, no-DPU site", + input: Declarations { + per_host: None, + site: Some(HostDpuPolicy::Ignore), + }, + expect: HostDpuPolicy::Ignore, + }, + Check { + scenario: "inheriting host, unset site", + input: Declarations { + per_host: Some(HostDpuPolicy::Manage), + site: None, + }, + expect: HostDpuPolicy::Manage, + }, + Check { + scenario: "inheriting host, managed site", + input: Declarations { + per_host: Some(HostDpuPolicy::Manage), + site: Some(HostDpuPolicy::Manage), + }, + expect: HostDpuPolicy::Manage, + }, + Check { + scenario: "inheriting host, NIC-mode site", + input: Declarations { + per_host: Some(HostDpuPolicy::Manage), + site: Some(HostDpuPolicy::UseAsNic), + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "inheriting host, no-DPU site", + input: Declarations { + per_host: Some(HostDpuPolicy::Manage), + site: Some(HostDpuPolicy::Ignore), + }, + expect: HostDpuPolicy::Ignore, + }, + Check { + scenario: "NIC-mode host, unset site", + input: Declarations { + per_host: Some(HostDpuPolicy::UseAsNic), + site: None, + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "NIC-mode host, managed site", + input: Declarations { + per_host: Some(HostDpuPolicy::UseAsNic), + site: Some(HostDpuPolicy::Manage), + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "NIC-mode host, NIC-mode site", + input: Declarations { + per_host: Some(HostDpuPolicy::UseAsNic), + site: Some(HostDpuPolicy::UseAsNic), + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "NIC-mode host, no-DPU site", + input: Declarations { + per_host: Some(HostDpuPolicy::UseAsNic), + site: Some(HostDpuPolicy::Ignore), + }, + expect: HostDpuPolicy::UseAsNic, + }, + Check { + scenario: "no-DPU host, unset site", + input: Declarations { + per_host: Some(HostDpuPolicy::Ignore), + site: None, + }, + expect: HostDpuPolicy::Ignore, + }, + Check { + scenario: "no-DPU host, managed site", + input: Declarations { + per_host: Some(HostDpuPolicy::Ignore), + site: Some(HostDpuPolicy::Manage), + }, + expect: HostDpuPolicy::Ignore, + }, + Check { + scenario: "no-DPU host, NIC-mode site", + input: Declarations { + per_host: Some(HostDpuPolicy::Ignore), + site: Some(HostDpuPolicy::UseAsNic), + }, + expect: HostDpuPolicy::Ignore, + }, + Check { + scenario: "no-DPU host, no-DPU site", + input: Declarations { + per_host: Some(HostDpuPolicy::Ignore), + site: Some(HostDpuPolicy::Ignore), + }, + expect: HostDpuPolicy::Ignore, + }, + ], + |Declarations { per_host, site }| HostDpuPolicy::resolve(per_host, site), ); } - /// An explicit per-host `NicMode` always wins, regardless of the - /// site-wide setting. This is the "I want this specific host in - /// NIC mode" override. #[test] - fn resolve_per_host_nic_mode_always_wins() { - for site_dpu_mode in [None, Some(DpuMode::DpuMode), Some(DpuMode::NoDpu)] { - assert_eq!( - DpuMode::resolve(Some(DpuMode::NicMode), site_dpu_mode), - DpuMode::NicMode, - "site_dpu_mode={site_dpu_mode:?}" - ); - } + fn host_dpu_policy_expects_managed_dpus() { + value_scenarios!( + run = HostDpuPolicy::expects_managed_dpus; + "manage" { + HostDpuPolicy::Manage => true, + } + "use as NIC" { + HostDpuPolicy::UseAsNic => false, + } + "ignore" { + HostDpuPolicy::Ignore => false, + } + ); } - /// An explicit per-host `NoDpu` always wins. Useful for hosts where - /// the operator knows there's genuinely no DPU hardware (as opposed - /// to "DPU present but used as NIC", which is `NicMode`). #[test] - fn resolve_per_host_no_dpu_always_wins() { - for site_dpu_mode in [None, Some(DpuMode::DpuMode), Some(DpuMode::NicMode)] { - assert_eq!( - DpuMode::resolve(Some(DpuMode::NoDpu), site_dpu_mode), - DpuMode::NoDpu, - "site_dpu_mode={site_dpu_mode:?}" - ); - } - } + fn host_dpu_policy_deserializes_new_and_legacy_values() { + scenarios!( + run = |json| serde_json::from_str::(json).map_err(drop); + "new policy values" { + r#""manage""# => Yields(HostDpuPolicy::Manage), + r#""use_as_nic""# => Yields(HostDpuPolicy::UseAsNic), + r#""ignore""# => Yields(HostDpuPolicy::Ignore), + } - /// Site-wide `NicMode` applies to hosts that don't declare a - /// per-host mode -- this is the whole point of the site-wide - /// setting (flip an entire site without per-host edits). - #[test] - fn resolve_site_wide_nic_mode_applies_when_per_host_is_unset() { - assert_eq!( - DpuMode::resolve(None, Some(DpuMode::NicMode)), - DpuMode::NicMode - ); - assert_eq!( - DpuMode::resolve(Some(DpuMode::DpuMode), Some(DpuMode::NicMode)), - DpuMode::NicMode + "legacy mode values" { + r#""dpu_mode""# => Yields(HostDpuPolicy::Manage), + r#""nic_mode""# => Yields(HostDpuPolicy::UseAsNic), + r#""no_dpu""# => Yields(HostDpuPolicy::Ignore), + } ); } - /// Same as above for `NoDpu`: site-wide setting applies when the - /// per-host value is unset (or the default `DpuMode` placeholder). #[test] - fn resolve_site_wide_no_dpu_applies_when_per_host_is_unset() { - assert_eq!(DpuMode::resolve(None, Some(DpuMode::NoDpu)), DpuMode::NoDpu); - assert_eq!( - DpuMode::resolve(Some(DpuMode::DpuMode), Some(DpuMode::NoDpu)), - DpuMode::NoDpu - ); - } + fn host_dpu_policy_serializes_and_round_trips_canonical_values() { + scenarios!( + run = |policy| { + let json = serde_json::to_string(&policy).map_err(drop)?; + let recovered = serde_json::from_str::(&json).map_err(drop)?; + Ok::<_, ()>((json, recovered)) + }; + "default/manage" { + HostDpuPolicy::default() => Yields(( + r#""manage""#.to_string(), + HostDpuPolicy::Manage, + )), + } - /// Site-wide `DpuMode` is indistinguishable from "not set" -- both - /// fall back to the absolute `DpuMode` default. Symmetric with the - /// per-host `DpuMode` behavior. - #[test] - fn resolve_site_wide_dpu_mode_resolves_to_dpu_mode() { - assert_eq!( - DpuMode::resolve(None, Some(DpuMode::DpuMode)), - DpuMode::DpuMode + "use as NIC" { + HostDpuPolicy::UseAsNic => Yields(( + r#""use_as_nic""#.to_string(), + HostDpuPolicy::UseAsNic, + )), + } + + "ignore" { + HostDpuPolicy::Ignore => Yields(( + r#""ignore""#.to_string(), + HostDpuPolicy::Ignore, + )), + } ); } - /// `is_dpu_managed()` returns true only for the default `DpuMode` - /// variant -- the two "not managed by NICo as DPU" cases both return - /// false, which is what site-explorer and state handlers use to skip - /// DPU-specific work. #[test] - fn is_dpu_managed_covers_both_skip_cases() { - assert!(DpuMode::DpuMode.is_dpu_managed()); - assert!(!DpuMode::NicMode.is_dpu_managed()); - assert!(!DpuMode::NoDpu.is_dpu_managed()); + fn expected_machine_deserializes_new_and_legacy_policy_fields() { + scenarios!( + run = |json| { + serde_json::from_str::(json) + .map(|em| em.data.dpu_policy) + .map_err(drop) + }; + "policy omitted" { + r#"{ + "bmc_mac_address": "AA:BB:CC:DD:EE:FF", + "bmc_username": "root", + "bmc_password": "pass", + "serial_number": "SN-1" + }"# => Yields(HostDpuPolicy::Manage), + } + + "new field and value" { + r#"{ + "bmc_mac_address": "AA:BB:CC:DD:EE:FF", + "bmc_username": "root", + "bmc_password": "pass", + "serial_number": "SN-1", + "dpu_policy": "use_as_nic" + }"# => Yields(HostDpuPolicy::UseAsNic), + } + + "legacy field and value" { + r#"{ + "bmc_mac_address": "AA:BB:CC:DD:EE:FF", + "bmc_username": "root", + "bmc_password": "pass", + "serial_number": "SN-1", + "dpu_mode": "no_dpu" + }"# => Yields(HostDpuPolicy::Ignore), + } + ); } /// JSON deserialization of `ExpectedMachine`, projecting to the diff --git a/crates/api-model/src/machine/mod.rs b/crates/api-model/src/machine/mod.rs index 3eb8e8dbc5..7fa75a3da0 100644 --- a/crates/api-model/src/machine/mod.rs +++ b/crates/api-model/src/machine/mod.rs @@ -355,11 +355,11 @@ impl ManagedHostStateSnapshot { /// Returns `true` if this managed host has at least one DPU snapshot /// attached -- i.e. a DPU we actively manage as a `Machine`. /// - /// A `false` return ("no managed DPUs") covers two cases that are intended - /// to be treated the same: actual zero-DPU hosts (`DpuMode::NoDpu`), and - /// `DpuMode::NicMode` hosts. The latter may acutally have DPUs, but - /// site-explorer puts them into NIC mode at ingestion, so no DPU snapshot - /// is ever attached. + /// A `false` return ("no managed DPUs") covers the two effective policies + /// that intentionally attach no DPU snapshots: `HostDpuPolicy::Ignore` and + /// `HostDpuPolicy::UseAsNic`. A `UseAsNic` host may have DPU hardware, but + /// site-explorer puts it into NIC mode at ingestion, so no DPU snapshot is + /// ever attached. /// /// Some callers combine this w/ `associated_dpu_machine_ids().is_empty()` /// to distinguish between truly no managed DPUs vs. DPU expected per diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index 2ed413d27b..60dbd32d04 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -814,7 +814,7 @@ impl EndpointExplorationReport { } } - pub fn nic_mode(&self) -> Option { + pub fn bluefield_operating_mode(&self) -> Option { if self.is_dpu() && !self.systems.is_empty() { self.systems[0].attributes.nic_mode } else { @@ -1417,7 +1417,7 @@ pub enum EndpointType { #[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ComputerSystemAttributes { - pub nic_mode: Option, + pub nic_mode: Option, pub is_infinite_boot_enabled: Option, } @@ -1720,15 +1720,19 @@ impl From> for MachineExpectation { } } +/// The operating mode reported by a BlueField device. +/// +/// This is observed hardware state, not the policy NICo applies to the host; +/// see [`crate::expected_machine::HostDpuPolicy`] for the desired behavior. #[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub enum NicMode { +pub enum BlueFieldOperatingMode { #[serde(rename = "DpuMode", alias = "Dpu")] Dpu, #[serde(rename = "NicMode", alias = "Nic")] Nic, } -impl Display for NicMode { +impl Display for BlueFieldOperatingMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } @@ -1926,7 +1930,7 @@ pub struct ExploredMlxDevice { /// `device_kind` is its factory SKU, and the two legitimately differ for a /// DPU reconfigured to run as a NIC. #[serde(default, skip_serializing_if = "Option::is_none")] - pub nic_mode: Option, + pub nic_mode: Option, } impl EndpointExplorationReport { @@ -2051,7 +2055,7 @@ pub fn collect_explored_mlx_devices(endpoints: &[ExploredEndpoint]) -> Vec().unwrap()) ); - assert_eq!(nic_dpu.nic_mode, Some(NicMode::Nic)); + assert_eq!(nic_dpu.nic_mode, Some(BlueFieldOperatingMode::Nic)); let supernic = &devices[1]; assert_eq!(supernic.device_kind, MlxDeviceKind::Bf3SuperNic); @@ -2448,7 +2452,7 @@ mod explored_mlx_device_tests { id: "Bluefield".to_string(), serial_number: Some(serial.to_string()), attributes: ComputerSystemAttributes { - nic_mode: Some(NicMode::Nic), + nic_mode: Some(BlueFieldOperatingMode::Nic), ..Default::default() }, ..Default::default() @@ -2504,6 +2508,37 @@ mod tests { use crate::firmware::FirmwareComponent; use crate::machine::machine_id::from_hardware_info; + #[test] + fn bluefield_operating_mode_preserves_legacy_serialized_values() { + scenarios!( + run = |json| serde_json::from_str::(json).map_err(drop); + "DPU mode canonical value" { + r#""DpuMode""# => Yields(BlueFieldOperatingMode::Dpu), + } + + "DPU mode alias" { + r#""Dpu""# => Yields(BlueFieldOperatingMode::Dpu), + } + + "NIC mode canonical value" { + r#""NicMode""# => Yields(BlueFieldOperatingMode::Nic), + } + + "NIC mode alias" { + r#""Nic""# => Yields(BlueFieldOperatingMode::Nic), + } + ); + + assert_eq!( + serde_json::to_string(&BlueFieldOperatingMode::Dpu).unwrap(), + r#""DpuMode""# + ); + assert_eq!( + serde_json::to_string(&BlueFieldOperatingMode::Nic).unwrap(), + r#""NicMode""# + ); + } + fn create_test_firmware(firmware_type: FirmwareComponentType, regex_pattern: &str) -> Firmware { let mut components = HashMap::new(); components.insert( @@ -2902,7 +2937,7 @@ mod tests { model: None, serial_number: Some("MT2242XZ00NX".to_string()), attributes: ComputerSystemAttributes { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), is_infinite_boot_enabled: None, }, pcie_devices: vec![], @@ -2973,7 +3008,7 @@ mod tests { model: None, serial_number: Some("MT2242XZ00NX".to_string()), attributes: ComputerSystemAttributes { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), is_infinite_boot_enabled: None, }, pcie_devices: vec![], diff --git a/crates/api-model/src/test_support/dpu.rs b/crates/api-model/src/test_support/dpu.rs index bc8bb826f2..87e419e508 100644 --- a/crates/api-model/src/test_support/dpu.rs +++ b/crates/api-model/src/test_support/dpu.rs @@ -19,9 +19,9 @@ use mac_address::MacAddress; use crate::hardware_info::HardwareInfo; use crate::site_explorer::{ - Chassis, ComputerSystem, ComputerSystemAttributes, EndpointExplorationError, - EndpointExplorationReport, EndpointType, EthernetInterface, Inventory, Manager, NicMode, - PCIeDevice, PowerState, Service, UefiDevicePath, + BlueFieldOperatingMode, Chassis, ComputerSystem, ComputerSystemAttributes, + EndpointExplorationError, EndpointExplorationReport, EndpointType, EthernetInterface, + Inventory, Manager, PCIeDevice, PowerState, Service, UefiDevicePath, }; use crate::test_support::HardwareInfoTemplate; @@ -40,10 +40,10 @@ pub struct DpuConfig { pub override_hosts_uefi_device_path: Option, pub hardware_info_template: HardwareInfoTemplate, /// The `nic_mode` value included in the DPU's `EndpointExplorationReport`. - /// Defaults to `Some(NicMode::Dpu)`; tests exercising the auto-correct - /// path override this to `Some(NicMode::Nic)` to simulate a DPU whose + /// Defaults to `Some(BlueFieldOperatingMode::Dpu)`; tests exercising the auto-correct + /// path override this to `Some(BlueFieldOperatingMode::Nic)` to simulate a DPU whose /// hardware mode doesn't match the operator-declared mode. - pub nic_mode: Option, + pub nic_mode: Option, } impl From<&DpuConfig> for HardwareInfo { diff --git a/crates/api-model/src/test_support/machine_snapshot.rs b/crates/api-model/src/test_support/machine_snapshot.rs index 4eebceb1cf..5ef509bb81 100644 --- a/crates/api-model/src/test_support/machine_snapshot.rs +++ b/crates/api-model/src/test_support/machine_snapshot.rs @@ -51,7 +51,7 @@ use crate::machine::{ }; use crate::machine_interface::InterfaceType; use crate::network_segment::NetworkSegmentType; -use crate::site_explorer::NicMode; +use crate::site_explorer::BlueFieldOperatingMode; use crate::state_history::StateHistoryRecord; use crate::test_support::dpu::DPU_BF3_INFO_JSON; use crate::test_support::{DpuConfig, HardwareInfoTemplate}; @@ -231,7 +231,7 @@ pub fn dpu_hardware_info(index: u8) -> HardwareInfo { last_exploration_error: None, override_hosts_uefi_device_path: None, hardware_info_template: HardwareInfoTemplate::Custom(DPU_BF3_INFO_JSON), - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), }; HardwareInfo::from(&config) } diff --git a/crates/bmc-explorer/src/computer_system.rs b/crates/bmc-explorer/src/computer_system.rs index ecc5f4fbcb..b56a36dcb2 100644 --- a/crates/bmc-explorer/src/computer_system.rs +++ b/crates/bmc-explorer/src/computer_system.rs @@ -21,9 +21,9 @@ use std::str::FromStr; use carbide_network::{deserialize_input_mac_to_address, sanitized_mac}; use mac_address::MacAddress; use model::site_explorer::{ - BootOption as ModelBootOption, BootOrder as ModelBootOrder, + BlueFieldOperatingMode, BootOption as ModelBootOption, BootOrder as ModelBootOrder, ComputerSystem as ModelComputerSystem, ComputerSystemAttributes, - EthernetInterface as ModelEthernetInterface, MachineSetupDiff, NicMode, PCIeDevice, + EthernetInterface as ModelEthernetInterface, MachineSetupDiff, PCIeDevice, PowerState as ModelPowerState, SecureBootStatus, UefiDevicePath as ModelUefiDevicePath, }; use nv_redfish::computer_system::boot_option::UefiDevicePath as BootOptionUefiDevicePath; @@ -200,7 +200,7 @@ impl ExploredComputerSystem { }) .ok() }); - nic_mode = Self::dpu_mode(&self.system, self.bios.as_ref(), oem_bf); + nic_mode = Self::bluefield_operating_mode(&self.system, self.bios.as_ref(), oem_bf); } let is_bf4_shape = chassis .members @@ -454,11 +454,11 @@ impl ExploredComputerSystem { .transpose() } - fn dpu_mode( + fn bluefield_operating_mode( system: &ComputerSystem, bios: Option<&Bios>, bf_ncs: &NvidiaComputerSystem, - ) -> Option { + ) -> Option { let hw_id = system.hardware_id(); let manufacturer = hw_id.manufacturer.map(|v| v.into_inner()); let model = hw_id.model.map(|v| v.into_inner()); @@ -472,8 +472,8 @@ impl ExploredComputerSystem { | Some("Bluefield 3 SmartNIC Main Card") => { use nv_redfish::oem::nvidia::bluefield::nvidia_computer_system::Mode; bf_ncs.mode().and_then(|v| match v { - Mode::DpuMode => Some(NicMode::Dpu), - Mode::NicMode => Some(NicMode::Nic), + Mode::DpuMode => Some(BlueFieldOperatingMode::Dpu), + Mode::NicMode => Some(BlueFieldOperatingMode::Nic), Mode::UnsupportedValue => None, }) } @@ -482,8 +482,8 @@ impl ExploredComputerSystem { bios.and_then(|bios| bios.attribute("NicMode")) .and_then(|attr| { attr.str_value().and_then(|v| match v { - "NicMode" => Some(NicMode::Nic), - "DpuMode" => Some(NicMode::Dpu), + "NicMode" => Some(BlueFieldOperatingMode::Nic), + "DpuMode" => Some(BlueFieldOperatingMode::Dpu), _ => None, }) }) diff --git a/crates/machine-a-tron/src/api_client.rs b/crates/machine-a-tron/src/api_client.rs index 5eb32ddc13..1d0fb7a582 100644 --- a/crates/machine-a-tron/src/api_client.rs +++ b/crates/machine-a-tron/src/api_client.rs @@ -493,14 +493,15 @@ impl ApiClient { /// Registers a mock expected machine. Static BMC (`bmc_ip_address`) is left unset here; /// real environments set it through the admin CLI / API when DHCP discovery is not used. - /// `dpu_mode` is the per-host operating mode -- pass `Some(NoDpu)` for zero-DPU mock hosts - /// or `Some(NicMode)` for DPU-in-NIC-mode mock hosts; `None` for normal DPU hosts. + /// `dpu_policy` is the per-host policy -- pass `Some(Ignore)` for zero-DPU + /// mock hosts or `Some(UseAsNic)` for DPU-in-NIC-mode mock hosts; `None` for + /// normal DPU hosts. pub async fn add_expected_machine( &self, bmc_mac_address: String, chassis_serial_number: String, rack_id: Option, - dpu_mode: Option, + dpu_policy: Option, ) -> ClientApiResult<()> { self.0 .add_expected_machine(ExpectedMachine { @@ -520,7 +521,7 @@ impl ApiClient { is_dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: dpu_mode.map(|m| m as i32), + dpu_mode: dpu_policy.map(|policy| policy as i32), bmc_ip_allocation: None, host_lifecycle_profile: None, }) diff --git a/crates/machine-a-tron/src/machine_a_tron.rs b/crates/machine-a-tron/src/machine_a_tron.rs index 895ad21a53..ada326ce46 100644 --- a/crates/machine-a-tron/src/machine_a_tron.rs +++ b/crates/machine-a-tron/src/machine_a_tron.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use bmc_mock::HostHardwareType; use bmc_mock::mac_address_pool::PoolConfig as MacAddressPoolConfig; use futures::future::try_join_all; -use rpc::forge::{DpuMode, VpcVirtualizationType}; +use rpc::forge::{HostDpuPolicy, VpcVirtualizationType}; use tokio::sync::mpsc; use uuid::Uuid; @@ -184,16 +184,16 @@ impl MachineATron { .await } _ => { - // Derive the expected `dpu_mode` from the machine's - // MachineConfig: zero-DPU hosts declare `NoDpu`, hosts - // running their DPUs as NICs declare `NicMode`, everything - // else defers to the absolute default (DpuMode). + // Derive the expected `dpu_policy` from the machine's + // MachineConfig: zero-DPU hosts declare `Ignore`, hosts + // running their DPUs as NICs declare `UseAsNic`, and + // everything else defers to the default (`Manage`). // Site-explorer's ingestion gate requires this explicit // declaration for any host without DPU PCIe devices. - let dpu_mode = if machine_config.dpu_per_host_count == 0 { - Some(DpuMode::NoDpu) + let dpu_policy = if machine_config.dpu_per_host_count == 0 { + Some(HostDpuPolicy::Ignore) } else if machine_config.dpus_in_nic_mode { - Some(DpuMode::NicMode) + Some(HostDpuPolicy::UseAsNic) } else { None }; @@ -203,7 +203,7 @@ impl MachineATron { host_info.bmc_mac_address.to_string(), host_info.serial.clone(), rack_id, - dpu_mode, + dpu_policy, ) .await } diff --git a/crates/machine-controller/src/handler.rs b/crates/machine-controller/src/handler.rs index 3f01c4a6ce..c9eb804104 100644 --- a/crates/machine-controller/src/handler.rs +++ b/crates/machine-controller/src/handler.rs @@ -788,8 +788,8 @@ impl MachineStateHandler { // the host in the first place; `associated_dpu_machine_ids` // is empty, and the outer branch above already transitions // to HostInit before we ever reach this. What's nice is, this - // also allows NicMode hosts to get actively reconfigured to - // NIC mode via `set_nic_mode` during site-explorer ingestion, + // also allows `UseAsNic` hosts to get actively reconfigured + // to NIC mode via `set_nic_mode` during site-explorer ingestion, // which is something we do, but `force_dpu_nic_mode` never did. let mut state_handler_outcome = StateHandlerOutcome::do_nothing(); for dpu_snapshot in &mh_snapshot.dpu_snapshots { @@ -6177,7 +6177,7 @@ impl StateHandler for HostMachineStateHandler { if !mh_snapshot.has_managed_dpus() { // No DPU to wait for going down/up -- skip // straight to BomValidating. Covers - // NicMode/NoDpu hosts and anything else + // `UseAsNic`/`Ignore` hosts and anything else // with no DPU snapshots; otherwise we'd // wait `dpu_wait_time` for a DPU that's // never going to come up. @@ -10838,11 +10838,11 @@ async fn set_boot_order_dpu_first_and_handle_no_dpu_error( /// Treat `Err(RedfishError::NoDpu)` as `Ok(None)` *only* when the host is /// declared zero-DPU (`expected_dpu_count == 0`). Other error variants and -/// successful results pass through untouched. The `dpu_mode` gate in +/// successful results pass through untouched. The host DPU policy gate in /// site-explorer is what guarantees `expected_dpu_count == 0` actually -/// means the host carries no managed DPU -- either `NoDpu` (no DPU hardware) -/// or `NicMode` (a DPU intentionally running as a plain NIC). Neither has a -/// DPU to answer Redfish, so a `NoDpu` error is expected, not a fault. +/// means the host carries no managed DPU -- either `Ignore` or `UseAsNic`. +/// Neither has a DPU to answer Redfish, so a `NoDpu` error is expected, not a +/// fault. fn handle_no_dpu_error( result: Result, RedfishError>, expected_dpu_count: usize, diff --git a/crates/preingestion-manager/src/lib.rs b/crates/preingestion-manager/src/lib.rs index a101c33f34..a016aa01b2 100644 --- a/crates/preingestion-manager/src/lib.rs +++ b/crates/preingestion-manager/src/lib.rs @@ -49,8 +49,8 @@ use libredfish::model::update_service::TransferProtocolType; use libredfish::{PowerState, Redfish, RedfishError, SystemPowerControl}; use model::firmware::{Firmware, FirmwareComponent, FirmwareComponentType, FirmwareEntry}; use model::site_explorer::{ - ExploredEndpoint, InitialBmcResetPhase, InitialResetPhase, NicMode, PowerDrainState, - PreingestionState, TimeSyncResetPhase, + BlueFieldOperatingMode, ExploredEndpoint, InitialBmcResetPhase, InitialResetPhase, + PowerDrainState, PreingestionState, TimeSyncResetPhase, }; use opentelemetry::metrics::Meter; use sqlx::PgPool; @@ -331,7 +331,7 @@ async fn one_endpoint( _ => None, }; if let Some(host_bmc_ip) = endpoint_host_bmc_ip - && endpoint.report.nic_mode() == Some(NicMode::Nic) + && endpoint.report.bluefield_operating_mode() == Some(BlueFieldOperatingMode::Nic) { tracing::info!( address = %endpoint.address, diff --git a/crates/redfish/src/libredfish/conv.rs b/crates/redfish/src/libredfish/conv.rs index 54a7a69657..896af38943 100644 --- a/crates/redfish/src/libredfish/conv.rs +++ b/crates/redfish/src/libredfish/conv.rs @@ -19,7 +19,9 @@ use bmc_vendor::BMCVendor; use model::attestation::spdm::{CaCertificate, Evidence}; use model::firmware::FirmwareComponentType; use model::machine::{MachineLastRebootRequestedMode, PowerState as MachinePowerState}; -use model::site_explorer::{BootOption, NicMode, PCIeDevice, PowerState, SystemStatus}; +use model::site_explorer::{ + BlueFieldOperatingMode, BootOption, PCIeDevice, PowerState, SystemStatus, +}; pub trait IntoLibredfish { fn into_libredfish(self) -> T; @@ -113,16 +115,16 @@ impl IntoModel for libredfish::model::BootOption { } } -impl IntoModel for libredfish::model::oem::nvidia_dpu::NicMode { - fn into_model(self) -> NicMode { +impl IntoModel for libredfish::model::oem::nvidia_dpu::NicMode { + fn into_model(self) -> BlueFieldOperatingMode { match self { - Self::Dpu => NicMode::Dpu, - Self::Nic => NicMode::Nic, + Self::Dpu => BlueFieldOperatingMode::Dpu, + Self::Nic => BlueFieldOperatingMode::Nic, } } } -impl IntoLibredfish for NicMode { +impl IntoLibredfish for BlueFieldOperatingMode { fn into_libredfish(self) -> libredfish::model::oem::nvidia_dpu::NicMode { match self { Self::Dpu => libredfish::model::oem::nvidia_dpu::NicMode::Dpu, diff --git a/crates/rpc/build.rs b/crates/rpc/build.rs index 76ec5963df..f58f629617 100644 --- a/crates/rpc/build.rs +++ b/crates/rpc/build.rs @@ -51,7 +51,23 @@ fn main() -> Result<(), Box> { ) .type_attribute( ".forge.DpuMode", - "#[derive(serde::Serialize, serde::Deserialize)]", + "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"snake_case\")]", + ) + .field_attribute( + ".forge.DpuMode.DPU_MODE_UNSPECIFIED", + "#[cfg_attr(feature = \"cli\", value(name = \"unspecified\", hide = true, alias = \"dpu-mode-unspecified\"))] #[serde(rename = \"unspecified\", alias = \"Unspecified\", alias = \"DpuModeUnspecified\", alias = \"dpu_mode_unspecified\", alias = \"DPU_MODE_UNSPECIFIED\")]", + ) + .field_attribute( + ".forge.DpuMode.DPU_MODE", + "#[cfg_attr(feature = \"cli\", value(name = \"manage\", alias = \"dpu-mode\"))] #[serde(rename = \"manage\", alias = \"DpuMode\", alias = \"dpu_mode\", alias = \"DPU_MODE\")]", + ) + .field_attribute( + ".forge.DpuMode.NIC_MODE", + "#[cfg_attr(feature = \"cli\", value(name = \"use-as-nic\", alias = \"nic-mode\"))] #[serde(rename = \"use_as_nic\", alias = \"NicMode\", alias = \"nic_mode\", alias = \"NIC_MODE\")]", + ) + .field_attribute( + ".forge.DpuMode.NO_DPU", + "#[cfg_attr(feature = \"cli\", value(name = \"ignore\", alias = \"no-dpu\"))] #[serde(rename = \"ignore\", alias = \"NoDpu\", alias = \"no_dpu\", alias = \"NO_DPU\")]", ) .type_attribute( ".forge.BmcIpAllocationType", diff --git a/crates/rpc/proto/forge.proto b/crates/rpc/proto/forge.proto index ee83b9e32d..28053611a3 100644 --- a/crates/rpc/proto/forge.proto +++ b/crates/rpc/proto/forge.proto @@ -6003,19 +6003,22 @@ message ExpectedHostNic { optional NetworkSegmentType network_segment_type = 7; } -// Per-host DPU operating mode. Lets site operators mix DPU-mode, -// NIC-mode, and no-DPU hosts within a single site. +// Legacy protobuf representation of the per-host policy for how NICo treats +// DPU hardware. The protobuf type and symbols remain named `DpuMode` for API +// compatibility; model and configuration code translate this boundary value +// immediately to `HostDpuPolicy`. // -// - DPU_MODE: The default. Attached DPUs are managed by NICo (DPU upgrades, -// overlay networking, etc). -// - NIC_MODE: DPUs are present physically but operate as plain NICs. Site -// explorer reconfigures them to NIC mode if needed; the host is then -// tracked as if it had no DPUs. -// - NO_DPU: no DPU hardware at all -- a plain host NIC on the underlay. +// - DPU_MODE: `HostDpuPolicy::Manage`. Attached DPUs are managed by NICo. At +// the per-host boundary this retains the legacy inheritance behavior and +// defers to the site policy. +// - NIC_MODE: `HostDpuPolicy::UseAsNic`. Physically present DPUs operate as +// plain NICs and the host is tracked as if it had no DPUs. +// - NO_DPU: `HostDpuPolicy::Ignore`. NICo does not configure or attach DPU +// hardware. // -// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," -// which comes from `[site_explorer] dpu_mode` (and falls back to -// DPU_MODE when that's unset). +// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," which +// comes from `[site_explorer] dpu_policy` (and falls back to `Manage` when +// that's unset). enum DpuMode { DPU_MODE_UNSPECIFIED = 0; DPU_MODE = 1; @@ -6075,9 +6078,11 @@ message ExpectedMachine { // When true, site-explorer skips BMC password rotation and stores the // factory-default credentials in Vault as-is. optional bool bmc_retain_credentials = 15; - // Per-host DPU operating mode. See `DpuMode` above. Unset means "use - // the site-wide `[site_explorer] dpu_mode` default" (which itself - // defaults to DPU_MODE when unset). + // Per-host DPU policy. See the legacy protobuf representation above. Unset means "use the + // site-wide `[site_explorer] dpu_policy` default" (which itself defaults to + // `Manage` when unset). The protobuf type and field keep their legacy names + // so existing binary, protobuf-JSON, and generated clients remain compatible; + // model and configuration surfaces call this `dpu_policy`. optional DpuMode dpu_mode = 16; // Per-host lifecycle profile. When unset in a patch/update request, the // existing DB value is preserved via COALESCE. diff --git a/crates/rpc/src/model/expected_machine.rs b/crates/rpc/src/model/expected_machine.rs index 9cf07fb31f..6da864b22e 100644 --- a/crates/rpc/src/model/expected_machine.rs +++ b/crates/rpc/src/model/expected_machine.rs @@ -18,8 +18,9 @@ use std::net::IpAddr; use mac_address::MacAddress; use model::expected_machine::{ - BmcIpAllocationType, DpuMode, ExpectedHostNic, ExpectedMachine, ExpectedMachineData, - ExpectedMachineRequest, HostLifecycleProfile, LinkedExpectedMachine, UnexpectedMachine, + BmcIpAllocationType, ExpectedHostNic, ExpectedMachine, ExpectedMachineData, + ExpectedMachineRequest, HostDpuPolicy, HostLifecycleProfile, LinkedExpectedMachine, + UnexpectedMachine, }; use model::metadata::Metadata; use model::network_segment::NetworkSegmentType; @@ -29,26 +30,26 @@ use crate as rpc; use crate::errors::RpcDataConversionError; use crate::model::RpcTryFrom; -impl From for rpc::forge::DpuMode { - fn from(mode: DpuMode) -> Self { - match mode { - DpuMode::DpuMode => rpc::forge::DpuMode::DpuMode, - DpuMode::NicMode => rpc::forge::DpuMode::NicMode, - DpuMode::NoDpu => rpc::forge::DpuMode::NoDpu, +impl From for rpc::forge::HostDpuPolicy { + fn from(policy: HostDpuPolicy) -> Self { + match policy { + HostDpuPolicy::Manage => rpc::forge::HostDpuPolicy::Manage, + HostDpuPolicy::UseAsNic => rpc::forge::HostDpuPolicy::UseAsNic, + HostDpuPolicy::Ignore => rpc::forge::HostDpuPolicy::Ignore, } } } -impl From for DpuMode { - fn from(mode: rpc::forge::DpuMode) -> Self { - match mode { - rpc::forge::DpuMode::DpuMode => DpuMode::DpuMode, - rpc::forge::DpuMode::NicMode => DpuMode::NicMode, - rpc::forge::DpuMode::NoDpu => DpuMode::NoDpu, +impl From for HostDpuPolicy { + fn from(policy: rpc::forge::HostDpuPolicy) -> Self { + match policy { + rpc::forge::HostDpuPolicy::Manage => HostDpuPolicy::Manage, + rpc::forge::HostDpuPolicy::UseAsNic => HostDpuPolicy::UseAsNic, + rpc::forge::HostDpuPolicy::Ignore => HostDpuPolicy::Ignore, // Unspecified (0) or any unknown value means "use the default", // which preserves behavior for old clients that don't send the // field at all. - rpc::forge::DpuMode::Unspecified => DpuMode::default(), + rpc::forge::HostDpuPolicy::Unspecified => HostDpuPolicy::default(), } } } @@ -189,11 +190,11 @@ impl From for rpc::forge::ExpectedMachine { .bmc_ip_address .map(|ip| ip.to_string()), bmc_retain_credentials: expected_machine.data.bmc_retain_credentials.filter(|&v| v), - // Only emit `dpu_mode` when it's non-default (which matches the - // bmc_retain_credentials filter pattern above). - dpu_mode: match expected_machine.data.dpu_mode { - DpuMode::DpuMode => None, - other => Some(rpc::forge::DpuMode::from(other) as i32), + // The wire field retains its legacy `dpu_mode` name for ProtoJSON + // compatibility. Only emit it when the policy is non-default. + dpu_mode: match expected_machine.data.dpu_policy { + HostDpuPolicy::Manage => None, + other => Some(rpc::forge::HostDpuPolicy::from(other) as i32), }, // Only emit `bmc_ip_allocation` when it's non-default (Auto), so an // unset field round-trips and older clients keep falling back to Auto. @@ -265,13 +266,13 @@ impl TryFrom for ExpectedMachineData { })?), }, bmc_retain_credentials: em.bmc_retain_credentials, - // `dpu_mode` is optional on the wire; missing / ::Unspecified - // both fall back to `DpuMode::default()`, which is ::DpuMode, - // so old clients continue to behave as before. - dpu_mode: em + // The legacy wire field `dpu_mode` is optional. Missing, + // `Unspecified`, and unknown numeric values retain the existing + // fallback to `HostDpuPolicy::Manage`. + dpu_policy: em .dpu_mode - .map(|i| rpc::forge::DpuMode::try_from(i).unwrap_or_default()) - .map(DpuMode::from) + .map(|i| rpc::forge::HostDpuPolicy::try_from(i).unwrap_or_default()) + .map(HostDpuPolicy::from) .unwrap_or_default(), // `bmc_ip_allocation` is optional on the wire; an unset field (and the // ::Unspecified discriminant) falls back to `BmcIpAllocationType::default()` @@ -324,42 +325,194 @@ fn metadata_from_request( #[cfg(test)] mod tests { use carbide_test_support::Outcome::*; - use carbide_test_support::{scenarios, value_scenarios}; + use carbide_test_support::{Check, check_values, scenarios, value_scenarios}; + use prost::Message; use super::*; - /// `DpuMode::from(rpc::forge::DpuMode)` maps each named variant onto its - /// model twin, and Unspecified (what old clients send) onto the default — - /// which keeps existing deployments behaving as before. The named rows also - /// stand in for the model -> rpc -> model round trip, since the rpc input is - /// exactly what `rpc::forge::DpuMode::from(model)` produces. + /// The legacy protobuf `DpuMode` boundary maps each named value onto its + /// policy-oriented model twin, and Unspecified (what old clients send) onto + /// the default. `rpc::forge::HostDpuPolicy` is a Rust-only alias that keeps + /// new callers on policy vocabulary without changing the descriptor. #[test] - fn rpc_dpu_mode_maps_to_model() { + fn rpc_host_dpu_policy_maps_to_model() { value_scenarios!( - run = DpuMode::from; + run = HostDpuPolicy::from; "unspecified maps to default" { - rpc::forge::DpuMode::Unspecified => DpuMode::default(), + rpc::forge::HostDpuPolicy::Unspecified => HostDpuPolicy::default(), + } + + "manage round trips" { + rpc::forge::HostDpuPolicy::Manage => HostDpuPolicy::Manage, + } + + "use as NIC round trips" { + rpc::forge::HostDpuPolicy::UseAsNic => HostDpuPolicy::UseAsNic, } - "dpu mode round trips" { - rpc::forge::DpuMode::DpuMode => DpuMode::DpuMode, + "ignore round trips" { + rpc::forge::HostDpuPolicy::Ignore => HostDpuPolicy::Ignore, + } + ); + } + + /// The host DPU policy default is Manage, which is what the Unspecified mapping + /// above relies on. + #[test] + fn host_dpu_policy_default_is_manage() { + assert_eq!(HostDpuPolicy::default(), HostDpuPolicy::Manage); + } + + /// The policy refactor must not change the protobuf bytes consumed by + /// existing clients: field 16 remains a varint and values 0 through 3 retain + /// their legacy meanings. + #[test] + fn host_dpu_policy_preserves_legacy_wire_encoding() { + check_values( + [ + Check { + scenario: "unspecified remains 0", + input: rpc::forge::HostDpuPolicy::Unspecified, + expect: vec![0x80, 0x01, 0x00], + }, + Check { + scenario: "manage remains DPU_MODE 1", + input: rpc::forge::HostDpuPolicy::Manage, + expect: vec![0x80, 0x01, 0x01], + }, + Check { + scenario: "use-as-NIC remains NIC_MODE 2", + input: rpc::forge::HostDpuPolicy::UseAsNic, + expect: vec![0x80, 0x01, 0x02], + }, + Check { + scenario: "ignore remains NO_DPU 3", + input: rpc::forge::HostDpuPolicy::Ignore, + expect: vec![0x80, 0x01, 0x03], + }, + ], + |policy| { + rpc::forge::ExpectedMachine { + dpu_mode: Some(policy as i32), + ..Default::default() + } + .encode_to_vec() + }, + ); + } + + /// Reflection-backed and generated clients must retain the legacy field, + /// enum type, and enum symbols even though new Rust/model code uses the + /// policy-oriented alias. + #[test] + fn host_dpu_policy_preserves_legacy_protojson_descriptor() { + let descriptor_set = + prost_types::FileDescriptorSet::decode(rpc::REFLECTION_API_SERVICE_DESCRIPTOR).unwrap(); + let forge = descriptor_set + .file + .iter() + .find(|file| file.package.as_deref() == Some("forge")) + .unwrap(); + let expected_machine = forge + .message_type + .iter() + .find(|message| message.name.as_deref() == Some("ExpectedMachine")) + .unwrap(); + let policy_field = expected_machine + .field + .iter() + .find(|field| field.number == Some(16)) + .unwrap(); + + assert_eq!(policy_field.name.as_deref(), Some("dpu_mode")); + assert_eq!(policy_field.json_name.as_deref(), Some("dpuMode")); + assert_eq!(policy_field.type_name.as_deref(), Some(".forge.DpuMode")); + + let policy_enum = forge + .enum_type + .iter() + .find(|enumeration| enumeration.name.as_deref() == Some("DpuMode")) + .unwrap(); + let names_and_numbers = policy_enum + .value + .iter() + .map(|value| (value.name.as_deref().unwrap(), value.number.unwrap())) + .collect::>(); + for legacy_value in [ + ("DPU_MODE_UNSPECIFIED", 0), + ("DPU_MODE", 1), + ("NIC_MODE", 2), + ("NO_DPU", 3), + ] { + assert!(names_and_numbers.contains(&legacy_value)); + } + + assert_eq!(rpc::forge::HostDpuPolicy::Manage.as_str_name(), "DPU_MODE"); + } + + /// Direct Serde consumers (notably admin file input) accept both the new + /// policy vocabulary and every legacy DpuMode spelling they used before. + #[test] + fn host_dpu_policy_deserializes_canonical_and_legacy_values() { + scenarios!( + run = |json| serde_json::from_str::(json).map_err(drop); + "canonical policy values" { + r#""manage""# => Yields(rpc::forge::HostDpuPolicy::Manage), + r#""use_as_nic""# => Yields(rpc::forge::HostDpuPolicy::UseAsNic), + r#""ignore""# => Yields(rpc::forge::HostDpuPolicy::Ignore), } - "nic mode round trips" { - rpc::forge::DpuMode::NicMode => DpuMode::NicMode, + "legacy Rust enum values" { + r#""DpuMode""# => Yields(rpc::forge::HostDpuPolicy::Manage), + r#""NicMode""# => Yields(rpc::forge::HostDpuPolicy::UseAsNic), + r#""NoDpu""# => Yields(rpc::forge::HostDpuPolicy::Ignore), } - "no dpu round trips" { - rpc::forge::DpuMode::NoDpu => DpuMode::NoDpu, + "legacy config values" { + r#""dpu_mode""# => Yields(rpc::forge::HostDpuPolicy::Manage), + r#""nic_mode""# => Yields(rpc::forge::HostDpuPolicy::UseAsNic), + r#""no_dpu""# => Yields(rpc::forge::HostDpuPolicy::Ignore), } ); } - /// The DpuMode default is DpuMode, which is what the Unspecified mapping above - /// relies on. #[test] - fn dpu_mode_default_is_dpu_mode() { - assert_eq!(DpuMode::default(), DpuMode::DpuMode); + fn host_dpu_policy_serializes_and_round_trips_canonical_values() { + scenarios!( + run = |policy| { + let json = serde_json::to_string(&policy).map_err(drop)?; + let recovered = + serde_json::from_str::(&json).map_err(drop)?; + Ok::<_, ()>((json, recovered)) + }; + "unspecified boundary value" { + rpc::forge::HostDpuPolicy::Unspecified => Yields(( + r#""unspecified""#.to_string(), + rpc::forge::HostDpuPolicy::Unspecified, + )), + } + + "manage" { + rpc::forge::HostDpuPolicy::Manage => Yields(( + r#""manage""#.to_string(), + rpc::forge::HostDpuPolicy::Manage, + )), + } + + "use as NIC" { + rpc::forge::HostDpuPolicy::UseAsNic => Yields(( + r#""use_as_nic""#.to_string(), + rpc::forge::HostDpuPolicy::UseAsNic, + )), + } + + "ignore" { + rpc::forge::HostDpuPolicy::Ignore => Yields(( + r#""ignore""#.to_string(), + rpc::forge::HostDpuPolicy::Ignore, + )), + } + ); } /// `BmcIpAllocationType::from(rpc::forge::BmcIpAllocationType)` maps each diff --git a/crates/rpc/src/model/site_explorer.rs b/crates/rpc/src/model/site_explorer.rs index 47e7343308..94787cf61c 100644 --- a/crates/rpc/src/model/site_explorer.rs +++ b/crates/rpc/src/model/site_explorer.rs @@ -17,12 +17,13 @@ use model::errors::{OperatorError, OperatorErrorSchema}; use model::site_explorer::{ - BootOption, BootOrder, Chassis, ComputerSystem, ComputerSystemAttributes, - EndpointExplorationReport, EthernetInterface, ExploredDpu, ExploredEndpoint, - ExploredEndpointSearchFilter, ExploredManagedHost, ExploredManagedHostSearchFilter, - ExploredMlxDevice, InternalLockdownStatus, Inventory, LockdownStatus, MachineSetupDiff, - MachineSetupStatus, Manager, MlxDeviceKind, NetworkAdapter, NicMode, PCIeDevice, PowerState, - SecureBootStatus, Service, SiteExplorationReport, SiteExplorerLastRun, SystemStatus, + BlueFieldOperatingMode, BootOption, BootOrder, Chassis, ComputerSystem, + ComputerSystemAttributes, EndpointExplorationReport, EthernetInterface, ExploredDpu, + ExploredEndpoint, ExploredEndpointSearchFilter, ExploredManagedHost, + ExploredManagedHostSearchFilter, ExploredMlxDevice, InternalLockdownStatus, Inventory, + LockdownStatus, MachineSetupDiff, MachineSetupStatus, Manager, MlxDeviceKind, NetworkAdapter, + PCIeDevice, PowerState, SecureBootStatus, Service, SiteExplorationReport, SiteExplorerLastRun, + SystemStatus, }; use crate as rpc; @@ -165,6 +166,15 @@ impl From for rpc::site_explorer::MlxDeviceKind { } } +impl From for rpc::site_explorer::BlueFieldOperatingMode { + fn from(mode: BlueFieldOperatingMode) -> Self { + match mode { + BlueFieldOperatingMode::Dpu => Self::Dpu, + BlueFieldOperatingMode::Nic => Self::Nic, + } + } +} + impl From for rpc::site_explorer::ExploredMlxDevice { fn from(device: ExploredMlxDevice) -> Self { rpc::site_explorer::ExploredMlxDevice { @@ -177,10 +187,9 @@ impl From for rpc::site_explorer::ExploredMlxDevice { firmware_version: device.firmware_version, description: device.description, dpu_bmc_ip: device.dpu_bmc_ip.map(|ip| ip.to_string()), - nic_mode: device.nic_mode.map(|m| match m { - NicMode::Nic => rpc::site_explorer::NicMode::Nic as i32, - NicMode::Dpu => rpc::site_explorer::NicMode::Dpu as i32, - }), + nic_mode: device + .nic_mode + .map(|mode| rpc::site_explorer::BlueFieldOperatingMode::from(mode) as i32), } } } @@ -188,10 +197,9 @@ impl From for rpc::site_explorer::ExploredMlxDevice { impl From for rpc::site_explorer::ComputerSystemAttributes { fn from(attributes: ComputerSystemAttributes) -> Self { rpc::site_explorer::ComputerSystemAttributes { - nic_mode: attributes.nic_mode.map(|a| match a { - NicMode::Nic => rpc::site_explorer::NicMode::Nic.into(), - NicMode::Dpu => rpc::site_explorer::NicMode::Dpu.into(), - }), + nic_mode: attributes + .nic_mode + .map(|mode| rpc::site_explorer::BlueFieldOperatingMode::from(mode).into()), } } } @@ -420,7 +428,9 @@ impl From for rpc::site_explorer::OperatorErrorSchema { #[cfg(test)] mod tests { + use carbide_test_support::value_scenarios; use model::site_explorer::EndpointExplorationError; + use prost::Message; use super::*; @@ -446,4 +456,61 @@ mod tests { assert_eq!(actual_schema.mitigation, expected_schema.mitigation); assert!(report.last_exploration_error.is_some()); } + + /// Reflection-backed and generated clients retain the legacy protobuf type + /// while new Rust callers use the observed-state alias. + #[test] + fn bluefield_operating_mode_preserves_legacy_protojson_descriptor() { + let descriptor_set = + prost_types::FileDescriptorSet::decode(rpc::REFLECTION_API_SERVICE_DESCRIPTOR).unwrap(); + let site_explorer = descriptor_set + .file + .iter() + .find(|file| file.package.as_deref() == Some("site_explorer")) + .unwrap(); + + let operating_mode = site_explorer + .enum_type + .iter() + .find(|enumeration| enumeration.name.as_deref() == Some("NicMode")) + .unwrap(); + let names_and_numbers = operating_mode + .value + .iter() + .map(|value| (value.name.as_deref().unwrap(), value.number.unwrap())) + .collect::>(); + assert_eq!(names_and_numbers, [("DPU", 0), ("NIC", 1)]); + + let explored_device = site_explorer + .message_type + .iter() + .find(|message| message.name.as_deref() == Some("ExploredMlxDevice")) + .unwrap(); + let mode_field = explored_device + .field + .iter() + .find(|field| field.number == Some(10)) + .unwrap(); + assert_eq!(mode_field.name.as_deref(), Some("nic_mode")); + assert_eq!(mode_field.json_name.as_deref(), Some("nicMode")); + assert_eq!( + mode_field.type_name.as_deref(), + Some(".site_explorer.NicMode") + ); + + assert_eq!( + rpc::site_explorer::BlueFieldOperatingMode::Nic.as_str_name(), + "NIC" + ); + value_scenarios!( + run = rpc::site_explorer::BlueFieldOperatingMode::from; + "DPU mode" { + BlueFieldOperatingMode::Dpu => rpc::site_explorer::BlueFieldOperatingMode::Dpu, + } + + "NIC mode" { + BlueFieldOperatingMode::Nic => rpc::site_explorer::BlueFieldOperatingMode::Nic, + } + ); + } } diff --git a/crates/rpc/src/protos/mod.rs b/crates/rpc/src/protos/mod.rs index 01f0490d54..80c7ccb50d 100644 --- a/crates/rpc/src/protos/mod.rs +++ b/crates/rpc/src/protos/mod.rs @@ -35,6 +35,20 @@ pub mod scout_firmware_upgrade { #[rustfmt::skip] pub mod forge { include!(concat!(env!("OUT_DIR"), "/forge.rs")); + + /// Policy-oriented Rust name for the legacy protobuf `DpuMode` boundary. + /// + /// The protobuf descriptor retains `DpuMode` for compatibility with + /// existing generated clients. New Rust callers should use this alias and + /// its policy-oriented associated constants. + pub type HostDpuPolicy = DpuMode; + + #[allow(non_upper_case_globals)] + impl DpuMode { + pub const Manage: Self = Self::DpuMode; + pub const UseAsNic: Self = Self::NicMode; + pub const Ignore: Self = Self::NoDpu; + } } #[allow(non_snake_case, unknown_lints, clippy::all)] @@ -65,6 +79,12 @@ pub mod mlx_device { #[rustfmt::skip] pub mod site_explorer { include!(concat!(env!("OUT_DIR"), "/site_explorer.rs")); + + /// Observed-state Rust name for the legacy protobuf `NicMode` boundary. + /// + /// The protobuf descriptor retains `NicMode` for compatibility with + /// existing generated clients. New Rust callers should use this alias. + pub type BlueFieldOperatingMode = NicMode; } #[allow(non_snake_case, unknown_lints, clippy::all)] diff --git a/crates/site-explorer/src/bmc_endpoint_explorer.rs b/crates/site-explorer/src/bmc_endpoint_explorer.rs index efb0ddde0c..46633dee0c 100644 --- a/crates/site-explorer/src/bmc_endpoint_explorer.rs +++ b/crates/site-explorer/src/bmc_endpoint_explorer.rs @@ -33,7 +33,7 @@ use model::expected_entity::{BmcCredentialsData, ExpectedEntity}; use model::expected_switch::ExpectedSwitch; use model::machine::MachineInterfaceSnapshot; use model::site_explorer::{ - EndpointExplorationError, EndpointExplorationReport, LockdownStatus, NicMode, + BlueFieldOperatingMode, EndpointExplorationError, EndpointExplorationReport, LockdownStatus, }; use sqlx::PgPool; @@ -507,7 +507,7 @@ impl BmcEndpointExplorer { &self, bmc_ip_address: SocketAddr, credentials: Credentials, - mode: NicMode, + mode: BlueFieldOperatingMode, ) -> Result<(), EndpointExplorationError> { self.redfish_client .set_nic_mode(bmc_ip_address, credentials, mode.into_libredfish()) @@ -1123,7 +1123,7 @@ impl EndpointExplorer for BmcEndpointExplorer { &self, bmc_ip_address: SocketAddr, interface: &MachineInterfaceSnapshot, - mode: NicMode, + mode: BlueFieldOperatingMode, ) -> Result<(), EndpointExplorationError> { let bmc_mac_address = interface.mac_address; diff --git a/crates/site-explorer/src/config.rs b/crates/site-explorer/src/config.rs index ff6cc46682..240b6ebdcb 100644 --- a/crates/site-explorer/src/config.rs +++ b/crates/site-explorer/src/config.rs @@ -26,7 +26,7 @@ use carbide_utils::config::{ }; use chrono::Duration; use duration_str::{deserialize_duration, deserialize_duration_chrono}; -use model::expected_machine::DpuMode; +use model::expected_machine::HostDpuPolicy; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// SiteExplorer related configuration for hardware discovery and ingestion. @@ -175,15 +175,14 @@ pub struct SiteExplorerConfig { #[serde(default = "SiteExplorerConfig::default_switches_created_per_run")] pub switches_created_per_run: u64, - /// Site-wide DPU operating mode. When set, applies to every host - /// that doesn't declare a per-host `ExpectedMachine.dpu_mode` - /// override (or that declares the default `DpuMode` variant, - /// which is indistinguishable from "unset"). Per-host `NicMode` / - /// `NoDpu` always wins. `None` means "site-wide setting unset" - /// and hosts fall back to the absolute default of - /// `DpuMode::DpuMode`. - #[serde(default)] - pub dpu_mode: Option, + /// Site-wide host DPU policy. Per-host `UseAsNic` and `Ignore` policies + /// override it; per-host `Manage` inherits it for backward compatibility. + /// `None` falls back to [`HostDpuPolicy::Manage`]. + /// + /// The legacy `dpu_mode` field and values remain accepted during + /// deserialization. + #[serde(default, alias = "dpu_mode")] + pub dpu_policy: Option, /// Controls which Redfish client implementation is used /// for hardware discovery (LibRedfish, NvRedfish, or @@ -214,7 +213,7 @@ impl Default for SiteExplorerConfig { create_switches: Self::default_create_switches(), switches_created_per_run: Self::default_switches_created_per_run(), rotate_switch_nvos_credentials: Self::default_rotate_switch_nvos_credentials(), - dpu_mode: None, + dpu_policy: None, explore_mode: Self::default_explore_mode(), } } @@ -244,7 +243,7 @@ impl PartialEq for SiteExplorerConfig { power_shelves_created_per_run, create_switches, switches_created_per_run, - dpu_mode, + dpu_policy, explore_mode, } = self; @@ -276,7 +275,7 @@ impl PartialEq for SiteExplorerConfig { && create_switches.load(AtomicOrdering::Relaxed) == other.create_switches.load(AtomicOrdering::Relaxed) && *switches_created_per_run == other.switches_created_per_run - && *dpu_mode == other.dpu_mode + && *dpu_policy == other.dpu_policy && *explore_mode == other.explore_mode } } diff --git a/crates/site-explorer/src/endpoint_explorer.rs b/crates/site-explorer/src/endpoint_explorer.rs index 65d16f1889..6e3f2b3df1 100644 --- a/crates/site-explorer/src/endpoint_explorer.rs +++ b/crates/site-explorer/src/endpoint_explorer.rs @@ -24,7 +24,7 @@ use mac_address::MacAddress; use model::expected_entity::ExpectedEntity; use model::machine::MachineInterfaceSnapshot; use model::site_explorer::{ - EndpointExplorationError, EndpointExplorationReport, LockdownStatus, NicMode, + BlueFieldOperatingMode, EndpointExplorationError, EndpointExplorationReport, LockdownStatus, }; use super::metrics::SiteExplorationMetrics; @@ -129,7 +129,7 @@ pub trait EndpointExplorer: Send + Sync + 'static { &self, address: SocketAddr, interface: &MachineInterfaceSnapshot, - mode: NicMode, + mode: BlueFieldOperatingMode, ) -> Result<(), EndpointExplorationError>; async fn is_viking( diff --git a/crates/site-explorer/src/lib.rs b/crates/site-explorer/src/lib.rs index cced6bad5e..f8aee196ca 100644 --- a/crates/site-explorer/src/lib.rs +++ b/crates/site-explorer/src/lib.rs @@ -51,8 +51,8 @@ use model::power_shelf::{NewPowerShelf, PowerShelfConfig}; use model::rack_type::RackProfileConfig; use model::resource_pool::common::CommonPools; use model::site_explorer::{ - EndpointExplorationError, EndpointExplorationReport, EndpointType, ExploredDpu, - ExploredEndpoint, ExploredManagedHost, ExploredManagedSwitch, MachineExpectation, NicMode, + BlueFieldOperatingMode, EndpointExplorationError, EndpointExplorationReport, EndpointType, + ExploredDpu, ExploredEndpoint, ExploredManagedHost, ExploredManagedSwitch, MachineExpectation, PowerState, PreingestionState, Service, SiteExplorerLastRun, is_bf3_dpu_part_number, is_bf3_supernic_part_number, is_bluefield_part_number, is_bluefield_system, }; @@ -83,7 +83,7 @@ use db::ObjectColumnFilter; use db::work_lock_manager::WorkLockManagerHandle; pub use managed_host::is_endpoint_in_managed_host; use model::DpuModel; -use model::expected_machine::DpuMode; +use model::expected_machine::HostDpuPolicy; use model::firmware::FirmwareComponentType; use model::network_segment::NetworkSegmentType; mod switch_creator; @@ -1209,16 +1209,16 @@ impl SiteExplorer { explored_dpus: HashMap, explored_hosts: HashMap, ) -> SiteExplorerResult> { - // Per-host DPU-mode resolution. Precedence: - // 1. Per-host `ExpectedMachine.dpu_mode` (NicMode / NoDpu wins). - // 2. Site-wide `SiteExplorerConfig.dpu_mode` setting. - // 3. Otherwise: `DpuMode::DpuMode` (the absolute default). - let site_dpu_mode = self.config.dpu_mode; - let effective_mode = |host_bmc_ip: &IpAddr| -> DpuMode { + // Per-host DPU policy resolution. Precedence: + // 1. Per-host `ExpectedMachine.dpu_policy` (`UseAsNic` / `Ignore` wins). + // 2. Site-wide `SiteExplorerConfig.dpu_policy` setting. + // 3. Otherwise: `HostDpuPolicy::Manage` (the absolute default). + let site_dpu_policy = self.config.dpu_policy; + let effective_policy = |host_bmc_ip: &IpAddr| -> HostDpuPolicy { let declared = expected_explored_endpoint_index .matched_expected_machine(host_bmc_ip) - .map(|em| em.data.dpu_mode); - DpuMode::resolve(declared, site_dpu_mode) + .map(|em| em.data.dpu_policy); + HostDpuPolicy::resolve(declared, site_dpu_policy) }; // Match HOST and DPU using the serial Redfish reports for the same // physical card. BF4 does not expose that serial on the DPU system @@ -1242,7 +1242,7 @@ impl SiteExplorer { // machine_interfaces row (matched by MAC), so the primary-flagged // row holds the full pair -- whatever the NIC type (integrated NICs, // SuperNICs, DPU host-PFs, DPUs in NIC mode). This sits before the - // zero-DPU/NoDpu and unmatched-host `continue`s below, so every + // `Ignore` and unmatched-host `continue`s below, so every // explored host is covered -- including a zero-DPU host whose primary // boots from a plain NIC. The UPDATE no-ops for MACs with no row // (e.g. a never-cabled NIC). Last-known-good: only NICs that resolve @@ -1271,22 +1271,40 @@ impl SiteExplorer { } } - // Resolve the operator-declared DPU mode for this host once; + // Resolve the operator-declared DPU policy for this host once; // it drives both auto-correction (`check_and_configure_dpu_mode` // below -- operator override wins over BF3 part-number heuristics) - // and the post-match attach decision (NicMode/NoDpu hosts emit - // a bare managed host regardless of what matched). - let host_dpu_mode = effective_mode(&ep.address); + // and the post-match attach decision (`UseAsNic` / `Ignore` policies + // emit a bare managed host regardless of what matched). + let host_dpu_policy = effective_policy(&ep.address); - // If an operator has declared this host `dpu_mode::NoDpu`, - // treat it as zero-DPU, regardless of what BMC hardware + // A declared `ExpectedHostNic.primary` (when the matched expected + // machine sets one) wins over the automatic DPU-PF pick, so the + // explored default names the same NIC the managed store will. + let declared_primary = expected_explored_endpoint_index + .matched_expected_machine(&ep.address) + .and_then(|expected| expected.data.declared_primary_mac()); + + // If the resolved policy says to ignore this host's DPUs, treat it + // as zero-DPU, regardless of what BMC hardware // enumeration says about attached DPUs. Without this check, // we can't ingest hosts which may have >= DPUs, but aren't // actively using them. For instance, a machine may have DPUs // that aren't actually cabled up, and we're instead using a // basic NIC. Since they aren't cabled, we'll never be able to // discover + link them; just ignore them entirely. - if matches!(host_dpu_mode, DpuMode::NoDpu) { + if matches!(host_dpu_policy, HostDpuPolicy::Ignore) { + // `Ignore` deliberately skips DPU discovery, but the host's + // explicitly declared primary NIC still needs to refresh the + // endpoint-level boot-interface pair. Keep the DPU slice empty + // so ignored DPU PFs can never become fallback primaries. + if let Some(mac_address) = ep + .report + .fetch_host_primary_interface_mac(&[], declared_primary) + { + queue_host_boot_interface(&ep, mac_address, &mut boot_interfaces); + } + managed_hosts.push(( ExploredManagedHost { host_bmc_ip: ep.address, @@ -1325,7 +1343,7 @@ impl SiteExplorer { pcie_device.part_number.as_deref(), pcie_device.serial_number.as_deref(), &dpu_sn_to_endpoint, - host_dpu_mode, + host_dpu_policy, &ep, &mut dpu_exploration, metrics, @@ -1362,7 +1380,7 @@ impl SiteExplorer { chassis.part_number.as_deref(), chassis.serial_number.as_deref(), &dpu_sn_to_endpoint, - host_dpu_mode, + host_dpu_policy, &ep, &mut dpu_exploration, metrics, @@ -1374,7 +1392,7 @@ impl SiteExplorer { network_adapter.part_number.as_deref(), network_adapter.serial_number.as_deref(), &dpu_sn_to_endpoint, - host_dpu_mode, + host_dpu_policy, &ep, &mut dpu_exploration, metrics, @@ -1414,7 +1432,7 @@ impl SiteExplorer { self.check_and_configure_dpu_mode( &dpu_ep, dpu_ep.report.dpu_part_number(), - host_dpu_mode, + host_dpu_policy, metrics, ) .await, @@ -1509,9 +1527,10 @@ impl SiteExplorer { } } else { // We power-cycled within the rate limit and the - // DPUs still aren't in the declared mode -- either - // the change is mid-flight (the host is booting, a - // pass or two of normal convergence) or this + // DPUs still aren't in the target operating + // mode -- either the change is mid-flight (the + // host is booting, a pass or two of normal + // convergence) or this // vendor's `PowerCycle` is a warm reset that never // actually drops power. Keep the pairing-blocker // signal standing so a host stuck in the warm-reset @@ -1530,29 +1549,26 @@ impl SiteExplorer { } continue; - } else if matches!(host_dpu_mode, DpuMode::DpuMode) { + } else if host_dpu_policy.expects_managed_dpus() { // Host has no DPU PCIe devices reported by its - // BMC, and the effective `dpu_mode` is the - // default (`DpuMode`) -- i.e. neither per-host - // on `ExpectedMachine.dpu_mode` nor site-wide on - // `[site_explorer] dpu_mode` declared this host - // as zero-DPU. We expect DPUs but found none -- + // BMC, and the effective policy is `Manage` -- + // i.e. neither per-host `ExpectedMachine.dpu_policy` + // nor site-wide `[site_explorer] dpu_policy` declared + // this host as zero-DPU. We expect DPUs but found none -- // probably a misconfiguration or a DPU-discovery // bug. Skip ingestion this cycle; site-explorer // will retry on the next iteration, giving the // operator a chance to either fix the host or - // declare it as `NoDpu`. + // select `HostDpuPolicy::Ignore` for it. // - // (`NoDpu` hosts are handled by the fast-path - // earlier in the loop; `NicMode` hosts fall - // through to the push below with an empty `dpus` - // vector -- the operator already declared - // "treat as zero-DPU.") + // `Ignore` hosts are handled by the fast-path earlier in + // the loop; `UseAsNic` hosts fall through to the push + // below with an empty `dpus` vector. tracing::warn!( address = %ep.address, exploration_report = ?ep, - ?host_dpu_mode, - "cannot identify managed host: site explorer sees no DPUs on this host and it isn't declared as `NoDpu`; declare `dpu_mode = \"no_dpu\"` to ingest as zero-DPU", + ?host_dpu_policy, + "cannot identify managed host: site explorer sees no DPUs on this host and its DPU policy is not `Ignore`; declare `dpu_policy = \"ignore\"` to ingest as zero-DPU", ); metrics.increment_host_dpu_pairing_blocker( PairingBlockerReason::NoDpuReportedByHost, @@ -1565,12 +1581,6 @@ impl SiteExplorer { // If we know the booting interface of the host, we should use this for deciding // primary interface. let mut is_sorted = false; - // A declared `ExpectedHostNic.primary` (when the matched expected - // machine sets one) wins over the automatic DPU-PF pick, so the - // explored default names the same NIC the managed store will. - let declared_primary = expected_explored_endpoint_index - .matched_expected_machine(&ep.address) - .and_then(|expected| expected.data.declared_primary_mac()); if let Some(mac_address) = ep .report .fetch_host_primary_interface_mac(&dpus_explored_for_host, declared_primary) @@ -1580,21 +1590,7 @@ impl SiteExplorer { // current report: if the MAC has no matching interface id in // this report, keep the last-known-good stored boot interface // rather than clobbering it with a partial record. - if let Some(interface_id) = ep.report.find_interface_id_for_mac(mac_address) { - boot_interfaces.push(( - ep.address, - MachineBootInterface { - mac_address, - interface_id: interface_id.to_string(), - }, - )); - } else { - tracing::debug!( - address = %ep.address, - %mac_address, - "boot interface MAC has no matching Redfish interface id in the report; keeping last-known-good stored boot interface", - ); - } + queue_host_boot_interface(&ep, mac_address, &mut boot_interfaces); let primary_dpu_position = dpus_explored_for_host .iter() @@ -1639,28 +1635,30 @@ impl SiteExplorer { }); } - // For NicMode hosts, don't attach DPUs even if matching + // For `UseAsNic` hosts, don't attach DPUs even if matching // discovered some: the operator has declared "treat this host // as zero-DPU". Any DPU hardware has already had `set_nic_mode` // issued by the check-and-configure step above if it was in // DPU mode; this cycle we just emit a bare host. - // For NoDpu hosts, we should have already returned/continued - // earlier on after detecting the host_dpu_mode as such, so + // For `Ignore` hosts, we should have already returned/continued + // earlier on after detecting the host policy as such, so // this shouldn't fire. - let dpus = match host_dpu_mode { - DpuMode::NicMode => { + let dpus = match host_dpu_policy { + HostDpuPolicy::UseAsNic => { metrics.increment_dpu_migration_signal( DpuMigrationSignal::RegisteredZeroDpuForNicMode, ); Vec::new() } - DpuMode::DpuMode => dpus_explored_for_host, - // Now that we continue/return early for NoDpu hosts, + HostDpuPolicy::Manage => dpus_explored_for_host, + // Now that we continue/return early for `Ignore` hosts, // we shouldn't actually get here. Probably could be // lazy and just leave it as Vec::new(), but I think // this firing would also surface a bug, which we // probably want. - DpuMode::NoDpu => unreachable!("NoDpu hosts should have already returned early"), + HostDpuPolicy::Ignore => { + unreachable!("ignored-DPU hosts should have already returned early") + } }; managed_hosts.push(( @@ -1728,7 +1726,7 @@ impl SiteExplorer { part_number: Option<&str>, serial_number: Option<&str>, dpu_sn_to_endpoint: &HashMap, - host_dpu_mode: DpuMode, + host_dpu_policy: HostDpuPolicy, host_ep: &ExploredEndpoint, exploration: &mut DpuExplorationState, metrics: &mut SiteExplorationMetrics, @@ -1755,7 +1753,7 @@ impl SiteExplorer { // Resolve the DPU's mode against what the host declared. This is the only // I/O, and may issue a `set_nic_mode` (in which case it returns `Ok(false)`). let mode_check = Some( - self.check_and_configure_dpu_mode(dpu_ep, part_number, host_dpu_mode, metrics) + self.check_and_configure_dpu_mode(dpu_ep, part_number, host_dpu_policy, metrics) .await, ); @@ -2908,8 +2906,8 @@ impl SiteExplorer { return Ok(true); } - match dpu_endpoint.report.nic_mode() { - Some(NicMode::Nic) => { + match dpu_endpoint.report.bluefield_operating_mode() { + Some(BlueFieldOperatingMode::Nic) => { // DPU's in NIC mode do not have full redfish functionality, // for example, we will not be able to retrieve the base GUID // from the redfish response. Skip the next check because the DPUs @@ -2920,7 +2918,7 @@ impl SiteExplorer { ); return Ok(true); } - Some(NicMode::Dpu) => {} + Some(BlueFieldOperatingMode::Dpu) => {} None if dpu_endpoint.report.dpu_pairing_serial_number().is_some() => { tracing::warn!( "Site explorer found an uningested DPU (bmc ip: {}) without a Redfish DPU/NIC mode; continuing because it has a host-pairing serial", @@ -2953,7 +2951,7 @@ impl SiteExplorer { async fn set_nic_mode( &self, dpu_endpoint: &ExploredEndpoint, - mode: NicMode, + mode: BlueFieldOperatingMode, ) -> SiteExplorerResult<()> { let bmc_target_port = self.config.override_target_port.unwrap_or(443); let bmc_target_addr = SocketAddr::new(dpu_endpoint.address, bmc_target_port); @@ -3286,13 +3284,13 @@ impl SiteExplorer { /// the corrected mode). /// /// The target is resolved in priority order: - /// 1. If the operator explicitly declared `DpuMode::NicMode` on the - /// `ExpectedMachine`, target NIC mode (per-host override). - /// 2. If the operator declared `DpuMode::NoDpu`, bail out -- the + /// 1. If the resolved host policy is [`HostDpuPolicy::UseAsNic`], target + /// NIC mode. + /// 2. If the resolved policy is [`HostDpuPolicy::Ignore`], bail out -- the /// `MachineValidation` state handler is where "hardware reports a /// DPU but operator said no DPU" gets surfaced as a health alert; /// we don't try to reconfigure in that case. - /// 3. Otherwise (operator default `DpuMode::DpuMode`), fall back to + /// 3. Otherwise ([`HostDpuPolicy::Manage`]), fall back to /// the existing BF3 SuperNIC / BF3 DPU part-number heuristic for /// backward compat: BF3 SuperNIC → NIC mode, BF3 DPU → DPU mode, /// BF2 / unknown → no-op. @@ -3300,16 +3298,16 @@ impl SiteExplorer { &self, dpu_ep: &ExploredEndpoint, dpu_part_number: Option<&str>, - host_dpu_mode: DpuMode, + host_dpu_policy: HostDpuPolicy, metrics: &mut SiteExplorationMetrics, ) -> SiteExplorerResult { - // Compute the target NIC mode. `None` means "no opinion -- don't + // Compute the target operating mode. `None` means "no opinion -- don't // attempt to reconfigure" (e.g., BF2 where the heuristic doesn't - // apply, or NoDpu where we defer to the health-check path). - let target_nic_mode: Option = match host_dpu_mode { - DpuMode::NicMode => Some(NicMode::Nic), - DpuMode::NoDpu => None, - DpuMode::DpuMode => { + // apply, or `Ignore` where we defer to the health-check path). + let target_operating_mode: Option = match host_dpu_policy { + HostDpuPolicy::UseAsNic => Some(BlueFieldOperatingMode::Nic), + HostDpuPolicy::Ignore => None, + HostDpuPolicy::Manage => { // Preserve existing BF3 part-number heuristics when the operator // hasn't explicitly chosen a mode. Missing part numbers only // disable this heuristic fallback; explicit modes above do not @@ -3318,9 +3316,9 @@ impl SiteExplorer { // default path does not infer or reconfigure BF4 mode, dpu_part_number.and_then(|dpu_part_number| { if is_bf3_supernic_part_number(dpu_part_number) { - Some(NicMode::Nic) + Some(BlueFieldOperatingMode::Nic) } else if is_bf3_dpu_part_number(dpu_part_number) { - Some(NicMode::Dpu) + Some(BlueFieldOperatingMode::Dpu) } else { None } @@ -3328,23 +3326,23 @@ impl SiteExplorer { } }; - let Some(target_nic_mode) = target_nic_mode else { + let Some(target_operating_mode) = target_operating_mode else { return Ok(true); }; - match dpu_ep.report.nic_mode() { - Some(observed) if observed == target_nic_mode => Ok(true), + match dpu_ep.report.bluefield_operating_mode() { + Some(observed) if observed == target_operating_mode => Ok(true), Some(observed) => { tracing::warn!( address = %dpu_ep.address, part_number = ?dpu_part_number, %observed, - ?target_nic_mode, - ?host_dpu_mode, + ?target_operating_mode, + ?host_dpu_policy, "site explorer found a DPU with a mode that does not match the target; will try to reconfigure" ); metrics.increment_dpu_migration_signal(DpuMigrationSignal::ModeMismatchFound); - self.set_nic_mode(dpu_ep, target_nic_mode).await?; + self.set_nic_mode(dpu_ep, target_operating_mode).await?; metrics.increment_dpu_migration_signal(DpuMigrationSignal::SetNicModeIssued); Ok(false) } @@ -3697,7 +3695,10 @@ pub(crate) fn u64_to_mac(value: u64) -> MacAddress { /// Whether a discovered DPU BMC is reporting that it's running as a plain NIC. fn is_dpu_in_nic_mode(dpu_ep: &ExploredEndpoint, host_ep: &ExploredEndpoint) -> bool { - let nic_mode = dpu_ep.report.nic_mode().is_some_and(|m| m == NicMode::Nic); + let nic_mode = dpu_ep + .report + .bluefield_operating_mode() + .is_some_and(|m| m == BlueFieldOperatingMode::Nic); if nic_mode { tracing::info!( address = %dpu_ep.address, @@ -3737,6 +3738,32 @@ fn duplicate_bluefield_serial<'a>( (!seen.insert(serial_number)).then_some(serial_number) } +/// Queues a complete host boot-interface pair for endpoint persistence. +/// +/// A report that resolves only the MAC must not overwrite the last-known-good +/// pair already stored for the endpoint. +fn queue_host_boot_interface( + endpoint: &ExploredEndpoint, + mac_address: MacAddress, + boot_interfaces: &mut Vec<(IpAddr, MachineBootInterface)>, +) { + if let Some(interface_id) = endpoint.report.find_interface_id_for_mac(mac_address) { + boot_interfaces.push(( + endpoint.address, + MachineBootInterface { + mac_address, + interface_id: interface_id.to_string(), + }, + )); + } else { + tracing::debug!( + address = %endpoint.address, + %mac_address, + "boot interface MAC has no matching Redfish interface id in the report; keeping last-known-good stored boot interface", + ); + } +} + /// State from exploring a host's DPUs and pairing them with DPU BMCs. /// /// The two counts are only ever incremented (monotonic), so the @@ -4097,7 +4124,7 @@ mod tests { } /// A BF2 DPU endpoint with its reported NIC mode forced to `nic_mode`. - fn bf2_dpu(nic_mode: Option) -> ExploredEndpoint { + fn bf2_dpu(nic_mode: Option) -> ExploredEndpoint { let mut report = load_bf2_ep_report(); report .systems @@ -4110,7 +4137,7 @@ mod tests { #[test] fn classify_running_as_dpu_when_in_dpu_mode() { - let dpu = bf2_dpu(Some(NicMode::Dpu)); + let dpu = bf2_dpu(Some(BlueFieldOperatingMode::Dpu)); let host = explored_endpoint(load_dell_ep_report()); // Mode already correct (`Ok(true)`) -> attach as a managed DPU. assert!(matches!( @@ -4126,7 +4153,7 @@ mod tests { #[test] fn classify_running_as_nic_when_dpu_reports_nic_mode() { - let dpu = bf2_dpu(Some(NicMode::Nic)); + let dpu = bf2_dpu(Some(BlueFieldOperatingMode::Nic)); let host = explored_endpoint(load_dell_ep_report()); assert!(matches!( classify_matched_dpu(&dpu, &host, Some(Ok(true))), @@ -4137,7 +4164,7 @@ mod tests { #[test] fn classify_needs_reconfig_when_set_nic_mode_was_issued() { // `Ok(false)` means `check_and_configure_dpu_mode` just issued a `set_nic_mode`. - let dpu = bf2_dpu(Some(NicMode::Nic)); + let dpu = bf2_dpu(Some(BlueFieldOperatingMode::Nic)); let host = explored_endpoint(load_dell_ep_report()); assert!(matches!( classify_matched_dpu(&dpu, &host, Some(Ok(false))), @@ -4147,7 +4174,7 @@ mod tests { #[test] fn classify_mode_check_failed_on_error() { - let dpu = bf2_dpu(Some(NicMode::Dpu)); + let dpu = bf2_dpu(Some(BlueFieldOperatingMode::Dpu)); let host = explored_endpoint(load_dell_ep_report()); let err = SiteExplorerError::InvalidArgument("boom".to_string()); assert!(matches!( diff --git a/crates/site-explorer/src/machine_creator.rs b/crates/site-explorer/src/machine_creator.rs index 9605c74206..83a790f966 100644 --- a/crates/site-explorer/src/machine_creator.rs +++ b/crates/site-explorer/src/machine_creator.rs @@ -254,7 +254,7 @@ impl MachineCreator { } } - // Own a declared integrated boot NIC so a DpuMode host can boot from it + // Own a declared integrated boot NIC so a managed-DPU host can boot from it // while its DPUs stay managed: the NIC becomes the host's HostInband // primary and the DPU admin links go dormant in the reconcile below. // Only for hosts with explored DPUs -- a zero-DPU host's NICs (including @@ -565,7 +565,7 @@ impl MachineCreator { Ok(Some(*machine_id)) } - /// Owns a declared integrated (non-DPU) host NIC as a DpuMode host's + /// Owns a declared integrated (non-DPU) host NIC as a managed-DPU host's /// HostInband boot interface, so a host with managed DPUs can still boot from /// an integrated NIC. The NIC carries `primary` into `machine_interfaces` on /// its first DHCP; the DPUs stay explored and linked, and their admin links @@ -573,7 +573,7 @@ impl MachineCreator { /// primary. /// /// Mirrors the host-NIC ownership in `create_zero_dpu_machine`, but for the - /// one declared NIC reached from the DpuMode path. No-op when nothing is + /// one declared NIC reached from the managed-DPU path. No-op when nothing is /// declared, or when the declared NIC is already owned (e.g. a declared DPU /// host-PF, which `attach_dpu_to_host` already owns). async fn own_declared_host_boot_nic( @@ -622,7 +622,7 @@ impl MachineCreator { .await?; tracing::info!( %declared_mac, %host_machine_id, - "Adopted declared integrated boot NIC as the DpuMode host's primary", + "Adopted declared integrated boot NIC as the managed-DPU host's primary", ); return Ok(()); } @@ -662,7 +662,7 @@ impl MachineCreator { .await?; tracing::info!( %declared_mac, %host_machine_id, - "Minted HostInband boot-NIC prediction for DpuMode host's declared integrated primary", + "Minted HostInband boot-NIC prediction for managed-DPU host's declared integrated primary", ); Ok(()) } diff --git a/crates/site-explorer/src/metrics.rs b/crates/site-explorer/src/metrics.rs index 89cd8c2994..9a5725878b 100644 --- a/crates/site-explorer/src/metrics.rs +++ b/crates/site-explorer/src/metrics.rs @@ -45,8 +45,8 @@ pub enum PairingBlockerReason { HostSystemReportMissing, /// Host's boot MAC not found in any discovered DPU BootInterfaceMacMismatch, - /// Host BMC reports no Bluefield PCIe devices but the host isn't - /// declared as `dpu_mode = "no_dpu"`. We expect DPUs but didn't + /// Host BMC reports no BlueField PCIe devices but the host policy is not + /// `Ignore`. We expect DPUs but didn't /// find any -- likely a misconfiguration or DPU-discovery bug. NoDpuReportedByHost, } @@ -68,7 +68,7 @@ impl Display for PairingBlockerReason { /// Signals emitted while migrating a DPU's NIC mode toward its declared target. /// Each marks a step in the flip-and-reset flow that drives a DPU into the -/// mode its host's `dpu_mode` calls for. +/// device mode its host's DPU policy calls for. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum DpuMigrationSignal { /// Found a DPU whose actual mode differs from the target; will reconfigure. @@ -77,8 +77,8 @@ pub enum DpuMigrationSignal { SetNicModeIssued, /// Requested a host power-cycle to apply a queued NIC-mode change. ResetRequested, - /// Registered a host with zero managed DPUs because its declared - /// `dpu_mode` is NicMode (distinct from NoDpu). + /// Registered a host with zero managed DPUs because its policy is + /// `UseAsNic` (distinct from `Ignore`). RegisteredZeroDpuForNicMode, } @@ -166,9 +166,9 @@ pub struct SiteExplorationMetrics { /// Generic category for the latest whole-run failure. `None` means success. pub run_failure_category: Option, /// Total count of DPU NIC-mode migration signals by kind. These track the - /// flip-and-reset flow that drives a DPU into the mode its host's - /// `dpu_mode` declares (mismatch found, `set_nic_mode` issued, reset - /// requested, and zero-DPU registered for a NicMode host). + /// flip-and-reset flow that drives a DPU into the device mode its host's + /// DPU policy declares (mismatch found, `set_nic_mode` issued, reset + /// requested, and zero-DPU registered for a `UseAsNic` host). pub dpu_migration_signals: HashMap, } @@ -756,8 +756,8 @@ impl SiteExplorerInstruments { .u64_observable_gauge("carbide_site_explorer_dpu_migration_signals_count") .with_description( "Number of DPU NIC-mode migration signals by signal type -- mode-mismatch found, \ - set_nic_mode issued, reset requested, and zero-DPU registered for a NicMode \ - host.", + set_nic_mode issued, reset requested, and zero-DPU registered for a host \ + whose DPU policy is use_as_nic.", ) .with_callback(move |observer| { metrics.if_available(|metrics, attrs| { diff --git a/crates/site-explorer/src/test_support/mock_endpoint_explorer.rs b/crates/site-explorer/src/test_support/mock_endpoint_explorer.rs index 9da66eb3bc..a68cbcf6d1 100644 --- a/crates/site-explorer/src/test_support/mock_endpoint_explorer.rs +++ b/crates/site-explorer/src/test_support/mock_endpoint_explorer.rs @@ -25,8 +25,8 @@ use mac_address::MacAddress; use model::expected_entity::ExpectedEntity; use model::machine::MachineInterfaceSnapshot; use model::site_explorer::{ - EndpointExplorationError, EndpointExplorationReport, InternalLockdownStatus, LockdownStatus, - NicMode, + BlueFieldOperatingMode, EndpointExplorationError, EndpointExplorationReport, + InternalLockdownStatus, LockdownStatus, }; use crate::{EndpointExplorer, SiteExplorationMetrics}; @@ -55,7 +55,7 @@ pub struct MockEndpointExplorer { /// Records every call to `set_nic_mode` (BMC address + requested target /// mode) so tests can assert the auto-correct path fired with the /// right arguments. - pub set_nic_mode_calls: Arc>>, + pub set_nic_mode_calls: Arc>>, /// Records IPs that `explore_endpoint` was called for. pub explore_endpoint_calls: Arc>>, /// Real explorer that `machine_setup`/`set_boot_order_dpu_first` forward to @@ -286,7 +286,7 @@ impl EndpointExplorer for MockEndpointExplorer { &self, address: SocketAddr, _interface: &MachineInterfaceSnapshot, - mode: NicMode, + mode: BlueFieldOperatingMode, ) -> Result<(), EndpointExplorationError> { self.set_nic_mode_calls .lock() diff --git a/crates/site-explorer/tests/integration/site_explorer.rs b/crates/site-explorer/tests/integration/site_explorer.rs index 78d5e198ed..bba7d8f122 100644 --- a/crates/site-explorer/tests/integration/site_explorer.rs +++ b/crates/site-explorer/tests/integration/site_explorer.rs @@ -32,13 +32,14 @@ use db::ObjectFilter; use db::sku::CURRENT_SKU_VERSION; use itertools::Itertools; use mac_address::MacAddress; -use model::expected_machine::{DpuMode, ExpectedMachine, ExpectedMachineData}; +use model::expected_machine::{ExpectedMachine, ExpectedMachineData}; use model::machine::machine_search_config::MachineSearchConfig; use model::machine::{LoadSnapshotOptions, Machine}; use model::metadata::Metadata; use model::site_explorer::{ - Chassis, ComputerSystem, EndpointExplorationError, EndpointExplorationReport, EndpointType, - ExploredDpu, ExploredManagedHost, NetworkAdapter, NicMode, PreingestionState, UefiDevicePath, + BlueFieldOperatingMode, Chassis, ComputerSystem, EndpointExplorationError, + EndpointExplorationReport, EndpointType, ExploredDpu, ExploredManagedHost, NetworkAdapter, + PreingestionState, UefiDevicePath, }; use model::test_support::{DpuConfig, ManagedHostConfig}; use rpc::forge::GetSiteExplorationRequest; @@ -391,10 +392,10 @@ async fn test_handle_redfish_error_powers_on_machine( Ok(()) } -/// Strict ingestion gate: a host whose BMC reports no DPU PCIe devices -/// and whose `ExpectedMachine` does not declare `NoDpu` is skipped (with -/// a warning + a `NoDpuReportedByHost` pairing-blocker metric) rather -/// than ingested. Operators must explicitly opt in to zero-DPU. +/// Strict ingestion gate: a host whose BMC reports no DPU PCIe devices and +/// whose effective `HostDpuPolicy` resolves to `Manage` is skipped (with a +/// warning + a `NoDpuReportedByHost` pairing-blocker metric) rather than +/// ingested. Operators opt in to zero-DPU through `UseAsNic` or `Ignore`. #[sqlx_test] async fn test_site_explorer_skips_unexpected_zero_dpu_host( pool: PgPool, @@ -404,8 +405,8 @@ async fn test_site_explorer_skips_unexpected_zero_dpu_host( let mut machine = env.new_machine("AA:AB:AC:AD:AA:11", "Vendor1"); machine.discover_dhcp(env.api()).await?; - // expected_machine WITHOUT a NoDpu declaration -- the host is - // "expected to have DPUs" by default. + // ExpectedMachine with the default `Manage` policy -- the host is expected + // to have DPUs. let mut txn = env.pool.begin().await?; db::expected_machine::create( &mut txn, @@ -460,7 +461,7 @@ async fn test_site_explorer_skips_unexpected_zero_dpu_host( let explored_managed_hosts = db::explored_managed_host::find_all(&env.pool).await?; assert!( explored_managed_hosts.is_empty(), - "strict gate should refuse to ingest a zero-DPU host without a `NoDpu` declaration, got {:?}", + "strict gate should refuse to ingest a zero-DPU host whose policy resolves to `Manage`, got {:?}", explored_managed_hosts, ); @@ -484,11 +485,11 @@ async fn test_site_explorer_skips_unexpected_zero_dpu_host( } /// Companion to `test_site_explorer_skips_unexpected_zero_dpu_host`: when -/// the operator explicitly declares `dpu_mode = "nic_mode"`, a host whose -/// BMC reports zero usable DPU PCIe devices (because anything that is a +/// the operator selects `HostDpuPolicy::UseAsNic`, a host whose BMC reports +/// zero usable DPU PCIe devices (because anything that is a /// BlueField has been stripped as "DPU in NIC mode") should be ingested as /// a zero-DPU managed host -- the operator has already opted into "treat -/// as zero-DPU" semantics by declaring NicMode. +/// as zero-DPU" semantics through the resolved policy. #[sqlx_test] async fn test_site_explorer_ingests_nic_mode_host_with_no_observed_dpus( pool: PgPool, @@ -506,7 +507,7 @@ async fn test_site_explorer_ingests_nic_mode_host_with_no_observed_dpus( bmc_mac_address: machine.mac, data: ExpectedMachineData { serial_number: "host-nic-mode-no-observed-dpus".to_string(), - dpu_mode: model::expected_machine::DpuMode::NicMode, + dpu_policy: model::expected_machine::HostDpuPolicy::UseAsNic, ..Default::default() }, }, @@ -548,20 +549,20 @@ async fn test_site_explorer_ingests_nic_mode_host_with_no_observed_dpus( assert_eq!( explored_managed_hosts.len(), 1, - "NicMode declaration should let the host through the strict gate even with zero observed DPUs", + "UseAsNic policy should let the host through the strict gate even with zero observed DPUs", ); assert!( explored_managed_hosts[0].dpus.is_empty(), - "NicMode hosts ingest with an empty `dpus` vector", + "UseAsNic hosts ingest with an empty `dpus` vector", ); Ok(()) } -/// Third member of the zero-DPU triad (alongside the `DpuMode::DpuMode` -/// skip test and the `DpuMode::NicMode` ingest test): a host explicitly -/// declared `dpu_mode = "no_dpu"` ingests as a zero-DPU managed host. The -/// `NoDpu` fast-path in `identify_managed_hosts` short-circuits before any +/// Third member of the zero-DPU triad (alongside the `Manage`-policy skip +/// test and the `UseAsNic`-policy ingest test): a host with +/// `HostDpuPolicy::Ignore` ingests as a zero-DPU managed host. The `Ignore` +/// fast-path in `identify_managed_hosts` short-circuits before any /// DPU PCIe enumeration, so this holds regardless of what the BMC reports. #[sqlx_test] async fn test_site_explorer_ingests_no_dpu_host( @@ -580,7 +581,7 @@ async fn test_site_explorer_ingests_no_dpu_host( bmc_mac_address: machine.mac, data: ExpectedMachineData { serial_number: "host-no-dpu-declared".to_string(), - dpu_mode: model::expected_machine::DpuMode::NoDpu, + dpu_policy: model::expected_machine::HostDpuPolicy::Ignore, ..Default::default() }, }, @@ -622,11 +623,11 @@ async fn test_site_explorer_ingests_no_dpu_host( assert_eq!( explored_managed_hosts.len(), 1, - "NoDpu declaration should ingest the host as zero-DPU", + "Ignore policy should ingest the host as zero-DPU", ); assert!( explored_managed_hosts[0].dpus.is_empty(), - "NoDpu hosts ingest with an empty `dpus` vector", + "Ignore hosts ingest with an empty `dpus` vector", ); Ok(()) @@ -786,7 +787,7 @@ async fn test_expected_machine_device_type_metrics( dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -812,7 +813,7 @@ async fn test_expected_machine_device_type_metrics( dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -838,7 +839,7 @@ async fn test_expected_machine_device_type_metrics( dpf_enabled: Some(true), bmc_ip_address: None, bmc_retain_credentials: None, - dpu_mode: Default::default(), + dpu_policy: Default::default(), bmc_ip_allocation: Default::default(), host_lifecycle_profile: Default::default(), }, @@ -1008,7 +1009,7 @@ async fn test_site_explorer_default_pause_ingestion_and_poweron( bmc_username: "ADMIN".into(), bmc_password: "Pwd2023x0x0x0x0x7".into(), serial_number: "VVG121GL".into(), - dpu_mode: model::expected_machine::DpuMode::NoDpu, + dpu_policy: model::expected_machine::HostDpuPolicy::Ignore, default_pause_ingestion_and_poweron: Some(true), ..Default::default() }, @@ -1194,8 +1195,8 @@ async fn test_site_explorer_main(pool: PgPool) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { - use model::expected_machine::{DpuMode, ExpectedMachine, ExpectedMachineData}; - use model::site_explorer::NicMode; + use model::expected_machine::{ExpectedMachine, ExpectedMachineData, HostDpuPolicy}; + use model::site_explorer::BlueFieldOperatingMode; let env = Env::new(pool).await; // DPU hardware reports DPU mode (so it looks like a "properly - // configured" DPU to the BF3-DPU heuristic) -- the operator-declared - // override is what forces the correction to NIC mode. + // configured" DPU to the BF3-DPU heuristic) -- the selected + // `HostDpuPolicy::UseAsNic` is what forces the correction to NIC mode. let dpu_config = DpuConfig { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), ..DpuConfig::default() }; let mock_host = model::test_support::ManagedHostConfig::default().with_dpus(vec![dpu_config.clone()]); let host_bmc_mac = mock_host.bmc_mac_address; - // Seed an ExpectedMachine with `dpu_mode: NicMode` that matches the - // mock host's BMC MAC. Site-explorer's per-host resolution will look - // this up by IP via the expected-endpoint index after DHCP assigns - // the host its BMC IP. + // Seed an ExpectedMachine whose policy resolves to + // `HostDpuPolicy::UseAsNic` and matches the mock host's BMC MAC. + // Site-explorer's per-host resolution will look this up by IP via the + // expected-endpoint index after DHCP assigns the host its BMC IP. let mut txn = env.pool.begin().await?; db::expected_machine::create( &mut txn, @@ -2685,7 +2686,7 @@ async fn test_site_explorer_auto_corrects_nic_mode_per_expected_machine( bmc_password: "PASS".to_string(), serial_number: "EM-866-NIC-OVERRIDE".to_string(), metadata: model::metadata::Metadata::new_with_default_name(), - dpu_mode: DpuMode::NicMode, + dpu_policy: HostDpuPolicy::UseAsNic, ..Default::default() }, }, @@ -2729,8 +2730,10 @@ async fn test_site_explorer_auto_corrects_nic_mode_per_expected_machine( .lock() .unwrap(); assert!( - calls.iter().any(|(_, mode)| *mode == NicMode::Nic), - "expected at least one set_nic_mode(Nic) call triggered by the operator's NicMode declaration; calls so far: {calls:?}" + calls + .iter() + .any(|(_, mode)| *mode == BlueFieldOperatingMode::Nic), + "expected at least one set_nic_mode(Nic) call triggered by the operator's UseAsNic policy; calls so far: {calls:?}" ); Ok(()) @@ -2749,10 +2752,10 @@ async fn test_site_explorer_power_cycles_non_dell_host_to_apply_nic_mode( ) -> Result<(), Box> { let env = Env::new(pool).await; - // DPU hardware reports DPU mode; the operator-declared NicMode override - // is what forces the correction (and therefore the power cycle). + // DPU hardware reports DPU mode; the operator's `UseAsNic` policy is what + // forces the correction (and therefore the power cycle). let dpu_config = DpuConfig { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), ..DpuConfig::default() }; let mock_host = ManagedHostConfig { @@ -2773,7 +2776,7 @@ async fn test_site_explorer_power_cycles_non_dell_host_to_apply_nic_mode( bmc_password: "PASS".to_string(), serial_number: "EM-866-NIC-POWERCYCLE".to_string(), metadata: Metadata::new_with_default_name(), - dpu_mode: DpuMode::NicMode, + dpu_policy: model::expected_machine::HostDpuPolicy::UseAsNic, ..Default::default() }, }, @@ -2821,7 +2824,9 @@ async fn test_site_explorer_power_cycles_non_dell_host_to_apply_nic_mode( .lock() .unwrap(); assert!( - nic_mode_calls.iter().any(|(_, mode)| *mode == NicMode::Nic), + nic_mode_calls + .iter() + .any(|(_, mode)| *mode == BlueFieldOperatingMode::Nic), "expected set_nic_mode(Nic) before the power cycle; calls so far: {nic_mode_calls:?}" ); @@ -2850,10 +2855,10 @@ async fn test_site_explorer_falls_back_to_ac_powercycle_when_powercycle_refused( ) -> Result<(), Box> { let env = Env::new(pool).await; - // DPU reports DPU mode; the operator-declared NicMode override forces the + // DPU reports DPU mode; the operator's `UseAsNic` policy forces the // correction (and therefore the reset). let dpu_config = DpuConfig { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), ..DpuConfig::default() }; let mock_host = ManagedHostConfig { @@ -2874,7 +2879,7 @@ async fn test_site_explorer_falls_back_to_ac_powercycle_when_powercycle_refused( bmc_password: "PASS".to_string(), serial_number: "EM-2635-AC-FALLBACK".to_string(), metadata: Metadata::new_with_default_name(), - dpu_mode: DpuMode::NicMode, + dpu_policy: model::expected_machine::HostDpuPolicy::UseAsNic, ..Default::default() }, }, @@ -2960,19 +2965,19 @@ async fn test_site_explorer_falls_back_to_ac_powercycle_when_powercycle_refused( /// as a host-reported one. The host BMC here enumerates no DPU over PCIe -- the /// usual reason the fallback exists (e.g. a GB200 that drops a DPU from its /// inventory) -- so the only link is the operator-listed serial, and the DPU is -/// still reporting DPU mode against a `NicMode` host. +/// still reporting DPU mode against a `HostDpuPolicy::UseAsNic` host. /// /// Before the fix the fallback path trusted the match as already-configured: it /// attached the DPU without a mode check, then dropped it to zero-DPU, so the -/// host registered as a NIC-mode host while the BlueField stayed in DPU mode and +/// host registered under `UseAsNic` while the BlueField stayed in DPU mode and /// `set_nic_mode` was never issued. Now the flip is issued, the host is /// power-cycled to apply it, and the host waits instead of settling this pass. #[sqlx_test] async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( pool: PgPool, ) -> Result<(), Box> { - use model::expected_machine::{DpuMode, ExpectedMachine, ExpectedMachineData}; - use model::site_explorer::NicMode; + use model::expected_machine::{ExpectedMachine, ExpectedMachineData, HostDpuPolicy}; + use model::site_explorer::BlueFieldOperatingMode; let env = Env::new(pool).await; @@ -2980,15 +2985,15 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( // DPU reports DPU mode; the host report carries no DPU device, so the // serial is the only thing that can pair them. let dpu_config = DpuConfig { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), serial: FALLBACK_DPU_SERIAL.to_string(), ..DpuConfig::default() }; let mock_host = ManagedHostConfig::default(); let host_bmc_mac = mock_host.bmc_mac_address; - // Operator declares the host NIC mode and lists the DPU's serial as a - // pairing fallback. + // Operator selects `HostDpuPolicy::UseAsNic` and lists the DPU's serial as + // a pairing fallback. let mut txn = env.pool.begin().await?; db::expected_machine::create( &mut txn, @@ -3000,7 +3005,7 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( bmc_password: "PASS".to_string(), serial_number: "EM-2631-FALLBACK-NIC".to_string(), metadata: model::metadata::Metadata::new_with_default_name(), - dpu_mode: DpuMode::NicMode, + dpu_policy: HostDpuPolicy::UseAsNic, fallback_dpu_serial_numbers: vec![FALLBACK_DPU_SERIAL.to_string()], ..Default::default() }, @@ -3037,7 +3042,7 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( } txn.commit().await?; // Second iteration: per-host matching falls through to the fallback-serial - // path, which must enforce the declared NIC mode. + // path, which must enforce the resolved `UseAsNic` policy. explorer.run_single_iteration().await.unwrap(); { @@ -3047,13 +3052,15 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( .lock() .unwrap(); assert!( - calls.iter().any(|(_, mode)| *mode == NicMode::Nic), - "fallback-matched DPU on a NicMode host should get set_nic_mode(Nic); calls so far: {calls:?}" + calls + .iter() + .any(|(_, mode)| *mode == BlueFieldOperatingMode::Nic), + "fallback-matched DPU on a UseAsNic host should get set_nic_mode(Nic); calls so far: {calls:?}" ); } // The host must not settle as a zero-DPU managed host until the flip has - // applied -- otherwise the database reads "NIC-mode host" while the + // applied -- otherwise the database records a `UseAsNic` host while the // BlueField is still physically in DPU mode. let explored_managed_hosts = db::explored_managed_host::find_all(&env.pool).await?; assert!( @@ -3087,16 +3094,16 @@ async fn test_site_explorer_enforces_nic_mode_on_fallback_serial_match( /// don't enumerate the DPU over PCIe at all. The host<->DPU serial match must /// therefore consider the chassis identity, not just `chassis.network_adapters[]`. /// -/// Here the operator declares NO `fallback_dpu_serial_numbers` and the default -/// `DpuMode`, so the only thing that can pair the host with its DPU is the -/// chassis-reported serial. The host must pair (and not fall through to the -/// zero-DPU path). +/// Here the operator declares NO `fallback_dpu_serial_numbers` and the host +/// resolves to `HostDpuPolicy::Manage`, so the only thing that can pair the +/// host with its DPU is the chassis-reported serial. The host must pair (and +/// not fall through to the zero-DPU path). #[sqlx_test] async fn test_site_explorer_pairs_dpu_from_chassis_serial( pool: PgPool, ) -> Result<(), Box> { use model::expected_machine::{ExpectedMachine, ExpectedMachineData}; - use model::site_explorer::NicMode; + use model::site_explorer::BlueFieldOperatingMode; let env = Env::new(pool).await; @@ -3104,7 +3111,7 @@ async fn test_site_explorer_pairs_dpu_from_chassis_serial( // The DPU's BMC reports DPU mode; the host BMC carries no DPU PCIe device, // only the BlueField chassis below, so the chassis serial is the only link. let dpu_config = DpuConfig { - nic_mode: Some(NicMode::Dpu), + nic_mode: Some(BlueFieldOperatingMode::Dpu), serial: CHASSIS_DPU_SERIAL.to_string(), ..DpuConfig::default() }; diff --git a/crates/site-explorer/tests/integration/zero_dpu.rs b/crates/site-explorer/tests/integration/zero_dpu.rs index 2eb3f676f7..eaed349f8e 100644 --- a/crates/site-explorer/tests/integration/zero_dpu.rs +++ b/crates/site-explorer/tests/integration/zero_dpu.rs @@ -26,7 +26,10 @@ use carbide_test_harness::network::segment::TestNetworkSegment; use carbide_test_harness::prelude::*; use carbide_test_harness::test_support::fixture_config::FixtureDefault as _; use mac_address::MacAddress; -use model::expected_machine::{DpuMode, ExpectedHostNic, ExpectedMachine, ExpectedMachineData}; +use model::expected_machine::{ + ExpectedHostNic, ExpectedMachine, ExpectedMachineData, HostDpuPolicy, +}; +use model::machine_boot_interface::MachineBootInterface; use model::test_support::ManagedHostConfig; struct ZeroDpuEnv { @@ -97,7 +100,7 @@ async fn register_zero_dpu_expected_machine( bmc_mac_address: managed_host.bmc_mac_address, data: ExpectedMachineData { serial_number: managed_host.serial.clone(), - dpu_mode: DpuMode::NoDpu, + dpu_policy: HostDpuPolicy::Ignore, ..Default::default() }, }, @@ -363,8 +366,8 @@ async fn test_predicted_live_boot_interface_id_outranks_retained_at_promotion( /// A zero-DPU host with no declared primary recovers its primary (boot) /// interface from the boot pair preserved across `--delete-interfaces` in /// `retained_boot_interfaces`. This is the DPU->NIC flip re-registration case: -/// the operator patches `dpu_mode = nic_mode` and force-deletes, declaring no -/// primary, and the host comes back with no managed DPU -- so neither the +/// the operator selects `HostDpuPolicy::UseAsNic` and force-deletes, declaring +/// no primary, and the host comes back with no managed DPU -- so neither the /// declared-primary nor the DPU-host-PF path names a primary. Without the /// retained fallback the re-ingested host would come up with no primary at all, /// leaving the machine-controller no boot target to set a boot order from. @@ -657,7 +660,7 @@ async fn test_zero_dpu_multi_nic_no_declaration_adopts_without_primary_collision non_dpu_macs: vec![nic_a, nic_b], ..ManagedHostConfig::default() }; - // No declared primary: NoDpu with empty host_nics. + // No declared primary: `Ignore` with empty host_nics. register_zero_dpu_expected_machine(&env, &mock_host).await?; // Both NICs lease before site-explorer runs -> two anonymous primary rows. @@ -749,10 +752,10 @@ async fn test_zero_dpu_multi_nic_no_declaration_adopts_without_primary_collision Ok(()) } -/// A zero-DPU host that declares one of its NICs `primary` mints that intent -/// onto the prediction, and DHCP promotion lands it: the declared NIC promotes -/// as primary and the other as non-primary -- even when the non-declared NIC -/// leases first. +/// A zero-DPU host that declares one of its NICs `primary` refreshes the +/// endpoint-level boot-interface pair and mints that intent onto the +/// prediction. DHCP promotion then lands the declared NIC as primary and the +/// other as non-primary -- even when the non-declared NIC leases first. #[sqlx_test] async fn test_zero_dpu_declared_primary_promotes_as_primary( pool: PgPool, @@ -775,7 +778,7 @@ async fn test_zero_dpu_declared_primary_promotes_as_primary( bmc_mac_address: mock_host.bmc_mac_address, data: ExpectedMachineData { serial_number: mock_host.serial.clone(), - dpu_mode: DpuMode::NoDpu, + dpu_policy: HostDpuPolicy::Ignore, host_nics: vec![ ExpectedHostNic { mac_address: primary_nic, @@ -817,11 +820,33 @@ async fn test_zero_dpu_declared_primary_promotes_as_primary( ); env.site_explorer.run_single_iteration().await?; let mut txn = env.pool.begin().await?; + db::explored_endpoints::set_boot_interface( + host_bmc_ip, + &MachineBootInterface { + mac_address: other_nic, + interface_id: "NIC.Embedded.2-1-1".to_string(), + }, + &mut txn, + ) + .await?; db::explored_endpoints::set_preingestion_complete(host_bmc_ip, &mut txn).await?; txn.commit().await?; env.site_explorer.run_single_iteration().await?; let mut txn = env.pool.begin().await?; + let endpoint = db::explored_endpoints::find_all_by_ip(host_bmc_ip, &mut txn) + .await? + .into_iter() + .next() + .expect("the host endpoint should still exist after ingestion"); + assert_eq!( + endpoint.boot_interface(), + Some(MachineBootInterface { + mac_address: primary_nic, + interface_id: "NIC.Embedded.1-1-1".to_string(), + }), + "Ignore policy should refresh a stale endpoint pair from the explicitly declared primary NIC", + ); let predicted_primary = db::predicted_machine_interface::find_by_mac_address(&mut txn, primary_nic) .await? diff --git a/rest-api/proto/core/gen/v1/nico_nico.pb.go b/rest-api/proto/core/gen/v1/nico_nico.pb.go index e21314299f..592e7c1882 100644 --- a/rest-api/proto/core/gen/v1/nico_nico.pb.go +++ b/rest-api/proto/core/gen/v1/nico_nico.pb.go @@ -2678,19 +2678,22 @@ func (OsImageStatus) EnumDescriptor() ([]byte, []int) { return file_nico_nico_proto_rawDescGZIP(), []int{48} } -// Per-host DPU operating mode. Lets site operators mix DPU-mode, -// NIC-mode, and no-DPU hosts within a single site. +// Legacy protobuf representation of the per-host policy for how NICo treats +// DPU hardware. The protobuf type and symbols remain named `DpuMode` for API +// compatibility; model and configuration code translate this boundary value +// immediately to `HostDpuPolicy`. // -// - DPU_MODE: The default. Attached DPUs are managed by NICo (DPU upgrades, -// overlay networking, etc). -// - NIC_MODE: DPUs are present physically but operate as plain NICs. Site -// explorer reconfigures them to NIC mode if needed; the host is then -// tracked as if it had no DPUs. -// - NO_DPU: no DPU hardware at all -- a plain host NIC on the underlay. +// - DPU_MODE: `HostDpuPolicy::Manage`. Attached DPUs are managed by NICo. At +// the per-host boundary this retains the legacy inheritance behavior and +// defers to the site policy. +// - NIC_MODE: `HostDpuPolicy::UseAsNic`. Physically present DPUs operate as +// plain NICs and the host is tracked as if it had no DPUs. +// - NO_DPU: `HostDpuPolicy::Ignore`. NICo does not configure or attach DPU +// hardware. // -// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," -// which comes from `[site_explorer] dpu_mode` (and falls back to -// DPU_MODE when that's unset). +// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," which +// comes from `[site_explorer] dpu_policy` (and falls back to `Manage` when +// that's unset). type DpuMode int32 const ( @@ -34545,9 +34548,11 @@ type ExpectedMachine struct { // When true, site-explorer skips BMC password rotation and stores the // factory-default credentials in Vault as-is. BmcRetainCredentials *bool `protobuf:"varint,15,opt,name=bmc_retain_credentials,json=bmcRetainCredentials,proto3,oneof" json:"bmc_retain_credentials,omitempty"` - // Per-host DPU operating mode. See `DpuMode` above. Unset means "use - // the site-wide `[site_explorer] dpu_mode` default" (which itself - // defaults to DPU_MODE when unset). + // Per-host DPU policy. See the legacy protobuf representation above. Unset means "use the + // site-wide `[site_explorer] dpu_policy` default" (which itself defaults to + // `Manage` when unset). The protobuf type and field keep their legacy names + // so existing binary, protobuf-JSON, and generated clients remain compatible; + // model and configuration surfaces call this `dpu_policy`. DpuMode *DpuMode `protobuf:"varint,16,opt,name=dpu_mode,json=dpuMode,proto3,enum=forge.DpuMode,oneof" json:"dpu_mode,omitempty"` // Per-host lifecycle profile. When unset in a patch/update request, the // existing DB value is preserved via COALESCE. diff --git a/rest-api/proto/core/src/v1/nico_nico.proto b/rest-api/proto/core/src/v1/nico_nico.proto index 9ca4bc4176..b92ab70424 100644 --- a/rest-api/proto/core/src/v1/nico_nico.proto +++ b/rest-api/proto/core/src/v1/nico_nico.proto @@ -6004,19 +6004,22 @@ message ExpectedHostNic { optional NetworkSegmentType network_segment_type = 7; } -// Per-host DPU operating mode. Lets site operators mix DPU-mode, -// NIC-mode, and no-DPU hosts within a single site. +// Legacy protobuf representation of the per-host policy for how NICo treats +// DPU hardware. The protobuf type and symbols remain named `DpuMode` for API +// compatibility; model and configuration code translate this boundary value +// immediately to `HostDpuPolicy`. // -// - DPU_MODE: The default. Attached DPUs are managed by NICo (DPU upgrades, -// overlay networking, etc). -// - NIC_MODE: DPUs are present physically but operate as plain NICs. Site -// explorer reconfigures them to NIC mode if needed; the host is then -// tracked as if it had no DPUs. -// - NO_DPU: no DPU hardware at all -- a plain host NIC on the underlay. +// - DPU_MODE: `HostDpuPolicy::Manage`. Attached DPUs are managed by NICo. At +// the per-host boundary this retains the legacy inheritance behavior and +// defers to the site policy. +// - NIC_MODE: `HostDpuPolicy::UseAsNic`. Physically present DPUs operate as +// plain NICs and the host is tracked as if it had no DPUs. +// - NO_DPU: `HostDpuPolicy::Ignore`. NICo does not configure or attach DPU +// hardware. // -// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," -// which comes from `[site_explorer] dpu_mode` (and falls back to -// DPU_MODE when that's unset). +// Unset and `DPU_MODE_UNSPECIFIED` both mean "use the site default," which +// comes from `[site_explorer] dpu_policy` (and falls back to `Manage` when +// that's unset). enum DpuMode { DPU_MODE_UNSPECIFIED = 0; DPU_MODE = 1; @@ -6076,9 +6079,11 @@ message ExpectedMachine { // When true, site-explorer skips BMC password rotation and stores the // factory-default credentials in Vault as-is. optional bool bmc_retain_credentials = 15; - // Per-host DPU operating mode. See `DpuMode` above. Unset means "use - // the site-wide `[site_explorer] dpu_mode` default" (which itself - // defaults to DPU_MODE when unset). + // Per-host DPU policy. See the legacy protobuf representation above. Unset means "use the + // site-wide `[site_explorer] dpu_policy` default" (which itself defaults to + // `Manage` when unset). The protobuf type and field keep their legacy names + // so existing binary, protobuf-JSON, and generated clients remain compatible; + // model and configuration surfaces call this `dpu_policy`. optional DpuMode dpu_mode = 16; // Per-host lifecycle profile. When unset in a patch/update request, the // existing DB value is preserved via COALESCE.