From 6edc4e86d26f339c6fad80bd6b5abcd3d74c56a0 Mon Sep 17 00:00:00 2001 From: Dennis Kempin Date: Thu, 30 Mar 2023 11:26:45 -0700 Subject: [PATCH] testvm: Refactor to use argh The code has been written before we used argh. The CLI is specified in tools/testvm. The implementation remains in impl/testvm.py so it can be shared with run_tests.py for automatically managing the VM when running tests. BUG=b:275717759 TEST=x86vm run/up/wait/stop TEST=run_tests --dut=vm Change-Id: I2bdb554e7ed17a9f397fd8fb02f754e4ec1e52d7 Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/4390729 Commit-Queue: Dennis Kempin Reviewed-by: Zihan Chen --- tools/aarch64vm | 6 +- tools/impl/common.py | 2 + tools/impl/testvm.py | 154 +++---------------------------------------- tools/run_tests | 1 - tools/testvm | 91 +++++++++++++++++++++++++ tools/x86vm | 6 +- 6 files changed, 109 insertions(+), 151 deletions(-) create mode 100755 tools/testvm diff --git a/tools/aarch64vm b/tools/aarch64vm index 5dfae6eb4..3461cbb7b 100755 --- a/tools/aarch64vm +++ b/tools/aarch64vm @@ -3,8 +3,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -from impl.testvm import main import sys +from impl.common import TOOLS_ROOT, cmd if __name__ == "__main__": - main("aarch64", sys.argv[1:]) + sys.exit( + cmd(sys.executable, TOOLS_ROOT / "testvm", *sys.argv[1:], "--arch=aarch64").fg(check=False) + ) diff --git a/tools/impl/common.py b/tools/impl/common.py index 52e6487b3..59d67da1a 100644 --- a/tools/impl/common.py +++ b/tools/impl/common.py @@ -61,6 +61,8 @@ import rich.live import rich.spinner import rich.text +# Hack: argh does not support type annotations. This prevents type errors. +argh: Any # type: ignore # File where to store http headers for gcloud authentication AUTH_HEADERS_FILE = Path(gettempdir()) / f"crosvm_gcloud_auth_headers_{getpass.getuser()}" diff --git a/tools/impl/testvm.py b/tools/impl/testvm.py index 235fd4b4c..a99c2d013 100755 --- a/tools/impl/testvm.py +++ b/tools/impl/testvm.py @@ -2,65 +2,24 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import argparse import itertools import json import os -import shutil import socket import subprocess import sys import time -import urllib.request as request +import typing from contextlib import closing from pathlib import Path from typing import Dict, Iterable, List, Literal, Optional, Tuple from .common import CACHE_DIR, download_file -USAGE = """%(prog)s {command} [options] - -Manages VMs for testing crosvm. - -Can run an x86_64 and an aarch64 vm via `./tools/x86vm` and `./tools/aarch64vm`. -The VM image will be downloaded and initialized on first use. - -The easiest way to use the VM is: - - $ ./tools/aarch64vm ssh - -Which will initialize and boot the VM, then wait for SSH to be available and -opens an SSH session. The VM will stay alive between calls. - -Alternatively, you can set up an SSH config to connect to the VM. First ensure -the VM ready: - - $ ./tools/aarch64vm wait - -Then append the VMs ssh config to your SSH config: - - $ ./tools/aarch64vm ssh_config >> ~/.ssh/config - -And connect as usual: - - $ ssh crosvm_$ARCH - -Commands: - - build: Download base image and create rootfs overlay. - up: Ensure that the VM is running in the background. - run: Run the VM in the foreground process for debugging. - wait: Boot the VM if it's offline and wait until it's available. - ssh: SSH into the VM. Boot and wait if it's not available. - ssh_config: Prints the ssh config needed to connnect to the VM. - stop: Gracefully shutdown the VM. - kill: Kill the QEMU process. Might damage the image file. - clean: Stop all VMs and delete all data. -""" - KVM_SUPPORT = os.access("/dev/kvm", os.W_OK) Arch = Literal["x86_64", "aarch64"] +ARCH_OPTIONS = typing.get_args(Arch) SCRIPT_DIR = Path(__file__).parent.resolve() SRC_DIR = SCRIPT_DIR.joinpath("testvm") @@ -302,10 +261,12 @@ def is_ssh_port_available(arch: Arch): return sock.connect_ex(("127.0.0.1", SSH_PORTS[arch])) != 0 -def up(arch: Arch): +def up(arch: Arch, reset: bool = False): + "Start the VM if it's not already running." if is_running(arch): return + build_if_needed(arch, reset) print("Booting VM...") run_qemu( arch, @@ -314,20 +275,10 @@ def up(arch: Arch): ) -def run(arch: Arch): - if is_running(arch): - raise Exception("VM is already running") - run_qemu( - arch, - rootfs_img_path(arch), - background=False, - ) - - def wait(arch: Arch, timeout: int = 120): - if not is_running(arch): - up(arch) - elif ping_vm(arch): + "Blocks until the VM is ready to use." + up(arch) + if ping_vm(arch): return print("Waiting for VM") @@ -340,92 +291,3 @@ def wait(arch: Arch, timeout: int = 120): print() return raise Exception("Timeout while waiting for VM") - - -def ssh(arch: Arch, timeout: int): - wait(arch, timeout) - ssh_exec(arch) - - -def ssh_config(arch: Arch): - print(f"Host crosvm_{arch}") - print(f" HostName localhost") - for opt, value in ssh_opts(arch).items(): - print(f" {opt} {value}") - - -def stop(arch: Arch): - if not is_running(arch): - print("VM is not running.") - return - ssh_exec(arch, "sudo poweroff") - - -def kill(arch: Arch): - if not is_running(arch): - print("VM is not running.") - return - kill_vm(arch) - - -def clean(arch: Arch): - if is_running(arch): - kill(arch) - if data_dir(arch).exists(): - shutil.rmtree(data_dir(arch)) - - -def main(arch: Arch, argv: List[str]): - COMMANDS = [ - "build", - "up", - "run", - "wait", - "ssh", - "ssh_config", - "stop", - "kill", - "clean", - ] - parser = argparse.ArgumentParser(usage=USAGE) - parser.add_argument("command", choices=COMMANDS) - parser.add_argument( - "--reset", - action="store_true", - help="Reset VM image to a fresh state. Removes all user modifications.", - ) - parser.add_argument( - "--timeout", - type=int, - default=60, - help="Timeout in seconds while waiting for the VM to come up.", - ) - args = parser.parse_args(argv) - - if args.command == "clean": - clean(arch) - return - - if args.command == "ssh_config": - ssh_config(arch) - return - - # Ensure the images are built regardless of which command we execute. - build_if_needed(arch, reset=args.reset) - - if args.command == "build": - return # Nothing left to do. - elif args.command == "run": - run(arch) - elif args.command == "up": - up(arch) - elif args.command == "ssh": - ssh(arch, args.timeout) - elif args.command == "stop": - stop(arch) - elif args.command == "kill": - kill(arch) - elif args.command == "wait": - wait(arch, args.timeout) - else: - print(f"Unknown command {args.command}") diff --git a/tools/run_tests b/tools/run_tests index 0d8a7a2af..c2fccb5d5 100755 --- a/tools/run_tests +++ b/tools/run_tests @@ -287,7 +287,6 @@ def main( if dut == "host": check_host_prerequisites(run_root_tests) if dut == "vm": - testvm.build_if_needed(get_vm_arch(triple)) testvm.up(get_vm_arch(triple)) nextest_args = [ diff --git a/tools/testvm b/tools/testvm new file mode 100755 index 000000000..39a14dc4b --- /dev/null +++ b/tools/testvm @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# 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. + +import shutil + +from impl.common import run_commands, argh +from impl import testvm + +USAGE = """Manages VMs for testing crosvm. + +Can run an x86_64 and an aarch64 vm via `./tools/x86vm` and `./tools/aarch64vm`. +The VM image will be downloaded and initialized on first use. + +The easiest way to use the VM is: + + $ ./tools/aarch64vm ssh + +Which will initialize and boot the VM, then wait for SSH to be available and +opens an SSH session. The VM will stay alive between calls. + +Alternatively, you can set up an SSH config to connect to the VM. First ensure +the VM ready: + + $ ./tools/aarch64vm wait +""" + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def up(arch: testvm.Arch = "x86_64", reset: bool = False): + "Start the VM if it's not already running." + testvm.up(arch, reset) + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def run(arch: testvm.Arch = "x86_64", reset: bool = False): + "Run the VM in foreground for debugging purposes." + if testvm.is_running(arch): + raise Exception("VM is already running") + testvm.build_if_needed(arch, reset) + testvm.run_qemu( + arch, + testvm.rootfs_img_path(arch), + background=False, + ) + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def wait(arch: testvm.Arch = "x86_64", timeout: int = 120): + "Blocks until the VM is ready to use." + testvm.wait(arch, timeout) + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def ssh(arch: testvm.Arch = "x86_64", timeout: int = 120): + "Starts an interactive shell via SSH, will ensure the VM is running." + testvm.up(arch) + wait(arch, timeout) + testvm.ssh_exec(arch) + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def stop(arch: testvm.Arch = "x86_64"): + "Gracefully stops the running VM." + if not testvm.is_running(arch): + print("VM is not running.") + return + testvm.ssh_exec(arch, "sudo poweroff") + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def kill(arch: testvm.Arch = "x86_64"): + "Kills the running VM with SIGKILL." + if not testvm.is_running(arch): + print("VM is not running.") + return + testvm.kill_vm(arch) + + +@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS) +def clean(arch: testvm.Arch = "x86_64"): + "Stops the VM and deletes all data." + if testvm.is_running(arch): + kill(arch) + if testvm.data_dir(arch).exists(): + shutil.rmtree(testvm.data_dir(arch)) + + +if __name__ == "__main__": + run_commands(up, run, wait, ssh, stop, kill, clean, usage=USAGE) diff --git a/tools/x86vm b/tools/x86vm index 4a3b4398a..8bfe200f4 100755 --- a/tools/x86vm +++ b/tools/x86vm @@ -3,8 +3,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -from impl.testvm import main import sys +from impl.common import TOOLS_ROOT, cmd if __name__ == "__main__": - main("x86_64", sys.argv[1:]) + sys.exit( + cmd(sys.executable, TOOLS_ROOT / "testvm", *sys.argv[1:], "--arch=x86_64").fg(check=False) + )