1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li 3*760c253cSXin Li# Copyright 2021 The ChromiumOS Authors 4*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 5*760c253cSXin Li# found in the LICENSE file. 6*760c253cSXin Li 7*760c253cSXin Li"""Wrapper script to automatically lock devices for crosperf.""" 8*760c253cSXin Li 9*760c253cSXin Liimport argparse 10*760c253cSXin Liimport contextlib 11*760c253cSXin Liimport dataclasses 12*760c253cSXin Liimport json 13*760c253cSXin Liimport os 14*760c253cSXin Liimport subprocess 15*760c253cSXin Liimport sys 16*760c253cSXin Lifrom typing import Any, Dict, List, Optional, Tuple 17*760c253cSXin Li 18*760c253cSXin Li 19*760c253cSXin Li# Have to do sys.path hackery because crosperf relies on PYTHONPATH 20*760c253cSXin Li# modifications. 21*760c253cSXin LiPARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 22*760c253cSXin Lisys.path.append(PARENT_DIR) 23*760c253cSXin Li 24*760c253cSXin Li 25*760c253cSXin Lidef main(sys_args: List[str]) -> Optional[str]: 26*760c253cSXin Li """Run crosperf_autolock. Returns error msg or None""" 27*760c253cSXin Li args, leftover_args = parse_args(sys_args) 28*760c253cSXin Li fleet_params = [ 29*760c253cSXin Li CrosfleetParams( 30*760c253cSXin Li board=args.board, pool=args.pool, lease_time=args.lease_time 31*760c253cSXin Li ) 32*760c253cSXin Li for _ in range(args.num_leases) 33*760c253cSXin Li ] 34*760c253cSXin Li if not fleet_params: 35*760c253cSXin Li return ( 36*760c253cSXin Li "No board names identified. If you want to use" 37*760c253cSXin Li " a known host, just use crosperf directly." 38*760c253cSXin Li ) 39*760c253cSXin Li try: 40*760c253cSXin Li _run_crosperf(fleet_params, args.dut_lock_timeout, leftover_args) 41*760c253cSXin Li except BoardLockError as e: 42*760c253cSXin Li _eprint("ERROR:", e) 43*760c253cSXin Li _eprint('May need to login to crosfleet? Run "crosfleet login"') 44*760c253cSXin Li _eprint( 45*760c253cSXin Li "The leases may also be successful later on. " 46*760c253cSXin Li 'Check with "crosfleet dut leases"' 47*760c253cSXin Li ) 48*760c253cSXin Li return "crosperf_autolock failed" 49*760c253cSXin Li except BoardReleaseError as e: 50*760c253cSXin Li _eprint("ERROR:", e) 51*760c253cSXin Li _eprint('May need to re-run "crosfleet dut abandon"') 52*760c253cSXin Li return "crosperf_autolock failed" 53*760c253cSXin Li return None 54*760c253cSXin Li 55*760c253cSXin Li 56*760c253cSXin Lidef parse_args(args: List[str]) -> Tuple[Any, List]: 57*760c253cSXin Li """Parse the CLI arguments.""" 58*760c253cSXin Li parser = argparse.ArgumentParser( 59*760c253cSXin Li "crosperf_autolock", 60*760c253cSXin Li description="Wrapper around crosperf" 61*760c253cSXin Li " to autolock DUTs from crosfleet.", 62*760c253cSXin Li formatter_class=argparse.ArgumentDefaultsHelpFormatter, 63*760c253cSXin Li ) 64*760c253cSXin Li parser.add_argument( 65*760c253cSXin Li "--board", 66*760c253cSXin Li type=str, 67*760c253cSXin Li help="Space or comma separated list of boards to lock", 68*760c253cSXin Li required=True, 69*760c253cSXin Li default=argparse.SUPPRESS, 70*760c253cSXin Li ) 71*760c253cSXin Li parser.add_argument( 72*760c253cSXin Li "--num-leases", 73*760c253cSXin Li type=int, 74*760c253cSXin Li help="Number of boards to lock.", 75*760c253cSXin Li metavar="NUM", 76*760c253cSXin Li default=1, 77*760c253cSXin Li ) 78*760c253cSXin Li parser.add_argument( 79*760c253cSXin Li "--pool", type=str, help="Pool to pull from.", default="DUT_POOL_QUOTA" 80*760c253cSXin Li ) 81*760c253cSXin Li parser.add_argument( 82*760c253cSXin Li "--dut-lock-timeout", 83*760c253cSXin Li type=float, 84*760c253cSXin Li metavar="SEC", 85*760c253cSXin Li help="Number of seconds we want to try to lease a board" 86*760c253cSXin Li " from crosfleet. This option does NOT change the" 87*760c253cSXin Li " lease length.", 88*760c253cSXin Li default=600, 89*760c253cSXin Li ) 90*760c253cSXin Li parser.add_argument( 91*760c253cSXin Li "--lease-time", 92*760c253cSXin Li type=int, 93*760c253cSXin Li metavar="MIN", 94*760c253cSXin Li help="Number of minutes to lock the board. Max is 1440.", 95*760c253cSXin Li default=1440, 96*760c253cSXin Li ) 97*760c253cSXin Li parser.epilog = ( 98*760c253cSXin Li "For more detailed flags, you have to read the args taken by the" 99*760c253cSXin Li " crosperf executable. Args are passed transparently to crosperf." 100*760c253cSXin Li ) 101*760c253cSXin Li return parser.parse_known_args(args) 102*760c253cSXin Li 103*760c253cSXin Li 104*760c253cSXin Liclass BoardLockError(Exception): 105*760c253cSXin Li """Error to indicate failure to lock a board.""" 106*760c253cSXin Li 107*760c253cSXin Li def __init__(self, msg: str): 108*760c253cSXin Li self.msg = "BoardLockError: " + msg 109*760c253cSXin Li super().__init__(self.msg) 110*760c253cSXin Li 111*760c253cSXin Li 112*760c253cSXin Liclass BoardReleaseError(Exception): 113*760c253cSXin Li """Error to indicate failure to release a board.""" 114*760c253cSXin Li 115*760c253cSXin Li def __init__(self, msg: str): 116*760c253cSXin Li self.msg = "BoardReleaseError: " + msg 117*760c253cSXin Li super().__init__(self.msg) 118*760c253cSXin Li 119*760c253cSXin Li 120*760c253cSXin Li@dataclasses.dataclass(frozen=True) 121*760c253cSXin Liclass CrosfleetParams: 122*760c253cSXin Li """Dataclass to hold all crosfleet parameterizations.""" 123*760c253cSXin Li 124*760c253cSXin Li board: str 125*760c253cSXin Li pool: str 126*760c253cSXin Li lease_time: int 127*760c253cSXin Li 128*760c253cSXin Li 129*760c253cSXin Lidef _eprint(*msg, **kwargs): 130*760c253cSXin Li print(*msg, file=sys.stderr, **kwargs) 131*760c253cSXin Li 132*760c253cSXin Li 133*760c253cSXin Lidef _run_crosperf( 134*760c253cSXin Li crosfleet_params: List[CrosfleetParams], 135*760c253cSXin Li lock_timeout: float, 136*760c253cSXin Li leftover_args: List[str], 137*760c253cSXin Li): 138*760c253cSXin Li """Autolock devices and run crosperf with leftover arguments. 139*760c253cSXin Li 140*760c253cSXin Li Raises: 141*760c253cSXin Li BoardLockError: When board was unable to be locked. 142*760c253cSXin Li BoardReleaseError: When board was unable to be released. 143*760c253cSXin Li """ 144*760c253cSXin Li if not crosfleet_params: 145*760c253cSXin Li raise ValueError("No crosfleet params given; cannot call crosfleet.") 146*760c253cSXin Li 147*760c253cSXin Li # We'll assume all the boards are the same type, which seems to be the case 148*760c253cSXin Li # in experiments that actually get used. 149*760c253cSXin Li passed_board_arg = crosfleet_params[0].board 150*760c253cSXin Li with contextlib.ExitStack() as stack: 151*760c253cSXin Li dut_hostnames = [] 152*760c253cSXin Li for param in crosfleet_params: 153*760c253cSXin Li print( 154*760c253cSXin Li f"Sent lock request for {param.board} for {param.lease_time} minutes" 155*760c253cSXin Li '\nIf this fails, you may need to run "crosfleet dut abandon <...>"' 156*760c253cSXin Li ) 157*760c253cSXin Li # May raise BoardLockError, abandoning previous DUTs. 158*760c253cSXin Li dut_hostname = stack.enter_context( 159*760c253cSXin Li crosfleet_machine_ctx( 160*760c253cSXin Li param.board, 161*760c253cSXin Li param.lease_time, 162*760c253cSXin Li lock_timeout, 163*760c253cSXin Li {"label-pool": param.pool}, 164*760c253cSXin Li ) 165*760c253cSXin Li ) 166*760c253cSXin Li if dut_hostname: 167*760c253cSXin Li print(f"Locked {param.board} machine: {dut_hostname}") 168*760c253cSXin Li dut_hostnames.append(dut_hostname) 169*760c253cSXin Li 170*760c253cSXin Li # We import crosperf late, because this import is extremely slow. 171*760c253cSXin Li # We don't want the user to wait several seconds just to get 172*760c253cSXin Li # help info. 173*760c253cSXin Li import crosperf 174*760c253cSXin Li 175*760c253cSXin Li for dut_hostname in dut_hostnames: 176*760c253cSXin Li crosperf.Main( 177*760c253cSXin Li [ 178*760c253cSXin Li sys.argv[0], 179*760c253cSXin Li "--no_lock", 180*760c253cSXin Li "True", 181*760c253cSXin Li "--remote", 182*760c253cSXin Li dut_hostname, 183*760c253cSXin Li "--board", 184*760c253cSXin Li passed_board_arg, 185*760c253cSXin Li ] 186*760c253cSXin Li + leftover_args 187*760c253cSXin Li ) 188*760c253cSXin Li 189*760c253cSXin Li 190*760c253cSXin Li@contextlib.contextmanager 191*760c253cSXin Lidef crosfleet_machine_ctx( 192*760c253cSXin Li board: str, 193*760c253cSXin Li lease_minutes: int, 194*760c253cSXin Li lock_timeout: float, 195*760c253cSXin Li dims: Dict[str, Any], 196*760c253cSXin Li abandon_timeout: float = 120.0, 197*760c253cSXin Li) -> Any: 198*760c253cSXin Li """Acquire dut from crosfleet, and release once it leaves the context. 199*760c253cSXin Li 200*760c253cSXin Li Args: 201*760c253cSXin Li board: Board type to lease. 202*760c253cSXin Li lease_minutes: Length of lease, in minutes. 203*760c253cSXin Li lock_timeout: How long to wait for a lock until quitting. 204*760c253cSXin Li dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' 205*760c253cSXin Li abandon_timeout: How long to wait for releasing until quitting. 206*760c253cSXin Li 207*760c253cSXin Li Yields: 208*760c253cSXin Li A string representing the crosfleet DUT hostname. 209*760c253cSXin Li 210*760c253cSXin Li Raises: 211*760c253cSXin Li BoardLockError: When board was unable to be locked. 212*760c253cSXin Li BoardReleaseError: When board was unable to be released. 213*760c253cSXin Li """ 214*760c253cSXin Li # This lock may raise an exception, but if it does, we can't release 215*760c253cSXin Li # the DUT anyways as we won't have the dut_hostname. 216*760c253cSXin Li dut_hostname = crosfleet_autolock(board, lease_minutes, dims, lock_timeout) 217*760c253cSXin Li try: 218*760c253cSXin Li yield dut_hostname 219*760c253cSXin Li finally: 220*760c253cSXin Li if dut_hostname: 221*760c253cSXin Li crosfleet_release(dut_hostname, abandon_timeout) 222*760c253cSXin Li 223*760c253cSXin Li 224*760c253cSXin Lidef crosfleet_autolock( 225*760c253cSXin Li board: str, lease_minutes: int, dims: Dict[str, Any], timeout_sec: float 226*760c253cSXin Li) -> str: 227*760c253cSXin Li """Lock a device using crosfleet, paramaterized by the board type. 228*760c253cSXin Li 229*760c253cSXin Li Args: 230*760c253cSXin Li board: Board of the DUT we want to lock. 231*760c253cSXin Li lease_minutes: Number of minutes we're trying to lease the DUT for. 232*760c253cSXin Li dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' 233*760c253cSXin Li timeout_sec: Number of seconds to try to lease the DUT. Default 120s. 234*760c253cSXin Li 235*760c253cSXin Li Returns: 236*760c253cSXin Li The hostname of the board, or empty string if it couldn't be parsed. 237*760c253cSXin Li 238*760c253cSXin Li Raises: 239*760c253cSXin Li BoardLockError: When board was unable to be locked. 240*760c253cSXin Li """ 241*760c253cSXin Li crosfleet_cmd_args = [ 242*760c253cSXin Li "crosfleet", 243*760c253cSXin Li "dut", 244*760c253cSXin Li "lease", 245*760c253cSXin Li "-json", 246*760c253cSXin Li '-reason="crosperf autolock"', 247*760c253cSXin Li f"-board={board}", 248*760c253cSXin Li f"-minutes={lease_minutes}", 249*760c253cSXin Li ] 250*760c253cSXin Li if dims: 251*760c253cSXin Li dims_arg = ",".join(f"{k}={v}" for k, v in dims.items()) 252*760c253cSXin Li crosfleet_cmd_args.extend(["-dims", f"{dims_arg}"]) 253*760c253cSXin Li 254*760c253cSXin Li try: 255*760c253cSXin Li output = subprocess.check_output( 256*760c253cSXin Li crosfleet_cmd_args, timeout=timeout_sec, encoding="utf-8" 257*760c253cSXin Li ) 258*760c253cSXin Li except subprocess.CalledProcessError as e: 259*760c253cSXin Li raise BoardLockError( 260*760c253cSXin Li f"crosfleet dut lease failed with exit code: {e.returncode}" 261*760c253cSXin Li ) 262*760c253cSXin Li except subprocess.TimeoutExpired as e: 263*760c253cSXin Li raise BoardLockError( 264*760c253cSXin Li f"crosfleet dut lease timed out after {timeout_sec}s;" 265*760c253cSXin Li " please abandon the dut manually." 266*760c253cSXin Li ) 267*760c253cSXin Li 268*760c253cSXin Li try: 269*760c253cSXin Li json_obj = json.loads(output) 270*760c253cSXin Li dut_hostname = json_obj["DUT"]["Hostname"] 271*760c253cSXin Li if not isinstance(dut_hostname, str): 272*760c253cSXin Li raise TypeError("dut_hostname was not a string") 273*760c253cSXin Li except (json.JSONDecodeError, IndexError, KeyError, TypeError) as e: 274*760c253cSXin Li raise BoardLockError( 275*760c253cSXin Li f"crosfleet dut lease output was parsed incorrectly: {e!r};" 276*760c253cSXin Li f" observed output was {output}" 277*760c253cSXin Li ) 278*760c253cSXin Li return _maybe_append_suffix(dut_hostname) 279*760c253cSXin Li 280*760c253cSXin Li 281*760c253cSXin Lidef crosfleet_release(dut_hostname: str, timeout_sec: float = 120.0): 282*760c253cSXin Li """Release a crosfleet device. 283*760c253cSXin Li 284*760c253cSXin Li Consider using the context managed crosfleet_machine_context 285*760c253cSXin Li 286*760c253cSXin Li Args: 287*760c253cSXin Li dut_hostname: Name of the device we want to release. 288*760c253cSXin Li timeout_sec: Number of seconds to try to release the DUT. Default is 120s. 289*760c253cSXin Li 290*760c253cSXin Li Raises: 291*760c253cSXin Li BoardReleaseError: Potentially failed to abandon the lease. 292*760c253cSXin Li """ 293*760c253cSXin Li crosfleet_cmd_args = [ 294*760c253cSXin Li "crosfleet", 295*760c253cSXin Li "dut", 296*760c253cSXin Li "abandon", 297*760c253cSXin Li dut_hostname, 298*760c253cSXin Li ] 299*760c253cSXin Li exit_code = subprocess.call(crosfleet_cmd_args, timeout=timeout_sec) 300*760c253cSXin Li if exit_code != 0: 301*760c253cSXin Li raise BoardReleaseError( 302*760c253cSXin Li f'"crosfleet dut abandon" had exit code {exit_code}' 303*760c253cSXin Li ) 304*760c253cSXin Li 305*760c253cSXin Li 306*760c253cSXin Lidef _maybe_append_suffix(hostname: str) -> str: 307*760c253cSXin Li if hostname.endswith(".cros") or ".cros." in hostname: 308*760c253cSXin Li return hostname 309*760c253cSXin Li return hostname + ".cros" 310*760c253cSXin Li 311*760c253cSXin Li 312*760c253cSXin Liif __name__ == "__main__": 313*760c253cSXin Li sys.exit(main(sys.argv[1:])) 314