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
3 changes: 3 additions & 0 deletions crates/admin-cli/src/dpu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod agent_upgrade_policy;
mod health_report;
mod network;
mod reprovision;
mod set_uefi_password;
mod status;
mod versions;

Expand Down Expand Up @@ -50,4 +51,6 @@ pub enum Cmd {
visible_alias = "hr"
)]
HealthReport(health_report::Args),
#[clap(about = "Set DPU UEFI password directly on the device (via Redfish)")]
SetUefiPassword(set_uefi_password::Args),
}
49 changes: 49 additions & 0 deletions crates/admin-cli/src/dpu/set_uefi_password/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use ::rpc::forge::SetDpuUefiPasswordRequest;
use clap::Parser;

use crate::machine::MachineQuery;

// Args wraps the shared MachineQuery as a subcommand
// specific newtype to allow sharing of MachineQuery, and still
// providing a subcommand-specific Run trait implementation.
#[derive(Parser, Debug, Clone)]
#[command(after_long_help = "\
EXAMPLES:

Set the UEFI password for a DPU by machine ID:
$ nico-admin-cli dpu set-uefi-password --query fm100ds038bg3qsho433vkg684heguv282qaggmrsh2ugn1qk096n2c6hcg

Set the UEFI password for a DPU selected by its BMC MAC address:
$ nico-admin-cli dpu set-uefi-password --query 00:11:22:33:44:55

")]
pub struct Args {
#[clap(flatten)]
pub inner: MachineQuery,
}

impl From<Args> for SetDpuUefiPasswordRequest {
fn from(args: Args) -> Self {
Self {
dpu_id: None,
machine_query: Some(args.inner.query),
}
}
}
30 changes: 30 additions & 0 deletions crates/admin-cli/src/dpu/set_uefi_password/cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use super::args::Args;
use crate::errors::CarbideCliResult;
use crate::rpc::ApiClient;

pub async fn set_uefi_password(data: Args, api_client: &ApiClient) -> CarbideCliResult<()> {
api_client.0.set_dpu_uefi_password(data).await?;
// A DPU stages the change through Redfish BIOS settings and schedules no job;
// it commits on the next DPU restart, so there is no job id to report.
println!(
"successfully staged the site-wide UEFI password on the DPU; it commits on the next DPU restart"
);
Ok(())
}
31 changes: 31 additions & 0 deletions crates/admin-cli/src/dpu/set_uefi_password/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

pub mod args;
pub mod cmd;

pub use args::Args;

use crate::cfg::run::Run;
use crate::cfg::runtime::RuntimeContext;
use crate::errors::CarbideCliResult;

impl Run for Args {
async fn run(self, ctx: &mut RuntimeContext) -> CarbideCliResult<()> {
cmd::set_uefi_password(self, &ctx.api_client).await
}
}
7 changes: 7 additions & 0 deletions crates/api-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,13 @@ impl Forge for Api {
crate::handlers::uefi::set_host_uefi_password(self, request).await
}

async fn set_dpu_uefi_password(
&self,
request: Request<rpc::SetDpuUefiPasswordRequest>,
) -> Result<Response<rpc::SetDpuUefiPasswordResponse>, Status> {
crate::handlers::uefi::set_dpu_uefi_password(self, request).await
}

