@@ -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 > (
0 commit comments