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
61 changes: 1 addition & 60 deletions crates/api-db/src/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use carbide_uuid::switch::SwitchId;
use chrono::prelude::*;
use config_version::{ConfigVersion, Versioned};
use health_report::{HealthReport, HealthReportApplyMode};
use mac_address::MacAddress;
use model::controller_outcome::PersistentStateHandlerOutcome;
use model::metadata::Metadata;
use model::rack::RackFirmwareUpgradeStatus;
Expand Down Expand Up @@ -459,34 +460,6 @@ pub async fn update(switch: &Switch, txn: &mut PgConnection) -> Result<Switch, D
Ok(switch.clone())
}

use mac_address::MacAddress;

#[derive(Debug, sqlx::FromRow)]
pub struct SwitchBmcInfoRow {
pub serial_number: String,
pub bmc_mac_address: MacAddress,
pub ip_address: IpAddr,
}

pub async fn list_switch_bmc_info(txn: &mut PgConnection) -> DatabaseResult<Vec<SwitchBmcInfoRow>> {
let sql = r#"
SELECT
es.serial_number,
es.bmc_mac_address,
mia.address as ip_address
FROM expected_switches es
JOIN machine_interfaces mi ON mi.mac_address = es.bmc_mac_address
JOIN machine_interface_addresses mia ON mia.interface_id = mi.id
JOIN network_segments ns ON ns.id = mi.segment_id
WHERE ns.network_segment_type = 'underlay'
"#;

sqlx::query_as(sql)
.fetch_all(txn)
.await
.map_err(|err| DatabaseError::new("list_switch_bmc_info", err))
}

/// Resolve SwitchIds to BMC IPs via the FK path:
/// switches.bmc_mac_address -> expected_switches.bmc_mac_address
/// -> machine_interfaces -> machine_interface_addresses (underlay) -> IP
Expand Down Expand Up @@ -611,38 +584,6 @@ pub async fn update_metadata(
}
}

#[derive(Debug, sqlx::FromRow)]
pub struct SwitchBmcRow {
pub switch_id: SwitchId,
pub bmc_mac: MacAddress,
pub bmc_ip: IpAddr,
}

/// Resolve SwitchIds to BMC MAC + IP via machine_interfaces.
pub async fn find_bmc_info_by_switch_ids(
db: impl crate::db_read::DbReader<'_>,
switch_ids: &[SwitchId],
) -> DatabaseResult<Vec<SwitchBmcRow>> {
let sql = r#"
SELECT DISTINCT ON (mi.switch_id)
mi.switch_id,
mi.mac_address AS bmc_mac,
mia.address AS bmc_ip
FROM machine_interfaces mi
JOIN machine_interface_addresses mia ON mia.interface_id = mi.id
JOIN network_segments ns ON ns.id = mi.segment_id
WHERE mi.switch_id = ANY($1)
AND ns.network_segment_type = 'underlay'
ORDER BY mi.switch_id
"#;

sqlx::query_as(sql)
.bind(switch_ids)
.fetch_all(db)
.await
.map_err(|err| DatabaseError::new("switch::find_bmc_info_by_switch_ids", err))
}

/// A switch resolved by its BMC MAC address, along with the rack it belongs
/// to. Used by the Component Manager state controller wrapper to build a
/// rack-level `MaintenanceScope` for the switches it's been asked to act on.
Expand Down
7 changes: 7 additions & 0 deletions crates/api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,13 @@ impl Forge for Api {
crate::handlers::credential::get_bmc_credentals(self, request).await
}

async fn get_switch_nvos_credentials(
&self,
request: Request<rpc::GetSwitchNvosCredentialsRequest>,
) -> Result<Response<rpc::GetBmcCredentialsResponse>, Status> {
crate::handlers::credential::get_switch_nvos_credentials(self, request).await
}

/// Network status of each managed host, as reported by forge-dpu-agent.
/// For use by forge-admin-cli
///
Expand Down
1 change: 1 addition & 0 deletions crates/api/src/auth/internal_rbac_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ impl InternalRBACRules {
x.perm("DeleteTenantKeyset", vec![SiteAgent]);
x.perm("ValidateTenantPublicKey", vec![SiteAgent, Ssh, SshRs]);
x.perm("GetBmcCredentials", vec![Health, BmcProxy]);
x.perm("GetSwitchNvosCredentials", vec![Health]);
x.perm("GetAllManagedHostNetworkStatus", vec![ForgeAdminCLI]);
x.perm(
"GetSiteExplorationReport",
Expand Down
61 changes: 60 additions & 1 deletion crates/api/src/handlers/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,10 @@ pub(crate) async fn get_bmc_credentals(
})
.await
.map_err(|e| CarbideError::internal(e.to_string()))?
.ok_or_else(|| CarbideError::internal("missing credentials".to_string()))?;
.ok_or_else(|| CarbideError::NotFoundError {
kind: "bmc_root_credentials",
id: req.mac_addr.clone(),
})?;

let (username, password) = match credentials {
Credentials::UsernamePassword { username, password } => (username, password),
Expand All @@ -424,6 +427,62 @@ pub(crate) async fn get_bmc_credentals(
}))
}

