1#!/usr/bin/env bash 2# SPDX-License-Identifier: LGPL-2.1-or-later 3# vi: ts=4 sw=4 tw=0 et: 4# 5# TODO: 6# * SW raid (mdadm) 7# * MD (mdadm) -> dm-crypt -> LVM 8# * iSCSI -> dm-crypt -> LVM 9set -e 10 11TEST_DESCRIPTION="systemd-udev storage tests" 12IMAGE_NAME="default" 13TEST_NO_NSPAWN=1 14# Save only journals of failing test cases by default (to conserve space) 15TEST_SAVE_JOURNAL="${TEST_SAVE_JOURNAL:-fail}" 16QEMU_TIMEOUT="${QEMU_TIMEOUT:-600}" 17 18# shellcheck source=test/test-functions 19. "${TEST_BASE_DIR:?}/test-functions" 20 21USER_QEMU_OPTIONS="${QEMU_OPTIONS:-}" 22USER_KERNEL_APPEND="${KERNEL_APPEND:-}" 23 24if ! get_bool "$QEMU_KVM"; then 25 echo "This test requires KVM, skipping..." 26 exit 0 27fi 28 29_host_has_feature() {( 30 set -e 31 32 case "${1:?}" in 33 btrfs) 34 modprobe -nv btrfs && command -v mkfs.btrfs && command -v btrfs || return $? 35 ;; 36 iscsi) 37 # Client/initiator (Open-iSCSI) 38 command -v iscsiadm && command -v iscsid || return $? 39 # Server/target (TGT) 40 command -v tgtadm && command -v tgtd || return $? 41 ;; 42 lvm) 43 command -v lvm || return $? 44 ;; 45 mdadm) 46 command -v mdadm || return $? 47 ;; 48 multipath) 49 command -v multipath && command -v multipathd || return $? 50 ;; 51 *) 52 echo >&2 "ERROR: Unknown feature '$1'" 53 # Make this a hard error to distinguish an invalid feature from 54 # a missing feature 55 exit 1 56 esac 57)} 58 59test_append_files() {( 60 local feature 61 # An associative array of requested (but optional) features and their 62 # respective "handlers" from test/test-functions 63 # 64 # Note: we install cryptsetup unconditionally, hence it's not explicitly 65 # checked for here 66 local -A features=( 67 [btrfs]=install_btrfs 68 [iscsi]=install_iscsi 69 [lvm]=install_lvm 70 [mdadm]=install_mdadm 71 [multipath]=install_multipath 72 ) 73 74 instmods "=block" "=md" "=nvme" "=scsi" 75 install_dmevent 76 image_install lsblk swapoff swapon wc wipefs 77 78 # Install the optional features if the host has the respective tooling 79 for feature in "${!features[@]}"; do 80 if _host_has_feature "$feature"; then 81 "${features[$feature]}" 82 fi 83 done 84 85 generate_module_dependencies 86 87 for i in {0..127}; do 88 dd if=/dev/zero of="${TESTDIR:?}/disk$i.img" bs=1M count=1 89 echo "device$i" >"${TESTDIR:?}/disk$i.img" 90 done 91)} 92 93_image_cleanup() { 94 mount_initdir 95 # Clean up certain "problematic" files which may be left over by failing tests 96 : >"${initdir:?}/etc/fstab" 97 : >"${initdir:?}/etc/crypttab" 98} 99 100test_run_one() { 101 local test_id="${1:?}" 102 103 if run_qemu "$test_id"; then 104 check_result_qemu || { echo "qemu test failed"; return 1; } 105 fi 106 107 return 0 108} 109 110test_run() { 111 local test_id="${1:?}" 112 local passed=() 113 local failed=() 114 local skipped=() 115 local ec state 116 117 mount_initdir 118 119 if get_bool "${TEST_NO_QEMU:=}" || ! find_qemu_bin; then 120 dwarn "can't run qemu, skipping" 121 return 0 122 fi 123 124 # Execute each currently defined function starting with "testcase_" 125 for testcase in "${TESTCASES[@]}"; do 126 _image_cleanup 127 echo "------ $testcase: BEGIN ------" 128 # Note for my future frustrated self: `fun && xxx` (as well as ||, if, while, 129 # until, etc.) _DISABLES_ the `set -e` behavior in _ALL_ nested function 130 # calls made from `fun()`, i.e. the function _CONTINUES_ even when a called 131 # command returned non-zero EC. That may unexpectedly hide failing commands 132 # if not handled properly. See: bash(1) man page, `set -e` section. 133 # 134 # So, be careful when adding clean up snippets in the testcase_*() functions - 135 # if the `test_run_one()` function isn't the last command, you have propagate 136 # the exit code correctly (e.g. `test_run_one() || return $?`, see below). 137 ec=0 138 "$testcase" "$test_id" || ec=$? 139 case $ec in 140 0) 141 passed+=("$testcase") 142 state="PASS" 143 ;; 144 77) 145 skipped+=("$testcase") 146 state="SKIP" 147 ;; 148 *) 149 failed+=("$testcase") 150 state="FAIL" 151 esac 152 echo "------ $testcase: END ($state) ------" 153 done 154 155 echo "Passed tests: ${#passed[@]}" 156 printf " * %s\n" "${passed[@]}" 157 echo "Skipped tests: ${#skipped[@]}" 158 printf " * %s\n" "${skipped[@]}" 159 echo "Failed tests: ${#failed[@]}" 160 printf " * %s\n" "${failed[@]}" 161 162 [[ ${#failed[@]} -eq 0 ]] || return 1 163 164 return 0 165} 166 167testcase_megasas2_basic() { 168 if ! "${QEMU_BIN:?}" -device help | grep 'name "megasas-gen2"'; then 169 echo "megasas-gen2 device driver is not available, skipping test..." 170 return 77 171 fi 172 173 local i 174 local qemu_opts=( 175 "-device megasas-gen2,id=scsi0" 176 "-device megasas-gen2,id=scsi1" 177 "-device megasas-gen2,id=scsi2" 178 "-device megasas-gen2,id=scsi3" 179 ) 180 181 for i in {0..127}; do 182 # Add 128 drives, 32 per bus 183 qemu_opts+=( 184 "-device scsi-hd,drive=drive$i,bus=scsi$((i / 32)).0,channel=0,scsi-id=$((i % 32)),lun=0" 185 "-drive format=raw,cache=unsafe,file=${TESTDIR:?}/disk$i.img,if=none,id=drive$i" 186 ) 187 done 188 189 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 190 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 191 test_run_one "${1:?}" 192} 193 194testcase_nvme_basic() { 195 if ! "${QEMU_BIN:?}" -device help | grep 'name "nvme"'; then 196 echo "nvme device driver is not available, skipping test..." 197 return 77 198 fi 199 200 local i 201 local qemu_opts=() 202 203 for i in {0..27}; do 204 qemu_opts+=( 205 "-device nvme,drive=nvme$i,serial=deadbeef$i,num_queues=8" 206 "-drive format=raw,cache=unsafe,file=${TESTDIR:?}/disk$i.img,if=none,id=nvme$i" 207 ) 208 done 209 210 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 211 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 212 test_run_one "${1:?}" 213} 214 215# Test for issue https://github.com/systemd/systemd/issues/20212 216testcase_virtio_scsi_identically_named_partitions() { 217 if ! "${QEMU_BIN:?}" -device help | grep 'name "virtio-scsi-pci"'; then 218 echo "virtio-scsi-pci device driver is not available, skipping test..." 219 return 77 220 fi 221 222 # Create 16 disks, with 8 partitions per disk (all identically named) 223 # and attach them to a virtio-scsi controller 224 local qemu_opts=("-device virtio-scsi-pci,id=scsi0,num_queues=4") 225 local diskpath="${TESTDIR:?}/namedpart0.img" 226 local i lodev qemu_timeout 227 228 dd if=/dev/zero of="$diskpath" bs=1M count=18 229 lodev="$(losetup --show -f -P "$diskpath")" 230 sfdisk "${lodev:?}" <<EOF 231label: gpt 232 233name="Hello world", size=2M 234name="Hello world", size=2M 235name="Hello world", size=2M 236name="Hello world", size=2M 237name="Hello world", size=2M 238name="Hello world", size=2M 239name="Hello world", size=2M 240name="Hello world", size=2M 241EOF 242 losetup -d "$lodev" 243 244 for i in {0..15}; do 245 diskpath="${TESTDIR:?}/namedpart$i.img" 246 if [[ $i -gt 0 ]]; then 247 cp -uv "${TESTDIR:?}/namedpart0.img" "$diskpath" 248 fi 249 250 qemu_opts+=( 251 "-device scsi-hd,drive=drive$i,bus=scsi0.0,channel=0,scsi-id=0,lun=$i" 252 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 253 ) 254 done 255 256 # Bump the timeout when collecting test coverage, since the test is a bit 257 # slower in that case 258 is_built_with_coverage && qemu_timeout=120 || qemu_timeout=60 259 260 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 261 # Limit the number of VCPUs and set a timeout to make sure we trigger the issue 262 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 263 QEMU_SMP=1 QEMU_TIMEOUT=$qemu_timeout test_run_one "${1:?}" || return $? 264 265 rm -f "${TESTDIR:?}"/namedpart*.img 266} 267 268testcase_multipath_basic_failover() { 269 if ! _host_has_feature "multipath"; then 270 echo "Missing multipath tools, skipping the test..." 271 return 77 272 fi 273 274 local qemu_opts=("-device virtio-scsi-pci,id=scsi") 275 local partdisk="${TESTDIR:?}/multipathpartitioned.img" 276 local image lodev nback ndisk wwn 277 278 dd if=/dev/zero of="$partdisk" bs=1M count=16 279 lodev="$(losetup --show -f -P "$partdisk")" 280 sfdisk "${lodev:?}" <<EOF 281label: gpt 282 283name="first_partition", size=5M 284uuid="deadbeef-dead-dead-beef-000000000000", name="failover_part", size=5M 285EOF 286 udevadm settle 287 mkfs.ext4 -U "deadbeef-dead-dead-beef-111111111111" -L "failover_vol" "${lodev}p2" 288 losetup -d "$lodev" 289 290 # Add 64 multipath devices, each backed by 4 paths 291 for ndisk in {0..63}; do 292 wwn="0xDEADDEADBEEF$(printf "%.4d" "$ndisk")" 293 # Use a partitioned disk for the first device to test failover 294 [[ $ndisk -eq 0 ]] && image="$partdisk" || image="${TESTDIR:?}/disk$ndisk.img" 295 296 for nback in {0..3}; do 297 qemu_opts+=( 298 "-device scsi-hd,drive=drive${ndisk}x${nback},serial=MPIO$ndisk,wwn=$wwn" 299 "-drive format=raw,cache=unsafe,file=$image,file.locking=off,if=none,id=drive${ndisk}x${nback}" 300 ) 301 done 302 done 303 304 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 305 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 306 test_run_one "${1:?}" || return $? 307 308 rm -f "$partdisk" 309} 310 311# Test case for issue https://github.com/systemd/systemd/issues/19946 312testcase_simultaneous_events() { 313 local qemu_opts=("-device virtio-scsi-pci,id=scsi") 314 local partdisk="${TESTDIR:?}/simultaneousevents.img" 315 316 dd if=/dev/zero of="$partdisk" bs=1M count=110 317 qemu_opts+=( 318 "-device scsi-hd,drive=drive1,serial=deadbeeftest" 319 "-drive format=raw,cache=unsafe,file=$partdisk,if=none,id=drive1" 320 ) 321 322 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 323 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 324 test_run_one "${1:?}" || return $? 325 326 rm -f "$partdisk" 327} 328 329testcase_lvm_basic() { 330 if ! _host_has_feature "lvm"; then 331 echo "Missing lvm tools, skipping the test..." 332 return 77 333 fi 334 335 local qemu_opts=("-device ahci,id=ahci0") 336 local diskpath i 337 338 # Attach 4 SATA disks to the VM (and set their model and serial fields 339 # to something predictable, so we can refer to them later) 340 for i in {0..3}; do 341 diskpath="${TESTDIR:?}/lvmbasic${i}.img" 342 dd if=/dev/zero of="$diskpath" bs=1M count=32 343 qemu_opts+=( 344 "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeeflvm$i" 345 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 346 ) 347 done 348 349 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 350 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 351 test_run_one "${1:?}" || return $? 352 353 rm -f "${TESTDIR:?}"/lvmbasic*.img 354} 355 356testcase_btrfs_basic() { 357 if ! _host_has_feature "btrfs"; then 358 echo "Missing btrfs tools/modules, skipping the test..." 359 return 77 360 fi 361 362 local qemu_opts=("-device ahci,id=ahci0") 363 local diskpath i size 364 365 for i in {0..3}; do 366 diskpath="${TESTDIR:?}/btrfsbasic${i}.img" 367 # Make the first disk larger for multi-partition tests 368 [[ $i -eq 0 ]] && size=350 || size=128 369 370 dd if=/dev/zero of="$diskpath" bs=1M count="$size" 371 qemu_opts+=( 372 "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefbtrfs$i" 373 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 374 ) 375 done 376 377 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 378 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 379 test_run_one "${1:?}" || return $? 380 381 rm -f "${TESTDIR:?}"/btrfsbasic*.img 382} 383 384testcase_iscsi_lvm() { 385 if ! _host_has_feature "iscsi" || ! _host_has_feature "lvm"; then 386 echo "Missing iSCSI client/server tools (Open-iSCSI/TGT) or LVM utilities, skipping the test..." 387 return 77 388 fi 389 390 local qemu_opts=("-device ahci,id=ahci0") 391 local diskpath i size 392 393 for i in {0..3}; do 394 diskpath="${TESTDIR:?}/iscsibasic${i}.img" 395 # Make the first disk larger for multi-partition tests 396 [[ $i -eq 0 ]] && size=150 || size=64 397 # Make the first disk larger for multi-partition tests 398 399 dd if=/dev/zero of="$diskpath" bs=1M count="$size" 400 qemu_opts+=( 401 "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefiscsi$i" 402 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 403 ) 404 done 405 406 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 407 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 408 test_run_one "${1:?}" || return $? 409 410 rm -f "${TESTDIR:?}"/iscsibasic*.img 411} 412 413testcase_long_sysfs_path() { 414 local brid 415 local testdisk="${TESTDIR:?}/longsysfspath.img" 416 local qemu_opts=( 417 "-drive if=none,id=drive0,format=raw,cache=unsafe,file=$testdisk" 418 "-device pci-bridge,id=pci_bridge0,bus=pci.0,chassis_nr=64" 419 ) 420 421 dd if=/dev/zero of="$testdisk" bs=1M count=64 422 lodev="$(losetup --show -f -P "$testdisk")" 423 sfdisk "${lodev:?}" <<EOF 424label: gpt 425 426name="test_swap", size=32M 427uuid="deadbeef-dead-dead-beef-000000000000", name="test_part", size=5M 428EOF 429 udevadm settle 430 mkswap -U "deadbeef-dead-dead-beef-111111111111" -L "swap_vol" "${lodev}p1" 431 mkfs.ext4 -U "deadbeef-dead-dead-beef-222222222222" -L "data_vol" "${lodev}p2" 432 losetup -d "$lodev" 433 434 # Create 25 additional PCI bridges, each one connected to the previous one 435 # (basically a really long extension cable), and attach a virtio drive to 436 # the last one. This should force udev into attempting to create a device 437 # unit with a _really_ long name. 438 for brid in {1..25}; do 439 qemu_opts+=("-device pci-bridge,id=pci_bridge$brid,bus=pci_bridge$((brid-1)),chassis_nr=$((64+brid))") 440 done 441 442 qemu_opts+=("-device virtio-blk-pci,drive=drive0,scsi=off,bus=pci_bridge$brid") 443 444 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 445 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 446 test_run_one "${1:?}" || return $? 447 448 rm -f "${testdisk:?}" 449} 450 451testcase_mdadm_basic() { 452 if ! _host_has_feature "mdadm"; then 453 echo "Missing mdadm tools/modules, skipping the test..." 454 return 77 455 fi 456 457 local qemu_opts=("-device ahci,id=ahci0") 458 local diskpath i size 459 460 for i in {0..4}; do 461 diskpath="${TESTDIR:?}/mdadmbasic${i}.img" 462 463 dd if=/dev/zero of="$diskpath" bs=1M count=64 464 qemu_opts+=( 465 "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefmdadm$i" 466 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 467 ) 468 done 469 470 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 471 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 472 test_run_one "${1:?}" || return $? 473 474 rm -f "${TESTDIR:?}"/mdadmbasic*.img 475} 476 477testcase_mdadm_lvm() { 478 if ! _host_has_feature "mdadm" || ! _host_has_feature "lvm"; then 479 echo "Missing mdadm tools/modules or LVM tools, skipping the test..." 480 return 77 481 fi 482 483 local qemu_opts=("-device ahci,id=ahci0") 484 local diskpath i size 485 486 for i in {0..4}; do 487 diskpath="${TESTDIR:?}/mdadmlvm${i}.img" 488 489 dd if=/dev/zero of="$diskpath" bs=1M count=64 490 qemu_opts+=( 491 "-device ide-hd,bus=ahci0.$i,drive=drive$i,model=foobar,serial=deadbeefmdadmlvm$i" 492 "-drive format=raw,cache=unsafe,file=$diskpath,if=none,id=drive$i" 493 ) 494 done 495 496 KERNEL_APPEND="systemd.setenv=TEST_FUNCTION_NAME=${FUNCNAME[0]} ${USER_KERNEL_APPEND:-}" 497 QEMU_OPTIONS="${qemu_opts[*]} ${USER_QEMU_OPTIONS:-}" 498 test_run_one "${1:?}" || return $? 499 500 rm -f "${TESTDIR:?}"/mdadmlvm*.img 501} 502# Allow overriding which tests should be run from the "outside", useful for manual 503# testing (make -C test/... TESTCASES="testcase1 testcase2") 504if [[ -v "TESTCASES" && -n "$TESTCASES" ]]; then 505 read -ra TESTCASES <<< "$TESTCASES" 506else 507 # This must run after all functions were defined, otherwise `declare -F` won't 508 # see them 509 mapfile -t TESTCASES < <(declare -F | awk '$3 ~ /^testcase_/ {print $3;}') 510fi 511 512do_test "$@" 513