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
16 changes: 15 additions & 1 deletion crates/admin-cli/src/expected_machines/add/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use carbide_utils::has_duplicates;
use carbide_uuid::rack::RackId;
use clap::Parser;
use mac_address::MacAddress;
use rpc::forge::{DpuMode, ExpectedHostNic};
use rpc::forge::{BmcIpAllocationType, DpuMode, ExpectedHostNic};
use serde::{Deserialize, Serialize};

use crate::errors::{CarbideCliError, CarbideCliResult};
Expand Down Expand Up @@ -51,6 +51,11 @@ Add a host whose DPU should be treated as a plain NIC:
--bmc-username admin --bmc-password mypassword --chassis-serial-number sample_serial-1 \
--dpu-mode nic-mode

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 \
--bmc-username admin --bmc-password mypassword --chassis-serial-number sample_serial-1 \
--bmc-ip-allocation retained

")]
pub struct Args {
#[clap(short = 'a', long, help = "BMC MAC Address of the expected machine")]
Expand Down Expand Up @@ -167,6 +172,14 @@ pub struct Args {
)]
pub dpu_mode: Option<DpuMode>,

#[clap(
long = "bmc-ip-allocation",
value_name = "BMC_IP_ALLOCATION",
value_enum,
help = "Per-host control over how this BMC's IP is assigned and retained. `auto` (default): infer from `--bmc-ip-address` -- a configured address is `fixed`, no address is `retained`; `dynamic`: a normal DHCP lease that may expire and change; `fixed`: the operator-specified `--bmc-ip-address` (static); `retained`: an auto-allocated address pinned as static (never expires). Unset defers to the server default (`auto`)."
)]
pub bmc_ip_allocation: Option<BmcIpAllocationType>,

#[clap(
long = "disable-lockdown",
value_name = "DISABLE_LOCKDOWN",
Expand Down Expand Up @@ -217,6 +230,7 @@ impl TryFrom<Args> for rpc::forge::ExpectedMachine {
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),
bmc_ip_allocation: value.bmc_ip_allocation.map(|m| m as i32),
host_lifecycle_profile: value.disable_lockdown.map(|dl| {
rpc::forge::HostLifecycleProfile {
disable_lockdown: Some(dl),
Expand Down
5 changes: 5 additions & 0 deletions crates/admin-cli/src/expected_machines/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub struct ExpectedMachineJson {
/// if that's also unset).
#[serde(default)]
pub dpu_mode: Option<rpc::forge::DpuMode>,
/// 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.
#[serde(default)]
pub bmc_ip_allocation: Option<rpc::forge::BmcIpAllocationType>,
/// Per-host lifecycle profile for settings that affect state-machine progression.
#[serde(default)]
pub host_lifecycle_profile: Option<HostLifecycleProfile>,
Expand Down
19 changes: 17 additions & 2 deletions crates/admin-cli/src/expected_machines/patch/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::DpuMode;
use rpc::forge::{BmcIpAllocationType, DpuMode};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

Expand Down Expand Up @@ -48,6 +48,7 @@ use crate::errors::CarbideCliError;
"sku_id",
"bmc_ip_address",
"dpu_mode",
"bmc_ip_allocation",
"dpf_enabled",
])))]
#[command(after_long_help = "\
Expand All @@ -69,6 +70,10 @@ Change the per-host DPU mode:
$ nico-admin-cli expected-machine patch --bmc-mac-address 00:11:22:33:44:55 \
--dpu-mode no-dpu

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 \
--bmc-ip-allocation retained

")]
pub struct Args {
#[clap(short = 'a', long, help = "BMC MAC Address of the expected machine")]
Expand Down Expand Up @@ -187,6 +192,15 @@ pub struct Args {
)]
pub dpu_mode: Option<DpuMode>,

#[clap(
long = "bmc-ip-allocation",
value_name = "BMC_IP_ALLOCATION",
value_enum,
group = "group",
help = "Per-host control over how this BMC's IP is assigned and retained. `auto` (default): infer from `--bmc-ip-address` -- a configured address is `fixed`, no address is `retained`; `dynamic`: a normal DHCP lease that may expire and change; `fixed`: the operator-specified `--bmc-ip-address` (static); `retained`: an auto-allocated address pinned as static (never expires). Unset preserves the existing per-host value."
)]
pub bmc_ip_allocation: Option<BmcIpAllocationType>,