pub(crate) async fn get_switch_nvos_credentials(
api: &Api,
request: tonic::Request<rpc::GetSwitchNvosCredentialsRequest>,
) -> Result<Response<rpc::GetBmcCredentialsResponse>, tonic::Status> {
crate::api::log_request_data(&request);

let req = request.into_inner();
let switch_id = req
.switch_id
.ok_or_else(|| CarbideError::InvalidArgument("switch_id is required".to_string()))?;

let bmc_mac_address = {
let mut txn = api.txn_begin().await?;
let switches = db::switch::find_by(
&mut txn,
db::ObjectColumnFilter::One(db::switch::IdColumn, &switch_id),
)
.await?;
let _ = txn.rollback().await;

let switch = switches
.first()
.ok_or_else(|| CarbideError::NotFoundError {
kind: "switch",
id: switch_id.to_string(),
})?;

switch
.bmc_mac_address
.ok_or_else(|| CarbideError::NotFoundError {
kind: "switch_bmc_mac_address",
id: switch_id.to_string(),
})?
};

let credentials = api
.credential_manager
.get_credentials(&CredentialKey::SwitchNvosAdmin { bmc_mac_address })
.await
.map_err(|e| CarbideError::internal(e.to_string()))?
.ok_or_else(|| CarbideError::NotFoundError {
kind: "switch_nvos_credentials",
id: switch_id.to_string(),
})?;

let Credentials::UsernamePassword { username, password } = credentials;

Ok(Response::new(rpc::GetBmcCredentialsResponse {
credentials: Some(rpc::BmcCredentials {
r#type: Some(rpc::bmc_credentials::Type::UsernamePassword(
rpc::UsernamePassword { username, password },
)),
}),
}))
}

async fn set_sitewide_bmc_root_credentials(
api: &Api,
password: String,
Expand Down
114 changes: 66 additions & 48 deletions crates/api/src/handlers/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,17 @@ pub async fn find_switch(
})?
};

let bmc_info_map: std::collections::HashMap<String, rpc::BmcInfo> = {
let rows = db_switch::list_switch_bmc_info(&mut txn)
let switch_ids: Vec<_> = switch_list.iter().map(|switch| switch.id).collect();
let endpoint_info_map: std::collections::HashMap<_, _> = if switch_ids.is_empty() {
std::collections::HashMap::new()
} else {
db_switch::find_switch_endpoints_by_ids(&mut *txn, &switch_ids)
.await
.map_err(|e| CarbideError::Internal {
message: format!("Failed to get switch BMC info: {}", e),
})?;

rows.into_iter()
.map(|row| {
(
row.bmc_mac_address.to_string(),
rpc::BmcInfo {
ip: Some(row.ip_address.to_string()),
mac: Some(row.bmc_mac_address.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
},
)
})
message: format!("Failed to get switch endpoint info: {}", e),
})?
.into_iter()
.map(|row| (row.switch_id, row))
.collect()
};

