Skip to content

Commit 1cd6d97

Browse files
feat(switch-controller): add ConfigureCertificate phase with RMS job polling
Extend the switch Configuring state machine with a certificate configuration sub-flow that runs after RotateOsPassword and before Validating. The handler submits an async RMS job via Component Manager and polls until completion. State machine (api-model): - Add ConfigureCertificateState { Start, WaitForComplete { job_id } } - Nest under ConfiguringState::ConfigureCertificate - RotateOsPassword now transitions into ConfigureCertificate(Start) Switch handler (configuring.rs): - Start: derive cert_name from switch.rack_id; build SwitchEndpoint from BMC MAC, NVOS interface, and vault credentials; call CM to start the job - WaitForComplete: poll CM for ConfigureSwitchCertificateState until Completed (→ Validating), Failed (→ Error), or in-progress (wait) - Skip certificate configuration when rack_id or component manager is absent Component Manager: - Expose configure_switch_certificate(endpoint, cert_name) → job_id - Expose get_configure_switch_certificate_job_status(job_id) → job status - Extend NvSwitchManager; implement in mock (configurable job status), NSM (unsupported), and RmsBackend (stub until librms RPCs land) - Add ConfigureSwitchCertificateState { Started, InProgress, Completed, Failed } Tests: - Integration tests for skip paths, Start → WaitForComplete, success/failure polling, and RotateOsPassword → ConfigureCertificate(Start) - Test fixtures for rack_id assignment and versioned state transitions Docs: - Add switch_configure_certificate.md with FSM detail and RMS sequence diagrams - Update switch.md transitions and link to the new design doc Signed-off-by: Vinod Chitrali <vchitrali@nvidia.com>
1 parent 51d0530 commit 1cd6d97

15 files changed

Lines changed: 1106 additions & 83 deletions

File tree

crates/api-core/src/tests/switch_state_controller/fixtures/switch.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
* limitations under the License.
1616
*/
1717

18+
use carbide_uuid::rack::RackId;
1819
use carbide_uuid::switch::SwitchId;
19-
use model::switch::SwitchControllerState;
20+
use db::switch as db_switch;
21+
use model::switch::{ConfigureCertificateState, ConfiguringState, SwitchControllerState};
2022
use sqlx::PgConnection;
2123

2224
/// Helper function to set switch controller state directly in database
@@ -34,6 +36,56 @@ pub async fn set_switch_controller_state(
3436
Ok(())
3537
}
3638

