@@ -1169,6 +1169,9 @@ async fn job(
11691169 return Err ( CommandError :: io ( path, ErrorKind :: AlreadyExists . into ( ) ) ) ;
11701170 }
11711171 }
1172+ if let Some ( client) = client. as_ref ( ) {
1173+ preflight_target ( ctx, client, & target) . await ?;
1174+ }
11721175 let streaming = if * streaming {
11731176 Streaming :: Output
11741177 } else {
@@ -1211,7 +1214,7 @@ async fn job(
12111214 }
12121215
12131216 ( JobCommand :: Stop { job_id } , Some ( client) ) => {
1214- job_stop ( ctx, client, & job_id) . await ?;
1217+ job_stop ( ctx, client, & job_id, None ) . await ?;
12151218 ctx. job_stopped ( & job_id) ;
12161219 Ok ( ( ) )
12171220 }
@@ -1403,6 +1406,7 @@ async fn job_start(
14031406 let mut last = JobStatusMap :: new ( ) ;
14041407 let mut settling = Settling :: default ( ) ;
14051408 let mut started = false ;
1409+ let mut stalled = false ;
14061410 let mut stopped = false ;
14071411 let mut sigint = signal ( SignalKind :: interrupt ( ) ) ?;
14081412 let status = loop {
@@ -1421,7 +1425,7 @@ async fn job_start(
14211425 }
14221426
14231427 _ = ticker. tick( ) => {
1424- last = match job_status_map( ctx, client, & job_id) . await {
1428+ last = match job_status_map( ctx, client, & job_id, job_target . single_baseboard ( ) ) . await {
14251429 Ok ( status) => status,
14261430 // The job may not be visible anywhere yet.
14271431 Err ( CommandError :: NotFound ( _) ) => JobStatusMap :: new( ) ,
@@ -1435,6 +1439,10 @@ async fn job_start(
14351439 if settling. done( & job_target, rack, & last) && started {
14361440 break last;
14371441 }
1442+ if !stalled && last. is_empty( ) && settling. polls >= WATCH_STALL_POLLS {
1443+ ctx. job_watch_stalled( & job_id) ;
1444+ stalled = true ;
1445+ }
14381446 }
14391447
14401448 // While the job runs, an interrupt stops it but keeps
@@ -1448,7 +1456,7 @@ async fn job_start(
14481456 break last;
14491457 }
14501458 for _ in 0 ..3 {
1451- match job_stop( ctx, client, & job_id) . await {
1459+ match job_stop( ctx, client, & job_id, job_target . single_baseboard ( ) ) . await {
14521460 Ok ( _) => {
14531461 ctx. job_stopped( & job_id) ;
14541462 stopped = true ;
@@ -1518,14 +1526,14 @@ async fn job_stop(
15181526 ctx : & mut impl CommandContext ,
15191527 client : & Client ,
15201528 job_id : & JobId ,
1529+ via : Option < & BaseboardId > ,
15211530) -> Result < ( ) , CommandError > {
1522- with_login ( ctx, client, async || {
1523- client
1524- . job_stop ( )
1525- . job_id ( job_id)
1526- . wait ( JobWait :: Stop )
1527- . send ( )
1528- . await
1531+ with_login_via ( ctx, client, via, async || {
1532+ let mut request = client. job_stop ( ) . job_id ( job_id) . wait ( JobWait :: Stop ) ;
1533+ if let Some ( via) = via {
1534+ request = request. via ( via. to_string ( ) ) ;
1535+ }
1536+ request. send ( ) . await
15291537 } )
15301538 . await ?;
15311539 Ok ( ( ) )
@@ -1537,19 +1545,26 @@ async fn job_status(
15371545 job_id : & JobId ,
15381546 style : StatusDisplayStyle ,
15391547) -> Result < ( ) , CommandError > {
1540- let status = job_status_map ( ctx, client, job_id) . await ?;
1548+ let status = job_status_map ( ctx, client, job_id, None ) . await ?;
15411549 ctx. job_status ( job_id, & status, style) ;
15421550 Ok ( ( ) )
15431551}
15441552
1545- /// Fetch a job's rack-wide status map.
1553+ /// Fetch a job's rack-wide status map. Routing `via` a single-sled
1554+ /// target gets its authoritative status and keeps the login on the
1555+ /// sled that already knows it.
15461556async fn job_status_map (
15471557 ctx : & mut impl CommandContext ,
15481558 client : & Client ,
15491559 job_id : & JobId ,
1560+ via : Option < & BaseboardId > ,
15501561) -> Result < JobStatusMap , CommandError > {
1551- let status = with_login ( ctx, client, async || {
1552- client. job_status ( ) . job_id ( job_id) . send ( ) . await
1562+ let status = with_login_via ( ctx, client, via, async || {
1563+ let mut request = client. job_status ( ) . job_id ( job_id) ;
1564+ if let Some ( via) = via {
1565+ request = request. via ( via. to_string ( ) ) ;
1566+ }
1567+ request. send ( ) . await
15531568 } )
15541569 . await ?
15551570 . into_inner ( ) ;
@@ -1571,7 +1586,7 @@ async fn job_watch(
15711586 let mut settling = Settling :: default ( ) ;
15721587 let mut sigint = signal ( SignalKind :: interrupt ( ) ) ?;
15731588 let status = loop {
1574- let status = match job_status_map ( ctx, client, job_id) . await {
1589+ let status = match job_status_map ( ctx, client, job_id, target . single_baseboard ( ) ) . await {
15751590 Ok ( status) => status,
15761591 Err ( error) => {
15771592 ctx. job_watch_finished ( job_id) ;
@@ -1597,6 +1612,10 @@ async fn job_watch(
15971612/// is likely still missing sleds.
15981613const WATCH_MIN_POLLS : usize = 5 ;
15991614
1615+ /// How many polls a watch may go without any sled reporting a status
1616+ /// before warning that the job may never run.
1617+ const WATCH_STALL_POLLS : usize = 15 ;
1618+
16001619/// Rolling settlement state for a watched job.
16011620#[ derive( Default ) ]
16021621struct Settling {
@@ -1707,7 +1726,7 @@ async fn job_output(
17071726 }
17081727
17091728 // Fetch output from every sled with a recorded status.
1710- let status = job_status_map ( ctx, client, & args. job_id ) . await ?;
1729+ let status = job_status_map ( ctx, client, & args. job_id , None ) . await ?;
17111730 if status. is_empty ( ) {
17121731 return Err ( CommandError :: NotFound ( format ! (
17131732 "Job `{}` not found" ,
@@ -1738,14 +1757,7 @@ async fn job_output_from(
17381757 } : JobOutput ,
17391758) -> Result < ( ) , CommandError > {
17401759 // Fetch job status for output length and hash.
1741- let status = job_status_try_from_json_map (
1742- with_login_via ( ctx, client, Some ( target) , async || {
1743- client. job_status ( ) . job_id ( job_id) . send ( ) . await
1744- } )
1745- . await ?
1746- . into_inner ( ) ,
1747- )
1748- . map_err ( CommandError :: BaseboardIdParseError ) ?;
1760+ let status = job_status_map ( ctx, client, & job_id, Some ( target) ) . await ?;
17491761
17501762 let JobOutputState {
17511763 stdout_len,
@@ -2081,6 +2093,32 @@ impl FromStr for TargetArg {
20812093 }
20822094}
20832095
2096+ /// Confirm before signing a job for a sled that doesn't appear in inventory.
2097+ #[ cfg( feature = "permslip" ) ]
2098+ async fn preflight_target (
2099+ ctx : & mut impl CommandContext ,
2100+ client : & Client ,
2101+ target : & Target ,
2102+ ) -> Result < ( ) , CommandError > {
2103+ let Target :: Sleds ( sleds) = target else {
2104+ return Ok ( ( ) ) ;
2105+ } ;
2106+ let Ok ( inventory) = client. versions ( ) . send ( ) . await else {
2107+ return Ok ( ( ) ) ;
2108+ } ;
2109+ let inventory = inventory. into_inner ( ) ;
2110+ for sled in sleds {
2111+ let known = match sled {
2112+ SledId :: Baseboard ( baseboard) => inventory. iter ( ) . any ( |s| & s. baseboard == baseboard) ,
2113+ SledId :: Cubby ( cubby) => inventory. iter ( ) . any ( |s| s. cubby == Some ( * cubby) ) ,
2114+ } ;
2115+ if !known {
2116+ ctx. really_target ( sled) ?;
2117+ }
2118+ }
2119+ Ok ( ( ) )
2120+ }
2121+
20842122/// Resolve a target argument to a target, matching bare serial
20852123/// numbers against the rack's sled inventory.
20862124async fn resolve_target_arg ( client : & Client , target : & TargetArg ) -> Result < Target , CommandError > {
@@ -2120,7 +2158,7 @@ async fn resolve_serial(
21202158 job_id : & JobId ,
21212159 serial : & str ,
21222160) -> Result < BaseboardId , CommandError > {
2123- let status = job_status_map ( ctx, client, job_id) . await ?;
2161+ let status = job_status_map ( ctx, client, job_id, None ) . await ?;
21242162 let mut matches = status
21252163 . keys ( )
21262164 . filter ( |b| b. serial_number . eq_ignore_ascii_case ( serial) ) ;
0 commit comments