@@ -382,6 +382,19 @@ async def print_resources(self):
382382 else :
383383 print (line )
384384
385+ async def _get_reservation_by_token (self , token ):
386+ request = labgrid_coordinator_pb2 .GetReservationsRequest ()
387+ try :
388+ response = await self .stub .GetReservations (request )
389+ except grpc .aio .AioRpcError as e :
390+ raise ServerError (e .details ()) from e
391+
392+ for res_pb in response .reservations :
393+ res = Reservation .from_pb2 (res_pb )
394+ if res .token == token :
395+ return res
396+ return None
397+
385398 async def print_places (self ):
386399 """Print out the places"""
387400 if self .args .sort_last_changed :
@@ -524,8 +537,13 @@ def get_acquired_place(self, place=None):
524537 async def print_place (self ):
525538 """Print out the current place and related resources"""
526539 place = self .get_place ()
540+ reservation = None
541+ if place .reservation :
542+ reservation = await self ._get_reservation_by_token (place .reservation )
543+
527544 print (f"Place '{ place .name } ':" )
528- place .show (level = 1 )
545+ place .show (level = 1 , reservation = reservation )
546+
529547 if place .acquired :
530548 for resource_path in place .acquired_resources :
531549 (exporter , group_name , cls , resource_name ) = resource_path
@@ -722,6 +740,20 @@ async def acquire(self):
722740 raise errors [0 ]
723741 raise ErrorGroup ("Multiple errors occurred during acquire" , errors )
724742
743+ async def lease (self ):
744+ errors = []
745+ places = self .get_place_names_from_env () if self .env else [self .args .place ]
746+ for place in places :
747+ try :
748+ await self ._lease_place (place )
749+ except Error as e :
750+ errors .append (e )
751+
752+ if errors :
753+ if len (errors ) == 1 :
754+ raise errors [0 ]
755+ raise ErrorGroup ("Multiple errors occurred during lease" , errors )
756+
725757 async def _acquire_place (self , place ):
726758 """Acquire a place, marking it unavailable for other clients"""
727759 place = self .get_place (place )
@@ -768,6 +800,55 @@ async def _acquire_place(self, place):
768800
769801 raise ServerError (e .details ())
770802
803+ async def _lease_place (self , place ):
804+ """Lease a place using a reservation"""
805+ place = self .get_place (place )
806+
807+ if place .acquired :
808+ host , user = place .acquired .split ("/" )
809+ allowhelp = f"'labgrid-client -p { place .name } allow { self .gethostname ()} /{ self .getuser ()} ' on { host } ."
810+
811+ if self .getuser () != user :
812+ raise UserError (
813+ f"Place { place .name } is already acquired by { place .acquired } . "
814+ f"To work simultaneously, { user } can execute { allowhelp } "
815+ )
816+
817+ if self .gethostname () == host :
818+ raise UserError (f"You have already acquired place { place .name } ." )
819+
820+ raise UserError (
821+ f"You have already acquired place { place .name } on { host } . To work simultaneously, execute { allowhelp } "
822+ )
823+
824+ if not self .args .allow_unmatched :
825+ self .check_matches (place )
826+
827+ request = labgrid_coordinator_pb2 .LeasePlaceRequest (placename = place .name )
828+
829+ try :
830+ await self .stub .LeasePlace (request )
831+ await self .sync_with_coordinator ()
832+ print (f"leased place { place .name } " )
833+ except grpc .aio .AioRpcError as e :
834+ # check potential failure causes
835+ for exporter , groups in sorted (self .resources .items ()):
836+ for group_name , group in sorted (groups .items ()):
837+ for resource_name , resource in sorted (group .items ()):
838+ resource_path = (exporter , group_name , resource .cls , resource_name )
839+ if not resource .acquired :
840+ continue
841+ match = place .getmatch (resource_path )
842+ if match is None :
843+ continue
844+ name = resource_name
845+ if match .rename :
846+ name = match .rename
847+ print (
848+ f"Matching resource '{ name } ' ({ exporter } /{ group_name } /{ resource .cls } /{ resource_name } ) already acquired by place '{ resource .acquired } '"
849+ ) # pylint: disable=line-too-long
850+ raise ServerError (e .details ())
851+
771852 async def release (self ):
772853 errors = []
773854 places = self .get_place_names_from_env () if self .env else [self .args .place ]
@@ -1591,16 +1672,25 @@ async def cancel_reservation(self):
15911672 except grpc .aio .AioRpcError as e :
15921673 raise ServerError (e .details ())
15931674
1594- async def _wait_reservation (self , token : str , verbose = True ):
1595- while True :
1596- request = labgrid_coordinator_pb2 .PollReservationRequest (token = token )
1675+ async def _extend_lease (self , token : str ) -> Reservation :
1676+ request = labgrid_coordinator_pb2 .ExtendLeaseRequest (token = token )
1677+ try :
1678+ response = await self .stub .ExtendLease (request )
1679+ except grpc .aio .AioRpcError as e :
1680+ raise ServerError (e .details ())
1681+ return Reservation .from_pb2 (response .reservation )
15971682
1598- try :
1599- response : labgrid_coordinator_pb2 .PollReservationResponse = await self .stub .PollReservation (request )
1600- except grpc .aio .AioRpcError as e :
1601- raise ServerError (e .details ())
1683+ async def _poll_reservation (self , token : str ) -> Reservation :
1684+ request = labgrid_coordinator_pb2 .PollReservationRequest (token = token )
1685+ try :
1686+ response : labgrid_coordinator_pb2 .PollReservationResponse = await self .stub .PollReservation (request )
1687+ except grpc .aio .AioRpcError as e :
1688+ raise ServerError (e .details ())
1689+ return Reservation .from_pb2 (response .reservation )
16021690
1603- res = Reservation .from_pb2 (response .reservation )
1691+ async def _wait_reservation (self , token : str , verbose = True ):
1692+ while True :
1693+ res = await self ._poll_reservation (token )
16041694 if verbose :
16051695 res .show ()
16061696 if res .state is ReservationState .waiting :
@@ -1612,6 +1702,37 @@ async def wait_reservation(self):
16121702 token = self .args .token
16131703 await self ._wait_reservation (token )
16141704
1705+ async def _get_lease_config (self ):
1706+ request = labgrid_coordinator_pb2 .GetLeaseConfigRequest ()
1707+ try :
1708+ response = await self .stub .GetLeaseConfig (request )
1709+ except grpc .aio .AioRpcError as e :
1710+ raise ServerError (e .details ()) from e
1711+ return response
1712+
1713+ async def extend_reservation (self ):
1714+ token = self .args .token
1715+
1716+ if not self .args .keepalive :
1717+ res = await self ._extend_lease (token )
1718+ if not self .args .quiet :
1719+ print (f"extended lease { res .token } until { datetime .fromtimestamp (res .timeout )} " )
1720+ return
1721+
1722+ lease_config = await self ._get_lease_config ()
1723+ interval = max (1.0 , lease_config .default_extend_duration / 2.0 )
1724+
1725+ while True :
1726+ res = await self ._extend_lease (token )
1727+ if res .state is ReservationState .expired :
1728+ raise UserError ("Reservation is expired; cannot extend lease" )
1729+ if not self .args .quiet :
1730+ print (
1731+ f"extended lease { res .token } until { datetime .fromtimestamp (res .timeout )} "
1732+ f"(next keepalive in { interval :.1f} s)"
1733+ )
1734+ await asyncio .sleep (interval )
1735+
16151736 async def print_reservations (self ):
16161737 request = labgrid_coordinator_pb2 .GetReservationsRequest ()
16171738
@@ -1959,6 +2080,12 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
19592080 )
19602081 subparser .set_defaults (func = ClientSession .acquire )
19612082
2083+ subparser = subparsers .add_parser ("lease" , help = "lease a place (time-limited, requires extension)" )
2084+ subparser .add_argument (
2085+ "--allow-unmatched" , action = "store_true" , help = "allow missing resources for matches when locking the place"
2086+ )
2087+ subparser .set_defaults (func = ClientSession .lease )
2088+
19622089 subparser = subparsers .add_parser ("release" , aliases = ("unlock" ,), help = "release a place" )
19632090 subparser .add_argument (
19642091 "-k" , "--kick" , action = "store_true" , help = "release a place even if it is acquired by a different user"
@@ -2217,6 +2344,21 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
22172344 subparser .add_argument ("token" , type = str , nargs = "?" )
22182345 subparser .set_defaults (func = ClientSession .wait_reservation )
22192346
2347+ subparser = subparsers .add_parser ("extend" , help = "extend a lease by the coordinator default duration" )
2348+ subparser .add_argument ("token" , type = str , nargs = "?" )
2349+ subparser .add_argument (
2350+ "--keepalive" ,
2351+ action = "store_true" ,
2352+ help = "keep extending the lease automatically using coordinator policy" ,
2353+ )
2354+ subparser .add_argument (
2355+ "-q" ,
2356+ "--quiet" ,
2357+ action = "store_true" ,
2358+ help = "do not print lease status on each extension" ,
2359+ )
2360+ subparser .set_defaults (func = ClientSession .extend_reservation )
2361+
22202362 subparser = subparsers .add_parser ("reservations" , help = "list current reservations" )
22212363 subparser .set_defaults (func = ClientSession .print_reservations )
22222364
@@ -2281,7 +2423,7 @@ def main():
22812423 if args .initial_state is None :
22822424 args .initial_state = initial_state
22832425
2284- if args .command in ["cancel-reservation" , "wait" ] and args .token is None :
2426+ if args .command in ["cancel-reservation" , "wait" , "extend" ] and args .token is None :
22852427 if token :
22862428 args .token = token
22872429 else :
0 commit comments