Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .taplo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
exclude = ["pxe/mkosi/**"]
exclude = [
"pxe/mkosi/**",
# Go-templated Helm config files: not valid standalone TOML.
"helm/charts/nico-api/files/carbide-api-config.toml",
"helm/charts/nico-bmc-proxy/files/carbide-bmc-proxy.toml",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why were these excludes added?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

breaks linter all the time when run locally, have no idea about CI

]
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/admin-cli/src/expected_machines/add/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ pub(crate) struct Args {
help = "If true, do not lock down the server as part of lifecycle management within the state machine. If unset or false, preserve the default behavior of locking down the server after configuring the BIOS."
)]
disable_lockdown: Option<bool>,

#[clap(
long = "bmc-vendor-override",
value_name = "BMC_VENDOR_OVERRIDE",
help = "Pin the Redfish BMC vendor for this host. Once set it governs how NICo talks to this BMC -- which libredfish driver each Redfish client dispatches on, the vendor recorded by Site Explorer, and therefore the firmware config lookup, the IPMI-vs-Redfish restart choice and the BMC console transport. Host lifecycle decisions keyed to the host's own DMI data are unaffected. A RedfishVendor variant name, case-sensitive (e.g. Dell, Supermicro, NvidiaDpu, Hpe, Lenovo). Unset means automatic detection. Not applied to credential rotation or factory bootstrap, which must reach a BMC before its vendor is usable."
)]
bmc_vendor_override: Option<String>,
}

impl Args {
Expand Down Expand Up @@ -258,6 +265,7 @@ impl TryFrom<Args> for rpc::forge::ExpectedMachine {
disable_lockdown: Some(dl),
}
}),
bmc_vendor_override: value.bmc_vendor_override,
})
}
}
6 changes: 6 additions & 0 deletions crates/admin-cli/src/expected_machines/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ pub(crate) struct ExpectedMachineJson {
/// Per-host lifecycle profile for settings that affect state-machine progression.
#[serde(default)]
pub(crate) host_lifecycle_profile: Option<HostLifecycleProfile>,
/// Operator pinned Redfish BMC vendor: a `RedfishVendor` variant name such
/// as `Dell`. Absent (`null` or omitted) leaves automatic detection in
/// place. On an update an empty string clears an existing pin; any non-empty
/// value must be a usable variant name or the write is rejected.
#[serde(default)]
pub(crate) bmc_vendor_override: Option<String>,
}

impl ExpectedMachineJson {
Expand Down
20 changes: 19 additions & 1 deletion crates/admin-cli/src/expected_machines/patch/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use crate::expected_machines::common::HostDpuPolicy;
"bmc_ip_allocation",
"dpf_enabled",
"interfaces",
"bmc_vendor_override",
])))]
#[command(after_long_help = "\
EXAMPLES:
Expand Down Expand Up @@ -86,6 +87,14 @@ Reset that role to Host and infer Fixed allocation from fixed_ip:
$ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \
--interfaces '[{\"mac_address\":\"02:00:00:00:20:01\",\"role\":\"unspecified\",\"ip_allocation\":\"unspecified\",\"fixed_ip\":\"192.0.2.10\"}]'

Pin the Redfish BMC vendor for a host:
$ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \
--bmc-vendor-override Dell

Clear the Redfish BMC vendor override (return to automatic detection):
$ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \
--bmc-vendor-override \"\"

")]
pub(crate) struct Args {
#[clap(short = 'a', long, help = "BMC MAC Address of the expected machine")]
Expand Down Expand Up @@ -229,6 +238,14 @@ pub(crate) struct Args {
help = "If true, do not lock down the server as part of lifecycle management within the state machine. If unset or false, preserve the default behavior of locking down the server after configuring the BIOS."
)]
pub(super) disable_lockdown: Option<bool>,

#[clap(
long = "bmc-vendor-override",
value_name = "BMC_VENDOR_OVERRIDE",
group = "group",
help = "Pin the Redfish BMC vendor for this host. Once set it governs how NICo talks to this BMC -- which libredfish driver each Redfish client dispatches on, the vendor recorded by Site Explorer, and therefore the firmware config lookup, the IPMI-vs-Redfish restart choice and the BMC console transport. Host lifecycle decisions keyed to the host's own DMI data are unaffected. A RedfishVendor variant name, case-sensitive (e.g. Dell, Supermicro, NvidiaDpu, Hpe, Lenovo). Redfish clients pick it up within a minute; the recorded vendor changes at the next successful exploration. Pass an empty string to clear the override (return to automatic detection). Not applied to credential rotation or factory bootstrap, which must reach a BMC before its vendor is usable."
)]
pub(super) bmc_vendor_override: Option<String>,
}

