Skip to content
Merged
74 changes: 54 additions & 20 deletions crates/api-core/src/instance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,27 @@ pub async fn allocate_instance(
.ok_or_else(|| CarbideError::internal("instance allocation returned no result".to_string()))
}

fn not_allocatable_error(machine_id: MachineId, reason: NotAllocatableReason) -> CarbideError {
match reason {
NotAllocatableReason::InvalidState(state) => CarbideError::InvalidArgument(format!(
"could not create instance on machine {machine_id} given machine state {state:?}"
)),
NotAllocatableReason::PendingInstanceCreation => CarbideError::InvalidArgument(format!(
"could not create instance on machine {machine_id}. machine is already used by another instance creation request",
)),
NotAllocatableReason::PendingBootConfiguration => {
CarbideError::FailedPrecondition(format!(
"machine {machine_id} has a pending boot configuration; retry after it has been applied"
))
}
NotAllocatableReason::NoDpuSnapshots => {
CarbideError::internal(format!("machine {machine_id} has no DPU. cannot allocate"))
}
NotAllocatableReason::MaintenanceMode => CarbideError::MaintenanceMode,
NotAllocatableReason::HealthAlert(_) => CarbideError::UnhealthyHost,
}
}

/// Allocates multiple instances in a single transaction.
/// Rolls back entirely if any allocation fails.
///
Expand Down Expand Up @@ -1517,26 +1538,20 @@ pub async fn batch_allocate_instances(
})?;

if let Err(e) = mh_snapshot.is_usable_as_instance(request.allow_unhealthy_machine) {
tracing::error!(
%machine_id,
error = %e,
"Host can not be used as instance due to reason",
);
return Err(match e {
NotAllocatableReason::InvalidState(s) => CarbideError::InvalidArgument(format!(
"could not create instance on machine {machine_id} given machine state {s:?}"
)),
NotAllocatableReason::PendingInstanceCreation => {
CarbideError::InvalidArgument(format!(
"could not create instance on machine {machine_id}. machine is already used by another instance creation request",
))
}
NotAllocatableReason::NoDpuSnapshots => CarbideError::internal(format!(
"machine {machine_id} has no DPU. cannot allocate"
)),
NotAllocatableReason::MaintenanceMode => CarbideError::MaintenanceMode,
NotAllocatableReason::HealthAlert(_) => CarbideError::UnhealthyHost,
});
if matches!(&e, NotAllocatableReason::PendingBootConfiguration) {
tracing::info!(
%machine_id,
error = %e,
"Host can not be used as instance due to reason",
);
} else {
tracing::error!(
%machine_id,
error = %e,
"Host can not be used as instance due to reason",
);
}
return Err(not_allocatable_error(machine_id, e));
}

if mh_snapshot.host_snapshot.config.dpf.used_for_ingestion
Expand Down Expand Up @@ -2335,6 +2350,25 @@ mod tests {
},
);
}

#[test]
fn pending_boot_configuration_has_a_safe_allocation_error() {
let machine_id = "fm100htes3rn1npvbtm5qd57dkilaag7ljugl1llmm7rfuq1ov50i0rpl30"
.parse()
.unwrap();

assert!(matches!(
not_allocatable_error(
machine_id,
NotAllocatableReason::PendingBootConfiguration,
),
CarbideError::FailedPrecondition(message)
if message
== format!(
"machine {machine_id} has a pending boot configuration; retry after it has been applied"
)
));
}
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/src/ipxe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ exit ||
let pxe_script = match &machine.current_state() {
ManagedHostState::Ready
| ManagedHostState::HostInit { .. }
| ManagedHostState::BootConfiguring { .. }
| ManagedHostState::BomValidating { .. }
| ManagedHostState::Measuring {
measuring_state: MeasuringState::WaitingForMeasurements,
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/src/tests/common/api_fixtures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ impl TestEnv {
ManagedHostState::HostInit { machine_state: mc }
}
ManagedHostState::Ready => state.clone(),
ManagedHostState::BootConfiguring { .. } => state.clone(),
ManagedHostState::Maintenance { .. } => state.clone(),
ManagedHostState::Assigned { .. } => state.clone(),
ManagedHostState::WaitingForCleanup { .. } => state.clone(),
Expand Down
32 changes: 32 additions & 0 deletions crates/api-core/src/tests/ipxe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ use chrono::Utc;
use common::api_fixtures::{
TestEnv, TestEnvOverrides, create_test_env, create_test_env_with_overrides, get_config,
};
use config_version::ConfigVersion;
use db::{self};
use futures_util::FutureExt;
use mac_address::MacAddress;
use model::machine::{
CleanupContext, DpuInitState, HostReprovisionState, MachineState, ManagedHostState,
ReadyBootConfigState, SetBootOrderInfo, SetBootOrderState,
};
use model::machine_boot_interface::MachineBootInterfaceTarget;
use model::test_support::ManagedHostConfig;
use rpc::forge::CloudInitInstructionsRequest;
use rpc::forge::forge_server::Forge;
Expand Down Expand Up @@ -341,6 +344,35 @@ async fn test_pxe_host(pool: sqlx::PgPool) {
.await;
assert!(instructions.pxe_script.contains("x86_64/scout.efi"));

move_machine_to_needed_state(
host_id,
&ManagedHostState::BootConfiguring {
desired_version: ConfigVersion::new(7),
desired_boot_interface: MachineBootInterfaceTarget::MacOnly(
"02:00:00:00:00:01".parse().unwrap(),
),
post_lock_verification_retry_count: 0,
boot_config_state: ReadyBootConfigState::SetBootOrder {
set_boot_order_info: SetBootOrderInfo {
set_boot_order_jid: None,
set_boot_order_state: SetBootOrderState::SetBootOrder,
retry_count: 0,
},
},
},
&env.pool,
)
.await;

let instructions = get_pxe_instructions(
&env,
host_interface_id,
rpc::forge::MachineArchitecture::X86,
None,
)
.await;
assert!(instructions.pxe_script.contains("x86_64/scout.efi"));

move_machine_to_needed_state(
host_id,
&ManagedHostState::HostReprovision {
Expand Down
Loading
Loading