xref: /aosp_15_r20/external/toolchain-utils/crosperf/crosperf_autolock.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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