diff options
Diffstat (limited to 'crosperf/crosperf_autolock.py')
-rwxr-xr-x | crosperf/crosperf_autolock.py | 508 |
1 files changed, 270 insertions, 238 deletions
diff --git a/crosperf/crosperf_autolock.py b/crosperf/crosperf_autolock.py index b593fa9c..011f01e3 100755 --- a/crosperf/crosperf_autolock.py +++ b/crosperf/crosperf_autolock.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -# Copyright 2021 The Chromium OS Authors. All rights reserved. +# Copyright 2021 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Wrapper script to automatically lock devices for crosperf.""" -import os -import sys import argparse -import subprocess import contextlib -import json -from typing import Optional, Any import dataclasses +import json +import os +import subprocess +import sys +from typing import Any, Dict, List, Optional, Tuple + # Have to do sys.path hackery because crosperf relies on PYTHONPATH # modifications. @@ -21,261 +22,292 @@ PARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PARENT_DIR) -def main(sys_args: list[str]) -> Optional[str]: - """Run crosperf_autolock. Returns error msg or None""" - args, leftover_args = parse_args(sys_args) - fleet_params = [ - CrosfleetParams(board=args.board, - pool=args.pool, - lease_time=args.lease_time) - for _ in range(args.num_leases) - ] - if not fleet_params: - return ('No board names identified. If you want to use' - ' a known host, just use crosperf directly.') - try: - _run_crosperf(fleet_params, args.dut_lock_timeout, leftover_args) - except BoardLockError as e: - _eprint('ERROR:', e) - _eprint('May need to login to crosfleet? Run "crosfleet login"') - _eprint('The leases may also be successful later on. ' - 'Check with "crosfleet dut leases"') - return 'crosperf_autolock failed' - except BoardReleaseError as e: - _eprint('ERROR:', e) - _eprint('May need to re-run "crosfleet dut abandon"') - return 'crosperf_autolock failed' - return None - - -def parse_args(args: list[str]) -> tuple[Any, list]: - """Parse the CLI arguments.""" - parser = argparse.ArgumentParser( - 'crosperf_autolock', - description='Wrapper around crosperf' - ' to autolock DUTs from crosfleet.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--board', - type=str, - help='Space or comma separated list of boards to lock', - required=True, - default=argparse.SUPPRESS) - parser.add_argument('--num-leases', - type=int, - help='Number of boards to lock.', - metavar='NUM', - default=1) - parser.add_argument('--pool', - type=str, - help='Pool to pull from.', - default='DUT_POOL_QUOTA') - parser.add_argument('--dut-lock-timeout', - type=float, - metavar='SEC', - help='Number of seconds we want to try to lease a board' - ' from crosfleet. This option does NOT change the' - ' lease length.', - default=600) - parser.add_argument('--lease-time', - type=int, - metavar='MIN', - help='Number of minutes to lock the board. Max is 1440.', - default=1440) - parser.epilog = ( - 'For more detailed flags, you have to read the args taken by the' - ' crosperf executable. Args are passed transparently to crosperf.') - return parser.parse_known_args(args) +def main(sys_args: List[str]) -> Optional[str]: + """Run crosperf_autolock. Returns error msg or None""" + args, leftover_args = parse_args(sys_args) + fleet_params = [ + CrosfleetParams( + board=args.board, pool=args.pool, lease_time=args.lease_time + ) + for _ in range(args.num_leases) + ] + if not fleet_params: + return ( + "No board names identified. If you want to use" + " a known host, just use crosperf directly." + ) + try: + _run_crosperf(fleet_params, args.dut_lock_timeout, leftover_args) + except BoardLockError as e: + _eprint("ERROR:", e) + _eprint('May need to login to crosfleet? Run "crosfleet login"') + _eprint( + "The leases may also be successful later on. " + 'Check with "crosfleet dut leases"' + ) + return "crosperf_autolock failed" + except BoardReleaseError as e: + _eprint("ERROR:", e) + _eprint('May need to re-run "crosfleet dut abandon"') + return "crosperf_autolock failed" + return None + + +def parse_args(args: List[str]) -> Tuple[Any, List]: + """Parse the CLI arguments.""" + parser = argparse.ArgumentParser( + "crosperf_autolock", + description="Wrapper around crosperf" + " to autolock DUTs from crosfleet.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--board", + type=str, + help="Space or comma separated list of boards to lock", + required=True, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--num-leases", + type=int, + help="Number of boards to lock.", + metavar="NUM", + default=1, + ) + parser.add_argument( + "--pool", type=str, help="Pool to pull from.", default="DUT_POOL_QUOTA" + ) + parser.add_argument( + "--dut-lock-timeout", + type=float, + metavar="SEC", + help="Number of seconds we want to try to lease a board" + " from crosfleet. This option does NOT change the" + " lease length.", + default=600, + ) + parser.add_argument( + "--lease-time", + type=int, + metavar="MIN", + help="Number of minutes to lock the board. Max is 1440.", + default=1440, + ) + parser.epilog = ( + "For more detailed flags, you have to read the args taken by the" + " crosperf executable. Args are passed transparently to crosperf." + ) + return parser.parse_known_args(args) class BoardLockError(Exception): - """Error to indicate failure to lock a board.""" + """Error to indicate failure to lock a board.""" - def __init__(self, msg: str): - self.msg = 'BoardLockError: ' + msg - super().__init__(self.msg) + def __init__(self, msg: str): + self.msg = "BoardLockError: " + msg + super().__init__(self.msg) class BoardReleaseError(Exception): - """Error to indicate failure to release a board.""" + """Error to indicate failure to release a board.""" - def __init__(self, msg: str): - self.msg = 'BoardReleaseError: ' + msg - super().__init__(self.msg) + def __init__(self, msg: str): + self.msg = "BoardReleaseError: " + msg + super().__init__(self.msg) @dataclasses.dataclass(frozen=True) class CrosfleetParams: - """Dataclass to hold all crosfleet parameterizations.""" - board: str - pool: str - lease_time: int + """Dataclass to hold all crosfleet parameterizations.""" + + board: str + pool: str + lease_time: int def _eprint(*msg, **kwargs): - print(*msg, file=sys.stderr, **kwargs) - - -def _run_crosperf(crosfleet_params: list[CrosfleetParams], lock_timeout: float, - leftover_args: list[str]): - """Autolock devices and run crosperf with leftover arguments. - - Raises: - BoardLockError: When board was unable to be locked. - BoardReleaseError: When board was unable to be released. - """ - if not crosfleet_params: - raise ValueError('No crosfleet params given; cannot call crosfleet.') - - # We'll assume all the boards are the same type, which seems to be the case - # in experiments that actually get used. - passed_board_arg = crosfleet_params[0].board - with contextlib.ExitStack() as stack: - dut_hostnames = [] - for param in crosfleet_params: - print( - f'Sent lock request for {param.board} for {param.lease_time} minutes' - '\nIf this fails, you may need to run "crosfleet dut abandon <...>"') - # May raise BoardLockError, abandoning previous DUTs. - dut_hostname = stack.enter_context( - crosfleet_machine_ctx( - param.board, - param.lease_time, - lock_timeout, - {'label-pool': param.pool}, - )) - if dut_hostname: - print(f'Locked {param.board} machine: {dut_hostname}') - dut_hostnames.append(dut_hostname) - - # We import crosperf late, because this import is extremely slow. - # We don't want the user to wait several seconds just to get - # help info. - import crosperf - for dut_hostname in dut_hostnames: - crosperf.Main([ - sys.argv[0], - '--no_lock', - 'True', - '--remote', - dut_hostname, - '--board', - passed_board_arg, - ] + leftover_args) + print(*msg, file=sys.stderr, **kwargs) + + +def _run_crosperf( + crosfleet_params: List[CrosfleetParams], + lock_timeout: float, + leftover_args: List[str], +): + """Autolock devices and run crosperf with leftover arguments. + + Raises: + BoardLockError: When board was unable to be locked. + BoardReleaseError: When board was unable to be released. + """ + if not crosfleet_params: + raise ValueError("No crosfleet params given; cannot call crosfleet.") + + # We'll assume all the boards are the same type, which seems to be the case + # in experiments that actually get used. + passed_board_arg = crosfleet_params[0].board + with contextlib.ExitStack() as stack: + dut_hostnames = [] + for param in crosfleet_params: + print( + f"Sent lock request for {param.board} for {param.lease_time} minutes" + '\nIf this fails, you may need to run "crosfleet dut abandon <...>"' + ) + # May raise BoardLockError, abandoning previous DUTs. + dut_hostname = stack.enter_context( + crosfleet_machine_ctx( + param.board, + param.lease_time, + lock_timeout, + {"label-pool": param.pool}, + ) + ) + if dut_hostname: + print(f"Locked {param.board} machine: {dut_hostname}") + dut_hostnames.append(dut_hostname) + + # We import crosperf late, because this import is extremely slow. + # We don't want the user to wait several seconds just to get + # help info. + import crosperf + + for dut_hostname in dut_hostnames: + crosperf.Main( + [ + sys.argv[0], + "--no_lock", + "True", + "--remote", + dut_hostname, + "--board", + passed_board_arg, + ] + + leftover_args + ) @contextlib.contextmanager -def crosfleet_machine_ctx(board: str, - lease_minutes: int, - lock_timeout: float, - dims: dict[str, Any], - abandon_timeout: float = 120.0) -> Any: - """Acquire dut from crosfleet, and release once it leaves the context. - - Args: - board: Board type to lease. - lease_minutes: Length of lease, in minutes. - lock_timeout: How long to wait for a lock until quitting. - dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' - abandon_timeout (optional): How long to wait for releasing until quitting. - - Yields: - A string representing the crosfleet DUT hostname. - - Raises: - BoardLockError: When board was unable to be locked. - BoardReleaseError: When board was unable to be released. - """ - # This lock may raise an exception, but if it does, we can't release - # the DUT anyways as we won't have the dut_hostname. - dut_hostname = crosfleet_autolock(board, lease_minutes, dims, lock_timeout) - try: - yield dut_hostname - finally: - if dut_hostname: - crosfleet_release(dut_hostname, abandon_timeout) - - -def crosfleet_autolock(board: str, lease_minutes: int, dims: dict[str, Any], - timeout_sec: float) -> str: - """Lock a device using crosfleet, paramaterized by the board type. - - Args: - board: Board of the DUT we want to lock. - lease_minutes: Number of minutes we're trying to lease the DUT for. - dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' - timeout_sec: Number of seconds to try to lease the DUT. Default 120s. - - Returns: - The hostname of the board, or empty string if it couldn't be parsed. - - Raises: - BoardLockError: When board was unable to be locked. - """ - crosfleet_cmd_args = [ - 'crosfleet', - 'dut', - 'lease', - '-json', - '-reason="crosperf autolock"', - f'-board={board}', - f'-minutes={lease_minutes}', - ] - if dims: - dims_arg = ','.join('{}={}'.format(k, v) for k, v in dims.items()) - crosfleet_cmd_args.extend(['-dims', f'{dims_arg}']) - - try: - output = subprocess.check_output(crosfleet_cmd_args, - timeout=timeout_sec, - encoding='utf-8') - except subprocess.CalledProcessError as e: - raise BoardLockError( - f'crosfleet dut lease failed with exit code: {e.returncode}') - except subprocess.TimeoutExpired as e: - raise BoardLockError(f'crosfleet dut lease timed out after {timeout_sec}s;' - ' please abandon the dut manually.') - - try: - json_obj = json.loads(output) - dut_hostname = json_obj['DUT']['Hostname'] - if not isinstance(dut_hostname, str): - raise TypeError('dut_hostname was not a string') - except (json.JSONDecodeError, IndexError, KeyError, TypeError) as e: - raise BoardLockError( - f'crosfleet dut lease output was parsed incorrectly: {e!r};' - f' observed output was {output}') - return _maybe_append_suffix(dut_hostname) +def crosfleet_machine_ctx( + board: str, + lease_minutes: int, + lock_timeout: float, + dims: Dict[str, Any], + abandon_timeout: float = 120.0, +) -> Any: + """Acquire dut from crosfleet, and release once it leaves the context. + + Args: + board: Board type to lease. + lease_minutes: Length of lease, in minutes. + lock_timeout: How long to wait for a lock until quitting. + dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' + abandon_timeout: How long to wait for releasing until quitting. + + Yields: + A string representing the crosfleet DUT hostname. + + Raises: + BoardLockError: When board was unable to be locked. + BoardReleaseError: When board was unable to be released. + """ + # This lock may raise an exception, but if it does, we can't release + # the DUT anyways as we won't have the dut_hostname. + dut_hostname = crosfleet_autolock(board, lease_minutes, dims, lock_timeout) + try: + yield dut_hostname + finally: + if dut_hostname: + crosfleet_release(dut_hostname, abandon_timeout) + + +def crosfleet_autolock( + board: str, lease_minutes: int, dims: Dict[str, Any], timeout_sec: float +) -> str: + """Lock a device using crosfleet, paramaterized by the board type. + + Args: + board: Board of the DUT we want to lock. + lease_minutes: Number of minutes we're trying to lease the DUT for. + dims: Dictionary of dimension arguments to pass to crosfleet's '-dims' + timeout_sec: Number of seconds to try to lease the DUT. Default 120s. + + Returns: + The hostname of the board, or empty string if it couldn't be parsed. + + Raises: + BoardLockError: When board was unable to be locked. + """ + crosfleet_cmd_args = [ + "crosfleet", + "dut", + "lease", + "-json", + '-reason="crosperf autolock"', + f"-board={board}", + f"-minutes={lease_minutes}", + ] + if dims: + dims_arg = ",".join(f"{k}={v}" for k, v in dims.items()) + crosfleet_cmd_args.extend(["-dims", f"{dims_arg}"]) + + try: + output = subprocess.check_output( + crosfleet_cmd_args, timeout=timeout_sec, encoding="utf-8" + ) + except subprocess.CalledProcessError as e: + raise BoardLockError( + f"crosfleet dut lease failed with exit code: {e.returncode}" + ) + except subprocess.TimeoutExpired as e: + raise BoardLockError( + f"crosfleet dut lease timed out after {timeout_sec}s;" + " please abandon the dut manually." + ) + + try: + json_obj = json.loads(output) + dut_hostname = json_obj["DUT"]["Hostname"] + if not isinstance(dut_hostname, str): + raise TypeError("dut_hostname was not a string") + except (json.JSONDecodeError, IndexError, KeyError, TypeError) as e: + raise BoardLockError( + f"crosfleet dut lease output was parsed incorrectly: {e!r};" + f" observed output was {output}" + ) + return _maybe_append_suffix(dut_hostname) def crosfleet_release(dut_hostname: str, timeout_sec: float = 120.0): - """Release a crosfleet device. - - Consider using the context managed crosfleet_machine_context - - Args: - dut_hostname: Name of the device we want to release. - timeout_sec: Number of seconds to try to release the DUT. Default is 120s. - - Raises: - BoardReleaseError: Potentially failed to abandon the lease. - """ - crosfleet_cmd_args = [ - 'crosfleet', - 'dut', - 'abandon', - dut_hostname, - ] - exit_code = subprocess.call(crosfleet_cmd_args, timeout=timeout_sec) - if exit_code != 0: - raise BoardReleaseError( - f'"crosfleet dut abandon" had exit code {exit_code}') + """Release a crosfleet device. + + Consider using the context managed crosfleet_machine_context + + Args: + dut_hostname: Name of the device we want to release. + timeout_sec: Number of seconds to try to release the DUT. Default is 120s. + + Raises: + BoardReleaseError: Potentially failed to abandon the lease. + """ + crosfleet_cmd_args = [ + "crosfleet", + "dut", + "abandon", + dut_hostname, + ] + exit_code = subprocess.call(crosfleet_cmd_args, timeout=timeout_sec) + if exit_code != 0: + raise BoardReleaseError( + f'"crosfleet dut abandon" had exit code {exit_code}' + ) def _maybe_append_suffix(hostname: str) -> str: - if hostname.endswith('.cros') or '.cros.' in hostname: - return hostname - return hostname + '.cros' + if hostname.endswith(".cros") or ".cros." in hostname: + return hostname + return hostname + ".cros" -if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) |