Skip to content

Commit

Permalink
Merge branch 'refs/heads/upstream-HEAD' into repo-HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
Delphix Engineering committed Aug 24, 2023
2 parents e842039 + d86972f commit f2f07f2
Show file tree
Hide file tree
Showing 7 changed files with 551 additions and 12 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)
7 changes: 7 additions & 0 deletions vmtest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ class Architecture(NamedTuple):
kernel_arch: str
# Directory under arch/ in the Linux kernel source tree.
kernel_srcarch: str
# Name of the architecture in Debian.
debian_arch: str
# Linux kernel configuration options.
kernel_config: str
# Flavor-specific Linux kernel configuration options.
Expand All @@ -224,6 +226,7 @@ class Architecture(NamedTuple):
name="aarch64",
kernel_arch="arm64",
kernel_srcarch="arm64",
debian_arch="arm64",
kernel_config="""
CONFIG_PCI_HOST_GENERIC=y
CONFIG_RTC_CLASS=y
Expand Down Expand Up @@ -253,6 +256,7 @@ class Architecture(NamedTuple):
name="arm",
kernel_arch="arm",
kernel_srcarch="arm",
debian_arch="armhf",
kernel_config="""
CONFIG_NR_CPUS=8
CONFIG_HIGHMEM=y
Expand Down Expand Up @@ -286,6 +290,7 @@ class Architecture(NamedTuple):
name="ppc64",
kernel_arch="powerpc",
kernel_srcarch="powerpc",
debian_arch="ppc64el",
kernel_config="""
CONFIG_PPC64=y
CONFIG_CPU_LITTLE_ENDIAN=y
Expand All @@ -308,6 +313,7 @@ class Architecture(NamedTuple):
name="s390x",
kernel_arch="s390",
kernel_srcarch="s390",
debian_arch="s390x",
kernel_config="""
# Needed for CONFIG_KEXEC_FILE.
CONFIG_CRYPTO_SHA256_S390=y
Expand All @@ -321,6 +327,7 @@ class Architecture(NamedTuple):
name="x86_64",
kernel_arch="x86_64",
kernel_srcarch="x86",
debian_arch="amd64",
kernel_config="""
CONFIG_RTC_CLASS=y
CONFIG_RTC_DRV_CMOS=y
Expand Down
8 changes: 4 additions & 4 deletions vmtest/kbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def apply_patches(kernel_dir: Path) -> None:
.decode()
.strip()
)
logging.info("applying patches for kernel version %s", version)
logger.info("applying patches for kernel version %s", version)
any_applied = False
for patch in _PATCHES:
for min_version, max_version in patch.versions:
Expand All @@ -122,7 +122,7 @@ async def apply_patches(kernel_dir: Path) -> None:
break
else:
continue
logging.info("applying %s", patch.name)
logger.info("applying %s", patch.name)
any_applied = True
proc = await asyncio.create_subprocess_exec(
"git",
Expand All @@ -147,9 +147,9 @@ async def apply_patches(kernel_dir: Path) -> None:
sys.stderr.buffer.write(stderr)
sys.stderr.buffer.flush()
raise
logging.info("already applied")
logger.info("already applied")
if not any_applied:
logging.info("no patches")
logger.info("no patches")


class KBuild:
Expand Down
Loading

0 comments on commit f2f07f2

Please sign in to comment.