async fn get_expected_machine(
&self,
request: Request<rpc::ExpectedMachineRequest>,
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/src/auth/internal_rbac_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ impl InternalRBACRules {
x.perm("UpdateInstancePhoneHomeLastContact", vec![Agent]);
x.perm("SetHostUefiPassword", vec![ForgeAdminCLI]);
x.perm("ClearHostUefiPassword", vec![ForgeAdminCLI]);
x.perm("SetDpuUefiPassword", vec![ForgeAdminCLI]);
x.perm(
"AddExpectedMachine",
vec![ForgeAdminCLI, SiteAgent, Flow, Machineatron],
Expand Down
2 changes: 1 addition & 1 deletion crates/api-core/src/cfg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ applicable.
| `vpc_peering_policy_on_existing` | `Option<VpcPeeringPolicy>` | — | `networking` | Policy for whether existing VPC peerings should be active. |
| `attestation_enabled` | `bool` | `false` | `security` | Enables TPM-based machine attestation (adds `Measuring` state before `Ready`). |
| `bmc_rotation_enabled` | `bool` | `false` | `security` | Site-wide kill-switch for passive BMC credential rotation. When `false` (default), a Ready host never auto-enters `RotatingBmc`; the force-converge escape hatch bypasses it. |
| `uefi_rotation_enabled` | `bool` | `false` | `security` | Site-wide kill-switch for passive UEFI credential rotation. When `false` (default), a Ready host never auto-enters `RotatingHostUefi`; the force-converge escape hatch bypasses it. |
| `uefi_rotation_enabled` | `bool` | `false` | `security` | Site-wide kill-switch for passive UEFI credential rotation (host and DPU). When `false` (default), a Ready host never auto-enters `RotatingHostUefi` nor drives its DPUs into `RotatingDpuUefi`; the per-machine force-converge escape hatch bypasses it. |
| `tpm_required` | `bool` | `true` | `security` | Require TPM module for machine registration. **Testing only** when `false`. |
| `machine_state_controller` | `MachineStateControllerConfig` | *(see below)* | `machines` | Machine state controller timing (see [MachineStateControllerConfig](#machinestatecontrollerconfig)). |
| `network_segment_state_controller` | `NetworkSegmentStateControllerConfig` | *(see below)* | `networking` | Network segment state controller timing. |
Expand Down
2 changes: 1 addition & 1 deletion crates/api-core/src/cfg/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ pub struct CarbideConfig {
#[serde(default)]
pub attestation_enabled: bool,

/// Site-wide enable for passive BMC credential rotation (REQ-2). When
/// Site-wide enable for passive BMC credential rotation. When
/// `false` (the default), a Ready host never enters `RotatingBmc` on its own
/// even if a device lags the staged site-wide target. This is the fleet
/// kill-switch for rolling the feature out site-by-site; the operator
Expand Down
181 changes: 181 additions & 0 deletions crates/api-core/src/handlers/uefi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,42 @@ pub(crate) async fn host_uefi_clear_credential_key(
Ok(CredentialKey::host_uefi_site_default(version))
}

/// The current site-wide DPU UEFI target version a device should be driven to,
/// from `sitewide_credential_rotation.target_version`. Mirrors
/// [`host_uefi_target_version`] for the `dpu_uefi` family: version 0 is the
/// legacy unversioned site-default baseline and a *missing* row is an error,
/// since the backfill migration seeds a `dpu_uefi` row on every site.
async fn dpu_uefi_target_version(conn: &mut sqlx::PgConnection) -> Result<u32, db::DatabaseError> {
let version = db::credential_rotation::current_target_version(
conn,
db::credential_rotation::CredentialRotationType::DpuUefi,
)
.await?
.ok_or_else(|| db::DatabaseError::Internal {
message: "no site-wide dpu_uefi rotation target row exists; the backfill migration \
seeds one for every active credential type, so a missing row indicates a \
broken or unmigrated database"
.to_string(),
})?;
u32::try_from(version).map_err(|e| db::DatabaseError::Internal {
message: format!("dpu UEFI target_version {version} is out of range for u32: {e}"),
})
}

/// The `CredentialKey` for the site-wide DPU UEFI password to *set* on a device:
/// the secret at the current `dpu_uefi` target version (table-driven; v0 = the
/// legacy unversioned site-default). The DPU analogue of
/// [`host_uefi_set_credential_key`]; only the database half (reads the version
/// and returns the key), so no connection is held across the remote reader.
pub(crate) async fn dpu_uefi_set_credential_key(
conn: &mut sqlx::PgConnection,
) -> Result<CredentialKey, CarbideError> {
let version = dpu_uefi_target_version(conn).await.map_err(|e| {
CarbideError::internal(format!("failed to read dpu UEFI target version: {e}"))
})?;
Ok(CredentialKey::dpu_uefi_site_default(version))
}

pub(crate) async fn clear_host_uefi_password(
api: &Api,
request: Request<rpc::ClearHostUefiPasswordRequest>,
Expand Down Expand Up @@ -425,3 +461,148 @@ pub(crate) async fn set_host_uefi_password(

Ok(Response::new(rpc::SetHostUefiPasswordResponse { job_id }))
}

/// Set a DPU's UEFI password directly on the device (the DPU equivalent of
/// [`set_host_uefi_password`]): stage the site-wide `dpu_uefi` credential through
/// the DPU's Redfish BIOS settings and record `dpu_uefi` convergence keyed by the
/// DPU BMC MAC. Like the host path, this stages the change and returns without
/// rebooting -- a DPU UEFI change only takes effect after a DPU restart, which
/// the DPU ingestion flow and the `RotatingDpuUefi` rotation state perform; this
/// direct path exists for operator testing, mirroring `SetHostUefiPassword`.
pub(crate) async fn set_dpu_uefi_password(
api: &Api,
request: Request<rpc::SetDpuUefiPasswordRequest>,
) -> Result<Response<rpc::SetDpuUefiPasswordResponse>, Status> {
log_request_data(&request);

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

let request = request.into_inner();

let machine_id = if let Some(query) = request.machine_query {
match db::machine::find_by_query(&mut txn, &query).await? {
Some(machine) => {
log_machine_id(&machine.id);
machine.id
}
None => {
return Err(CarbideError::NotFoundError {
kind: "machine",
id: query,
}
.into());
}
}
} else {
convert_and_log_machine_id(request.dpu_id.as_ref())?
};

if !machine_id.machine_type().is_dpu() {
return Err(CarbideError::InvalidArgument(
"SetDpuUefiPassword targets a DPU machine; use SetHostUefiPassword for a host".into(),
)
.into());
}

let snapshot = db::managed_host::load_snapshot(
&mut txn,
&machine_id,
LoadSnapshotOptions {
include_history: false,
include_instance_data: false,
host_health_config: api.runtime_config.host_health,
},
)
.await?
.ok_or_else(|| CarbideError::NotFoundError {
kind: "machine",
id: machine_id.to_string(),
})?;

// The loaded snapshot is keyed by the host machine; the DPU we are targeting
// is one of its `dpu_snapshots`. Locate it so we drive the DPU's own BMC.
let dpu = snapshot
.dpu_snapshots
.iter()
.find(|d| d.id == machine_id)
.ok_or_else(|| CarbideError::NotFoundError {
kind: "dpu",
id: machine_id.to_string(),
})?;

let addr = dpu.bmc_addr().ok_or_else(|| {
CarbideError::InvalidArgument("specified DPU does not have BMC address".into())
})?;

// A known DPU BMC MAC is a hard precondition: it keys the dpu_uefi rotation
// bookkeeping recorded below, so reject up front rather than driving the
// device and only then discovering we cannot track its convergence.
let dpu_bmc_mac = dpu.status.bmc_info.mac.ok_or_else(|| {
CarbideError::InvalidArgument("specified DPU does not have a known BMC MAC address".into())
})?;

let bmc_access_info =
db::machine_interface::lookup_bmc_access_info(&mut txn, addr.ip(), Some(addr.port()))
.await?;

// Resolve the site-wide DPU UEFI credential key to set (table-driven; v0 =
// the legacy unversioned site-default). This is the DB half; do it while the
// txn is open.
let dpu_uefi_key = dpu_uefi_set_credential_key(&mut txn).await?;

// Commit before the remote reader (Vault) request and the redfish call so the
// connection is not held across them, then read the actual secret.
txn.commit().await?;
let dpu_uefi_credentials =
read_uefi_credentials(api.redfish_pool.credential_reader(), &dpu_uefi_key).await?;

let redfish_client = api
.redfish_pool
.client_by_info(&bmc_access_info)
.await
.map_err(|e| {
tracing::error!(error = %e, "unable to create redfish client");
CarbideError::RedfishClientCreation {
inner: e.into(),
machine_id,
}
})?;

// A DPU stages the UEFI change through Redfish BIOS settings and schedules no
// job (it commits on the next DPU restart), so there is no job id to return.
api.redfish_pool
.uefi_setup(redfish_client.as_ref(), true, dpu_uefi_credentials)
.await
.map_err(|e| {
tracing::error!(error = %e, "Failed to run uefi_setup call for DPU");
CarbideError::internal(format!("failed redfish uefi_setup subtask: {e}"))
})?;

// Mirror the host path's optimistic convergence record: the change is staged
// through Redfish BIOS settings and commits on the next DPU restart; record
// dpu_uefi convergence (keyed by the DPU BMC MAC, as ingestion and the
// backfill do) so the rotation bookkeeping tracks this DPU. If the staged
// change ultimately fails to apply, this is inaccurate -- the same
// optimism the host set path carries.
api.with_txn(|txn| {
async move {
db::credential_rotation::record_device_converged(
txn,
dpu_bmc_mac,
db::credential_rotation::CredentialRotationType::DpuUefi,
)
.await?;
Ok::<(), db::DatabaseError>(())
}
.boxed()
})
.await?
.map_err(|e| {
tracing::error!(error = %e, "Failed to record dpu_uefi convergence");
CarbideError::Internal {
message: format!("Failed to record DPU UEFI convergence: {e}"),
}
})?;

Ok(Response::new(rpc::SetDpuUefiPasswordResponse {}))
}
15 changes: 9 additions & 6 deletions crates/api-core/src/handlers/uefi_credential_rotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ use crate::api::{Api, log_machine_id, log_request_data};
/// its next sweep; this handler only writes the flag (it performs no Redfish
/// work itself).
///
/// Unlike a BMC, a UEFI credential only ever belongs to a machine (a host, or a
/// DPU once DPU UEFI rotation ships), never a switch or power shelf, so the
/// target is always a single machine id.
/// Unlike a BMC, a UEFI credential only ever belongs to a machine (a host or a
/// DPU -- both are machine rows), never a switch or power shelf, so the target
/// is always a single machine id. A DPU is addressed by its own `machine_id` or
/// its DPU BMC MAC, driving `RotatingDpuUefi`; a host drives `RotatingHostUefi`.
pub(crate) async fn trigger_uefi_credential_rotation(
api: &Api,
request: Request<rpc::UefiCredentialRotationRequest>,
Expand Down Expand Up @@ -89,10 +90,12 @@ async fn resolve_target(
.transpose()?;

// A MAC uniquely names the BMC of one machine; resolve which machine owns
// it. UEFI rotation is keyed by the BMC MAC (host UEFI by the host BMC MAC;
// it. UEFI rotation is keyed by the BMC MAC (host UEFI by the host BMC MAC,
// DPU UEFI by the DPU BMC MAC), so the machine resolver's BMC-MAC lookup is
// the right one -- and unlike a bare BMC credential, a UEFI credential only
// ever belongs to a machine, never a switch or power shelf.
// the right one for either -- `find_machine_id_by_bmc_mac` matches any
// machine's BMC interface, DPU rows included -- and unlike a bare BMC
// credential, a UEFI credential only ever belongs to a machine, never a
// switch or power shelf.
let mac_machine_id = match bmc_mac {
Some(mac) => Some(
db::machine_topology::find_machine_id_by_bmc_mac(txn, mac)
Expand Down
Loading
Loading