diff options
Diffstat (limited to '')
-rwxr-xr-x | debian/tests/check-log | 266 | ||||
-rw-r--r-- | debian/tests/control | 55 | ||||
-rwxr-xr-x | debian/tests/hooks/drop-hostname | 17 | ||||
-rwxr-xr-x | debian/tests/hooks/persistent-net | 32 | ||||
-rwxr-xr-x | debian/tests/qemu-ata-only | 24 | ||||
-rwxr-xr-x | debian/tests/qemu-busybox | 21 | ||||
-rwxr-xr-x | debian/tests/qemu-klibc | 21 | ||||
-rwxr-xr-x | debian/tests/qemu-net | 128 | ||||
-rwxr-xr-x | debian/tests/qemu-panic-shell | 35 | ||||
-rwxr-xr-x | debian/tests/qemu-separate-usr | 37 | ||||
-rwxr-xr-x | debian/tests/qemu-virtio-only | 23 | ||||
-rwxr-xr-x | debian/tests/run-qemu | 50 | ||||
-rw-r--r-- | debian/tests/test-common | 204 |
13 files changed, 913 insertions, 0 deletions
diff --git a/debian/tests/check-log b/debian/tests/check-log new file mode 100755 index 0000000..01b24b9 --- /dev/null +++ b/debian/tests/check-log @@ -0,0 +1,266 @@ +#!/usr/bin/python3 + +# pylint: disable=invalid-name +# pylint: enable=invalid-name + +"""Run given checks on the log output.""" + +import argparse +import json +import pathlib +import re +import shlex +import sys +import typing + + +class Check: + """The Check class contains all the checks that the caller can run.""" + + def __init__(self, log: str) -> None: + self.errors = 0 + self.command_outputs = self._extract_command_outputs_from_log(log) + + def _error(self, msg: str) -> None: + print("ERROR: " + msg) + self.errors += 1 + + @staticmethod + def _extract_command_outputs_from_log(log: str) -> dict[str, str]: + """Extract command outputs from the given log output. + + The output must be framed by a header and a footer line. The header + line contains the key surrounded by 10 # characters. The footer + line consist of 40 # characters. Returns a mapping from the output + key to the command output. + """ + marker = "#" * 10 + footer = "#" * 40 + matches = re.findall( + f"^{marker} ([^#]+) {marker}\n(.*?\n){footer}$", + log, + flags=re.DOTALL | re.MULTILINE, + ) + return {m[0]: m[1] for m in matches} + + def get_commands_from_ps_output(self) -> list[str]: + """Get list of command from `ps -ww aux` output.""" + ps_output = self.command_outputs["ps -ww aux"] + lines = ps_output.strip().split("\n")[1:] + commands = [] + for line in lines: + columns = re.split(r"\s+", line, maxsplit=10) + commands.append(columns[10]) + return commands + + def get_ip_addr(self) -> list[typing.Any]: + """Get IP address information from `ip addr` JSON output.""" + ip_addr = json.loads(self.command_outputs["ip -json addr"]) + assert isinstance(ip_addr, list) + return ip_addr + + def get_ip_route(self) -> list[typing.Any]: + """Get IP route information from `ip route` JSON output.""" + ip_route = json.loads(self.command_outputs["ip -json route"]) + assert isinstance(ip_route, list) + return ip_route + + def run_checks(self, args: list[str]) -> int: + """Run the checks and return the number of errors found. + + The methods of this class can be u + """ + if not args: + return self.errors + + try: + check = getattr(self, args[0]) + except AttributeError: + self._error(f"Check '{args[0]}' not found.") + return self.errors + check_args = [] + + for arg in args[1:]: + if not hasattr(self, arg): + check_args.append(arg) + continue + + check(*check_args) + check = getattr(self, arg) + check_args = [] + + check(*check_args) + return self.errors + + def _check_is_subset(self, actual: set[str], expected: set[str]) -> None: + """Check that the first dictionary is a subset of the second one. + + Log errors if the sets are different. + """ + unexpected = actual - expected + if unexpected: + self._error(f"Not expected entries: {unexpected}") + + def _check_is_subdict( + self, + expected_dict: dict[str, str], + actual_dict: dict[str, str], + log_prefix: str, + ) -> None: + """Check that the first dictionary is a subset of the second one. + + Log errors if differences are found. + """ + missing_keys = set(expected_dict.keys()) - set(actual_dict.keys()) + if missing_keys: + self._error(f"{log_prefix}Missing keys: {missing_keys}") + for key, expected_value in sorted(expected_dict.items()): + actual_value = actual_dict.get(key, "") + if expected_value != actual_value: + self._error( + f"{log_prefix}Value for key '{key}' differs:" + f" '{expected_value}' expected, but got '{actual_value}'" + ) + + # Below are all checks that the user might call. + + def has_hostname(self, hostname_pattern: str) -> None: + """Check that the hostname matches the given regular expression.""" + hostname = self.command_outputs["hostname"].strip() + if re.fullmatch(hostname_pattern, hostname): + print(f"hostname '{hostname}' matches pattern '{hostname_pattern}'") + return + self._error( + f"hostname '{hostname}' does not match" + f" expected pattern '{hostname_pattern}'" + ) + + def has_interface_mtu(self, device_pattern: str, expected_mtu: str) -> None: + """Check that a matching network device has the expected MTU set.""" + for device in self.get_ip_addr(): + if not re.fullmatch(device_pattern, device["ifname"]): + continue + if str(device["mtu"]) == expected_mtu: + print(f"device {device['ifname']} has MTU {device['mtu']}") + return + self._error( + f"device {device['ifname']} has MTU {device['mtu']}" + f" but expected {expected_mtu}" + ) + return + self._error(f"no link found that matches '{device_pattern}'") + + def has_ip_addr(self, family: str, addr_pattern: str, device_pattern: str) -> None: + """Check that a matching network device has a matching IP address.""" + for device in self.get_ip_addr(): + if not re.fullmatch(device_pattern, device["ifname"]): + continue + for addr in device["addr_info"]: + if addr["family"] != family or addr["scope"] != "global": + continue + address = f"{addr['local']}/{addr['prefixlen']}" + if re.fullmatch(addr_pattern, address): + print(f"found addr {address} for {device['ifname']}: {addr}") + return + self._error( + f"addr {address} for {device['ifname']}" + f" does not match {addr_pattern}: {addr}" + ) + return + name = {"inet": "IPv4", "inet6": "IPv6"}[family] + self._error( + f"no link found that matches '{device_pattern}' and has an {name} address" + ) + + def has_ipv4_addr(self, addr_pattern: str, device_pattern: str) -> None: + """Check that a matching network device has a matching IPv4 address.""" + self.has_ip_addr("inet", addr_pattern, device_pattern) + + def has_ipv6_addr(self, addr_pattern: str, device_pattern: str) -> None: + """Check that a matching network device has a matching IPv6 address.""" + self.has_ip_addr("inet6", addr_pattern, device_pattern) + + def has_ipv4_default_route(self, gateway_pattern: str, device_pattern: str) -> None: + """Check that the IPv4 default route is via a matching gateway and device.""" + for route in self.get_ip_route(): + if route["dst"] != "default": + continue + if not re.fullmatch(gateway_pattern, route["gateway"]) or not re.fullmatch( + device_pattern, route["dev"] + ): + self._error( + f"Default IPv4 route does not match expected gateway pattern" + f" '{gateway_pattern}' or dev pattern '{device_pattern}': {route}" + ) + continue + print( + f"found IPv4 default route via {route['gateway']}" + f" for {route['dev']}: {route}" + ) + return + self._error("no IPv4 default route found") + + def has_net_conf(self, min_expected_files: str, *expected_net_confs: str) -> None: + """Compare the /run/net*.conf files. + + There must be at least `min_expected_files` /run/net*.conf files + in the log output and no unexpected one. The format for + `expected_net_confs` is `<file name>=<expected content>`. + """ + + expected = dict(nc.split("=", maxsplit=1) for nc in expected_net_confs) + prog = re.compile(r"/run/net[^#]+\.conf") + got = { + key: value for key, value in self.command_outputs.items() if prog.match(key) + } + + if len(got) < int(min_expected_files): + self._error( + f"Expected at least {min_expected_files} /run/net*.conf files," + f" but got only {len(got)}: {set(got.keys())}" + ) + self._check_is_subset(set(got.keys()), set(expected.keys())) + + for net_dev in sorted(got.keys()): + log_prefix = f"{net_dev}: " + expected_net_conf = parse_net_conf(expected.get(net_dev, "")) + actual_net_conf = parse_net_conf(got.get(net_dev, "")) + self._check_is_subdict(expected_net_conf, actual_net_conf, log_prefix) + print(f"compared {len(expected_net_conf)} items from {net_dev}") + + def has_no_running_processes(self) -> None: + """Check that there are no remaining running processes from the initrd.""" + processes = drop_kernel_processes(self.get_commands_from_ps_output()) + if len(processes) == 2: + print(f"found only expected init and ps process: {processes}") + return + self._error( + f"Expected only init and ps process, but got {len(processes)}: {processes}" + ) + + +def drop_kernel_processes(processes: list[str]) -> list[str]: + """Return a list of processes with the kernel processes dropped.""" + return [p for p in processes if not p.startswith("[") or not p.endswith("]")] + + +def parse_net_conf(net_conf: str) -> dict[str, str]: + """Parse /run/net*.conf file and return a key to value mapping.""" + items = shlex.split(net_conf) + return dict(item.split("=", maxsplit=1) for item in items) + + +def main(arguments: list[str]) -> int: + """Run given checks on the log output. Return number of errors.""" + parser = argparse.ArgumentParser() + parser.add_argument("log_file", metavar="log-file") + parser.add_argument("checks", metavar="check", nargs="+") + args = parser.parse_args(arguments) + + log = pathlib.Path(args.log_file).read_text(encoding="ascii") + check = Check(log) + return check.run_checks(args.checks) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..f0fd817 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,55 @@ +Tests: qemu-klibc +Architecture: amd64 armhf s390x +Depends: genext2fs, + ipxe-qemu, + klibc-utils, + linux-image-generic, + qemu-efi-arm [armhf], + qemu-kvm, + zstd, + @ + +Tests: qemu-busybox +Architecture: amd64 armhf s390x +Depends: busybox | busybox-initramfs, + genext2fs, + ipxe-qemu, + klibc-utils, + linux-image-generic, + qemu-efi-arm [armhf], + qemu-kvm, + zstd, + @ + +Tests: qemu-ata-only +Architecture: amd64 +Depends: genext2fs, klibc-utils, linux-image-generic, qemu-kvm, zstd, @ + +Tests: qemu-virtio-only qemu-separate-usr qemu-panic-shell +Architecture: amd64 arm64 armhf ppc64el s390x +Depends: genext2fs, + ipxe-qemu, + klibc-utils, + linux-image-generic, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-kvm, + seabios [ppc64el], + zstd, + @ + +Tests: qemu-net +Architecture: amd64 arm64 armhf ppc64el s390x +Depends: genext2fs, + iproute2, + ipxe-qemu, + klibc-utils, + linux-image-generic, + procps, + python3, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-kvm, + seabios [ppc64el], + zstd, + @ diff --git a/debian/tests/hooks/drop-hostname b/debian/tests/hooks/drop-hostname new file mode 100755 index 0000000..e7b8500 --- /dev/null +++ b/debian/tests/hooks/drop-hostname @@ -0,0 +1,17 @@ +#!/bin/sh + +PREREQ="" + +prereqs() +{ + echo "$PREREQ" +} + +case "$1" in +prereqs) + prereqs + exit 0 + ;; +esac + +rm -f "${DESTDIR}/etc/hostname" diff --git a/debian/tests/hooks/persistent-net b/debian/tests/hooks/persistent-net new file mode 100755 index 0000000..819109e --- /dev/null +++ b/debian/tests/hooks/persistent-net @@ -0,0 +1,32 @@ +#!/bin/sh + +PREREQ="" + +prereqs() +{ + echo "$PREREQ" +} + +case "$1" in +prereqs) + prereqs + exit 0 + ;; +esac + +persist_net() { + name="$1" + mac="$2" + + mkdir -p "${DESTDIR}/etc/systemd/network" + cat >"${DESTDIR}/etc/systemd/network/10-persistent-${name}.link" <<EOF +[Match] +MACAddress=${mac} + +[Link] +Name=${name} +EOF +} + +persist_net lan0 "52:54:00:65:43:21" +persist_net lan1 "52:54:00:12:34:56" diff --git a/debian/tests/qemu-ata-only b/debian/tests/qemu-ata-only new file mode 100755 index 0000000..ef742a5 --- /dev/null +++ b/debian/tests/qemu-ata-only @@ -0,0 +1,24 @@ +#!/bin/sh -e + +# Note: The qemu machines used on arm64, armhf, ppc64el, and s390x have no IDE +SUPPORTED_FLAVOURS='amd64 generic' +ROOTDISK_QEMU_IF=ide +ROOTDISK_LINUX_NAME=sda +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=list +BUSYBOX=n +FSTYPE=ext2 +EOF +cat >"${CONFDIR}/modules" <<EOF +ext2 +ata_piix +sd_mod +EOF +build_initramfs + +build_rootfs_ext2 + +run_qemu +check_no_network_configuration diff --git a/debian/tests/qemu-busybox b/debian/tests/qemu-busybox new file mode 100755 index 0000000..c29c41f --- /dev/null +++ b/debian/tests/qemu-busybox @@ -0,0 +1,21 @@ +#!/bin/sh -e + +# The qemu machines on arm64 and ppc64el are too slow for MODULES=most. +SUPPORTED_FLAVOURS='amd64 armmp s390x generic' +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=most +BUSYBOX=y +FSTYPE=ext2 +EOF +build_initramfs +lsinitramfs "${INITRAMFS}" | grep -qw busybox + +build_rootfs_ext2 + +run_qemu +check_no_network_configuration + +# Check that fsck ran +grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}" diff --git a/debian/tests/qemu-klibc b/debian/tests/qemu-klibc new file mode 100755 index 0000000..2111dab --- /dev/null +++ b/debian/tests/qemu-klibc @@ -0,0 +1,21 @@ +#!/bin/sh -e + +# The qemu machines on arm64 and ppc64el are too slow for MODULES=most. +SUPPORTED_FLAVOURS='amd64 armmp s390x generic' +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=most +BUSYBOX=n +FSTYPE=ext2 +EOF +build_initramfs +! lsinitramfs "${INITRAMFS}" | grep -qw busybox + +build_rootfs_ext2 + +run_qemu +check_no_network_configuration + +# Check that fsck ran +grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}" diff --git a/debian/tests/qemu-net b/debian/tests/qemu-net new file mode 100755 index 0000000..5ebd538 --- /dev/null +++ b/debian/tests/qemu-net @@ -0,0 +1,128 @@ +#!/bin/sh +set -eu + +# Some simple tests of the initramfs network configuration. + +# The basic idea is to make an ext2 root image that only ships a /sbin/init to +# just gather some data and shutdown again and boot it in qemu system +# emulation (not KVM, so it can be run in the autopkgtest architecture without +# hoping nested kvm works). Currently it only sets up qemu user networking +# which limits our ability to be clever. In the long run we should set up a +# tun and a bridge and specify the mac address of the NICs in the emulated +# system and run dnsmasq on it so we test ipv6 and can control which ips which +# nics get and so on -- but this is still better than nothing. + +# TODO: Add a test case for classless static routes. This needs support in +# Qemu first. Following patch should be refreshed: +# https://lore.kernel.org/all/20180314190814.22631-1-benjamin.drung@profitbricks.com/ + +SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic' +ROOTDISK_QEMU_IF=virtio +ROOTDISK_LINUX_NAME=vda +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=list +BUSYBOX=n +FSTYPE=ext2 +EOF +cat >"${CONFDIR}/modules" <<EOF +ext2 +virtio_pci +virtio_blk +virtio_net +EOF +install -m 755 debian/tests/hooks/drop-hostname "${CONFDIR}/hooks/drop-hostname" +install -m 755 debian/tests/hooks/persistent-net "${CONFDIR}/hooks/persistent-net" +build_initramfs + +prepare_network_dumping_rootfs +build_rootfs_ext2 + +EXPECTED_DHCP_LAN0=" +DEVICE='lan0' +PROTO='dhcp' +IPV4ADDR='10.0.3.15' +IPV4BROADCAST='10.0.3.255' +IPV4NETMASK='255.255.255.0' +IPV4GATEWAY='10.0.3.2' +IPV4DNS0='10.0.3.3' +HOSTNAME='pizza' +DNSDOMAIN='example.com' +ROOTSERVER='10.0.3.2' +filename='/path/to/bootfile2' +DOMAINSEARCH='test.' +" +EXPECTED_DHCP_LAN1=" +DEVICE='lan1' +PROTO='dhcp' +IPV4ADDR='10.0.2.15' +IPV4BROADCAST='10.0.2.255' +IPV4NETMASK='255.255.255.0' +IPV4GATEWAY='10.0.2.2' +IPV4DNS0='10.0.2.3' +HOSTNAME='goulash' +DNSDOMAIN='test' +ROOTSERVER='10.0.2.2' +filename='/path/to/bootfile' +DOMAINSEARCH='example. example.net.' +" + +run_qemu "ip=dhcp" +check_output "Begin: Waiting up to 180 secs for any network device to become available" +./debian/tests/check-log "${OUTPUT}" has_no_running_processes \ + has_hostname "goulash|pizza" \ + has_interface_mtu "lan[01]" 1500 \ + has_ipv4_addr "10\.0\.[23]\.15/24" "lan[01]" \ + has_ipv4_default_route "10\.0\.[23]\.2" "lan[01]" \ + has_net_conf 1 "/run/net-lan0.conf=${EXPECTED_DHCP_LAN0}" "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}" + +# Test _set_netdev_from_ip_param +run_qemu "ip=:::::lan1:dhcp" +check_output "Begin: Waiting up to 180 secs for lan1 to become available" +./debian/tests/check-log "${OUTPUT}" has_no_running_processes \ + has_hostname "goulash" \ + has_interface_mtu "lan1" 1500 \ + has_ipv4_addr "10\.0\.2\.15/24" "lan1" \ + has_ipv4_default_route "10\.0\.2\.2" "lan1" \ + has_net_conf 1 "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}" + +# Test setting the IP address manually +run_qemu "ip=10.0.2.100::10.0.2.2:255.0.0.0:lasagne:lan1:" +check_output "Begin: Waiting up to 180 secs for lan1 to become available" +./debian/tests/check-log "${OUTPUT}" has_no_running_processes \ + has_hostname "lasagne" \ + has_interface_mtu "lan1" 1500 \ + has_ipv4_addr "10\.0\.2\.100/8" "lan1" \ + has_ipv4_default_route "10\.0\.2\.2" "lan1" \ + has_net_conf 1 "/run/net-lan1.conf=DEVICE='lan1' +PROTO='none' +IPV4ADDR='10.0.2.100' +IPV4BROADCAST='10.255.255.255' +IPV4NETMASK='255.0.0.0' +IPV4GATEWAY='10.0.2.2' +IPV4DNS0='0.0.0.0' +HOSTNAME='lasagne' +DNSDOMAIN='' +ROOTSERVER='0.0.0.0' +filename='' +DOMAINSEARCH=''" + +# Test DHCP configuration with BOOTIF specified +run_qemu "BOOTIF=01-52-54-00-12-34-56 ip=dhcp" +check_output "Begin: Waiting up to 180 secs for device with address 52:54:00:12:34:56 to become available" +./debian/tests/check-log "${OUTPUT}" has_no_running_processes \ + has_hostname "goulash" \ + has_interface_mtu "lan1" 1500 \ + has_ipv4_addr "10\.0\.2\.15/24" "lan1" \ + has_ipv4_default_route "10\.0\.2\.2" "lan1" \ + has_net_conf 1 "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}" + +run_qemu "ip=on" +check_output "Begin: Waiting up to 180 secs for any network device to become available" +./debian/tests/check-log "${OUTPUT}" has_no_running_processes \ + has_hostname "goulash|pizza" \ + has_interface_mtu "lan[01]" 1500 \ + has_ipv4_addr "10\.0\.[23]\.15/24" "lan[01]" \ + has_ipv4_default_route "10\.0\.[23]\.2" "lan[01]" \ + has_net_conf 1 "/run/net-lan0.conf=${EXPECTED_DHCP_LAN0}" "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}" diff --git a/debian/tests/qemu-panic-shell b/debian/tests/qemu-panic-shell new file mode 100755 index 0000000..296010f --- /dev/null +++ b/debian/tests/qemu-panic-shell @@ -0,0 +1,35 @@ +#!/bin/sh -e + +SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic' +ROOTDISK_QEMU_IF=virtio +ROOTDISK_LINUX_NAME=nonexistent +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=list +BUSYBOX=n +FSTYPE=ext2 +EOF +cat >"${CONFDIR}/modules" <<EOF +ext2 +virtio_pci +virtio_blk +EOF +build_initramfs + +build_rootfs_ext2 + +run_qemu_nocheck +check_no_network_configuration +grep -qF "ALERT! /dev/nonexistent does not exist. Dropping to a shell!" "${OUTPUT}" +grep -qF "(initramfs) " "${OUTPUT}" + +run_qemu_nocheck "panic=-1" +check_no_network_configuration +grep -qF "Rebooting automatically due to panic= boot argument" "${OUTPUT}" +! grep -qF "(initramfs) " "${OUTPUT}" + +run_qemu_nocheck "panic=0" +check_no_network_configuration +grep -qF "Halting automatically due to panic= boot argument" "${OUTPUT}" +! grep -qF "(initramfs) " "${OUTPUT}" diff --git a/debian/tests/qemu-separate-usr b/debian/tests/qemu-separate-usr new file mode 100755 index 0000000..9a252f6 --- /dev/null +++ b/debian/tests/qemu-separate-usr @@ -0,0 +1,37 @@ +#!/bin/sh -e + +SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic' +ROOTDISK_QEMU_IF=virtio +ROOTDISK_LINUX_NAME=vda +USRDISK="$(mktemp)" +USRDISK_QEMU_IF=virtio +USRDISK_LINUX_NAME=vdb +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=list +BUSYBOX=n +FSTYPE=ext2 +EOF +cat >"${CONFDIR}/modules" <<EOF +ext2 +virtio_pci +virtio_blk +EOF +build_initramfs + +# Set up /usr filesystem and fstab entry for it +mkdir -p "${ROOTDIR}/etc" +echo > "${ROOTDIR}/etc/fstab" "/dev/${USRDISK_LINUX_NAME} /usr ext2 defaults 0 2" +USRDIR="$(mktemp -d)" +mv "${ROOTDIR}/usr/"* "${USRDIR}" + +build_rootfs_ext2 +build_fs_ext2 "${USRDIR}" "${USRDISK}" + +run_qemu +check_no_network_configuration + +# Check that fsck ran on both devices +grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}" +grep -q "^/dev/${USRDISK_LINUX_NAME}: clean," "${OUTPUT}" diff --git a/debian/tests/qemu-virtio-only b/debian/tests/qemu-virtio-only new file mode 100755 index 0000000..79670cd --- /dev/null +++ b/debian/tests/qemu-virtio-only @@ -0,0 +1,23 @@ +#!/bin/sh -e + +SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic' +ROOTDISK_QEMU_IF=virtio +ROOTDISK_LINUX_NAME=vda +. debian/tests/test-common + +cat >>"${CONFDIR}/initramfs.conf" <<EOF +MODULES=list +BUSYBOX=n +FSTYPE=ext2 +EOF +cat >"${CONFDIR}/modules" <<EOF +ext2 +virtio_pci +virtio_blk +EOF +build_initramfs + +build_rootfs_ext2 + +run_qemu +check_no_network_configuration diff --git a/debian/tests/run-qemu b/debian/tests/run-qemu new file mode 100755 index 0000000..d3c3e99 --- /dev/null +++ b/debian/tests/run-qemu @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +# Run qemu-system for the system architecture + +if test "$#" -lt 3; then + echo "${0##*/}: Error: Not enough parameters." >&2 + echo "Usage: ${0##*/} kernel initrd append [extra_args]" >&2 + exit 1 +fi + +kernel="$1" +initrd="$2" +append="$3" +shift 3 + +ARCHITECTURE=$(dpkg --print-architecture) + +case "$ARCHITECTURE" in +arm64) + machine="virt,gic-version=max" + cpu="max,pauth-impdef=on" + efi_code=/usr/share/AAVMF/AAVMF_CODE.fd + efi_vars=/usr/share/AAVMF/AAVMF_VARS.fd + ;; +armhf) + machine="virt" + efi_code=/usr/share/AAVMF/AAVMF32_CODE.fd + efi_vars=/usr/share/AAVMF/AAVMF32_VARS.fd + console=ttyAMA0 + ;; +ppc64el) + machine="cap-ccf-assist=off,cap-cfpc=broken,cap-ibs=broken,cap-sbbc=broken" + console=hvc0 + ;; +esac + +if test -f "${efi_vars-}"; then + efi_vars_copy="$(mktemp -t "${efi_vars##*/}.XXXXXXXXXX")" + cp "$efi_vars" "$efi_vars_copy" +fi + +set -- ${machine:+-machine "${machine}"} -cpu "${cpu-max}" -m 1G \ + ${efi_code:+-drive "file=${efi_code},if=pflash,format=raw,read-only=on"} \ + ${efi_vars:+-drive "file=${efi_vars_copy},if=pflash,format=raw"} \ + -device virtio-rng-pci,rng=rng0 -object rng-random,filename=/dev/urandom,id=rng0 \ + -nodefaults -no-reboot -kernel "${kernel}" -initrd "${initrd}" "$@" \ + -append "console=${console:-ttyS0},115200 ro ${append}" +echo "${0##*/}: qemu-system-${ARCHITECTURE} $*" +exec "qemu-system-${ARCHITECTURE}" "$@" diff --git a/debian/tests/test-common b/debian/tests/test-common new file mode 100644 index 0000000..981aafe --- /dev/null +++ b/debian/tests/test-common @@ -0,0 +1,204 @@ +# -*- mode: sh -*- + +# Find kernel flavour and release +KVER= +for flavour in $SUPPORTED_FLAVOURS; do + KVER="$(dpkg-query -Wf '${Depends}' "linux-image-${flavour}" 2>/dev/null | tr ',' '\n' | sed -n 's/^ *linux-image-\([-a-z0-9+.]*\).*/\1/p')" + if [ "$KVER" ]; then + break + fi +done +if [ -z "$KVER" ]; then + echo >&2 "E: Test must set SUPPORTED_FLAVOURS and depend on those flavours" + exit 2 +fi + +case "$(dpkg --print-architecture)" in +arm64) + # The Ubuntu arm64 autopkgtest runs rarely into the 1200 seconds timeout. + QEMU_TIMEOUT=1800 + ;; +armhf) + # qemu-busybox on Ubuntu armhf runs into the 300 seconds timeout. + QEMU_TIMEOUT=600 + ;; +ppc64el) + # Slowest execution seen in Ubuntu ppc64el autopkgtest: 230 seconds + QEMU_TIMEOUT=600 + ;; +*) + QEMU_TIMEOUT=120 +esac + +if [ -n "${AUTOPKGTEST_TMP-}" ]; then + export TMPDIR="${AUTOPKGTEST_TMP}" +fi + +BASEDIR="$(mktemp -d -t initramfs-test.XXXXXXXXXX)" + +# Skeleton configuration directory +CONFDIR="${BASEDIR}/config" +mkdir -p "${CONFDIR}" +cp conf/initramfs.conf "${CONFDIR}/initramfs.conf" +echo "RESUME=none" >>"${CONFDIR}/initramfs.conf" +mkdir "${CONFDIR}/hooks" +touch "${CONFDIR}/modules" +mkdir "${CONFDIR}/scripts" + +# initramfs image file +INITRAMFS="${BASEDIR}/initrd.img" + +# root disk image file +ROOTDISK="${BASEDIR}/rootdisk.raw" + +# root disk interface type (for qemu) and device name (for Linux) +test -n "${ROOTDISK_QEMU_IF}" || ROOTDISK_QEMU_IF=virtio +test -n "${ROOTDISK_LINUX_NAME}" || ROOTDISK_LINUX_NAME=vda + +# Create a root fs with a trivial userspace +ROOTDIR="${BASEDIR}/rootdir" +INIT_MESSAGE='root fs init system started successfully' +for subdir in "" dev proc run sys usr usr/bin usr/lib usr/lib64 usr/sbin; do + mkdir "${ROOTDIR}/${subdir}" +done +for subdir in bin lib lib64 sbin; do + ln -s "usr/$subdir" "${ROOTDIR}/${subdir}" +done +cat >"${ROOTDIR}/sbin/init" <<EOF +#!/bin/sh -e +test -b /dev/${ROOTDISK_LINUX_NAME} +test -d /proc/1 +test -d /run/initramfs +test -d /sys/class +test -d /usr/bin +echo '${INIT_MESSAGE}' +poweroff +EOF +chmod a+x "${ROOTDIR}/sbin/init" +cp /usr/lib/klibc/bin/sh "${ROOTDIR}/bin/sh" +cp /usr/lib/klibc/bin/poweroff "${ROOTDIR}/bin/poweroff" +cp "$(dpkg -L libklibc | grep '/klibc-.*\.so$')" "${ROOTDIR}/lib/" + +# VM output file +OUTPUT="${BASEDIR}/output.log" + +prepare_network_dumping_rootfs() { + local root_dir="${1-$ROOTDIR}" + cat >"${root_dir}/usr/sbin/init" <<EOF +#!/bin/sh +echo "I: Executing /usr/sbin/init from root fs" +# Stop the kernel from spamming the output +current_printk=\$(sysctl kernel.printk) +sysctl -w kernel.printk="4 4 1 7" +# Run twice, once for the human, once for the test harness +echo "I: ip addr" +ip addr +echo "I: ip route" +ip route +echo "I: ip -6 route" +ip -6 route +for file in /run/net*.conf; do + [ -f \$file ] || continue; + echo "########## \$file ##########" + cat \$file + echo "########################################" +done +echo "########## hostname ##########" +cat /proc/sys/kernel/hostname +echo "########################################" +echo "########## ip -json addr ##########" +ip -json addr +echo "########################################" +echo "########## ip -json route ##########" +ip -json route +echo "########################################" +echo "########## ip -json -6 route ##########" +ip -json -6 route +echo "########################################" +echo "########## ps -ww aux ##########" +ps -ww aux +echo "########################################" +sysctl -w "\${current_printk}" +echo '${INIT_MESSAGE}' +poweroff +EOF + + . /usr/share/initramfs-tools/hook-functions + verbose=y + DESTDIR="$root_dir" + for binary in /usr/bin/cat /usr/bin/ip /usr/bin/ps /usr/sbin/sysctl; do + copy_exec "$binary" + done +} + +build_initramfs() { + echo "build_initramfs: /usr/sbin/mkinitramfs -d ${CONFDIR} -o ${INITRAMFS} ${KVER}" + /usr/sbin/mkinitramfs -d "${CONFDIR}" -o "${INITRAMFS}" "${KVER}" +} + +build_fs_ext2() { + local dir="${1}" + local disk="${2}" + + # Get directory size + local blocks="$(du --summarize "${dir}" | cut -f 1)" + local inodes="$(du --summarize --inodes "${dir}" | cut -f 1)" + + # Add fudge factor + blocks="$((blocks + 28 + blocks / 4))" + inodes="$((inodes + 10))" + + # genext2fs writes status messages to stderr; hide that from + # autopkgtest + genext2fs 2>&1 -b "${blocks}" -N "${inodes}" -U -d "${dir}" "${disk}" +} + +build_rootfs_ext2() { + build_fs_ext2 "${ROOTDIR}" "${ROOTDISK}" +} + +_run_qemu() { + local extra_params="$*" + + echo "I: Running qemu (with a timeout of $QEMU_TIMEOUT seconds)..." + timeout --foreground "$QEMU_TIMEOUT" \ + debian/tests/run-qemu /boot/vmlinu*-"${KVER}" "${INITRAMFS}" \ + "root=/dev/${ROOTDISK_LINUX_NAME} ${extra_params}" -nographic \ + -drive "file=${ROOTDISK},if=${ROOTDISK_QEMU_IF},media=disk,format=raw" \ + ${USRDISK:+-drive "file=${USRDISK},if=${USRDISK_QEMU_IF},media=disk,format=raw"} \ + -device "virtio-net-pci,netdev=lan0,mac=52:54:00:65:43:21" \ + -netdev "user,id=lan0,net=10.0.3.0/24,ipv6-net=fec7::/48,hostname=pizza,dnssearch=test,domainname=example.com,bootfile=/path/to/bootfile2" \ + -device "virtio-net-pci,netdev=lan1,mac=52:54:00:12:34:56" \ + -netdev "user,id=lan1,hostname=goulash,dnssearch=example,dnssearch=example.net,domainname=test,bootfile=/path/to/bootfile" \ + -chardev stdio,id=char0 -serial chardev:char0 | tee "${OUTPUT}" +} + +run_qemu_nocheck() { + # hide error messages from autopkgtest + _run_qemu 2>&1 "$@" +} + +run_qemu() { + _run_qemu "panic=-1 $*" + grep -qF "${INIT_MESSAGE}" "${OUTPUT}" +} + +check_no_output() { + local msg="$1" + if grep -qF "${msg}" "${OUTPUT}"; then + echo >&2 "E: Message '${msg}' found in log output '${OUTPUT}." + exit 1 + fi +} + +check_output() { + local msg="$1" + if ! grep -qF "${msg}" "${OUTPUT}"; then + echo >&2 "E: Message '${msg}' not found in log output '${OUTPUT}." + exit 1 + fi +} + +check_no_network_configuration() { + check_no_output "Waiting up to 180 secs for" +} |