impl Args {
Expand Down Expand Up @@ -257,8 +274,9 @@ impl Args {
&& self.dpu_policy.is_none()
&& self.bmc_ip_allocation.is_none()
&& self.interfaces.is_none()
&& self.bmc_vendor_override.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 sku-id or rack-id or bmc-ip-address or dpu-policy or bmc-ip-allocation or dpf-enabled or interfaces".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 sku-id or rack-id or bmc-ip-address or dpu-policy or bmc-ip-allocation or dpf-enabled or interfaces or bmc-vendor-override".to_string()));
}
if self
.fallback_dpu_serial_numbers
Expand Down
1 change: 1 addition & 0 deletions crates/admin-cli/src/expected_machines/patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ impl Run for Args {
disable_lockdown: Some(dl),
}),
self.interfaces,
self.bmc_vendor_override,
)
.await?;
Ok(())
Expand Down
1 change: 1 addition & 0 deletions crates/admin-cli/src/expected_machines/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl Run for Args {
}
}),
interfaces,
expected_machine.bmc_vendor_override,
)
.await?;
Ok(())
Expand Down
5 changes: 5 additions & 0 deletions crates/admin-cli/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,7 @@ impl ApiClient {
bmc_ip_allocation: Option<::rpc::forge::BmcIpAllocationType>,
host_lifecycle_profile: Option<::rpc::forge::HostLifecycleProfile>,
interfaces: Option<String>,
bmc_vendor_override: Option<String>,
) -> Result<(), CarbideCliError> {
let get_req = match (bmc_mac_address, id) {
(Some(_), Some(_)) => {
Expand Down Expand Up @@ -1059,6 +1060,9 @@ impl ApiClient {
replace_host_nics: replace_interfaces,
host_lifecycle_profile: host_lifecycle_profile
.or(expected_machine.host_lifecycle_profile),
// Patch semantics. Use the flag when given, where an empty string
// clears the override, and otherwise keep the stored value.
bmc_vendor_override: bmc_vendor_override.or(expected_machine.bmc_vendor_override),
};

Ok(self.0.update_expected_machine(request).await?)
Expand Down Expand Up @@ -1104,6 +1108,7 @@ impl ApiClient {
disable_lockdown: hlp.disable_lockdown,
}
}),
bmc_vendor_override: machine.bmc_vendor_override,
})
.collect(),
};
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ lazy_static = { workspace = true }
libredfish = { workspace = true }
librms = { workspace = true }
mac_address = { workspace = true }
moka = { workspace = true }
num_cpus = { workspace = true }
nv-redfish = { workspace = true }
nv-redfish-dispatcher = { workspace = true }
Expand Down
70 changes: 70 additions & 0 deletions crates/api-core/src/handlers/expected_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,42 @@ fn validate_expected_machine_for_insert(machine: &ExpectedMachine) -> Result<(),
.bmc_ip_allocation
.validate(machine.data.bmc_ip_address.is_some())
.map_err(|msg| CarbideError::InvalidArgument(msg.to_string()))?;
validate_bmc_vendor_override(machine)?;

Ok(())
}

/// Reject a `bmc_vendor_override` libredfish could not use, so a typo fails the
/// write rather than being stored as a pin that never applies. `Unknown` is
/// refused too, being the pool sentinel for an uninitialized client. An empty
/// string is rejected as well: absence is `None`, so an empty value is a
/// malformed record, not a pin. The wire layer maps an empty override to `None`
/// before it reaches here (see the proto conversion), so a clear request never
/// arrives as an empty string.
fn validate_bmc_vendor_override(machine: &ExpectedMachine) -> Result<(), CarbideError> {
use libredfish::model::service_root::RedfishVendor;

let Some(name) = machine.data.bmc_vendor_override.as_deref() else {
return Ok(());
};

if name.is_empty() {
return Err(CarbideError::InvalidArgument(
"bmc_vendor_override must not be empty; omit it for automatic detection".to_string(),
));
}

match carbide_redfish::libredfish::conv::redfish_vendor_from_str(name) {
Some(vendor) if vendor != RedfishVendor::Unknown => Ok(()),
_ => Err(CarbideError::InvalidArgument(format!(
// xtask:allow-error-case: the vendor spellings are case-sensitive values
"bmc_vendor_override {name:?} is not a usable Redfish vendor name, \
which is the exact case-sensitive variant spelling \
such as \"Dell\", \"Supermicro\" or \"NvidiaDpu\""
))),
}
}

