Skip to content
Open
9 changes: 2 additions & 7 deletions crates/admin-cli/src/credential/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,8 @@ pub enum UefiCredentialType {
/// 1:1 onto `rpc::forge::RotationCredentialType` (minus its proto3 `Unspecified`
/// zero value).
///
/// NVOS is listed even though the server does not own that password yet:
/// requesting it today returns a `FailedPrecondition` explaining it is gated on
/// set-NVOS-from-factory (REQ-6). Exposing it here keeps the "which families are
/// supported" policy in exactly one place -- the server's `to_rotation_type` --
/// so enabling NVOS later is a pure server change, and an operator who tries it
/// now gets that actionable error instead of a bare "invalid value" from argument
/// parsing.
/// NVOS is listed because the server can stage site-wide NVOS targets. Device
/// convergence depends on the switch-controller and component-manager path.
#[derive(ValueEnum, Parser, Debug, Clone)]
pub enum RotationCredentialKind {
Bmc,
Expand Down
3 changes: 3 additions & 0 deletions crates/admin-cli/src/credential/rotate/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Rotate the site-wide BMC root password, letting the server auto-generate a stron
Rotate the host UEFI password to an explicit value:
$ nico-admin-cli credential rotate --type=host-uefi --password=Str0ng-Explicit-Pw!

Stage a site-wide NVOS target password after seeding current per-switch credentials:
$ nico-admin-cli credential rotate --type=nvos --password=MyNvosPassword-2026

Rotate the SuperNIC lockdown IKM with an audit note:
$ nico-admin-cli credential rotate --type=lockdown-ikm --reason=\"quarterly rotation\"

Expand Down
7 changes: 3 additions & 4 deletions crates/admin-cli/src/credential/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ fn rotate_maps_to_proto() {
req.credential_type,
RotationCredentialType::RotationBmc as i32
);

assert!(req.password.is_none());
}

Expand Down Expand Up @@ -404,8 +405,7 @@ fn rotation_credential_kind_to_proto() {
RotationCredentialType::from(RotationCredentialKind::DpuUefi),
RotationCredentialType::RotationDpuUefi
));
// NVOS maps through even though the server rejects it today (FailedPrecondition
// until REQ-6); the CLI exposes it so support is a pure server-side change.

assert!(matches!(
RotationCredentialType::from(RotationCredentialKind::Nvos),
RotationCredentialType::RotationNvos
Expand Down Expand Up @@ -463,8 +463,7 @@ fn uefi_credential_type_value_enum() {
}

// rotation_credential_kind_value_enum ensures RotationCredentialKind parses from
// kebab-case strings, including nvos (which the CLI exposes so the server can
// return its FailedPrecondition rather than arg parsing rejecting it outright).
// kebab-case strings.
#[test]
fn rotation_credential_kind_value_enum() {
use clap::ValueEnum;
Expand Down
11 changes: 9 additions & 2 deletions crates/admin-cli/src/expected_switch/update/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
*/

use rpc::forge::ExpectedSwitch;
use rpc::forge_api_client::expected_switch_update_mask;

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

pub async fn update(data: Args, api_client: &ApiClient) -> color_eyre::Result<()> {
let request: ExpectedSwitch = data.try_into()?;
api_client.0.update_expected_switch(request).await?;
let patch: ExpectedSwitch = data.try_into()?;
let update_mask = expected_switch_update_mask(&patch);

api_client
.0
.patch_expected_switch(patch, &update_mask)
.await?;

Ok(())
}
7 changes: 7 additions & 0 deletions crates/api-core/src/credentials/bmc_session_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,13 @@ mod tests {

#[async_trait]
impl CredentialWriter for CountingCredentialManager {
async fn get_credentials_from_writer(
&self,
key: &CredentialKey,
) -> Result<Option<Credentials>, SecretsError> {
CredentialReader::get_credentials(self, key).await
}

async fn set_credentials(
&self,
_key: &CredentialKey,
Expand Down
12 changes: 10 additions & 2 deletions crates/api-core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use carbide_redfish::libredfish::RedfishClientCreationError;
use carbide_redfish::libredfish::dpu_bios::is_dpu_bios_attributes_not_ready;
use carbide_uuid::machine::MachineId;
use config_version::ConfigVersionParseError;
use db::credential_rotation::CredentialRotationType;
use db::ip_allocator::DhcpError;
use db::machine_interface_address::AddressAlreadyInUseError;
use db::resource_pool::ResourcePoolDatabaseError;
Expand Down Expand Up @@ -304,8 +305,15 @@ impl From<DatabaseError> for CarbideError {
DatabaseError::InvalidArgument(e) => InvalidArgument(e),
DatabaseError::InvalidConfiguration(e) => InvalidConfiguration(e),
DatabaseError::MissingArgument(e) => MissingArgument(e),
// A corrupted/absent site-wide rotation invariant is an internal
// state error, not a client-correctable one.
// NVOS intentionally has no target until its first versioned secret
// is published, so status requests can encounter this normal state.
DatabaseError::MissingSitewideRotationTarget(CredentialRotationType::Nvos) => {
FailedPrecondition(
"no site-wide NVOS credential rotation target has been published".to_string(),
)
}
// Other credential families are seeded during migration. A missing
// target for them is a corrupted invariant.
DatabaseError::MissingSitewideRotationTarget(credential_type) => Internal {
message: format!(
"no site-wide rotation target for credential type: {credential_type:?}"
Expand Down
11 changes: 9 additions & 2 deletions crates/api-core/src/handlers/component_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ pub(crate) fn component_manager_error_to_status(err: ComponentManagerError) -> S
ComponentManagerError::NotFound(msg) => Status::not_found(msg),
ComponentManagerError::InvalidArgument(msg) => Status::invalid_argument(msg),
ComponentManagerError::Unsupported(msg) => Status::unimplemented(msg),
ComponentManagerError::OperationOutcomeUnknown(msg) => Status::failed_precondition(msg),
ComponentManagerError::RejectedBeforeDispatch(msg) => Status::failed_precondition(msg),
ComponentManagerError::OperationOutcomeUnknown(msg) => Status::unavailable(msg),
ComponentManagerError::Internal(msg) => Status::internal(msg),
ComponentManagerError::Transport(e) => Status::unavailable(format!("transport error: {e}")),
ComponentManagerError::Status(s) => s,
Expand Down Expand Up @@ -2653,10 +2654,16 @@ mod tests {
expected_code: Code::Unimplemented,
message_contains: Some("not implemented"),
},
ErrorToStatusCase {
scenario: "operation rejected before dispatch",
error: ComponentManagerError::RejectedBeforeDispatch("request rejected".into()),
expected_code: Code::FailedPrecondition,
message_contains: Some("request rejected"),
},
ErrorToStatusCase {
scenario: "operation outcome unknown",
error: ComponentManagerError::OperationOutcomeUnknown("lost job id".into()),
expected_code: Code::FailedPrecondition,
expected_code: Code::Unavailable,
message_contains: Some("lost job id"),
},
ErrorToStatusCase {
Expand Down
Loading
Loading