Skip to content
Merged
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
12 changes: 11 additions & 1 deletion crates/admin-cli/src/expected_machines/add/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ 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-policy nic

Add a host interface with a fixed IP:
$ 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 \
--host_nics '[{\"mac_address\":\"02:00:00:00:10:01\",\"role\":\"host\",\"ip_allocation\":\"fixed\",\"fixed_ip\":\"192.0.2.21\"}]'

Add a DPU OS interface that retains its DHCP address:
$ 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 \
--host_nics '[{\"mac_address\":\"02:00:00:00:20:01\",\"role\":\"dpu_os\",\"ip_allocation\":\"retained\"}]'

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 \
Expand Down Expand Up @@ -123,7 +133,7 @@ pub struct Args {
#[clap(
long = "host_nics",
value_name = "HOST_NICS",
help = "Host NICs as a JSON array of ExpectedHostNic objects (fields: mac_address, network_segment_type, fixed_ip, fixed_mask, fixed_gateway, primary; legacy: nic_type)",
help = "Host NICs as a JSON array of ExpectedHostNic objects (fields: mac_address, role, ip_allocation, network_segment_type, fixed_ip, fixed_mask, fixed_gateway, primary; legacy: nic_type). Accepted values: role=host|dpu_os|dpu_bmc and ip_allocation=dynamic|fixed|retained. An omitted role defaults to host. When ip_allocation is omitted, fixed_ip implies fixed; otherwise it defaults to dynamic.",
action = clap::ArgAction::Append
)]
pub host_nics: Option<String>,
Expand Down
2 changes: 1 addition & 1 deletion crates/admin-cli/src/expected_machines/patch/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ pub struct Args {
long = "host_nics",
value_name = "HOST_NICS",
group = "group",
help = "Host NICs as a JSON array of ExpectedHostNic objects (fields: mac_address, network_segment_type, fixed_ip, fixed_mask, fixed_gateway, primary; legacy: nic_type). Replaces the machine's full host NIC list."
help = "Host NICs as a JSON array of ExpectedHostNic objects (fields: mac_address, role, ip_allocation, network_segment_type, fixed_ip, fixed_mask, fixed_gateway, primary; legacy: nic_type). Accepted values: role=host|dpu_os|dpu_bmc|unspecified and ip_allocation=dynamic|fixed|retained|unspecified. Replaces the machine's full host NIC list. For a matching stored MAC, omitting role preserves the stored role; role=unspecified resets it to host. Omitting ip_allocation preserves the stored policy when the presence of fixed_ip is unchanged; ip_allocation=unspecified resets it to fixed_ip inference."
)]
pub host_nics: Option<String>,

Expand Down
3 changes: 1 addition & 2 deletions crates/admin-cli/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,7 @@ impl ApiClient {
Ok(self.0.update_expected_machine(request).await?)
}

/// Replaces the entire expected-machine table from JSON. Entries with `bmc_ip_address` trigger
/// the same static BMC `machine_interface` setup as a single create.
/// Replaces the entire expected-machine table from JSON.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub async fn replace_all_expected_machines(
&self,
expected_machine_list: Vec<ExpectedMachineJson>,
Expand Down
102 changes: 50 additions & 52 deletions crates/api-core/src/dhcp/discover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,12 +480,11 @@ pub async fn discover_dhcp(
)?;
let is_v6_observation = address_family == IpAddressFamily::Ipv6
&& message_kind == Some(DhcpMessageKind::V6InfoRequest);
let mut host_nic: Option<ExpectedHostNic> = None;
// `is_primary_nic` reflects the matched ExpectedHostNic's `primary` flag.
// - `Some(true)` -- the operator flagged this NIC as the host's boot interface.
// - `Some(false)` -- another NIC on this host is the declared primary.
// - `None` -- no declaration, use the default at interface creation time.
let mut is_primary_nic: Option<bool> = None;
let mut expected_interface: Option<ExpectedHostNic> = None;
// `host_primary_declaration` is intentionally Host-only. A DPU OS
// interface is always primary and a DPU BMC interface is never primary;
// api-db derives those values from the matched interface role.
let mut host_primary_declaration: Option<bool> = None;

