diff --git a/crates/api-web/src/managed_host.rs b/crates/api-web/src/managed_host.rs index 0dba7c859c..295a757035 100644 --- a/crates/api-web/src/managed_host.rs +++ b/crates/api-web/src/managed_host.rs @@ -53,6 +53,7 @@ struct ManagedHostShow { active_state_filter: String, active_time_in_state_above_sla_filter: String, states: Vec, + health_alert_ids: Vec, gpus: Vec, active_gpu_filter: String, ibs: Vec, @@ -456,6 +457,7 @@ pub async fn show_html( let mut hosts = Vec::new(); let mut models_per_vendor: HashMap> = HashMap::new(); let mut states = HashSet::new(); + let mut health_alert_ids = HashSet::new(); let mut gpus = HashSet::new(); let mut ibs = HashSet::new(); let mut mems = HashSet::new(); @@ -481,6 +483,9 @@ pub async fn show_html( states.insert(short_state(&m.state).to_string()); gpus.insert(m.num_gpus); ibs.insert(m.num_ib_ifs); + for alert in &m.health_probe_alerts { + health_alert_ids.insert(alert.id.to_string()); + } let barry = mem_to_size(&m.host_memory); mems.insert((barry, format!("{barry} GiB"))); @@ -528,6 +533,15 @@ pub async fn show_html( if active_health_alerts_filter == "unhealthy" && m.health_probe_alerts.is_empty() { continue; } + if active_health_alerts_filter != "healthy" + && active_health_alerts_filter != "unhealthy" + && !m + .health_probe_alerts + .iter() + .any(|a| a.id.to_string() == active_health_alerts_filter) + { + continue; + } } if active_maintenance_filter != "all" { if active_maintenance_filter == "active" && !m.maintenance_reference.is_empty() { @@ -570,6 +584,8 @@ pub async fn show_html( let states: Vec = states.into_iter().sorted_unstable().collect(); + let health_alert_ids: Vec = health_alert_ids.into_iter().sorted_unstable().collect(); + let gpus: Vec = gpus .into_iter() .map(|x| x.to_string()) @@ -633,6 +649,7 @@ pub async fn show_html( active_state_filter, active_time_in_state_above_sla_filter, states, + health_alert_ids, gpus, active_gpu_filter, ibs, diff --git a/crates/api-web/src/tests/managed_host.rs b/crates/api-web/src/tests/managed_host.rs index 40453a3084..5cdbe4918e 100644 --- a/crates/api-web/src/tests/managed_host.rs +++ b/crates/api-web/src/tests/managed_host.rs @@ -337,3 +337,102 @@ async fn test_managed_host_html_uses_runtime_sla_config(pool: sqlx::PgPool) { assert!(body_str.contains("bubble warning")); assert!(body_str.contains("Assigned/BootingWithDiscoveryImage")); } + +#[crate::sqlx_test] +async fn test_managed_host_health_alert_exact_filter(pool: sqlx::PgPool) -> eyre::Result<()> { + let env = TestEnv::new(pool).await; + let mh_alerting = env.create_ready_managed_host(1).await.0; + let mh_healthy = env.create_ready_managed_host(2).await.0; + + let report = HealthReport { + source: "mock-bmc-intrusion".to_string(), + triggered_by: None, + observed_at: None, + successes: vec![], + alerts: vec![HealthProbeAlert { + id: HealthProbeId::from_str("IntrusionSensorTriggered")?, + target: Some("HostBMC".to_string()), + in_alert_since: None, + message: "Physical Chassis Intrusion Alert".to_string(), + tenant_message: None, + classifications: vec![ + HealthAlertClassification::hardware(), + HealthAlertClassification::sensor_critical(), + HealthAlertClassification::prevent_allocations(), + ], + }], + }; + + let mut txn = env.test_harness.db_txn().await; + db::machine::insert_health_report( + &mut txn, + &mh_alerting.host.id, + HealthReportApplyMode::Merge, + &report, + false, + ) + .await?; + txn.commit().await?; + + let app = make_test_app(&env.test_harness); + let response = app + .oneshot( + web_request_builder() + .uri("/admin/managed-host?health-alerts-filter=IntrusionSensorTriggered") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body_bytes = response + .into_body() + .collect() + .await + .expect("Empty response body?") + .to_bytes(); + let body_str = std::str::from_utf8(&body_bytes).expect("Invalid UTF-8 in body"); + + // Only the alerting host should show up, not the healthy one. + assert!(body_str.contains(&mh_alerting.host.id.to_string())); + assert!(!body_str.contains(&mh_healthy.host.id.to_string())); + + // The dynamic dropdown should include this alert ID as the selected option. + assert!( + body_str.contains(r#" + {% for a in health_alert_ids %} + + {% endfor %}