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 <denniskempin@google.com>
Reviewed-by: Zihan Chen <zihanchen@google.com>
This commit is contained in:
Dennis Kempin 2023-03-30 11:26:45 -07:00 committed by crosvm LUCI
parent 86df65f6d2
commit 6edc4e86d2
6 changed files with 109 additions and 151 deletions

View file

@ -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)
)

View file

@ -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()}"

View file

@ -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}")

View file

@ -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 = [

91
tools/testvm Executable file
View file

@ -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)

View file

@ -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)
)