Skip to content

Commit 63d8174

Browse files
committed
fix: correct Basilica API types and SSH key support
- Fix CPU offering fields: vcpu_count, system_memory_gb, hourly_rate (string) - Fix balance field type: string not float - Poll secure-cloud CPU rentals via list endpoint (not /rentals/{id}) - Add SSH connectivity check before declaring container ready - Add BASILICA_SSH_KEY config for SSH identity file - Pass SSH key through all ssh_exec/scp_to calls - Tested end-to-end: provision -> clone -> install -> agent -> test -> destroy
1 parent 432107b commit 63d8174

File tree

4 files changed

+168
-89
lines changed

4 files changed

+168
-89
lines changed

src/basilica/client.rs

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ impl BasilicaClient {
129129
self.handle_response(resp, "start_cpu_rental").await
130130
}
131131

132+
pub async fn list_cpu_rentals(&self) -> Result<SecureCloudRentalListResponse> {
133+
let url = format!("{}/secure-cloud/cpu-rentals", self.base_url);
134+
let resp = self.client.get(&url).send().await
135+
.context("Failed to list CPU rentals")?;
136+
self.handle_response(resp, "list_cpu_rentals").await
137+
}
138+
132139
pub async fn stop_cpu_rental(&self, rental_id: &str) -> Result<StopRentalResponse> {
133140
let url = format!("{}/secure-cloud/cpu-rentals/{}/stop", self.base_url, rental_id);
134141
info!("Stopping Basilica CPU rental: {}", rental_id);
@@ -211,63 +218,83 @@ impl BasilicaClient {
211218
let offerings = self.list_cpu_offerings().await?;
212219
let offering = offerings.nodes.iter()
213220
.filter(|o| {
214-
min_cpu.map_or(true, |c| o.cpu_count.unwrap_or(0) >= c)
215-
&& min_memory_gb.map_or(true, |m| o.memory_gb.unwrap_or(0) >= m)
221+
o.availability.unwrap_or(false)
222+
&& min_cpu.map_or(true, |c| o.vcpu_count.unwrap_or(0) >= c)
223+
&& min_memory_gb.map_or(true, |m| o.system_memory_gb.unwrap_or(0) >= m)
224+
})
225+
.min_by(|a, b| {
226+
let rate_a: f64 = a.hourly_rate.as_deref().and_then(|s| s.parse().ok()).unwrap_or(f64::MAX);
227+
let rate_b: f64 = b.hourly_rate.as_deref().and_then(|s| s.parse().ok()).unwrap_or(f64::MAX);
228+
rate_a.partial_cmp(&rate_b).unwrap_or(std::cmp::Ordering::Equal)
216229
})
217-
.min_by_key(|o| o.hourly_rate_cents.unwrap_or(u32::MAX))
218230
.context("No CPU offering matches the requested specs")?;
219231

220232
info!(
221-
"Selected CPU offering: {} ({}cpu, {}GB, {}c/hr)",
233+
"Selected CPU offering: {} ({}vcpu, {}GB, ${}/hr, {})",
222234
offering.id,
223-
offering.cpu_count.unwrap_or(0),
224-
offering.memory_gb.unwrap_or(0),
225-
offering.hourly_rate_cents.unwrap_or(0),
235+
offering.vcpu_count.unwrap_or(0),
236+
offering.system_memory_gb.unwrap_or(0),
237+
offering.hourly_rate.as_deref().unwrap_or("?"),
238+
offering.provider.as_deref().unwrap_or("?"),
226239
);
227240

228241
let rental = self.start_cpu_rental(&offering.id, ssh_key_id).await?;
229242
info!("CPU rental started: {} (status: {})", rental.rental_id, rental.status);
230243

231-
self.wait_for_ssh(&rental.rental_id).await
244+
self.wait_for_cpu_ssh(&rental.rental_id).await
232245
}
233246

234-
/// Poll rental status until SSH is available or timeout.
235-
async fn wait_for_ssh(&self, rental_id: &str) -> Result<ContainerInfo> {
247+
/// Poll secure-cloud CPU rental status until SSH is available or timeout.
248+
async fn wait_for_cpu_ssh(&self, rental_id: &str) -> Result<ContainerInfo> {
236249
for attempt in 1..=MAX_POLL_ATTEMPTS {
237250
tokio::time::sleep(std::time::Duration::from_secs(POLL_INTERVAL_SECS)).await;
238251

239-
let status = self.get_rental(rental_id).await;
240-
match status {
241-
Ok(s) => {
242-
debug!(
243-
"Rental {} poll {}/{}: status={}",
244-
rental_id, attempt, MAX_POLL_ATTEMPTS, s.status
245-
);
246-
247-
if s.status == "running" || s.status == "active" {
248-
if let Some(ref creds) = s.ssh_credentials {
249-
if creds.host.is_some() {
250-
info!("Rental {} is ready with SSH access", rental_id);
252+
match self.list_cpu_rentals().await {
253+
Ok(list) => {
254+
if let Some(r) = list.rentals.iter().find(|r| r.rental_id == rental_id) {
255+
debug!(
256+
"Rental {} poll {}/{}: status={} ip={:?}",
257+
rental_id, attempt, MAX_POLL_ATTEMPTS, r.status, r.ip_address
258+
);
259+
260+
if r.status == "running" || r.status == "active" {
261+
if let Some(ref ip) = r.ip_address {
262+
let ssh_user = r.ssh_command.as_deref()
263+
.and_then(|c| c.strip_prefix("ssh "))
264+
.and_then(|c| c.split('@').next())
265+
.unwrap_or("root")
266+
.to_string();
267+
268+
// Verify SSH is actually reachable before returning
269+
let ssh_key_path = std::env::var("BASILICA_SSH_KEY").ok();
270+
if !self.check_ssh_ready(ip, 22, &ssh_user, ssh_key_path.as_deref()).await {
271+
debug!("Rental {} has IP {} but SSH not ready yet", rental_id, ip);
272+
continue;
273+
}
274+
275+
info!("Rental {} is ready: {}@{}", rental_id, ssh_user, ip);
251276
return Ok(ContainerInfo {
252277
rental_id: rental_id.to_string(),
253-
status: s.status,
254-
ssh_host: creds.host.clone(),
255-
ssh_port: creds.port,
256-
ssh_user: creds.username.clone(),
257-
ssh_command: creds.ssh_command.clone(),
258-
provider: s.node.clone(),
259-
created_at: s.created_at.clone(),
278+
status: r.status.clone(),
279+
ssh_host: Some(ip.clone()),
280+
ssh_port: Some(22),
281+
ssh_user: Some(ssh_user),
282+
ssh_command: r.ssh_command.clone(),
283+
provider: r.provider.clone(),
284+
created_at: r.created_at.clone(),
260285
});
261286
}
262287
}
263-
}
264288

265-
if s.status == "failed" || s.status == "error" || s.status == "terminated" {
266-
anyhow::bail!("Rental {} entered terminal state: {}", rental_id, s.status);
289+
if r.status == "failed" || r.status == "error" || r.status == "stopped" {
290+
anyhow::bail!("Rental {} entered terminal state: {}", rental_id, r.status);
291+
}
292+
} else {
293+
warn!("Rental {} not found in CPU rental list", rental_id);
267294
}
268295
}
269296
Err(e) => {
270-
warn!("Failed to poll rental {}: {}", rental_id, e);
297+
warn!("Failed to poll CPU rentals: {}", e);
271298
}
272299
}
273300
}
@@ -279,6 +306,33 @@ impl BasilicaClient {
279306
)
280307
}
281308

309+
/// Check if SSH is reachable on the given host by running a simple command.
310+
async fn check_ssh_ready(&self, host: &str, port: u16, user: &str, ssh_key: Option<&str>) -> bool {
311+
let target = format!("{}@{}", user, host);
312+
let port_str = port.to_string();
313+
let mut args = vec![
314+
"-o", "StrictHostKeyChecking=no",
315+
"-o", "UserKnownHostsFile=/dev/null",
316+
"-o", "ConnectTimeout=5",
317+
"-o", "LogLevel=ERROR",
318+
];
319+
if let Some(key) = ssh_key {
320+
args.extend_from_slice(&["-i", key]);
321+
}
322+
args.extend_from_slice(&["-p", &port_str, &target, "echo ok"]);
323+
let result = tokio::process::Command::new("ssh")
324+
.args(&args)
325+
.stdout(std::process::Stdio::piped())
326+
.stderr(std::process::Stdio::piped())
327+
.output()
328+
.await;
329+
330+
match result {
331+
Ok(output) => output.status.success(),
332+
Err(_) => false,
333+
}
334+
}
335+
282336
// ── Response handling ──
283337

284338
async fn handle_response<T: serde::de::DeserializeOwned>(

src/basilica/types.rs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ pub struct RentalListItem {
7474
pub struct CpuOffering {
7575
pub id: String,
7676
pub provider: Option<String>,
77-
pub cpu_count: Option<u32>,
78-
pub memory_gb: Option<u32>,
77+
pub vcpu_count: Option<u32>,
78+
pub system_memory_gb: Option<u32>,
7979
pub storage_gb: Option<u32>,
80-
pub hourly_rate_cents: Option<u32>,
80+
pub hourly_rate: Option<String>,
8181
pub region: Option<String>,
82+
pub availability: Option<bool>,
8283
}
8384

8485
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -104,6 +105,26 @@ pub struct SecureCloudRentalResponse {
104105
pub is_spot: Option<bool>,
105106
}
106107

108+
#[derive(Debug, Clone, Serialize, Deserialize)]
109+
pub struct SecureCloudRentalListResponse {
110+
pub rentals: Vec<SecureCloudRentalListItem>,
111+
pub total_count: Option<u32>,
112+
}
113+
114+
#[derive(Debug, Clone, Serialize, Deserialize)]
115+
pub struct SecureCloudRentalListItem {
116+
pub rental_id: String,
117+
pub status: String,
118+
pub ip_address: Option<String>,
119+
pub ssh_command: Option<String>,
120+
pub provider: Option<String>,
121+
pub hourly_cost: Option<f64>,
122+
pub created_at: Option<String>,
123+
pub stopped_at: Option<String>,
124+
pub vcpu_count: Option<u32>,
125+
pub system_memory_gb: Option<u32>,
126+
}
127+
107128
#[derive(Debug, Clone, Deserialize)]
108129
pub struct StopRentalResponse {
109130
pub rental_id: String,
@@ -214,7 +235,7 @@ pub struct HealthResponse {
214235

215236
#[derive(Debug, Clone, Serialize, Deserialize)]
216237
pub struct BalanceResponse {
217-
pub balance: Option<f64>,
238+
pub balance: Option<String>,
218239
pub last_updated: Option<String>,
219240
}
220241

@@ -318,16 +339,18 @@ mod tests {
318339
#[test]
319340
fn test_cpu_offering_deserializes() {
320341
let json = r#"{
321-
"id": "cpu-offer-1",
322-
"provider": "citadel",
323-
"cpu_count": 4,
324-
"memory_gb": 8,
342+
"id": "hyperstack-127",
343+
"provider": "hyperstack",
344+
"vcpu_count": 4,
345+
"system_memory_gb": 4,
325346
"storage_gb": 100,
326-
"hourly_rate_cents": 10,
327-
"region": "us-east"
347+
"hourly_rate": "0.3832400",
348+
"region": "NORWAY-1",
349+
"availability": true
328350
}"#;
329351
let offering: CpuOffering = serde_json::from_str(json).unwrap();
330-
assert_eq!(offering.id, "cpu-offer-1");
331-
assert_eq!(offering.cpu_count, Some(4));
352+
assert_eq!(offering.id, "hyperstack-127");
353+
assert_eq!(offering.vcpu_count, Some(4));
354+
assert_eq!(offering.availability, Some(true));
332355
}
333356
}

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct Config {
3434
pub sudo_password: Option<String>,
3535
pub trusted_validators: Vec<String>,
3636
pub basilica_api_token: Option<String>,
37+
pub basilica_ssh_key: Option<String>,
3738
}
3839

3940
impl Config {
@@ -83,6 +84,9 @@ impl Config {
8384
basilica_api_token: std::env::var("BASILICA_API_TOKEN")
8485
.ok()
8586
.filter(|s| !s.is_empty()),
87+
basilica_ssh_key: std::env::var("BASILICA_SSH_KEY")
88+
.ok()
89+
.filter(|s| !s.is_empty()),
8690
trusted_validators: std::env::var("TRUSTED_VALIDATORS")
8791
.unwrap_or_default()
8892
.split(',')

0 commit comments

Comments
 (0)