Skip to content

Commit

Permalink
vmtest: add experimental top-level CLI to test all architectures
Browse files Browse the repository at this point in the history
Now that we can create root filesystems for different architectures and
run VMs using them, glue it all together for running the test suite on
different architectures. I'll probably change this around before using
it in the CI, but I wanted to share the current progress with others.

Signed-off-by: Omar Sandoval <[email protected]>
  • Loading branch information
osandov committed Aug 23, 2023
1 parent 7efa3b2 commit d86972f
Showing 1 changed file with 307 additions and 0 deletions.
307 changes: 307 additions & 0 deletions vmtest/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
#!/usr/bin/env python3

from collections import OrderedDict
import logging
import os
from pathlib import Path
import shlex
import subprocess
import sys

from util import KernelVersion
from vmtest.config import (
ARCHITECTURES,
HOST_ARCHITECTURE,
KERNEL_FLAVORS,
SUPPORTED_KERNEL_VERSIONS,
Kernel,
)
from vmtest.download import DownloadCompiler, DownloadKernel, download_in_thread
from vmtest.kmod import build_kmod
from vmtest.rootfsbuild import build_drgn_in_rootfs
from vmtest.vm import LostVMError, run_in_vm

logger = logging.getLogger(__name__)


class _ProgressPrinter:
def __init__(self, file):
self._file = file
if hasattr(file, "fileno"):
try:
columns = os.get_terminal_size(file.fileno())[0]
self._color = True
except OSError:
columns = 80
self._color = False
self._header = "#" * columns
self._passed = {}
self._failed = {}

def _green(self, s: str) -> str:
if self._color:
return "\033[32m" + s + "\033[0m"
else:
return s

def _red(self, s: str) -> str:
if self._color:
return "\033[31m" + s + "\033[0m"
else:
return s

def update(self, category: str, name: str, passed: bool):
d = self._passed if passed else self._failed
d.setdefault(category, []).append(name)

if self._failed:
header = self._red(self._header)
else:
header = self._green(self._header)

print(header, file=self._file)
print(file=self._file)

if self._passed:
first = True
for category, names in self._passed.items():
if first:
first = False
print(self._green("Passed:"), end=" ")
else:
print(" ", end=" ")
print(f"{category}: {', '.join(names)}")
if self._failed:
first = True
for category, names in self._failed.items():
if first:
first = False
print(self._red("Failed:"), end=" ")
else:
print(" ", end=" ")
print(f"{category}: {', '.join(names)}")

print(file=self._file)
print(header, file=self._file, flush=True)


def _kdump_works(kernel: Kernel) -> bool:
if kernel.arch.name == "aarch64":
# kexec fails with "kexec: setup_2nd_dtb failed." on older versions.
# See
# http://lists.infradead.org/pipermail/kexec/2020-November/021740.html.
return KernelVersion(kernel.release) >= KernelVersion("5.10")
elif kernel.arch.name == "arm":
# Without virtual address translation, we can't debug vmcores. Besides,
# kexec fails with "Could not find a free area of memory of 0xXXX
# bytes...".
return False
elif kernel.arch.name == "ppc64":
# Without virtual address translation, we can't debug vmcores.
return False
# Before 6.1, sysrq-c hangs.
# return KernelVersion(kernel.release) >= KernelVersion("6.1")
elif kernel.arch.name == "s390x":
# Before 5.15, sysrq-c hangs.
return KernelVersion(kernel.release) >= KernelVersion("5.15")
elif kernel.arch.name == "x86_64":
return True
else:
assert False, kernel.arch.name


if __name__ == "__main__":
import argparse

logging.basicConfig(
format="%(asctime)s:%(levelname)s:%(name)s:%(message)s", level=logging.INFO
)
parser = argparse.ArgumentParser(
description="test drgn in a virtual machine",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-d",
"--directory",
metavar="DIR",
type=Path,
default="build/vmtest",
help="directory for vmtest artifacts",
)
parser.add_argument(
"-a",
"--architecture",
dest="architectures",
action="append",
choices=["all", "foreign", *sorted(ARCHITECTURES)],
default=argparse.SUPPRESS,
required=HOST_ARCHITECTURE is None,
help="architecture to test, "
'"all" to test all supported architectures, '
'or "foreign" to test all supported architectures other than the host architecture; '
"may be given multiple times"
+ (
"" if HOST_ARCHITECTURE is None else f" (default: {HOST_ARCHITECTURE.name})"
),
)
parser.add_argument(
"-k",
"--kernel",
metavar="PATTERN|{all," + ",".join(KERNEL_FLAVORS) + "}",
dest="kernels",
action="append",
default=argparse.SUPPRESS,
help="kernel to test, "
'"all" to test all supported kernels, '
"or flavor name to test all supported kernels of a specific flavor; "
"may be given multiple times (default: none)",
)
parser.add_argument(
"-l",
"--local",
action="store_true",
help="run local tests",
)
args = parser.parse_args()

