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