#[clap(
long = "disable-lockdown",
value_name = "DISABLE_LOCKDOWN",
Expand Down Expand Up @@ -219,8 +233,9 @@ impl Args {
&& self.dpf_enabled.is_none()
&& self.bmc_ip_address.is_none()
&& self.dpu_mode.is_none()
&& self.bmc_ip_allocation.is_none()
{
return Err(CarbideCliError::GenericError("One of the following options must be specified: bmc-user-name and bmc-password or chassis-serial-number or fallback-dpu-serial-number or bmc-ip-address or dpu-mode or dpf-enabled".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-mode or bmc-ip-allocation or dpf-enabled".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 @@ -51,6 +51,7 @@ impl Run for Args {
self.bmc_ip_address,
self.bmc_retain_credentials,
self.dpu_mode,
self.bmc_ip_allocation,
self.disable_lockdown
.map(|dl| ::rpc::forge::HostLifecycleProfile {
disable_lockdown: Some(dl),
Expand Down
168 changes: 168 additions & 0 deletions crates/admin-cli/src/expected_machines/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,3 +651,171 @@ fn validate_patch_with_dpu_mode_only() {
_ => panic!("expected Patch variant"),
}
}

// `--bmc-ip-allocation` is optional on `add`; unset is treated downstream as the
// server default (`auto`), which retains an auto-allocated BMC address.
#[test]
fn parse_add_without_bmc_ip_allocation() {
let cmd = Cmd::try_parse_from([
"expected-machine",
"add",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-username",
"admin",
"--bmc-password",
"secret",
"--chassis-serial-number",
"SN12345",
])
.expect("should parse without --bmc-ip-allocation");

match cmd {
Cmd::Add(args) => {
assert!(
args.bmc_ip_allocation.is_none(),
"--bmc-ip-allocation should be optional"
);
}
_ => panic!("expected Add variant"),
}
}

// `--bmc-ip-allocation <value>` parses to the matching BmcIpAllocationType variant
// on both `add` and `patch`. The closure pulls bmc_ip_allocation off whichever
// variant parsed; each row pins the parsed `Some(variant)`.
#[test]
fn parse_bmc_ip_allocation_to_its_variant() {
scenarios!(
run = |argv| {
Cmd::try_parse_from(argv.iter().copied())
.map(|cmd| match cmd {
Cmd::Add(args) => args.bmc_ip_allocation,
Cmd::Patch(args) => args.bmc_ip_allocation,
_ => panic!("expected Add or Patch variant"),
})
.map_err(drop)
};
"add --bmc-ip-allocation retained" {
&[
"expected-machine",
"add",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-username",
"admin",
"--bmc-password",
"secret",
"--chassis-serial-number",
"SN12345",
"--bmc-ip-allocation",
"retained",
][..] => Yields(Some(rpc::forge::BmcIpAllocationType::Retained)),
}

"add --bmc-ip-allocation dynamic" {
&[
"expected-machine",
"add",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-username",
"admin",
"--bmc-password",
"secret",
"--chassis-serial-number",
"SN12345",
"--bmc-ip-allocation",
"dynamic",
][..] => Yields(Some(rpc::forge::BmcIpAllocationType::Dynamic)),
}

"add --bmc-ip-allocation auto" {
&[
"expected-machine",
"add",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-username",
"admin",
"--bmc-password",
"secret",
"--chassis-serial-number",
"SN12345",
"--bmc-ip-allocation",
"auto",
][..] => Yields(Some(rpc::forge::BmcIpAllocationType::Auto)),
}

"patch --bmc-ip-allocation retained" {
&[
"expected-machine",
"patch",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-ip-allocation",
"retained",
][..] => Yields(Some(rpc::forge::BmcIpAllocationType::Retained)),
}

"patch --bmc-ip-allocation fixed" {
&[
"expected-machine",
"patch",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-ip-allocation",
"fixed",
][..] => Yields(Some(rpc::forge::BmcIpAllocationType::Fixed)),
}
);
}

// clap rejects `--bmc-ip-allocation` values that don't match the enum.
#[test]
fn parse_add_rejects_invalid_bmc_ip_allocation() {
let result = Cmd::try_parse_from([
"expected-machine",
"add",
"--bmc-mac-address",
"1a:2b:3c:4d:5e:6f",
"--bmc-username",
"admin",
"--bmc-password",
"secret",
"--chassis-serial-number",
"SN12345",
"--bmc-ip-allocation",
"garbage",
]);
assert!(
result.is_err(),
"clap should reject --bmc-ip-allocation with an invalid value"
);
}

// `patch --bmc-ip-allocation retained` alone (no other patchable fields) must
// satisfy clap's ArgGroup and `Args::validate()`'s "at least one field" check.
// A patch that sets only this field.
#[test]
fn validate_patch_with_bmc_ip_allocation_only() {
let cmd = Cmd::try_parse_from([
"expected-machine",
"patch",
"--bmc-mac-address",
"00:00:00:00:00:00",
"--bmc-ip-allocation",
"retained",
])
.expect("patch --bmc-ip-allocation alone should parse (ArgGroup)");

match cmd {
Cmd::Patch(args) => {
assert!(
args.validate().is_ok(),
"patch --bmc-ip-allocation alone should validate"
);
}
_ => panic!("expected Patch variant"),
}
}
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 @@ -69,6 +69,7 @@ impl Run for Args {
expected_machine.bmc_ip_address,
expected_machine.bmc_retain_credentials,
expected_machine.dpu_mode,
expected_machine.bmc_ip_allocation,
expected_machine.host_lifecycle_profile.map(|hlp| {
::rpc::forge::HostLifecycleProfile {
disable_lockdown: hlp.disable_lockdown,
Expand Down
7 changes: 7 additions & 0 deletions crates/admin-cli/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,7 @@ impl ApiClient {
bmc_ip_address: Option<String>,
bmc_retain_credentials: Option<bool>,
dpu_mode: Option<::rpc::forge::DpuMode>,
bmc_ip_allocation: Option<::rpc::forge::BmcIpAllocationType>,
host_lifecycle_profile: Option<::rpc::forge::HostLifecycleProfile>,
) -> Result<(), CarbideCliError> {
let get_req = match (bmc_mac_address, id) {
Expand Down Expand Up @@ -912,6 +913,11 @@ impl ApiClient {
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),
// Use the flag value if given, else preserve the stored per-host
// value (patch semantics).
bmc_ip_allocation: bmc_ip_allocation
.map(|m| m as i32)
.or(expected_machine.bmc_ip_allocation),
host_lifecycle_profile: host_lifecycle_profile
.or(expected_machine.host_lifecycle_profile),
};
Expand Down Expand Up @@ -949,6 +955,7 @@ impl ApiClient {
bmc_ip_address: machine.bmc_ip_address,
bmc_retain_credentials: machine.bmc_retain_credentials,
dpu_mode: machine.dpu_mode.map(|m| m 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 {
disable_lockdown: hlp.disable_lockdown,
Expand Down
10 changes: 10 additions & 0 deletions crates/api-core/src/handlers/expected_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ pub(crate) fn validate_expected_machine_for_insert(
machine: &ExpectedMachine,
) -> Result<(), CarbideError> {
validate_at_most_one_primary_host_nic(&machine.data.host_nics)?;
machine
.data
.bmc_ip_allocation
.validate(machine.data.bmc_ip_address.is_some())
.map_err(|msg| CarbideError::InvalidArgument(msg.to_string()))?;

Ok(())
}
Expand Down Expand Up @@ -219,6 +224,11 @@ pub(crate) async fn update(
};

validate_at_most_one_primary_host_nic(&machine.data.host_nics)?;
machine
.data
.bmc_ip_allocation
.validate(machine.data.bmc_ip_address.is_some())
.map_err(|msg| CarbideError::InvalidArgument(msg.to_string()))?;

let mut txn = api.txn_begin().await?;

Expand Down
Loading
Loading