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
14 changes: 14 additions & 0 deletions crates/api-core/src/handlers/component_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ fn component_manager_error_to_status(err: ComponentManagerError) -> Status {
ComponentManagerError::Unavailable(msg) => Status::unavailable(msg),
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::Internal(msg) => Status::internal(msg),
ComponentManagerError::Transport(e) => Status::unavailable(format!("transport error: {e}")),
ComponentManagerError::Status(s) => s,
Expand Down Expand Up @@ -2577,6 +2579,18 @@ mod tests {
expected_code: Code::InvalidArgument,
message_contains: None,
},
ErrorToStatusCase {
scenario: "unsupported operation",
error: ComponentManagerError::Unsupported("not implemented".into()),
expected_code: Code::Unimplemented,
message_contains: Some("not implemented"),
},
ErrorToStatusCase {
scenario: "operation outcome unknown",
error: ComponentManagerError::OperationOutcomeUnknown("lost job id".into()),
expected_code: Code::FailedPrecondition,
message_contains: Some("lost job id"),
},
ErrorToStatusCase {
scenario: "internal",
error: ComponentManagerError::Internal("oops".into()),
Expand Down
3 changes: 3 additions & 0 deletions crates/component-manager/src/component_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub async fn build_component_manager(
rms_switch_system_image_client.clone(),
db,
rack_profiles.clone(),
config.rms.nvos_password_rotation_enabled,
))
}
NvSwitchBackend::Mock => Arc::new(crate::mock::MockNvSwitchManager::default()),
Expand Down Expand Up @@ -163,6 +164,7 @@ pub async fn build_component_manager(
rms_switch_system_image_client.clone(),
db,
rack_profiles.clone(),
config.rms.nvos_password_rotation_enabled,
))
}
PowerShelfBackend::Mock => Arc::new(crate::mock::MockPowerShelfManager),
Expand All @@ -185,6 +187,7 @@ pub async fn build_component_manager(
rms_switch_system_image_client.clone(),
db,
rack_profiles.clone(),
config.rms.nvos_password_rotation_enabled,
))
}
ComputeBackend::Core => {
Expand Down
27 changes: 27 additions & 0 deletions crates/component-manager/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub struct ComponentManagerConfig {
#[serde(default)]
pub compute_tray_backend: ComputeBackend,

#[serde(default)]
pub rms: RmsBackendConfig,

#[serde(default)]
pub nsm: Option<BackendEndpointConfig>,
#[serde(default)]
Expand Down Expand Up @@ -50,6 +53,16 @@ pub struct ComponentManagerConfig {
pub compute_tray_use_state_controller: bool,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RmsBackendConfig {
/// Enables the NVOS password-rotation backend capability.
///
/// End-to-end rotation remains unavailable until orchestration is implemented.
/// TODO: Remove this gate once end-to-end rotation is enabled by default.
#[serde(default)]
pub nvos_password_rotation_enabled: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should carbide config variable under RMS

}

/// Identifies a switch service that should use installed mTLS certificates.
///
/// Values mirror RMS `SwitchService` and are serialized in TOML as snake_case.
Expand Down Expand Up @@ -189,6 +202,20 @@ mod tests {
assert_eq!(cfg.nv_switch_backend, NvSwitchBackend::Rms);
assert_eq!(cfg.power_shelf_backend, PowerShelfBackend::Rms);
assert_eq!(cfg.compute_tray_backend, ComputeBackend::Rms);
assert!(!cfg.rms.nvos_password_rotation_enabled);
}

#[test]
fn rms_nvos_password_rotation_can_be_enabled() {
let cfg: ComponentManagerConfig = toml::from_str(
r#"
[rms]
nvos_password_rotation_enabled = true
"#,
)
.expect("NVOS password rotation setting should deserialize");

assert!(cfg.rms.nvos_password_rotation_enabled);
}

/// One `BackendTlsConfig` worth of path inputs for a resolver table.
Expand Down
14 changes: 14 additions & 0 deletions crates/component-manager/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ pub enum ComponentManagerError {
#[error("invalid argument: {0}")]
InvalidArgument(String),

/// The selected backend does not implement or enable the requested operation.
#[error("unsupported operation: {0}")]
Unsupported(String),

/// A mutating request may have reached the backend, but no job handle is
/// available. Callers must reconcile observed credential state
/// instead of retrying the mutation directly.
///
/// For [`crate::nv_switch_manager::NvSwitchManager::start_password_rotation`],
/// every other error guarantees that the backend did not accept the password
/// mutation and the caller may release any staged submission marker.
#[error("operation outcome unknown: {0}")]
OperationOutcomeUnknown(String),

#[error("internal error: {0}")]
Internal(String),

Expand Down
114 changes: 114 additions & 0 deletions crates/component-manager/src/nv_switch_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,64 @@ impl std::fmt::Display for Backend {
}
}

/// Backend-neutral classification of a terminal password-rotation failure.
///
/// This classification supplies reconciliation context; it does not by itself
/// determine whether retrying the password mutation is safe.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchPasswordRotationFailure {
/// The switch rejected the current credential.
Unauthenticated,

/// The backend rejected the request parameters.
InvalidArgument,

/// Another update prevented the password mutation from running.
UpdateInProgress,

/// Communication with the switch failed or returned an invalid response.
Communication,

/// The backend could not locate the target switch.
TargetNotFound,

/// The backend timed out while waiting for the password mutation.
TimedOut,

/// The backend reported an internal or otherwise unclassified failure.
Backend,

/// The backend returned an unset or unrecognized failure code.
Unknown,
}

/// Backend observation of an asynchronous switch OS password-rotation job.
///
/// This describes the backend job, not the credential state observed on the
/// switch. In particular, `NotFound` does not prove that the password mutation
/// was never accepted or did not complete.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchPasswordRotationState {
/// The backend cannot currently resolve the job. This does not establish
/// the mutation outcome and requires credential-state reconciliation.
NotFound,

/// The backend returned a job state that cannot be classified.
Unknown,

/// The backend accepted the job and it has not reached a terminal state.
Pending,

/// The backend reports that the password mutation completed successfully.
/// Callers may still need to promote and verify the staged credential.
Completed,

/// The backend reports that the job terminated unsuccessfully. The failure
/// class does not by itself identify which credential currently
/// authenticates.
Failed(SwitchPasswordRotationFailure),
}

/// Physical network identifiers for an NV-Switch, used to register with and
/// operate against the backend service (NSM).
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -99,10 +157,24 @@ pub struct ConfigureSwitchCertificateJobStatus {
/// and handle registration with the backend service internally. The
/// service-generated UUID is used for the actual operation and never exposed
/// to the caller; results are keyed by `bmc_mac`.
///
/// Password rotation is split into capability discovery, submission, and
/// observation. This keeps backend-specific job handling here while leaving
/// retry safety and credential reconciliation to the orchestration layer.
#[async_trait::async_trait]
pub trait NvSwitchManager: Send + Sync + Debug + 'static {
fn name(&self) -> &str;

/// Reports whether this backend is configured to support asynchronous OS
/// password rotation.
///
/// `false` means callers must not submit rotation work. `true` reports
/// configured capability, not current backend or switch health. The default
/// keeps existing backends disabled until they implement the full contract.
fn supports_password_rotation(&self) -> bool {
false
}

fn supports_firmware_object_json(&self) -> bool {
false
}
Expand Down Expand Up @@ -148,4 +220,46 @@ pub trait NvSwitchManager: Send + Sync + Debug + 'static {
&self,
job_id: &str,
) -> Result<ConfigureSwitchCertificateJobStatus, ComponentManagerError>;

/// Starts an asynchronous rotation from the endpoint's current NVOS
/// credential to `next_password`.
///
/// On accepted submission, returns a backend job ID that can be passed to
/// [`Self::get_password_rotation_job_status`].
/// Job IDs are opaque: callers must preserve the exact value and must not
/// infer backend state from its contents. Implementations must not log
/// `next_password` or otherwise expose it outside the backend request.
///
/// If dispatch may have reached the backend but no job ID is available, the
/// implementation returns [`ComponentManagerError::OperationOutcomeUnknown`].
/// Every other error guarantees that no password mutation was accepted.
/// Callers that cancel or time out this future before it returns must treat
/// the outcome as unknown unless they can prove dispatch did not occur. The
/// default implementation returns [`ComponentManagerError::Unsupported`].
async fn start_password_rotation(
&self,
_endpoint: &SwitchEndpoint,
_next_password: &str,
) -> Result<String, ComponentManagerError> {
Err(ComponentManagerError::Unsupported(format!(
"switch password rotation is not supported by the {} backend",
self.name()
)))
}

/// Returns the latest backend observation for a submitted rotation job.
///
/// A job that cannot be resolved is returned as
/// [`SwitchPasswordRotationState::NotFound`], not as an error. An error means
/// no job observation was obtained and does not imply a terminal job state.
/// The default implementation returns [`ComponentManagerError::Unsupported`].
async fn get_password_rotation_job_status(
Comment thread
vinodchitraliNVIDIA marked this conversation as resolved.
&self,
_job_id: &str,
) -> Result<SwitchPasswordRotationState, ComponentManagerError> {
Err(ComponentManagerError::Unsupported(format!(
"switch password rotation is not supported by the {} backend",
self.name()
)))
}
}
Loading
Loading