| #!/usr/bin/env bash |
| # SPDX-License-Identifier: LGPL-2.1-or-later |
| # vi: ts=4 sw=4 tw=0 et: |
| # |
| # TODO: |
| # * SW raid (mdadm) |
| # * MD (mdadm) -> dm-crypt -> LVM |
| # * iSCSI -> dm-crypt -> LVM |
| set -e |
| |
| TEST_DESCRIPTION="systemd-udev storage tests" |
| IMAGE_NAME="default" |
| TEST_NO_NSPAWN=1 |
| # Save only journals of failing test cases by default (to conserve space) |
| TEST_SAVE_JOURNAL="${TEST_SAVE_JOURNAL:-fail}" |
| QEMU_TIMEOUT="${QEMU_TIMEOUT:-600}" |
| |
| # shellcheck source=test/test-functions |
| . "${TEST_BASE_DIR:?}/test-functions" |
| |
| USER_QEMU_OPTIONS="${QEMU_OPTIONS:-}" |
| USER_KERNEL_APPEND="${KERNEL_APPEND:-}" |
| |
| if ! get_bool "$QEMU_KVM"; then |
| echo "This test requires KVM, skipping..." |
| exit 0 |
| fi |
| |
| _host_has_feature() {( |
| set -e |
| |
| case "${1:?}" in |
| btrfs) |
| modprobe -nv btrfs && command -v mkfs.btrfs && command -v btrfs || return $? |
| ;; |
| iscsi) |
| # Client/initiator (Open-iSCSI) |
| command -v iscsiadm && command -v iscsid || return $? |
| # Server/target (TGT) |
| command -v tgtadm && command -v tgtd || return $? |
| ;; |
| lvm) |
| command -v lvm || return $? |
| ;; |
| multipath) |
| command -v multipath && command -v multipathd || return $? |
| ;; |
| *) |
| echo >&2 "ERROR: Unknown feature '$1'" |
| # Make this a hard error to distinguish an invalid feature from |
| # a missing feature |
| exit 1 |
| esac |
| )} |
| |
| test_append_files() {( |
| local feature |
| # An associative array of requested (but optional) features and their |
| # respective "handlers" from test/test-functions |
| # |
| # Note: we install cryptsetup unconditionally, hence it's not explicitly |
| # checked for here |
| local -A features=( |
| [btrfs]=install_btrfs |
| [iscsi]=install_iscsi |
| [lvm]=install_lvm |
| [multipath]=install_multipath |
| ) |
| |
| instmods "=block" "=md" "=nvme" "=scsi" |
| install_dmevent |
| image_install lsblk swapoff swapon wc wipefs |
| |
| # Install the optional features if the host has the respective tooling |
| for feature in "${!features[@]}"; do |
| if _host_has_feature "$feature"; then |
| "${features[$feature]}" |
| fi |
| done |
| |
| generate_module_dependencies |
| |
| for i in {0..127}; do |
| dd if=/dev/zero of="${TESTDIR:?}/disk$i.img" bs=1M count=1 |
| echo "device$i" >"${TESTDIR:?}/disk$i.img" |
| done |
| )} |
| |
| _image_cleanup() { |
| mount_initdir |
| # Clean up certain "problematic" files which may be left over by failing tests |
| : >"${initdir:?}/etc/fstab" |
| : >"${initdir:?}/etc/crypttab" |
| } |
| |
| test_run_one() { |
| local test_id="${1:?}" |
| |
| if run_qemu "$test_id"; then |
| check_result_qemu || { echo "QEMU test failed"; return 1; } |
| fi |
| |
| return 0 |
| } |
| |
| test_run() { |
| local test_id="${1:?}" |
| local passed=() |
| local failed=() |
| local skipped=() |
| local ec state |
| |
| mount_initdir |
| |
| if get_bool "${TEST_NO_QEMU:=}" || ! find_qemu_bin; then |
| dwarn "can't run QEMU, skipping" |
| return 0 |
| fi |
| |
| # Execute each currently defined function starting with "testcase_" |
| for testcase in "${TESTCASES[@]}"; do |
| _image_cleanup |
| echo "------ $testcase: BEGIN ------" |
| # Note for my future frustrated self: `fun && xxx` (as well as ||, if, while, |
| # until, etc.) _DISABLES_ the `set -e` behavior in _ALL_ nested function |
| # calls made from `fun()`, i.e. the function _CONTINUES_ even when a called |
| # command returned non-zero EC. That may unexpectedly hide failing commands |
| # if not handled properly. See: bash(1) man page, `set -e` section. |
| # |
| # So, be careful when adding clean up snippets in the testcase_*() functions - |
| # if the `test_run_one()` function isn't the last command, you have propagate |
| # the exit code correctly (e.g. `test_run_one() || return $?`, see below). |
| ec=0 |
| "$testcase" "$test_id" || ec=$? |
| case $ec in |
| 0) |
| passed+=("$testcase") |
| state="PASS" |
| ;; |
| 77) |
| skipped+=("$testcase") |
| state="SKIP" |
| ;; |
| *) |
| failed+=("$testcase") |
| state="FAIL" |
| esac |
| echo "------ $testcase: END ($state) ------" |
| done |
| |
| echo "Passed tests: ${#passed[@]}" |
| printf " * %s\n" "${passed[@]}" |
| echo "Skipped tests: ${#skipped[@]}" |
| printf " * %s\n" "${skipped[@]}" |
| echo "Failed tests: ${#failed[@]}" |
| printf " * %s\n" "${failed[@]}" |
| |
| [[ ${#failed[@]} -eq 0 ]] || return 1 |
| |
| return 0 |
| } |
| |
| testcase_megasas2_basic() { |
| if ! "${QEMU_BIN:?}" -device help | grep 'name "megasas-gen2"'; then |
| echo "megasas-gen2 device driver is not available, skipping test..." |
| return 77 |
| fi |
| |
| local i |
| local qemu_opts=( |
| "-device megasas-gen2,id=scsi0" |
| "-device megasas-gen2,id=scsi1" |
| "-device megasas-gen2,id=scsi2" |
| "-device megasas-gen2,id=scsi3" |
| ) |
| |
| for i in {0..127}; do |
| # Add 128 drives, 32 per bus |
| qemu_opts+=( |
| "-device scsi-hd,drive=drive$i,bus=scsi$((i / 32)).0,channel=0,scsi-id=$((i % 32)),lun=0" |
| "-drive format=raw,cache=unsafe,file=${TESTDIR:?}/disk$i.img,if=none,id=drive$i" |
| ) |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" |
| } |
| |
| testcase_nvme_basic() { |
| if ! "${QEMU_BIN:?}" -device help | grep 'name "nvme"'; then |
| echo "nvme device driver is not available, skipping test..." |
| return 77 |
| fi |
| |
| local i |
| local qemu_opts=() |
| |
| for i in {0..27}; do |
| qemu_opts+=( |
| "-device nvme,drive=nvme$i,serial=deadbeef$i,num_queues=8" |
| "-drive format=raw,cache=unsafe,file=${TESTDIR:?}/disk$i.img,if=none,id=nvme$i" |
| ) |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" |
| } |
| |
| # Test for issue https://github.com/systemd/systemd/issues/20212 |
| testcase_virtio_scsi_identically_named_partitions() { |
| if ! "${QEMU_BIN:?}" -device help | grep 'name "virtio-scsi-pci"'; then |
| echo "virtio-scsi-pci device driver is not available, skipping test..." |
| return 77 |
| fi |
| |
| # Create 16 disks, with 8 partitions per disk (all identically named) |
| # and attach them to a virtio-scsi controller |
| local qemu_opts=("-device virtio-scsi-pci,id=scsi0,num_queues=4") |
| local diskpath="${TESTDIR:?}/namedpart0.img" |
| local i lodev qemu_timeout |
| |
| dd if=/dev/zero of="$diskpath" bs=1M count=18 |
| lodev="$(losetup --show -f -P "$diskpath")" |
| sfdisk "${lodev:?}" <<EOF |
| label: gpt |
| |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| name="Hello world", size=2M |
| EOF |
| losetup -d "$lodev" |
| |
| for i in {0..15}; do |
| diskpath="${TESTDIR:?}/namedpart$i.img" |
| if [[ $i -gt 0 ]]; then |
| cp -uv "${TESTDIR:?}/namedpart0.img" "$diskpath" |
| fi |
| |
| qemu_opts+=( |
| "-device scsi-hd,drive=drive$i,bus=scsi0.0,channel=0,scsi-id=0,lun=$i" |
| "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" |
| ) |
| done |
| |
| # Bump the timeout when collecting test coverage, since the test is a bit |
| # slower in that case |
| is_built_with_coverage && qemu_timeout=120 || qemu_timeout=60 |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| # Limit the number of VCPUs and set a timeout to make sure we trigger the issue |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| QEMU_SMP=1 QEMU_TIMEOUT=$qemu_timeout test_run_one "${1:?}" || return $? |
| |
| rm -f "${TESTDIR:?}"/namedpart*.img |
| } |
| |
| testcase_multipath_basic_failover() { |
| if ! _host_has_feature "multipath"; then |
| echo "Missing multipath tools, skipping the test..." |
| return 77 |
| fi |
| |
| local qemu_opts=("-device virtio-scsi-pci,id=scsi") |
| local partdisk="${TESTDIR:?}/multipathpartitioned.img" |
| local image lodev nback ndisk wwn |
| |
| dd if=/dev/zero of="$partdisk" bs=1M count=16 |
| lodev="$(losetup --show -f -P "$partdisk")" |
| sfdisk "${lodev:?}" <<EOF |
| label: gpt |
| |
| name="first_partition", size=5M |
| uuid="deadbeef-dead-dead-beef-000000000000", name="failover_part", size=5M |
| EOF |
| udevadm settle |
| mkfs.ext4 -U "deadbeef-dead-dead-beef-111111111111" -L "failover_vol" "${lodev}p2" |
| losetup -d "$lodev" |
| |
| # Add 64 multipath devices, each backed by 4 paths |
| for ndisk in {0..63}; do |
| wwn="0xDEADDEADBEEF$(printf "%.4d" "$ndisk")" |
| # Use a partitioned disk for the first device to test failover |
| [[ $ndisk -eq 0 ]] && image="$partdisk" || image="${TESTDIR:?}/disk$ndisk.img" |
| |
| for nback in {0..3}; do |
| qemu_opts+=( |
| "-device scsi-hd,drive=drive${ndisk}x${nback},serial=MPIO$ndisk,wwn=$wwn" |
| "-drive format=raw,cache=unsafe,file=$image,file.locking=off,if=none,id=drive${ndisk}x${nback}" |
| ) |
| done |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "$partdisk" |
| } |
| |
| # Test case for issue https://github.com/systemd/systemd/issues/19946 |
| testcase_simultaneous_events() { |
| local qemu_opts=("-device virtio-scsi-pci,id=scsi") |
| local partdisk="${TESTDIR:?}/simultaneousevents.img" |
| |
| dd if=/dev/zero of="$partdisk" bs=1M count=110 |
| qemu_opts+=( |
| "-device scsi-hd,drive=drive1,serial=deadbeeftest" |
| "-drive format=raw,cache=unsafe,file=$partdisk,if=none,id=drive1" |
| ) |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "$partdisk" |
| } |
| |
| testcase_lvm_basic() { |
| if ! _host_has_feature "lvm"; then |
| echo "Missing lvm tools, skipping the test..." |
| return 77 |
| fi |
| |
| local qemu_opts=("-device ahci,id=ahci0") |
| local diskpath i |
| |
| # Attach 4 SATA disks to the VM (and set their model and serial fields |
| # to something predictable, so we can refer to them later) |
| for i in {0..3}; do |
| diskpath="${TESTDIR:?}/lvmbasic${i}.img" |
| dd if=/dev/zero of="$diskpath" bs=1M count=32 |
| qemu_opts+=( |
| "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeeflvm$i" |
| "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" |
| ) |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "${TESTDIR:?}"/lvmbasic*.img |
| } |
| |
| testcase_btrfs_basic() { |
| if ! _host_has_feature "btrfs"; then |
| echo "Missing btrfs tools/modules, skipping the test..." |
| return 77 |
| fi |
| |
| local qemu_opts=("-device ahci,id=ahci0") |
| local diskpath i size |
| |
| for i in {0..3}; do |
| diskpath="${TESTDIR:?}/btrfsbasic${i}.img" |
| # Make the first disk larger for multi-partition tests |
| [[ $i -eq 0 ]] && size=350 || size=128 |
| |
| dd if=/dev/zero of="$diskpath" bs=1M count="$size" |
| qemu_opts+=( |
| "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefbtrfs$i" |
| "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" |
| ) |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "${TESTDIR:?}"/btrfsbasic*.img |
| } |
| |
| testcase_iscsi_lvm() { |
| if ! _host_has_feature "iscsi" || ! _host_has_feature "lvm"; then |
| echo "Missing iSCSI client/server tools (Open-iSCSI/TGT) or LVM utilities, skipping the test..." |
| return 77 |
| fi |
| |
| local qemu_opts=("-device ahci,id=ahci0") |
| local diskpath i size |
| |
| for i in {0..3}; do |
| diskpath="${TESTDIR:?}/iscsibasic${i}.img" |
| # Make the first disk larger for multi-partition tests |
| [[ $i -eq 0 ]] && size=150 || size=64 |
| # Make the first disk larger for multi-partition tests |
| |
| dd if=/dev/zero of="$diskpath" bs=1M count="$size" |
| qemu_opts+=( |
| "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefiscsi$i" |
| "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" |
| ) |
| done |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "${TESTDIR:?}"/iscsibasic*.img |
| } |
| |
| testcase_long_sysfs_path() { |
| local brid |
| local testdisk="${TESTDIR:?}/longsysfspath.img" |
| local qemu_opts=( |
| "-drive if=none,id=drive0,format=raw,cache=unsafe,file=$testdisk" |
| "-device pci-bridge,id=pci_bridge0,bus=pci.0,chassis_nr=64" |
| ) |
| |
| dd if=/dev/zero of="$testdisk" bs=1M count=64 |
| lodev="$(losetup --show -f -P "$testdisk")" |
| sfdisk "${lodev:?}" <<EOF |
| label: gpt |
| |
| name="test_swap", size=32M |
| uuid="deadbeef-dead-dead-beef-000000000000", name="test_part", size=5M |
| EOF |
| udevadm settle |
| mkswap -U "deadbeef-dead-dead-beef-111111111111" -L "swap_vol" "${lodev}p1" |
| mkfs.ext4 -U "deadbeef-dead-dead-beef-222222222222" -L "data_vol" "${lodev}p2" |
| losetup -d "$lodev" |
| |
| # Create 25 additional PCI bridges, each one connected to the previous one |
| # (basically a really long extension cable), and attach a virtio drive to |
| # the last one. This should force udev into attempting to create a device |
| # unit with a _really_ long name. |
| for brid in {1..25}; do |
| qemu_opts+=("-device pci-bridge,id=pci_bridge$brid,bus=pci_bridge$((brid-1)),chassis_nr=$((64+brid))") |
| done |
| |
| qemu_opts+=("-device virtio-blk-pci,drive=drive0,scsi=off,bus=pci_bridge$brid") |
| |
| KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" |
| QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" |
| test_run_one "${1:?}" || return $? |
| |
| rm -f "${testdisk:?}" |
| } |
| |
| # Allow overriding which tests should be run from the "outside", useful for manual |
| # testing (make -C test/... TESTCASES="testcase1 testcase2") |
| if [[ -v "TESTCASES" && -n "$TESTCASES" ]]; then |
| read -ra TESTCASES <<< "$TESTCASES" |
| else |
| # This must run after all functions were defined, otherwise `declare -F` won't |
| # see them |
| mapfile -t TESTCASES < <(declare -F | awk '$3 ~ /^testcase_/ {print $3;}') |
| fi |
| |
| do_test "$@" |