/// Create missing expected_machines that aren't already in the database,
/// calling `validate_expected_machine_for_insert` for each new entry. This is currently
/// purely used by the expected_machines.json import path only, but lives
Expand Down Expand Up @@ -1401,4 +1433,42 @@ mod tests {
let parsed: ExpectedMachineData = replacement.try_into().unwrap();
assert!(validate_expected_interface_role_and_allocation(&parsed.interfaces).is_err());
}

/// A pin libredfish cannot use has to fail the write. Storing it would leave
/// an operator believing the vendor is pinned while every client keeps
/// detecting, with only a server side warn to say otherwise.
#[test]
fn bmc_vendor_override_rejects_names_libredfish_cannot_use() {
fn validate(stored: Option<&str>) -> Result<(), CarbideError> {
validate_bmc_vendor_override(&ExpectedMachine {
id: None,
bmc_mac_address: "02:00:00:00:00:01".parse().expect("valid test MAC"),
data: ExpectedMachineData {
bmc_vendor_override: stored.map(str::to_string),
..Default::default()
},
})
}

assert!(validate(None).is_ok(), "no pin is always valid");
assert!(
validate(Some("")).is_err(),
"an empty string is not a pin; the wire layer maps a clear request to \
no pin before validation, so a literal empty value here is malformed"
);
assert!(validate(Some("Dell")).is_ok(), "an exact variant name");
assert!(
validate(Some("NvidiaDpu")).is_ok(),
"a multi-word variant name"
);

// Case matters, so the near miss an operator is most likely to type has
// to be rejected rather than silently stored.
assert!(validate(Some("dell")).is_err(), "wrong case");
assert!(validate(Some("Del")).is_err(), "typo");
assert!(
validate(Some("Unknown")).is_err(),
"the uninitialized-client sentinel is not a pinnable vendor"
);
}
}
4 changes: 2 additions & 2 deletions crates/api-core/src/handlers/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::str::FromStr;
use ::rpc::errors::RpcDataConversionError;
use ::rpc::forge::{self as rpc, AdminForceDeleteMachineResponse};
use ::rpc::model::RpcTryFrom;
use carbide_redfish::libredfish::RedfishAuth;
use carbide_redfish::libredfish::{RedfishAuth, VendorSelection};
use carbide_secrets::credentials::{BmcCredentialType, CredentialKey};
use carbide_uuid::infiniband::IBPartitionId;
use carbide_uuid::instance::InstanceId;
Expand Down Expand Up @@ -1103,7 +1103,7 @@ pub(crate) async fn invoke_power(
RedfishAuth::Key(CredentialKey::BmcCredentials {
credential_type: BmcCredentialType::BmcRoot { bmc_mac_address },
}),
None,
VendorSelection::Detect,
)
.await
.map_err(|e| CarbideError::internal(e.to_string()))?;
Expand Down
4 changes: 2 additions & 2 deletions crates/api-core/src/handlers/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::time::Duration;
use ::rpc::errors::RpcDataConversionError;
use ::rpc::forge as rpc;
use ::rpc::model::machine::ManagedHostStateSnapshotRpc;
use carbide_redfish::libredfish::RedfishAuth;
use carbide_redfish::libredfish::{RedfishAuth, VendorSelection};
use carbide_secrets::credentials::{BmcCredentialType, CredentialKey, Credentials};
use carbide_uuid::machine::MachineId;
use libredfish::SystemPowerControl;
Expand Down Expand Up @@ -703,7 +703,7 @@ pub(crate) async fn admin_force_delete_machine(
RedfishAuth::Key(CredentialKey::BmcCredentials {
credential_type: BmcCredentialType::BmcRoot { bmc_mac_address },
}),
None,
VendorSelection::Detect,
)
.await
{
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ mod measured_boot;
mod mqtt_state_change_hook;
mod network_segment;
mod node_auth;
mod redfish_vendor_override;
mod scout_stream;
pub mod secrets;
mod setup;
Expand Down
Loading