let parsed_mac: MacAddress = mac_address.parse()?;
let mut predicted_interface_for_observation = None;
Expand All @@ -500,23 +499,23 @@ pub async fn discover_dhcp(
match db::machine::find_existing_machine(&mut txn, parsed_mac, parsed_relay).await? {
Some(existing_machine) => Some(existing_machine),
None => {
if let Some(expected_interface) =
if let Some(predicted_interface) =
db::predicted_machine_interface::find_by_mac_address(&mut txn, parsed_mac)
.await?
{
if is_v6_observation {
predicted_interface_for_observation = Some(expected_interface);
predicted_interface_for_observation = Some(predicted_interface);
None
} else {
// remember expected machine id for later rack update
machine_interface::move_predicted_machine_interface_to_machine(
&mut txn,
&expected_interface,
&predicted_interface,
parsed_relay,
api.runtime_config.retained_boot_interface_window,
)
.await?;
Some(expected_interface.machine_id)
Some(predicted_interface.machine_id)
}
} else {
// DPA allocation is currently IPv4-only. The overlay
Expand All @@ -537,50 +536,48 @@ pub async fn discover_dhcp(
return Ok(resp);
}

// Now lets check expected machine data to see if there's any
// useful configuration we need to address, such as primary NIC
// assignment and/or static DHCP reservation allocations.
// ExpectedMachine is the ingestion template for an unknown
// interface, so this is where its role, segment guard, Host
// primary declaration, and allocation policy join the normal
// DHCP path.
//
// For static DHCP reservations, we do this here for the simple
// reason that it's a good place to put it. If an operator force
// deletes a machine and its interfaces, how would we put them
// back? The answer is the same way they would be put back in a
// dynamic allocation -- during DHCPDISCOVER/DHCPREQUEST. We see
// that a static DHCP reservation is configured per expected
// machine data, so we make an idempotent call to ensure that
// allocation exists, and if not, is created.
// Why also materialize a fixed reservation here? An operator
// may force-delete a machine and all of its interfaces. The
// next DHCP request is then the first point where we see that
// MAC again, so the idempotent preallocation rebuilds the
// reservation before the common find-or-create path runs.
if let Some(m) =
expected_machine::find_by_host_mac_address(&mut txn, parsed_mac)
.await
.map_err(CarbideError::from)?
{
// The host's declared primary NIC (if any) decides whether this
// MAC is its boot interface; the matched NIC also carries any
// static reservation need handled below.
if let Some(declared_primary_mac) = m.data.declared_primary_mac() {
is_primary_nic = Some(declared_primary_mac == parsed_mac);
}
host_nic = m
expected_interface = m
.data
.host_nics
.iter()
.find(|nic| nic.mac_address == parsed_mac)
.find(|interface| interface.mac_address == parsed_mac)
.cloned();
if let Some(ref nic) = host_nic
&& let Some(fixed_ip) = nic.fixed_ip
&& fixed_ip.is_address_family(address_family)
{
// It looks like there's a DHCP reservation for this address,
// so make an idempotent call to ensure we have a preallocated
// machine interface (and machine interface address) for it,
// creating one if needed.
db::machine_interface::preallocate_machine_interface(
&mut txn,
parsed_mac,
fixed_ip,
api.runtime_config.retained_boot_interface_window,
)
.await?;
if let Some(interface) = expected_interface.as_ref() {
if interface.role.is_host()
&& let Some(declared_primary_mac) = m.data.declared_primary_mac()
{
host_primary_declaration = Some(declared_primary_mac == parsed_mac);
}
if interface
.fixed_ip
.is_some_and(|fixed_ip| fixed_ip.is_address_family(address_family))
{
// The fixed reservation must exist before the common
// path looks up this MAC. This call also applies the
// interface's role and segment guard while the row is
// still unassociated.
db::machine_interface::preallocate_expected_machine_interface(
&mut txn,
interface,
api.runtime_config.retained_boot_interface_window,
)
.await?;
}
}
} else if let Some(m) =
expected_machine::find_by_bmc_mac_address(&mut txn, parsed_mac)
Expand Down Expand Up @@ -633,7 +630,7 @@ pub async fn discover_dhcp(
let network_segments = db::machine_interface::network_segments_for_dhcp_relays(
&mut txn,
std::slice::from_ref(&parsed_relay),
host_nic.as_ref(),
expected_interface.as_ref(),
)
.await?;
let exact_link_address_match = |segment: &NetworkSegment| {
Expand Down Expand Up @@ -693,9 +690,10 @@ pub async fn discover_dhcp(
&& interfaces.is_empty()
&& predicted_interface_for_observation.is_none()
{
// Anonymous reserved requests can receive segment options without
// creating observed rows. Known or predicted interfaces must
// continue through common safety checks and DHCP bookkeeping.
// Reserved requests without an existing or predicted interface can
// receive segment options without creating an observed row. Known
// or predicted interfaces must continue through common safety
// checks and DHCP bookkeeping.
let record = options_only_dhcpv6_record_from_segment(
&mut txn,
parsed_mac,
Expand Down Expand Up @@ -733,8 +731,8 @@ pub async fn discover_dhcp(
existing_machine_id,
parsed_mac,
std::slice::from_ref(&parsed_relay),
host_nic,
is_primary_nic,
expected_interface,
host_primary_declaration,
api.runtime_config.retained_boot_interface_window,
)
.await?
Expand All @@ -747,8 +745,8 @@ pub async fn discover_dhcp(
parsed_mac,
std::slice::from_ref(&parsed_relay),
machine_interface::FindOrCreateMachineInterfaceOptions {
host_nic,
is_primary: is_primary_nic,
host_nic: expected_interface,
is_primary: host_primary_declaration,
retained_window: api.runtime_config.retained_boot_interface_window,
},
address_family,
Expand Down
Loading
Loading