Expand All @@ -99,13 +89,33 @@ pub async fn find_switch(
let switches: Vec<rpc::Switch> = switch_list
.into_iter()
.map(|s| {
let bmc_info = s
.bmc_mac_address
.as_ref()
.and_then(|mac| bmc_info_map.get(&mac.to_string()).cloned());
let endpoint_info = endpoint_info_map.get(&s.id);

rpc::Switch::try_from(s).map(|mut rpc_switch| {
rpc_switch.bmc_info = bmc_info;
rpc_switch.bmc_info = endpoint_info.map(|row| rpc::BmcInfo {
ip: Some(row.bmc_ip.to_string()),
mac: Some(row.bmc_mac.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
});
rpc_switch.nvos_info = endpoint_info.and_then(|row| {
let (Some(nvos_mac), Some(nvos_ip)) =
(row.nvos_mac.as_ref(), row.nvos_ip.as_ref())
else {
return None;
};

Some(rpc::BmcInfo {
ip: Some(nvos_ip.to_string()),
mac: Some(nvos_mac.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
})
});
rpc_switch
})
})
Expand Down Expand Up @@ -158,40 +168,48 @@ pub async fn find_by_ids(
)
.await?;

let bmc_info_map: std::collections::HashMap<_, _> = {
let rows = db_switch::find_bmc_info_by_switch_ids(&mut txn, &switch_ids)
let endpoint_info_map: std::collections::HashMap<_, _> =
db_switch::find_switch_endpoints_by_ids(&mut txn, &switch_ids)
.await
.map_err(|e| CarbideError::Internal {
message: format!("Failed to get switch BMC info: {}", e),
})?;

rows.into_iter()
.map(|row| {
(
row.switch_id,
rpc::BmcInfo {
ip: Some(row.bmc_ip.to_string()),
mac: Some(row.bmc_mac.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
},
)
})
.collect()
};
message: format!("Failed to get switch endpoint info: {}", e),
})?
.into_iter()
.map(|row| (row.switch_id, row))
.collect();

let _ = txn.rollback().await;

let switches: Vec<rpc::Switch> = switch_list
.into_iter()
.map(|s| {
let id = s.id;
let bmc_info = bmc_info_map.get(&id).cloned();
let endpoint_info = endpoint_info_map.get(&s.id);

rpc::Switch::try_from(s).map(|mut rpc_switch| {
rpc_switch.bmc_info = bmc_info;
rpc_switch.bmc_info = endpoint_info.map(|row| rpc::BmcInfo {
ip: Some(row.bmc_ip.to_string()),
mac: Some(row.bmc_mac.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
});
rpc_switch.nvos_info = endpoint_info.and_then(|row| {
let (Some(nvos_mac), Some(nvos_ip)) =
(row.nvos_mac.as_ref(), row.nvos_ip.as_ref())
else {
return None;
};

Some(rpc::BmcInfo {
ip: Some(nvos_ip.to_string()),
mac: Some(nvos_mac.to_string()),
version: None,
firmware_version: None,
port: None,
machine_interface_id: None,
})
});
rpc_switch
})
})
Expand Down
47 changes: 46 additions & 1 deletion crates/api/src/tests/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/

use forge_secrets::credentials::{
BgpCredentialType, CredentialKey, CredentialReader, CredentialType, Credentials,
BgpCredentialType, CredentialKey, CredentialReader, CredentialType, CredentialWriter,
Credentials,
};
use rpc::forge::forge_server::Forge;
use rpc::forge::{
Expand All @@ -26,6 +27,7 @@ use tonic::Code;

use crate::handlers::credential::MAX_BGP_PASSWORD_LENGTH;
use crate::tests::common::api_fixtures::create_test_env;
use crate::tests::common::api_fixtures::site_explorer::new_switch;

#[crate::sqlx_test]
async fn test_create_host_uefi_credential_when_missing(pool: sqlx::PgPool) {
Expand Down Expand Up @@ -243,3 +245,46 @@ async fn test_create_bgp_credential_validates_max_password_length(pool: sqlx::Pg
})
);
}

#[crate::sqlx_test]
async fn test_get_switch_nvos_credentials(pool: sqlx::PgPool) -> eyre::Result<()> {
let env = create_test_env(pool).await;
let switch_id = new_switch(&env, Some("Switch1".to_string()), None).await?;
let bmc_mac_address = db::switch::find_switch_endpoints_by_ids(&env.pool, &[switch_id])
.await?
.first()
.expect("switch endpoint row")
.bmc_mac;

env.test_credential_manager
.set_credentials(
&CredentialKey::SwitchNvosAdmin { bmc_mac_address },
&Credentials::UsernamePassword {
username: "nvos-admin".to_string(),
password: "nvos-secret".to_string(),
},
)
.await?;

let response = env
.api
.get_switch_nvos_credentials(tonic::Request::new(
rpc::forge::GetSwitchNvosCredentialsRequest {
switch_id: Some(switch_id),
},
))
.await?
.into_inner();

let credentials = response.credentials.expect("credentials");
let Some(rpc::forge::bmc_credentials::Type::UsernamePassword(username_password)) =
credentials.r#type
else {
panic!("expected username/password credentials");
};

assert_eq!(username_password.username, "nvos-admin");
assert_eq!(username_password.password, "nvos-secret");

Ok(())
}
Loading
Loading