architecture_names = []
if hasattr(args, "architectures"):
for name in args.architectures:
if name == "all":
architecture_names.extend(ARCHITECTURES)
elif name == "foreign":
architecture_names.extend(
[
arch.name
for arch in ARCHITECTURES.values()
if arch is not HOST_ARCHITECTURE
]
)
else:
architecture_names.append(name)
architectures = [
ARCHITECTURES[name] for name in OrderedDict.fromkeys(architecture_names)
]
else:
architectures = [HOST_ARCHITECTURE]

if hasattr(args, "kernels"):
kernels = []
for pattern in args.kernels:
if pattern == "all":
kernels.extend(
[
version + ".*" + flavor
for version in SUPPORTED_KERNEL_VERSIONS
for flavor in KERNEL_FLAVORS
]
)
elif pattern in KERNEL_FLAVORS:
kernels.extend(
[version + ".*" + pattern for version in SUPPORTED_KERNEL_VERSIONS]
)
else:
kernels.append(pattern)
args.kernels = OrderedDict.fromkeys(kernels)
else:
args.kernels = []

if not args.kernels and not args.local:
parser.error("at least one of -k/--kernel or -l/--local is required")

if args.kernels:
to_download = [DownloadCompiler(arch) for arch in architectures]
for pattern in args.kernels:
for arch in architectures:
to_download.append(DownloadKernel(arch, pattern))
else:
to_download = []

progress = _ProgressPrinter(sys.stderr)

with download_in_thread(args.directory, to_download) as downloads:
for arch in architectures:
if arch is HOST_ARCHITECTURE:
subprocess.check_call(
[sys.executable, "setup.py", "build_ext", "-i"],
env={
**os.environ,
"CONFIGURE_FLAGS": "--enable-compiler-warnings=error",
},
)
if args.local:
logger.info("running local tests on %s", arch.name)
status = subprocess.call(
[
sys.executable,
"-m",
"pytest",
"-v",
"--ignore=tests/linux_kernel",
]
)
progress.update(arch.name, "local", status == 0)
else:
rootfs = args.directory / arch.name / "rootfs"
build_drgn_in_rootfs(rootfs)
if args.local:
logger.info("running local tests on %s", arch.name)
status = subprocess.call(
[
"unshare",
"--map-root-user",
"--map-users=auto",
"--map-groups=auto",
"--fork",
"--pid",
"--mount-proc=" + str(rootfs / "proc"),
"sh",
"-c",
r"""
set -e
mount --bind . "$1/mnt"
chroot "$1" sh -c 'cd /mnt && pytest -v --ignore=tests/linux_kernel'
""",
"sh",
rootfs,
]
)
progress.update(arch.name, "local", status == 0)
for kernel in downloads:
if not isinstance(kernel, Kernel):
continue
kmod = build_kmod(args.directory, kernel)
if _kdump_works(kernel):
kdump_command = """\
"$PYTHON" -Bm vmtest.enter_kdump
# We should crash and not reach this.
exit 1
"""
else:
kdump_command = ""
test_command = rf"""
set -e
export PYTHON={shlex.quote(sys.executable)}
export DRGN_TEST_KMOD={shlex.quote(str(kmod))}
export DRGN_RUN_LINUX_KERNEL_TESTS=1
if [ -e /proc/vmcore ]; then
"$PYTHON" -Bm pytest -v tests/linux_kernel/vmcore
else
insmod "$DRGN_TEST_KMOD"
"$PYTHON" -Bm pytest -v tests/linux_kernel --ignore=tests/linux_kernel/vmcore
{kdump_command}
fi
"""
try:
status = run_in_vm(
test_command,
kernel,
args.directory / kernel.arch.name / "rootfs",
args.directory,
)
except LostVMError as e:
print("error:", e, file=sys.stderr)
status = -1
progress.update(kernel.arch.name, kernel.release, status == 0)

0 comments on commit d86972f

Please sign in to comment.