39+
pub async fn set_switch_rack_id(
40+
txn: &mut PgConnection,
41+
switch_id: &SwitchId,
42+
rack_id: &RackId,
43+
) -> Result<(), sqlx::Error> {
44+
sqlx::query("UPDATE switches SET rack_id = $1 WHERE id = $2")
45+
.bind(rack_id)
46+
.bind(switch_id)
47+
.execute(txn)
48+
.await?;
49+
Ok(())
50+
}
51+
52+
pub async fn transition_switch_controller_state(
53+
txn: &mut PgConnection,
54+
switch_id: &SwitchId,
55+
new_state: SwitchControllerState,
56+
) -> Result<(), Box<dyn std::error::Error>> {
57+
let switch = db_switch::find_by_id(txn, switch_id)
58+
.await?
59+
.expect("switch should exist");
60+
db_switch::try_update_controller_state(
61+
txn,
62+
*switch_id,
63+
switch.controller_state.version,
64+
switch.controller_state.version.increment(),
65+
&new_state,
66+
)
67+
.await?;
68+
Ok(())
69+
}
70+
71+
pub fn configure_certificate_start_state() -> SwitchControllerState {
72+
SwitchControllerState::Configuring {
73+
config_state: ConfiguringState::ConfigureCertificate {
74+
configure_certificate: ConfigureCertificateState::Start,
75+
},
76+
}
77+
}
78+
79+
pub fn configure_certificate_wait_state(job_id: &str) -> SwitchControllerState {
80+
SwitchControllerState::Configuring {
81+
config_state: ConfiguringState::ConfigureCertificate {
82+
configure_certificate: ConfigureCertificateState::WaitForComplete {
83+
job_id: job_id.to_string(),
84+
},
85+
},
86+
}
87+
}
88+
3789
/// Helper function to mark switch as deleted
3890
pub async fn mark_switch_as_deleted(
3991
txn: &mut PgConnection,

crates/api-core/src/tests/switch_state_controller/mod.rs

Lines changed: 292 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ use std::time::Duration;
2121
use carbide_switch_controller::context::SwitchStateHandlerServices;
2222
use carbide_switch_controller::handler::SwitchStateHandler;
2323
use carbide_switch_controller::io::SwitchStateControllerIO;
24+
use carbide_uuid::rack::RackId;
2425
use component_manager::compute_tray_manager::Backend;
2526
use component_manager::config::ComponentManagerConfig;
27+
use component_manager::mock::MockNvSwitchManager;
28+
use component_manager::nv_switch_manager::ConfigureSwitchCertificateJobStatus;
2629
use db::switch as db_switch;
2730
use forge_secrets::test_support::credentials::TestCredentialManager;
28-
use model::switch::{ConfiguringState, SwitchControllerState};
31+
use model::component_manager::ConfigureSwitchCertificateState;
32+
use model::switch::{
33+
ConfigureCertificateState, ConfiguringState, SwitchControllerState, ValidatingState,
34+
};
2935
use rpc::forge::forge_server::Forge;
3036
use state_controller::config::IterationConfig;
3137
use state_controller::controller::StateController;
@@ -36,7 +42,10 @@ use crate::tests::common::api_fixtures::create_test_env;
3642

3743
mod fixtures;
3844
mod maintenance;
39-
use fixtures::switch::{mark_switch_as_deleted, set_switch_controller_state};
45+
use fixtures::switch::{
46+
configure_certificate_start_state, configure_certificate_wait_state, mark_switch_as_deleted,
47+
set_switch_controller_state, set_switch_rack_id, transition_switch_controller_state,
48+
};
4049

4150
async fn build_test_component_manager(
4251
env: &common::api_fixtures::TestEnv,
@@ -65,6 +74,284 @@ async fn build_test_component_manager(
6574
.map(Arc::new)
6675
}
6776

77+
async fn run_switch_controller_with_services(
78+
pool: sqlx::PgPool,
79+
work_lock_manager_handle: db::work_lock_manager::WorkLockManagerHandle,
80+
services: SwitchStateHandlerServices,
81+
) {
82+
let cancel_token = CancellationToken::new();
83+
let mut controller = StateController::<SwitchStateControllerIO>::builder()
84+
.iteration_config(IterationConfig {
85+
iteration_time: Duration::from_millis(50),
86+
processor_dispatch_interval: Duration::from_millis(10),
87+
..Default::default()
88+
})
89+
.database(pool, work_lock_manager_handle)
90+
.processor_id(uuid::Uuid::new_v4().to_string())
91+
.services(services.into())
92+
.state_handler(Arc::new(SwitchStateHandler::default()))
93+
.build_for_manual_iterations(cancel_token)
94+
.unwrap();
95+
controller.run_single_iteration().await;
96+
}
97+
98+
fn mock_component_manager(
99+
nv_switch: Arc<dyn component_manager::nv_switch_manager::NvSwitchManager>,
100+
) -> Arc<component_manager::component_manager::ComponentManager> {
101+
Arc::new(component_manager::component_manager::ComponentManager::new(
102+
nv_switch,
103+
Arc::new(component_manager::mock::MockPowerShelfManager),
104+
Arc::new(component_manager::mock::MockComputeTrayManager),
105+
false,
106+
false,
107+
false,
108+
))
109+
}
110+
111+
#[crate::sqlx_test]
112+
async fn test_configure_certificate_start_skips_without_rack_id(
113+
pool: sqlx::PgPool,
114+
) -> Result<(), Box<dyn std::error::Error>> {
115+
let env = create_test_env(pool.clone()).await;
116+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
117+
118+
let mut txn = pool.begin().await?;
119+
transition_switch_controller_state(
120+
txn.as_mut(),
121+
&switch_id,
122+
configure_certificate_start_state(),
123+
)
124+
.await?;
125+
txn.commit().await?;
126+
127+
env.run_switch_controller_iteration().await;
128+
129+
let mut txn = pool.acquire().await?;
130+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
131+
.await?
132+
.expect("switch should exist");
133+
assert!(matches!(
134+
switch.controller_state.value,
135+
SwitchControllerState::Configuring {
136+
config_state: ConfiguringState::RotateOsPassword,
137+
}
138+
));
139+
assert!(switch.rack_id.is_none());
140+
141+
Ok(())
142+
}
143+
144+
#[crate::sqlx_test]
145+
async fn test_configure_certificate_start_skips_without_component_manager(
146+
pool: sqlx::PgPool,
147+
) -> Result<(), Box<dyn std::error::Error>> {
148+
let env = create_test_env(pool.clone()).await;
149+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
150+
let rack_id = RackId::new(uuid::Uuid::new_v4().to_string());
151+
152+
let mut txn = pool.begin().await?;
153+
set_switch_rack_id(txn.as_mut(), &switch_id, &rack_id).await?;
154+
transition_switch_controller_state(
155+
txn.as_mut(),
156+
&switch_id,
157+
configure_certificate_start_state(),
158+
)
159+
.await?;
160+
txn.commit().await?;
161+
162+
run_switch_controller_with_services(
163+
pool.clone(),
164+
env.api.work_lock_manager_handle.clone(),
165+
SwitchStateHandlerServices {
166+
db_pool: pool.clone(),
167+
component_manager: None,
168+
credential_manager: env.test_credential_manager.clone(),
169+
},
170+
)
171+
.await;
172+
173+
let mut txn = pool.acquire().await?;
174+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
175+
.await?
176+
.expect("switch should exist");
177+
assert!(matches!(
178+
switch.controller_state.value,
179+
SwitchControllerState::Configuring {
180+
config_state: ConfiguringState::RotateOsPassword,
181+
}
182+
));
183+
184+
Ok(())
185+
}
186+
187+
#[crate::sqlx_test]
188+
async fn test_configure_certificate_start_transitions_to_wait_for_complete_with_rack_id(
189+
pool: sqlx::PgPool,
190+
) -> Result<(), Box<dyn std::error::Error>> {
191+
let env = create_test_env(pool.clone()).await;
192+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
193+
let rack_id = RackId::new(uuid::Uuid::new_v4().to_string());
194+
195+
let mut txn = pool.begin().await?;
196+
set_switch_rack_id(txn.as_mut(), &switch_id, &rack_id).await?;
197+
transition_switch_controller_state(
198+
txn.as_mut(),
199+
&switch_id,
200+
configure_certificate_start_state(),
201+
)
202+
.await?;
203+
txn.commit().await?;
204+
205+
run_switch_controller_with_services(
206+
pool.clone(),
207+
env.api.work_lock_manager_handle.clone(),
208+
SwitchStateHandlerServices {
209+
db_pool: pool.clone(),
210+
component_manager: Some(mock_component_manager(Arc::new(MockNvSwitchManager::default()))),
211+
credential_manager: env.test_credential_manager.clone(),
212+
},
213+
)
214+
.await;
215+
216+
let mut txn = pool.acquire().await?;
217+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
218+
.await?
219+
.expect("switch should exist");
220+
assert!(matches!(
221+
switch.controller_state.value,
222+
SwitchControllerState::Configuring {
223+
config_state: ConfiguringState::ConfigureCertificate {
224+
configure_certificate: ConfigureCertificateState::WaitForComplete {
225+
ref job_id
226+
},
227+
},
228+
} if job_id == "mock-switch-cert-job"
229+
));
230+
assert_eq!(switch.rack_id.as_ref(), Some(&rack_id));
231+
232+
Ok(())
233+
}
234+
235+
#[crate::sqlx_test]
236+
async fn test_configure_certificate_wait_for_complete_transitions_to_rotate_os_password(
237+
pool: sqlx::PgPool,
238+
) -> Result<(), Box<dyn std::error::Error>> {
239+
let env = create_test_env(pool.clone()).await;
240+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
241+
242+
let mut txn = pool.begin().await?;
243+
transition_switch_controller_state(
244+
txn.as_mut(),
245+
&switch_id,
246+
configure_certificate_wait_state("mock-switch-cert-job"),
247+
)
248+
.await?;
249+
txn.commit().await?;
250+
251+
run_switch_controller_with_services(
252+
pool.clone(),
253+
env.api.work_lock_manager_handle.clone(),
254+
SwitchStateHandlerServices {
255+
db_pool: pool.clone(),
256+
component_manager: Some(mock_component_manager(Arc::new(MockNvSwitchManager::default()))),
257+
credential_manager: env.test_credential_manager.clone(),
258+
},
259+
)
260+
.await;
261+
262+
let mut txn = pool.acquire().await?;
263+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
264+
.await?
265+
.expect("switch should exist");
266+
assert!(matches!(
267+
switch.controller_state.value,
268+
SwitchControllerState::Configuring {
269+
config_state: ConfiguringState::RotateOsPassword,
270+
}
271+
));
272+
273+
Ok(())
274+
}
275+
276+
#[crate::sqlx_test]
277+
async fn test_configure_certificate_wait_for_complete_transitions_to_error_on_failure(
278+
pool: sqlx::PgPool,
279+
) -> Result<(), Box<dyn std::error::Error>> {
280+
let env = create_test_env(pool.clone()).await;
281+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
282+
283+
let mut txn = pool.begin().await?;
284+
transition_switch_controller_state(
285+
txn.as_mut(),
286+
&switch_id,
287+
configure_certificate_wait_state("mock-switch-cert-job"),
288+
)
289+
.await?;
290+
txn.commit().await?;
291+
292+
let failing_mock = MockNvSwitchManager::default().with_certificate_job_status(
293+
ConfigureSwitchCertificateJobStatus {
294+
state: ConfigureSwitchCertificateState::Failed,
295+
error: Some("cert install failed".to_string()),
296+
},
297+
);
298+
run_switch_controller_with_services(
299+
pool.clone(),
300+
env.api.work_lock_manager_handle.clone(),
301+
SwitchStateHandlerServices {
302+
db_pool: pool.clone(),
303+
component_manager: Some(mock_component_manager(Arc::new(failing_mock))),
304+
credential_manager: env.test_credential_manager.clone(),
305+
},
306+
)
307+
.await;
308+
309+
let mut txn = pool.acquire().await?;
310+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
311+
.await?
312+
.expect("switch should exist");
313+
assert!(matches!(
314+
switch.controller_state.value,
315+
SwitchControllerState::Error { ref cause } if cause == "cert install failed"
316+
));
317+
318+
Ok(())
319+
}
320+
321+
#[crate::sqlx_test]
322+
async fn test_rotate_os_password_transitions_to_validating(
323+
pool: sqlx::PgPool,
324+
) -> Result<(), Box<dyn std::error::Error>> {
325+
let env = create_test_env(pool.clone()).await;
326+
let switch_id = common::api_fixtures::site_explorer::new_switch(&env, None, None).await?;
327+
328+
let mut txn = pool.begin().await?;
329+
transition_switch_controller_state(
330+
txn.as_mut(),
331+
&switch_id,
332+
SwitchControllerState::Configuring {
333+
config_state: ConfiguringState::RotateOsPassword,
334+
},
335+
)
336+
.await?;
337+
txn.commit().await?;
338+
339+
env.run_switch_controller_iteration().await;
340+
341+
let mut txn = pool.acquire().await?;
342+
let switch = db_switch::find_by_id(&mut txn, &switch_id)
343+
.await?
344+
.expect("switch should exist");
345+
assert!(matches!(
346+
switch.controller_state.value,
347+
SwitchControllerState::Validating {
348+
validating_state: ValidatingState::ValidationComplete,
349+
}
350+
));
351+
352+
Ok(())
353+
}
354+
68355
#[crate::sqlx_test]
69356
async fn test_switch_state_transition_validation(
70357
pool: sqlx::PgPool,
@@ -194,9 +481,9 @@ async fn test_switch_deletion_with_state_controller(
194481
}
195482

196483
/// Tests the entire Switch ControllerState transition flow: Initializing -> Configuring
197-
/// (RotateOsPassword) -> Validating (ValidationComplete) -> BomValidating
198-
/// (BomValidationComplete) -> Ready. Uses the real SwitchStateHandler so each state handler
199-
/// performs its transition.
484+
/// (ConfigureCertificate) -> Configuring (RotateOsPassword) -> Validating (ValidationComplete)
485+
/// -> BomValidating (BomValidationComplete) -> Ready. Uses the real SwitchStateHandler so each
486+
/// state handler performs its transition.
200487
#[crate::sqlx_test]
201488
async fn test_switch_entire_state_transition_flow(
202489
pool: sqlx::PgPool,

0 commit comments

Comments
 (0)