1#!/usr/bin/env bash
2# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
3# SPDX-License-Identifier: LGPL-2.1-or-later
4#
5# shellcheck disable=SC2030,SC2031
6# ex: ts=8 sw=4 sts=4 et filetype=sh tw=180
7# Note: the shellcheck line above disables warning for variables which were
8#       modified in a subshell. In our case this behavior is expected, but
9#       `shellcheck` can't distinguish this because of poor variable tracking,
10#       which results in warning for every instance of such variable used
11#       throughout this file.
12#       See:
13#           * comment in function install_verity_minimal()
14#           * koalaman/shellcheck#280
15set -o pipefail
16
17PATH=/sbin:/bin:/usr/sbin:/usr/bin
18export PATH
19
20os_release=$(test -e /etc/os-release && echo /etc/os-release || echo /usr/lib/os-release)
21# shellcheck source=/dev/null
22source "$os_release"
23[[ "$ID" = "debian" || " $ID_LIKE " = *" debian "* ]] && LOOKS_LIKE_DEBIAN=yes || LOOKS_LIKE_DEBIAN=""
24[[ "$ID" = "arch" || " $ID_LIKE " = *" arch "* ]] && LOOKS_LIKE_ARCH=yes || LOOKS_LIKE_ARCH=""
25[[ " $ID_LIKE " = *" suse "* ]] && LOOKS_LIKE_SUSE=yes || LOOKS_LIKE_SUSE=""
26KERNEL_VER="${KERNEL_VER-$(uname -r)}"
27QEMU_TIMEOUT="${QEMU_TIMEOUT:-infinity}"
28NSPAWN_TIMEOUT="${NSPAWN_TIMEOUT:-infinity}"
29TIMED_OUT=  # will be 1 after run_* if *_TIMEOUT is set and test timed out
30[[ "$LOOKS_LIKE_SUSE" ]] && FSTYPE="${FSTYPE:-btrfs}" || FSTYPE="${FSTYPE:-ext4}"
31UNIFIED_CGROUP_HIERARCHY="${UNIFIED_CGROUP_HIERARCHY:-default}"
32EFI_MOUNT="${EFI_MOUNT:-$(bootctl -x 2>/dev/null || echo /boot)}"
33QEMU_MEM="${QEMU_MEM:-512M}"
34# Note that defining a different IMAGE_NAME in a test setup script will only result
35# in default.img being copied and renamed. It can then be extended by defining
36# a test_append_files() function. The $1 parameter will be the root directory.
37# To force creating a new image from scratch (eg: to encrypt it), also define
38# TEST_FORCE_NEWIMAGE=1 in the test setup script.
39IMAGE_NAME=${IMAGE_NAME:-default}
40STRIP_BINARIES="${STRIP_BINARIES:-yes}"
41TEST_REQUIRE_INSTALL_TESTS="${TEST_REQUIRE_INSTALL_TESTS:-1}"
42TEST_PARALLELIZE="${TEST_PARALLELIZE:-0}"
43LOOPDEV=
44
45# Simple wrapper to unify boolean checks.
46# Note: this function needs to stay near the top of the file, so we can use it
47#       in code in the outermost scope.
48get_bool() {
49    # Make the value lowercase to make the regex matching simpler
50    local _bool="${1,,}"
51
52    # Consider empty value as "false"
53    if [[ -z "$_bool" || "$_bool" =~ ^(0|no|false)$ ]]; then
54        return 1
55    elif [[ "$_bool" =~ ^(1|yes|true)$ ]]; then
56        return 0
57    else
58        echo >&2 "Value '$_bool' is not a valid boolean value"
59        exit 1
60    fi
61}
62
63# Decide if we can (and want to) run qemu with KVM acceleration.
64# Check if nested KVM is explicitly enabled (TEST_NESTED_KVM). If not,
65# check if it's not explicitly disabled (TEST_NO_KVM) and we're not already
66# running under KVM. If these conditions are met, enable KVM (and possibly
67# nested KVM), otherwise disable it.
68if get_bool "${TEST_NESTED_KVM:=}" || (! get_bool "${TEST_NO_KVM:=}" && [[ "$(systemd-detect-virt -v)" != kvm ]]); then
69    QEMU_KVM=yes
70else
71    QEMU_KVM=no
72fi
73
74if ! ROOTLIBDIR=$(pkg-config --variable=systemdutildir systemd); then
75    echo "WARNING! Cannot determine rootlibdir from pkg-config, assuming /usr/lib/systemd" >&2
76    ROOTLIBDIR=/usr/lib/systemd
77fi
78
79# The calling test.sh scripts have TEST_BASE_DIR set via their Makefile, but we don't need them to provide it
80TEST_BASE_DIR=${TEST_BASE_DIR:-$(realpath "$(dirname "${BASH_SOURCE[0]}")")}
81TEST_UNITS_DIR="$(realpath "$TEST_BASE_DIR/units")"
82SOURCE_DIR=$(realpath "$TEST_BASE_DIR/..")
83TOOLS_DIR="$SOURCE_DIR/tools"
84# These variables are used by test scripts
85export TEST_BASE_DIR TEST_UNITS_DIR SOURCE_DIR TOOLS_DIR
86
87# note that find-build-dir.sh will return $BUILD_DIR if provided, else it will try to find it
88if get_bool "${NO_BUILD:=}"; then
89    BUILD_DIR="$SOURCE_DIR"
90elif ! BUILD_DIR="$("$TOOLS_DIR"/find-build-dir.sh)"; then
91    echo "ERROR: no build found, please set BUILD_DIR or use NO_BUILD" >&2
92    exit 1
93fi
94
95PATH_TO_INIT="$ROOTLIBDIR/systemd"
96SYSTEMD_JOURNALD="${SYSTEMD_JOURNALD:-$(command -v "$BUILD_DIR/systemd-journald" || command -v "$ROOTLIBDIR/systemd-journald")}"
97SYSTEMD_JOURNAL_REMOTE="${SYSTEMD_JOURNAL_REMOTE:-$(command -v "$BUILD_DIR/systemd-journal-remote" || command -v "$ROOTLIBDIR/systemd-journal-remote" || echo "")}"
98SYSTEMD="${SYSTEMD:-$(command -v "$BUILD_DIR/systemd" || command -v "$ROOTLIBDIR/systemd")}"
99SYSTEMD_NSPAWN="${SYSTEMD_NSPAWN:-$(command -v "$BUILD_DIR/systemd-nspawn" || command -v systemd-nspawn)}"
100JOURNALCTL="${JOURNALCTL:-$(command -v "$BUILD_DIR/journalctl" || command -v journalctl)}"
101
102TESTFILE="${BASH_SOURCE[1]}"
103if [ -z "$TESTFILE" ]; then
104    echo "ERROR: test-functions must be sourced from one of the TEST-*/test.sh scripts" >&2
105    exit 1
106fi
107TESTNAME="$(basename "$(dirname "$(realpath "$TESTFILE")")")"
108STATEDIR="$BUILD_DIR/test/$TESTNAME"
109STATEFILE="$STATEDIR/.testdir"
110IMAGESTATEDIR="$STATEDIR/.."
111TESTLOG="$STATEDIR/test.log"
112
113if ! [[ "$TESTNAME" =~ ^TEST\-([0-9]+)\-.+$ ]]; then
114    echo "ERROR: Test name '$TESTNAME' is not in the expected format: TEST-[0-9]+-*" >&2
115    exit 1
116fi
117TESTID="${BASH_REMATCH[1]:?}"
118
119if [[ ! -f "$TEST_UNITS_DIR/testsuite-$TESTID.service" ]]; then
120    echo "ERROR: Test '$TESTNAME' is missing its service file '$TEST_UNITS_DIR/testsuite-$TESTID.service" >&2
121    exit 1
122fi
123
124BASICTOOLS=(
125    awk
126    base64
127    basename
128    bash
129    capsh
130    cat
131    chmod
132    chown
133    cmp
134    cryptsetup
135    cut
136    date
137    dd
138    diff
139    dirname
140    dmsetup
141    echo
142    env
143    false
144    flock
145    getconf
146    getent
147    getfacl
148    grep
149    gunzip
150    gzip
151    head
152    ionice
153    ip
154    ldd
155    ln
156    loadkeys
157    login
158    losetup
159    lz4cat
160    mkfifo
161    mktemp
162    modprobe
163    mount
164    mountpoint
165    mv
166    nc
167    nproc
168    readlink
169    rev
170    rm
171    rmdir
172    sed
173    seq
174    setfattr
175    setfont
176    setsid
177    sfdisk
178    sh
179    sleep
180    stat
181    su
182    sulogin
183    sysctl
184    tail
185    tar
186    tee
187    test
188    timeout
189    touch
190    tr
191    true
192    truncate
193    umount
194    uname
195    unshare
196    wc
197    xargs
198    xzcat
199)
200
201DEBUGTOOLS=(
202    cp
203    df
204    dhclient
205    dmesg
206    du
207    find
208    free
209    grep
210    hostname
211    id
212    less
213    ln
214    ls
215    mkdir
216    ping
217    ps
218    route
219    sort
220    strace
221    stty
222    tty
223    vi
224)
225
226is_built_with_asan() {
227    if ! type -P objdump >/dev/null; then
228        ddebug "Failed to find objdump. Assuming systemd hasn't been built with ASAN."
229        return 1
230    fi
231
232    # Borrowed from https://github.com/google/oss-fuzz/blob/cd9acd02f9d3f6e80011cc1e9549be526ce5f270/infra/base-images/base-runner/bad_build_check#L182
233    local _asan_calls
234    _asan_calls="$(objdump -dC "$SYSTEMD_JOURNALD" | grep -E "(callq?|brasl?|bl)\s.+__asan" -c)"
235    if ((_asan_calls < 1000)); then
236        return 1
237    else
238        return 0
239    fi
240}
241
242is_built_with_coverage() {
243    if get_bool "${NO_BUILD:=}" || ! command -v meson >/dev/null; then
244        return 1
245    fi
246
247    meson configure "${BUILD_DIR:?}" | grep 'b_coverage' | awk '{ print $2 }' | grep -q 'true'
248}
249
250IS_BUILT_WITH_ASAN=$(is_built_with_asan && echo yes || echo no)
251IS_BUILT_WITH_COVERAGE=$(is_built_with_coverage && echo yes || echo no)
252
253if get_bool "$IS_BUILT_WITH_ASAN"; then
254    STRIP_BINARIES=no
255    SKIP_INITRD="${SKIP_INITRD:-yes}"
256    PATH_TO_INIT=$ROOTLIBDIR/systemd-under-asan
257    QEMU_MEM="2048M"
258    QEMU_SMP="${QEMU_SMP:-4}"
259
260    # We need to correctly distinguish between gcc's and clang's ASan DSOs.
261    if ASAN_RT_NAME="$(awk '/libasan.so/ {x=$1; exit} END {print x; exit x==""}' < <(ldd "$SYSTEMD"))"; then
262        ASAN_COMPILER=gcc
263        ASAN_RT_PATH="$(readlink -f "$(${CC:-gcc} --print-file-name "$ASAN_RT_NAME")")"
264    elif ASAN_RT_NAME="$(awk '/libclang_rt.asan/ {x=$1; exit} END {print x; exit x==""}' < <(ldd "$SYSTEMD"))"; then
265        ASAN_COMPILER=clang
266        ASAN_RT_PATH="$(readlink -f "$(${CC:-clang} --print-file-name "$ASAN_RT_NAME")")"
267
268        # As clang's ASan DSO is usually in a non-standard path, let's check if
269        # the environment is set accordingly. If not, warn the user and exit.
270        # We're not setting the LD_LIBRARY_PATH automagically here, because
271        # user should encounter (and fix) the same issue when running the unit
272        # tests (meson test)
273        if ldd "$SYSTEMD" | grep -q "libclang_rt.asan.*not found"; then
274            echo >&2 "clang's ASan DSO ($ASAN_RT_NAME) is not present in the runtime library path"
275            echo >&2 "Consider setting LD_LIBRARY_PATH=${ASAN_RT_PATH%/*}"
276            exit 1
277        fi
278    else
279        echo >&2 "systemd is not linked against the ASan DSO"
280        echo >&2 "gcc does this by default, for clang compile with -shared-libasan"
281        exit 1
282    fi
283
284    echo "Detected ASan RT '$ASAN_RT_NAME' located at '$ASAN_RT_PATH'"
285fi
286
287find_qemu_bin() {
288    QEMU_BIN="${QEMU_BIN:-""}"
289    # SUSE and Red Hat call the binary qemu-kvm. Debian and Gentoo call it kvm.
290    if get_bool "$QEMU_KVM"; then
291        [[ -n "$QEMU_BIN" ]] || QEMU_BIN="$(command -v kvm qemu-kvm 2>/dev/null | grep '^/' -m1)"
292    fi
293
294    [[ -n "$ARCH" ]] || ARCH="$(uname -m)"
295    case $ARCH in
296    x86_64)
297        # QEMU's own build system calls it qemu-system-x86_64
298        [[ -n "$QEMU_BIN" ]] || QEMU_BIN="$(command -v qemu-system-x86_64 2>/dev/null | grep '^/' -m1)"
299        ;;
300    i*86)
301        # new i386 version of QEMU
302        [[ -n "$QEMU_BIN" ]] || QEMU_BIN="$(command -v qemu-system-i386 2>/dev/null | grep '^/' -m1)"
303
304        # i386 version of QEMU
305        [[ -n "$QEMU_BIN" ]] || QEMU_BIN="$(command -v qemu 2>/dev/null | grep '^/' -m1)"
306        ;;
307    ppc64*)
308        [[ -n "$QEMU_BIN" ]] || QEMU_BIN="$(command -v qemu-system-ppc64 2>/dev/null | grep '^/' -m1)"
309        ;;
310    esac
311
312    if [[ ! -e "$QEMU_BIN" ]]; then
313        echo "Could not find a suitable qemu binary" >&2
314        return 1
315    fi
316}
317
318# Compares argument #1=X.Y.Z (X&Y&Z = numeric) to the version of the installed qemu
319# returns 0 if newer or equal
320# returns 1 if older
321# returns 2 if failing
322qemu_min_version() {
323    find_qemu_bin || return 2
324
325    # get version from binary
326    local qemu_ver
327    qemu_ver="$("$QEMU_BIN" --version | awk '/^QEMU emulator version ([0-9]*\.[0-9]*\.[0-9]*)/ {print $4}')"
328
329    # Check version string format
330    echo "$qemu_ver" | grep -q '^[0-9]*\.[0-9]*\.[0-9]*$' || return 2
331    echo "$1" | grep -q '^[0-9]*\.[0-9]*\.[0-9]*$' || return 2
332
333    # compare as last command to return that value
334    printf "%s\n%s\n" "$1" "$qemu_ver" | sort -V -C
335}
336
337# Return 0 if qemu did run (then you must check the result state/logs for actual
338# success), or 1 if qemu is not available.
339run_qemu() {
340    if [ -f /etc/machine-id ]; then
341        read -r MACHINE_ID </etc/machine-id
342        [ -z "$INITRD" ] && [ -e "$EFI_MOUNT/$MACHINE_ID/$KERNEL_VER/initrd" ] \
343            && INITRD="$EFI_MOUNT/$MACHINE_ID/$KERNEL_VER/initrd"
344        [ -z "$KERNEL_BIN" ] && [ -e "$EFI_MOUNT/$MACHINE_ID/$KERNEL_VER/linux" ] \
345            && KERNEL_BIN="$EFI_MOUNT/$MACHINE_ID/$KERNEL_VER/linux"
346    fi
347
348    local CONSOLE=ttyS0
349
350    rm -f "$initdir"/{testok,failed,skipped}
351    # make sure the initdir is not mounted to avoid concurrent access
352    cleanup_initdir
353    umount_loopback
354
355    if [[ ! "$KERNEL_BIN" ]]; then
356        if get_bool "$LOOKS_LIKE_ARCH"; then
357            KERNEL_BIN=/boot/vmlinuz-linux
358        else
359            [ "$ARCH" ] || ARCH=$(uname -m)
360            case $ARCH in
361                ppc64*)
362                    # Ubuntu ppc64* calls the kernel binary as vmlinux-*, RHEL/CentOS
363                    # uses the "standard" vmlinuz- prefix
364                    [[ -e "/boot/vmlinux-$KERNEL_VER" ]] && KERNEL_BIN="/boot/vmlinux-$KERNEL_VER" || KERNEL_BIN="/boot/vmlinuz-$KERNEL_VER"
365                    CONSOLE=hvc0
366                    ;;
367                *)
368                    KERNEL_BIN="/boot/vmlinuz-$KERNEL_VER"
369                    ;;
370            esac
371        fi
372    fi
373
374    local default_fedora_initrd="/boot/initramfs-${KERNEL_VER}.img"
375    local default_debian_initrd="/boot/initrd.img-${KERNEL_VER}"
376    local default_arch_initrd="/boot/initramfs-linux-fallback.img"
377    local default_suse_initrd="/boot/initrd-${KERNEL_VER}"
378    if [[ ! "$INITRD" ]]; then
379        if [[ -e "$default_fedora_initrd" ]]; then
380            INITRD="$default_fedora_initrd"
381        elif [[ "$LOOKS_LIKE_DEBIAN" && -e "$default_debian_initrd" ]]; then
382            INITRD="$default_debian_initrd"
383        elif [[ "$LOOKS_LIKE_ARCH" && -e "$default_arch_initrd" ]]; then
384            INITRD="$default_arch_initrd"
385        elif [[ "$LOOKS_LIKE_SUSE" && -e "$default_suse_initrd" ]]; then
386            INITRD="$default_suse_initrd"
387        fi
388    fi
389
390    # If QEMU_SMP was not explicitly set, try to determine the value 'dynamically'
391    # i.e. use the number of online CPUs on the host machine. If the nproc utility
392    # is not installed or there's some other error when calling it, fall back
393    # to the original value (QEMU_SMP=1).
394    if [[ -z "${QEMU_SMP:=}" ]]; then
395        if ! QEMU_SMP=$(nproc); then
396            dwarn "nproc utility is not installed, falling back to QEMU_SMP=1"
397            QEMU_SMP=1
398        fi
399    fi
400
401    find_qemu_bin || return 1
402
403    # Umount initdir to avoid concurrent access to the filesystem
404    _umount_dir "$initdir"
405
406    local kernel_params=()
407    local qemu_options=()
408    local qemu_cmd=("$QEMU_BIN")
409
410    if [[ "$UNIFIED_CGROUP_HIERARCHY" = "yes" ]]; then
411        kernel_params+=("systemd.unified_cgroup_hierarchy=yes")
412    elif [[ "$UNIFIED_CGROUP_HIERARCHY" = "no" ]]; then
413        kernel_params+=("systemd.unified_cgroup_hierarchy=no" "systemd.legacy_systemd_cgroup_controller=yes")
414    elif [[ "$UNIFIED_CGROUP_HIERARCHY" = "hybrid" ]]; then
415        kernel_params+=("systemd.unified_cgroup_hierarchy=no" "systemd.legacy_systemd_cgroup_controller=no")
416    elif [[ "$UNIFIED_CGROUP_HIERARCHY" != "default" ]]; then
417        dfatal "Unknown UNIFIED_CGROUP_HIERARCHY. Got $UNIFIED_CGROUP_HIERARCHY, expected [yes|no|hybrid|default]"
418        exit 1
419    fi
420
421    if get_bool "$LOOKS_LIKE_SUSE"; then
422        kernel_params+=("rd.hostonly=0")
423    fi
424
425    kernel_params+=(
426        "root=LABEL=systemd_boot"
427        "rw"
428        "raid=noautodetect"
429        "rd.luks=0"
430        "loglevel=2"
431        "init=$PATH_TO_INIT"
432        "console=$CONSOLE"
433        "SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/testsuite-$1.units:/usr/lib/systemd/tests/testdata/units:"
434        "systemd.unit=testsuite.target"
435        "systemd.wants=testsuite-$1.service"
436    )
437
438    if ! get_bool "$INTERACTIVE_DEBUG"; then
439        kernel_params+=("systemd.wants=end.service")
440    fi
441
442    [ -e "$IMAGE_PRIVATE" ] && image="$IMAGE_PRIVATE" || image="$IMAGE_PUBLIC"
443    qemu_options+=(
444        -smp "$QEMU_SMP"
445        -net none
446        -m "$QEMU_MEM"
447        -nographic
448        -kernel "$KERNEL_BIN"
449        -drive "format=raw,cache=unsafe,file=$image"
450    )
451
452    if [[ -n "${QEMU_OPTIONS:=}" ]]; then
453        local user_qemu_options
454        read -ra user_qemu_options <<< "$QEMU_OPTIONS"
455        qemu_options+=("${user_qemu_options[@]}")
456    fi
457
458    if [[ -n "${KERNEL_APPEND:=}" ]]; then
459        local user_kernel_append
460        read -ra user_kernel_append <<< "$KERNEL_APPEND"
461        kernel_params+=("${user_kernel_append[@]}")
462    fi
463
464    if [[ "$INITRD" ]] && ! get_bool "$SKIP_INITRD"; then
465        qemu_options+=(-initrd "$INITRD")
466    fi
467
468    # Let's use KVM if possible
469    if [[ -c /dev/kvm ]] && get_bool $QEMU_KVM; then
470        qemu_options+=(-machine "accel=kvm" -enable-kvm -cpu host)
471    fi
472
473    if [[ "$QEMU_TIMEOUT" != "infinity" ]]; then
474        qemu_cmd=(timeout --foreground "$QEMU_TIMEOUT" "$QEMU_BIN")
475    fi
476
477    (set -x; "${qemu_cmd[@]}" "${qemu_options[@]}" -append "${kernel_params[*]}")
478    rc=$?
479    if [ "$rc" -eq 124 ] && [ "$QEMU_TIMEOUT" != "infinity" ]; then
480        derror "Test timed out after ${QEMU_TIMEOUT}s"
481        TIMED_OUT=1
482    else
483        [ "$rc" != 0 ] && derror "qemu failed with exit code $rc"
484    fi
485    return 0
486}
487
488# Return 0 if nspawn did run (then you must check the result state/logs for actual
489# success), or 1 if nspawn is not available.
490run_nspawn() {
491    [[ -d /run/systemd/system ]] || return 1
492    rm -f "${initdir:?}"/{testok,failed,skipped}
493
494    local nspawn_cmd=()
495    local nspawn_options=(
496        "--register=no"
497        "--kill-signal=SIGKILL"
498        "--directory=${1:?}"
499        "--setenv=SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/testsuite-$2.units:/usr/lib/systemd/tests/testdata/units:"
500    )
501    local kernel_params=(
502        "$PATH_TO_INIT"
503        "systemd.unit=testsuite.target"
504        "systemd.wants=testsuite-$2.service"
505    )
506
507    if ! get_bool "$INTERACTIVE_DEBUG"; then
508        kernel_params+=("systemd.wants=end.service")
509    fi
510
511    if [[ -n "${NSPAWN_ARGUMENTS:=}" ]]; then
512        local user_nspawn_arguments
513        read -ra user_nspawn_arguments <<< "$NSPAWN_ARGUMENTS"
514        nspawn_options+=("${user_nspawn_arguments[@]}")
515    fi
516
517    if [[ -n "${KERNEL_APPEND:=}" ]]; then
518        local user_kernel_append
519        read -ra user_kernel_append <<< "$KERNEL_APPEND"
520        kernel_params+=("${user_kernel_append[@]}")
521    fi
522
523    if [[ "$UNIFIED_CGROUP_HIERARCHY" = "hybrid" ]]; then
524        dwarn "nspawn doesn't support SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=hybrid, skipping"
525        exit
526    elif [[ "$UNIFIED_CGROUP_HIERARCHY" = "yes" || "$UNIFIED_CGROUP_HIERARCHY" = "no" ]]; then
527        nspawn_cmd+=(env "SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=$UNIFIED_CGROUP_HIERARCHY")
528    elif [[ "$UNIFIED_CGROUP_HIERARCHY" = "default" ]]; then
529        nspawn_cmd+=(env "--unset=UNIFIED_CGROUP_HIERARCHY" "--unset=SYSTEMD_NSPAWN_UNIFIED_HIERARCHY")
530    else
531        dfatal "Unknown UNIFIED_CGROUP_HIERARCHY. Got $UNIFIED_CGROUP_HIERARCHY, expected [yes|no|hybrid|default]"
532        exit 1
533    fi
534
535    if [[ "$NSPAWN_TIMEOUT" != "infinity" ]]; then
536        nspawn_cmd+=(timeout --foreground "$NSPAWN_TIMEOUT" "$SYSTEMD_NSPAWN")
537    else
538        nspawn_cmd+=("$SYSTEMD_NSPAWN")
539    fi
540
541    (set -x; "${nspawn_cmd[@]}" "${nspawn_options[@]}" "${kernel_params[@]}")
542    rc=$?
543    if [ "$rc" -eq 124 ] && [ "$NSPAWN_TIMEOUT" != "infinity" ]; then
544        derror "Test timed out after ${NSPAWN_TIMEOUT}s"
545        TIMED_OUT=1
546    else
547        [ "$rc" != 0 ] && derror "nspawn failed with exit code $rc"
548    fi
549    return 0
550}
551
552# Build two very minimal root images, with two units, one is the same and one is different across them
553install_verity_minimal() {
554    dinfo "Set up a set of minimal images for verity verification"
555    if [ -e "$initdir/usr/share/minimal.raw" ]; then
556        return
557    fi
558    if ! command -v mksquashfs >/dev/null 2>&1; then
559        dfatal "mksquashfs not found"
560        exit 1
561    fi
562    if ! command -v veritysetup >/dev/null 2>&1; then
563        dfatal "veritysetup not found"
564        exit 1
565    fi
566    # Local modifications of some global variables is intentional in this
567    # subshell (SC2030)
568    # shellcheck disable=SC2030
569    (
570        BASICTOOLS=(
571            bash
572            cat
573            grep
574            mount
575            sleep
576        )
577        oldinitdir="$initdir"
578        rm -rfv "$TESTDIR/minimal"
579        export initdir="$TESTDIR/minimal"
580        # app0 will use TemporaryFileSystem=/var/lib, app1 will need the mount point in the base image
581        mkdir -p "$initdir/usr/lib/systemd/system" "$initdir/usr/lib/extension-release.d" "$initdir/etc" "$initdir/var/tmp" "$initdir/opt" "$initdir/var/lib/app1"
582        setup_basic_dirs
583        install_basic_tools
584        # Shellcheck treats [[ -v VAR ]] as an assignment to avoid a different
585        # issue, thus falsely triggering SC2030 in this case
586        # See: koalaman/shellcheck#1409
587        if [[ -v ASAN_RT_PATH ]]; then
588            # If we're compiled with ASan, install the ASan RT (and its dependencies)
589            # into the verity images to get rid of the annoying errors about
590            # missing $LD_PRELOAD libraries.
591            inst_libs "$ASAN_RT_PATH"
592            inst_library "$ASAN_RT_PATH"
593            # Create a dummy LSan suppression file otherwise gcc's ASan
594            # complains as it doesn't exist in the minimal image
595            # (i.e. when running TEST-29 or TEST-50 under sanitizers)
596            touch "$initdir/systemd-lsan.supp"
597        fi
598        cp "$os_release" "$initdir/usr/lib/os-release"
599        ln -s ../usr/lib/os-release "$initdir/etc/os-release"
600        touch "$initdir/etc/machine-id" "$initdir/etc/resolv.conf"
601        touch "$initdir/opt/some_file"
602        echo MARKER=1 >>"$initdir/usr/lib/os-release"
603        echo "PORTABLE_PREFIXES=app0 minimal minimal-app0" >>"$initdir/usr/lib/os-release"
604        cat >"$initdir/usr/lib/systemd/system/minimal-app0.service" <<EOF
605[Service]
606ExecStartPre=cat /usr/lib/os-release
607ExecStart=sleep 120
608EOF
609        cp "$initdir/usr/lib/systemd/system/minimal-app0.service" "$initdir/usr/lib/systemd/system/minimal-app0-foo.service"
610
611        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_0.raw" -noappend
612        veritysetup format "$oldinitdir/usr/share/minimal_0.raw" "$oldinitdir/usr/share/minimal_0.verity" | \
613            grep '^Root hash:' | cut -f2 | tr -d '\n' >"$oldinitdir/usr/share/minimal_0.roothash"
614
615        sed -i "s/MARKER=1/MARKER=2/g" "$initdir/usr/lib/os-release"
616        rm "$initdir/usr/lib/systemd/system/minimal-app0-foo.service"
617        cp "$initdir/usr/lib/systemd/system/minimal-app0.service" "$initdir/usr/lib/systemd/system/minimal-app0-bar.service"
618
619        mksquashfs "$initdir" "$oldinitdir/usr/share/minimal_1.raw" -noappend
620        veritysetup format "$oldinitdir/usr/share/minimal_1.raw" "$oldinitdir/usr/share/minimal_1.verity" | \
621            grep '^Root hash:' | cut -f2 | tr -d '\n' >"$oldinitdir/usr/share/minimal_1.roothash"
622
623        # Rolling distros like Arch do not set VERSION_ID
624        local version_id=""
625        if grep -q "^VERSION_ID=" "$os_release"; then
626            version_id="$(grep "^VERSION_ID=" "$os_release")"
627        fi
628
629        export initdir="$TESTDIR/app0"
630        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
631        grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app0"
632        echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app0"
633        cat >"$initdir/usr/lib/systemd/system/app0.service" <<EOF
634[Service]
635Type=oneshot
636RemainAfterExit=yes
637ExecStart=/opt/script0.sh
638TemporaryFileSystem=/var/lib
639StateDirectory=app0
640RuntimeDirectory=app0
641EOF
642        cat >"$initdir/opt/script0.sh" <<EOF
643#!/bin/bash
644set -e
645test -e /usr/lib/os-release
646echo bar > \${STATE_DIRECTORY}/foo
647cat /usr/lib/extension-release.d/extension-release.app0
648EOF
649        chmod +x "$initdir/opt/script0.sh"
650        echo MARKER=1 >"$initdir/usr/lib/systemd/system/some_file"
651        mksquashfs "$initdir" "$oldinitdir/usr/share/app0.raw" -noappend
652
653        export initdir="$TESTDIR/app1"
654        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
655        grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
656        ( echo "${version_id}"
657          echo "SYSEXT_SCOPE=portable"
658          echo "PORTABLE_PREFIXES=app1" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
659        setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
660        cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
661[Service]
662Type=oneshot
663RemainAfterExit=yes
664ExecStart=/opt/script1.sh
665StateDirectory=app1
666RuntimeDirectory=app1
667EOF
668        cat >"$initdir/opt/script1.sh" <<EOF
669#!/bin/bash
670set -e
671test -e /usr/lib/os-release
672echo baz > \${STATE_DIRECTORY}/foo
673cat /usr/lib/extension-release.d/extension-release.app2
674EOF
675        chmod +x "$initdir/opt/script1.sh"
676        echo MARKER=1 >"$initdir/usr/lib/systemd/system/other_file"
677        mksquashfs "$initdir" "$oldinitdir/usr/share/app1.raw" -noappend
678    )
679}
680
681setup_basic_environment() {
682    # create the basic filesystem layout
683    setup_basic_dirs
684
685    install_systemd
686    install_missing_libraries
687    install_config_files
688    install_zoneinfo
689    create_rc_local
690    install_basic_tools
691    install_libnss
692    install_pam
693    install_dbus
694    install_fonts
695    install_keymaps
696    install_terminfo
697    install_execs
698    install_fs_tools
699    install_modules
700    install_plymouth
701    install_haveged
702    install_debug_tools
703    install_ld_so_conf
704    install_testuser
705    has_user_dbus_socket && install_user_dbus
706    setup_selinux
707    strip_binaries
708    instmods veth
709    install_depmod_files
710    generate_module_dependencies
711    if get_bool "$IS_BUILT_WITH_ASAN"; then
712        create_asan_wrapper
713    fi
714    if get_bool "$TEST_INSTALL_VERITY_MINIMAL"; then
715        install_verity_minimal
716    fi
717}
718
719setup_selinux() {
720    dinfo "Setup SELinux"
721    # don't forget KERNEL_APPEND='... selinux=1 ...'
722    if ! get_bool "$SETUP_SELINUX"; then
723        dinfo "SETUP_SELINUX != yes, skipping SELinux configuration"
724        return 0
725    fi
726
727    local conf_dir=/etc/selinux
728    local fixfiles_tools=(bash uname cat sort uniq awk grep egrep head expr find rm secon setfiles)
729
730    # Make sure the following statement can't expand to "/" to prevent
731    # a potential where-are-my-backups situation
732    rm -rf "${initdir:?}/$conf_dir"
733    if ! cp -ar "$conf_dir" "$initdir/$conf_dir"; then
734        dfatal "Failed to copy $conf_dir"
735        exit 1
736    fi
737
738    touch "$initdir/.autorelabel"
739    mkdir -p "$initdir/usr/lib/systemd/tests/testdata/units/basic.target.wants"
740    ln -sf ../autorelabel.service "$initdir/usr/lib/systemd/tests/testdata/units/basic.target.wants/"
741
742    image_install "${fixfiles_tools[@]}"
743    image_install fixfiles
744    image_install sestatus
745}
746
747install_valgrind() {
748    if ! type -p valgrind; then
749        dfatal "Failed to install valgrind"
750        exit 1
751    fi
752
753    local valgrind_bins valgrind_libs valgrind_dbg_and_supp
754
755    readarray -t valgrind_bins < <(strace -e execve valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if /^execve\("([^"]+)"/')
756    image_install "${valgrind_bins[@]}"
757
758    readarray -t valgrind_libs < <(LD_DEBUG=files valgrind /bin/true 2>&1 >/dev/null | perl -lne 'print $1 if m{calling init: (/.*vgpreload_.*)}')
759    image_install "${valgrind_libs[@]}"
760
761    readarray -t valgrind_dbg_and_supp < <(
762        strace -e open valgrind /bin/true 2>&1 >/dev/null |
763        perl -lne 'if (my ($fname) = /^open\("([^"]+).*= (?!-)\d+/) { print $fname if $fname =~ /debug|\.supp$/ }'
764    )
765    image_install "${valgrind_dbg_and_supp[@]}"
766}
767
768create_valgrind_wrapper() {
769    local valgrind_wrapper="$initdir/$ROOTLIBDIR/systemd-under-valgrind"
770    ddebug "Create $valgrind_wrapper"
771    cat >"$valgrind_wrapper" <<EOF
772#!/usr/bin/env bash
773
774mount -t proc proc /proc
775exec valgrind --leak-check=full --track-fds=yes --log-file=/valgrind.out $ROOTLIBDIR/systemd "\$@"
776EOF
777    chmod 0755 "$valgrind_wrapper"
778}
779
780create_asan_wrapper() {
781    local asan_wrapper="$initdir/$ROOTLIBDIR/systemd-under-asan"
782    dinfo "Create ASan wrapper as '$asan_wrapper'"
783
784    [[ -z "$ASAN_RT_PATH" ]] && dfatal "ASAN_RT_PATH is empty, but it shouldn't be"
785
786    # clang: install llvm-symbolizer to generate useful reports
787    # See: https://clang.llvm.org/docs/AddressSanitizer.html#symbolizing-the-reports
788    [[ "$ASAN_COMPILER" == "clang" ]] && image_install "llvm-symbolizer"
789
790    cat >"$asan_wrapper" <<EOF
791#!/usr/bin/env bash
792
793set -x
794
795echo "ASan RT: $ASAN_RT_PATH"
796if [[ ! -e "$ASAN_RT_PATH" ]]; then
797    echo >&2 "Couldn't find ASan RT at '$ASAN_RT_PATH', can't continue"
798    exit 1
799fi
800
801# Suppress certain leaks reported by LSan (either in external tools or bogus
802# ones)
803# Docs: # https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#suppressions
804#
805# - fsck is called by systemd-homed and is reporting a leak we're not interested
806#   in
807# - libLLVM is a "side effect" caused by the previous fsck leak
808cat >/systemd-lsan.supp <<INNER_EOF
809leak:/bin/fsck$
810leak:/sbin/fsck$
811leak:/lib/libLLVM
812INNER_EOF
813
814DEFAULT_LSAN_OPTIONS=${LSAN_OPTIONS:-}:suppressions=/systemd-lsan.supp
815DEFAULT_ASAN_OPTIONS=${ASAN_OPTIONS:-strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1}
816DEFAULT_UBSAN_OPTIONS=${UBSAN_OPTIONS:-print_stacktrace=1:print_summary=1:halt_on_error=1}
817DEFAULT_ENVIRONMENT="ASAN_OPTIONS=\$DEFAULT_ASAN_OPTIONS UBSAN_OPTIONS=\$DEFAULT_UBSAN_OPTIONS LSAN_OPTIONS=\$DEFAULT_LSAN_OPTIONS"
818
819# As right now bash is the PID 1, we can't expect PATH to have a sane value.
820# Let's make one to prevent unexpected "<bin> not found" issues in the future
821export PATH="/sbin:/bin:/usr/sbin:/usr/bin"
822
823mount -t proc proc /proc
824mount -t sysfs sysfs /sys
825mount -o remount,rw /
826
827# A lot of services (most notably dbus) won't start without preloading libasan
828# See https://github.com/systemd/systemd/issues/5004
829DEFAULT_ENVIRONMENT="\$DEFAULT_ENVIRONMENT LD_PRELOAD=$ASAN_RT_PATH"
830
831if [[ "$ASAN_COMPILER" == "clang" ]]; then
832  # Let's add the ASan DSO's path to the dynamic linker's cache. This is pretty
833  # unnecessary for gcc & libasan, however, for clang this is crucial, as its
834  # runtime ASan DSO is in a non-standard (library) path.
835  echo "${ASAN_RT_PATH%/*}" >/etc/ld.so.conf.d/asan-path-override.conf
836  ldconfig
837fi
838echo DefaultEnvironment=\$DEFAULT_ENVIRONMENT >>/etc/systemd/system.conf
839echo DefaultTimeoutStartSec=180s >>/etc/systemd/system.conf
840echo DefaultStandardOutput=journal+console >>/etc/systemd/system.conf
841
842# ASAN and syscall filters aren't compatible with each other.
843find / -name '*.service' -type f | xargs sed -i 's/^\\(MemoryDeny\\|SystemCall\\)/#\\1/'
844
845# The redirection of ASAN reports to a file prevents them from ending up in /dev/null.
846# But, apparently, sometimes it doesn't work: https://github.com/google/sanitizers/issues/886.
847JOURNALD_CONF_DIR=/etc/systemd/system/systemd-journald.service.d
848mkdir -p "\$JOURNALD_CONF_DIR"
849printf "[Service]\nEnvironment=ASAN_OPTIONS=\$DEFAULT_ASAN_OPTIONS:log_path=/systemd-journald.asan.log UBSAN_OPTIONS=\$DEFAULT_UBSAN_OPTIONS:log_path=/systemd-journald.ubsan.log\n" >"\$JOURNALD_CONF_DIR/env.conf"
850
851# Sometimes UBSan sends its reports to stderr regardless of what is specified in log_path
852# Let's try to catch them by redirecting stderr (and stdout just in case) to a file
853# See https://github.com/systemd/systemd/pull/12524#issuecomment-491108821
854printf "[Service]\nStandardOutput=file:/systemd-journald.out\n" >"\$JOURNALD_CONF_DIR/out.conf"
855
856# 90s isn't enough for some services to finish when literally everything is run
857# under ASan+UBSan in containers, which, in turn, are run in VMs.
858# Let's limit which environments such services should be executed in.
859mkdir -p /etc/systemd/system/systemd-hwdb-update.service.d
860printf "[Unit]\nConditionVirtualization=container\n\n[Service]\nTimeoutSec=240s\n" >/etc/systemd/system/systemd-hwdb-update.service.d/env-override.conf
861
862# Let's override another hard-coded timeout that kicks in too early
863mkdir -p /etc/systemd/system/systemd-journal-flush.service.d
864printf "[Service]\nTimeoutSec=180s\n" >/etc/systemd/system/systemd-journal-flush.service.d/timeout.conf
865
866# D-Bus has troubles during system shutdown causing it to fail. Although it's
867# harmless, it causes unnecessary noise in the logs, so let's disable LSan's
868# at_exit check just for the dbus.service
869mkdir -p /etc/systemd/system/dbus.service.d
870printf "[Service]\nEnvironment=ASAN_OPTIONS=leak_check_at_exit=false\n" >/etc/systemd/system/dbus.service.d/disable-lsan.conf
871
872# Some utilities run via IMPORT/RUN/PROGRAM udev directives fail because
873# they're uninstrumented (like dmsetup). Let's add a simple rule which sets
874# LD_PRELOAD to the ASan RT library to fix this.
875mkdir -p /etc/udev/rules.d
876cat >/etc/udev/rules.d/00-set-LD_PRELOAD.rules <<INNER_EOF
877SUBSYSTEM=="block", ENV{LD_PRELOAD}="$ASAN_RT_PATH"
878INNER_EOF
879chmod 0644 /etc/udev/rules.d/00-set-LD_PRELOAD.rules
880
881# The 'mount' utility doesn't behave well under libasan, causing unexpected
882# fails during boot and subsequent test results check:
883# bash-5.0# mount -o remount,rw -v /
884# mount: /dev/sda1 mounted on /.
885# bash-5.0# echo \$?
886# 1
887# Let's workaround this by clearing the previously set LD_PRELOAD env variable,
888# so the libasan library is not loaded for this particular service
889unset_ld_preload() {
890    local _dropin_dir="/etc/systemd/system/\$1.service.d"
891    mkdir -p "\$_dropin_dir"
892    printf "[Service]\nUnsetEnvironment=LD_PRELOAD\n" >"\$_dropin_dir/unset_ld_preload.conf"
893}
894
895unset_ld_preload systemd-remount-fs
896unset_ld_preload testsuite-
897
898export ASAN_OPTIONS=\$DEFAULT_ASAN_OPTIONS:log_path=/systemd.asan.log UBSAN_OPTIONS=\$DEFAULT_UBSAN_OPTIONS
899exec "$ROOTLIBDIR/systemd" "\$@"
900EOF
901
902    chmod 0755 "$asan_wrapper"
903}
904
905create_strace_wrapper() {
906    local strace_wrapper="$initdir/$ROOTLIBDIR/systemd-under-strace"
907    ddebug "Create $strace_wrapper"
908    cat >"$strace_wrapper" <<EOF
909#!/usr/bin/env bash
910
911exec strace -f -D -o /strace.out "$ROOTLIBDIR/systemd" "\$@"
912EOF
913    chmod 0755 "$strace_wrapper"
914}
915
916install_fs_tools() {
917    dinfo "Install fsck"
918    image_install /sbin/fsck*
919    image_install -o /bin/fsck*
920
921    # fskc.reiserfs calls reiserfsck. so, install it
922    image_install -o reiserfsck
923
924    # we use mkfs in system-repart tests
925    image_install /sbin/mkfs.ext4
926    image_install /sbin/mkfs.vfat
927}
928
929install_modules() {
930    dinfo "Install modules"
931
932    instmods loop
933    instmods vfat
934    instmods nls_ascii =nls
935    instmods dummy
936
937    if get_bool "$LOOKS_LIKE_SUSE"; then
938        instmods ext4
939    fi
940}
941
942install_dmevent() {
943    instmods dm_crypt =crypto
944    inst_binary dmeventd
945    image_install "${ROOTLIBDIR:?}"/system/dm-event.{service,socket}
946    if get_bool "$LOOKS_LIKE_DEBIAN"; then
947        # dmsetup installs 55-dm and 60-persistent-storage-dm on Debian/Ubuntu
948        # and since buster/bionic 95-dm-notify.rules
949        # see https://gitlab.com/debian-lvm/lvm2/blob/master/debian/patches/udev.patch
950        inst_rules 55-dm.rules 60-persistent-storage-dm.rules 95-dm-notify.rules
951    else
952        inst_rules 10-dm.rules 13-dm-disk.rules 95-dm-notify.rules
953    fi
954    if get_bool "$LOOKS_LIKE_SUSE"; then
955        inst_rules 60-persistent-storage.rules 61-persistent-storage-compat.rules 99-systemd.rules
956    fi
957}
958
959install_multipath() {
960    instmods "=md" multipath
961    image_install kpartx /lib/udev/kpartx_id lsmod mpathpersist multipath multipathd partx
962    image_install "${ROOTLIBDIR:?}"/system/multipathd.{service,socket}
963    if get_bool "$LOOKS_LIKE_DEBIAN"; then
964        inst_rules 56-dm-parts.rules 56-dm-mpath.rules 60-multipath.rules 68-del-part-nodes.rules 95-kpartx.rules
965    else
966        inst_rules 11-dm-mpath.rules 11-dm-parts.rules 62-multipath.rules 66-kpartx.rules 68-del-part-nodes.rules
967    fi
968    mkdir -p "${initdir:?}/etc/multipath"
969
970    local file
971    while read -r file; do
972        # Install libraries required by the given library
973        inst_libs "$file"
974        # Install the library itself and create necessary symlinks
975        inst_library "$file"
976    done < <(find /lib*/multipath -type f)
977
978    if get_bool "$LOOKS_LIKE_ARCH"; then
979        # On Arch the multipath libraries are not linked against libgcc_s.so.1,
980        # but it's still required at runtime
981        inst_library "/lib64/libgcc_s.so.1"
982    fi
983}
984
985install_lvm() {
986    image_install lvm
987    image_install "${ROOTLIBDIR:?}"/system/lvm2-lvmpolld.{service,socket}
988    image_install "${ROOTLIBDIR:?}"/system/{blk-availability,lvm2-monitor}.service
989    image_install -o "/lib/tmpfiles.d/lvm2.conf"
990    if get_bool "$LOOKS_LIKE_DEBIAN"; then
991        inst_rules 56-lvm.rules 69-lvm-metad.rules
992    else
993        # Support the new udev autoactivation introduced in lvm 2.03.14
994        # https://sourceware.org/git/?p=lvm2.git;a=commit;h=67722b312390cdab29c076c912e14bd739c5c0f6
995        # Static autoactivation (via lvm2-activation-generator) was dropped
996        # in lvm 2.03.15
997        # https://sourceware.org/git/?p=lvm2.git;a=commit;h=ee8fb0310c53ed003a43b324c99cdfd891dd1a7c
998        if [[ -f /lib/udev/rules.d/69-dm-lvm.rules ]]; then
999            inst_rules 11-dm-lvm.rules 69-dm-lvm.rules
1000        else
1001            image_install "${ROOTLIBDIR:?}"/system-generators/lvm2-activation-generator
1002            image_install "${ROOTLIBDIR:?}"/system/lvm2-pvscan@.service
1003            inst_rules 11-dm-lvm.rules 69-dm-lvm-metad.rules
1004        fi
1005    fi
1006    mkdir -p "${initdir:?}/etc/lvm"
1007}
1008
1009install_btrfs() {
1010    instmods btrfs
1011    # Not all utilities provided by btrfs-progs are listed here; extend the list
1012    # if necessary
1013    image_install btrfs btrfstune mkfs.btrfs
1014    inst_rules 64-btrfs-dm.rules
1015}
1016
1017install_iscsi() {
1018    # Install both client and server side stuff by default
1019    local inst="${1:-}"
1020    local file
1021
1022    # Install client-side stuff ("initiator" in iSCSI jargon) - Open-iSCSI in this case
1023    # (open-iscsi on Debian, iscsi-initiator-utils on Fedora, etc.)
1024    if [[ -z "$inst" || "$inst" =~ (client|initiator) ]]; then
1025        image_install iscsi-iname iscsiadm iscsid iscsistart
1026        image_install -o "${ROOTLIBDIR:?}"/system/iscsi-{init,onboot,shutdown}.service
1027        image_install "${ROOTLIBDIR:?}"/system/iscsid.{service,socket}
1028        image_install "${ROOTLIBDIR:?}"/system/iscsi.service
1029        mkdir -p "${initdir:?}"/var/lib/iscsi/{ifaces,isns,nodes,send_targets,slp,static}
1030        mkdir -p "${initdir:?}/etc/iscsi"
1031        echo "iscsid.startup = /bin/systemctl start iscsid.socket" >"${initdir:?}/etc/iscsi/iscsid.conf"
1032        inst_simple "/etc/iscsi/initiatorname.iscsi"
1033    fi
1034
1035    # Install server-side stuff ("target" in iSCSI jargon) - TGT in this case
1036    # (tgt on Debian, scsi-target-utils on Fedora, etc.)
1037    if [[ -z "$inst" || "$inst" =~ (server|target) ]]; then
1038        image_install tgt-admin tgt-setup-lun tgtadm tgtd tgtimg
1039        image_install -o /etc/sysconfig/tgtd
1040        image_install "${ROOTLIBDIR:?}"/system/tgtd.service
1041        mkdir -p "${initdir:?}/etc/tgt"
1042        touch "${initdir:?}"/etc/tgt/{tgtd,targets}.conf
1043        # Install perl modules required by tgt-admin
1044        #
1045        # Forgive me father for I have sinned. The monstrosity below appends
1046        # a perl snippet to the `tgt-admin` perl script on the fly, which
1047        # dumps a list of files (perl modules) required by `tgt-admin` at
1048        # the runtime plus any DSOs loaded via DynaLoader. This list is then
1049        # passed to `inst_simple` which installs the necessary files into the image
1050        #
1051        # shellcheck disable=SC2016
1052        while read -r file; do
1053            inst_simple "$file"
1054        done < <(perl -- <(cat "$(command -v tgt-admin)" <(echo -e 'use DynaLoader; print map { "$_\n" } values %INC; print join("\n", @DynaLoader::dl_shared_objects)')) -p | awk '/^\// { print $1 }')
1055    fi
1056}
1057
1058install_mdadm() {
1059    local unit
1060    local mdadm_units=(
1061        system/mdadm-grow-continue@.service
1062        system/mdadm-last-resort@.service
1063        system/mdadm-last-resort@.timer
1064        system/mdmon@.service
1065        system/mdmonitor-oneshot.service
1066        system/mdmonitor-oneshot.timer
1067        system/mdmonitor.service
1068        system-shutdown/mdadm.shutdown
1069    )
1070
1071    image_install mdadm mdmon
1072    inst_rules 01-md-raid-creating.rules 63-md-raid-arrays.rules 64-md-raid-assembly.rules 69-md-clustered-confirm-device.rules
1073    # Fedora/CentOS/RHEL ships this rule file
1074    [[ -f /lib/udev/rules.d/65-md-incremental.rules ]] && inst_rules 65-md-incremental.rules
1075
1076    for unit in "${mdadm_units[@]}"; do
1077        image_install "${ROOTLIBDIR:?}/$unit"
1078    done
1079}
1080
1081install_compiled_systemd() {
1082    dinfo "Install compiled systemd"
1083
1084    local ninja_bin
1085    ninja_bin="$(type -P ninja || type -P ninja-build)"
1086    if [[ -z "$ninja_bin" ]]; then
1087        dfatal "ninja was not found"
1088        exit 1
1089    fi
1090    (set -x; DESTDIR="$initdir" "$ninja_bin" -C "$BUILD_DIR" install)
1091
1092    # If we are doing coverage runs, copy over the binary notes files, as lcov expects to
1093    # find them in the same directory as the runtime data counts
1094    if get_bool "$IS_BUILT_WITH_COVERAGE"; then
1095        mkdir -p "${initdir}/${BUILD_DIR:?}/"
1096        rsync -am --include='*/' --include='*.gcno' --exclude='*' "${BUILD_DIR:?}/" "${initdir}/${BUILD_DIR:?}/"
1097        # Set effective & default ACLs for the build dir so unprivileged
1098        # processes can write gcda files with coverage stats
1099        setfacl -R -m 'd:o:rwX' -m 'o:rwX' "${initdir}/${BUILD_DIR:?}/"
1100    fi
1101}
1102
1103install_debian_systemd() {
1104    dinfo "Install debian systemd"
1105
1106    local files
1107
1108    while read -r deb; do
1109        files="$(dpkg-query -L "$deb" 2>/dev/null)" || continue
1110        ddebug "Install debian files from package $deb"
1111        for file in $files; do
1112            [ -e "$file" ] || continue
1113            [ -d "$file" ] && continue
1114            inst "$file"
1115        done
1116    done < <(grep -E '^Package:' "${SOURCE_DIR}/debian/control" | cut -d ':' -f 2)
1117}
1118
1119install_suse_systemd() {
1120    local testsdir=/usr/lib/systemd/tests
1121    local pkgs
1122
1123    dinfo "Install SUSE systemd"
1124
1125    pkgs=(
1126        systemd
1127        systemd-container
1128        systemd-coredump
1129        systemd-experimental
1130        systemd-journal-remote
1131        systemd-portable
1132        udev
1133    )
1134
1135    for p in "${pkgs[@]}"; do
1136        rpm -q "$p" &>/dev/null || continue
1137
1138        ddebug "Install files from package $p"
1139        while read -r f; do
1140            [ -e "$f" ] || continue
1141            [ -d "$f" ] && continue
1142            inst "$f"
1143        done < <(rpm -ql "$p")
1144    done
1145
1146    # we only need testsdata dir as well as the unit tests (for
1147    # TEST-02-UNITTESTS) in the image.
1148    dinfo "Install unit tests and testdata directory"
1149
1150    mkdir -p "$initdir/$testsdir"
1151    cp "$testsdir"/test-* "$initdir/$testsdir/"
1152    cp -a "$testsdir/testdata" "$initdir/$testsdir/"
1153
1154    # On openSUSE, these dirs are not created at package install for now on.
1155    mkdir -p "$initdir/var/log/journal/remote"
1156}
1157
1158install_distro_systemd() {
1159    dinfo "Install distro systemd"
1160
1161    if get_bool "$LOOKS_LIKE_DEBIAN"; then
1162        install_debian_systemd
1163    elif get_bool "$LOOKS_LIKE_SUSE"; then
1164        install_suse_systemd
1165    else
1166        dfatal "NO_BUILD not supported for this distro"
1167        exit 1
1168    fi
1169}
1170
1171install_systemd() {
1172    dinfo "Install systemd"
1173    if get_bool "$NO_BUILD"; then
1174        install_distro_systemd
1175    else
1176        install_compiled_systemd
1177    fi
1178
1179    # remove unneeded documentation
1180    rm -fr "${initdir:?}"/usr/share/{man,doc}
1181
1182    # enable debug logging in PID1
1183    echo LogLevel=debug >>"$initdir/etc/systemd/system.conf"
1184    if [[ -n "$TEST_SYSTEMD_LOG_LEVEL" ]]; then
1185        echo DefaultEnvironment=SYSTEMD_LOG_LEVEL="$TEST_SYSTEMD_LOG_LEVEL" >>"$initdir/etc/systemd/system.conf"
1186    fi
1187    # store coredumps in journal
1188    echo Storage=journal >>"$initdir/etc/systemd/coredump.conf"
1189    # Propagate SYSTEMD_UNIT_PATH to user systemd managers
1190    mkdir "$initdir/etc/systemd/system/user@.service.d/"
1191    echo -e "[Service]\nPassEnvironment=SYSTEMD_UNIT_PATH\n" >"$initdir/etc/systemd/system/user@.service.d/override.conf"
1192
1193    # When built with gcov, disable ProtectSystem= and ProtectHome= in the test
1194    # images, since it prevents gcov to write the coverage reports (*.gcda
1195    # files)
1196    if get_bool "$IS_BUILT_WITH_COVERAGE"; then
1197        mkdir -p "$initdir/etc/systemd/system/service.d/"
1198        echo -e "[Service]\nProtectSystem=no\nProtectHome=no\n" >"$initdir/etc/systemd/system/service.d/99-gcov-override.conf"
1199        # Similarly, set ReadWritePaths= to the $BUILD_DIR in the test image
1200        # to make the coverage work with units utilizing DynamicUser=yes. Do
1201        # this only for services from TEST-20, as setting this system-wide
1202        # has many undesirable side-effects
1203        mkdir -p "$initdir/etc/systemd/system/test20-.service.d/"
1204        echo -e "[Service]\nReadWritePaths=${BUILD_DIR:?}\n" >"$initdir/etc/systemd/system/test20-.service.d/99-gcov-rwpaths-override.conf"
1205    fi
1206
1207    # If we're built with -Dportabled=false, tests with systemd-analyze
1208    # --profile will fail. Since we need just the profile (text) files, let's
1209    # copy them into the image if they don't exist there.
1210    local portable_dir="${initdir:?}${ROOTLIBDIR:?}/portable"
1211    if [[ ! -d "$portable_dir/profile/strict" ]]; then
1212        dinfo "Couldn't find portable profiles in the test image"
1213        dinfo "Copying them directly from the source tree"
1214        mkdir -p "$portable_dir"
1215        cp -frv "${SOURCE_DIR:?}/src/portable/profile" "$portable_dir"
1216    fi
1217}
1218
1219get_ldpath() {
1220    local rpath
1221    rpath="$(objdump -p "${1:?}" 2>/dev/null | awk "/R(UN)?PATH/ { print \"$initdir\" \$2 }" | paste -sd :)"
1222
1223    if [ -z "$rpath" ] ; then
1224        echo "$BUILD_DIR"
1225    else
1226        echo "$rpath"
1227    fi
1228}
1229
1230install_missing_libraries() {
1231    dinfo "Install missing libraries"
1232    # install possible missing libraries
1233    for i in "${initdir:?}"{,/usr}/{sbin,bin}/* "$initdir"{,/usr}/lib/systemd/{,tests/{,manual/,unsafe/}}*; do
1234        LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(get_ldpath "$i")" inst_libs "$i"
1235    done
1236
1237    local lib path
1238    # A number of dependencies is now optional via dlopen, so the install
1239    # script will not pick them up, since it looks at linkage.
1240    for lib in libcryptsetup libidn libidn2 pwquality libqrencode tss2-esys tss2-rc tss2-mu tss2-tcti-device libfido2 libbpf libelf libdw; do
1241        ddebug "Searching for $lib via pkg-config"
1242        if pkg-config --exists "$lib"; then
1243                path="$(pkg-config --variable=libdir "$lib")"
1244                if [ -z "${path}" ]; then
1245                    ddebug "$lib.pc does not contain a libdir variable, skipping"
1246                    continue
1247                fi
1248
1249                if ! [[ ${lib} =~ ^lib ]]; then
1250                        lib="lib${lib}"
1251                fi
1252                # Some pkg-config files are broken and give out the wrong paths
1253                # (eg: libcryptsetup), so just ignore them
1254                inst_libs "${path}/${lib}.so" || true
1255                inst_library "${path}/${lib}.so" || true
1256        else
1257            ddebug "$lib.pc not found, skipping"
1258            continue
1259        fi
1260    done
1261}
1262
1263cleanup_loopdev() {
1264    if [ -n "${LOOPDEV:=}" ]; then
1265        ddebug "losetup -d $LOOPDEV"
1266        losetup -d "${LOOPDEV}"
1267        unset LOOPDEV
1268    fi
1269}
1270
1271trap cleanup_loopdev EXIT INT QUIT PIPE
1272
1273create_empty_image() {
1274    if [ -z "${IMAGE_NAME:=}" ]; then
1275        echo "create_empty_image: \$IMAGE_NAME not set"
1276        exit 1
1277    fi
1278
1279    local size=500
1280    if ! get_bool "$NO_BUILD"; then
1281        if meson configure "${BUILD_DIR:?}" | grep 'static-lib\|standalone-binaries' | awk '{ print $2 }' | grep -q 'true'; then
1282            size=$((size+=200))
1283        fi
1284        if meson configure "${BUILD_DIR:?}" | grep 'link-.*-shared' | awk '{ print $2 }' | grep -q 'false'; then
1285            size=$((size+=200))
1286        fi
1287        if get_bool "$IS_BUILT_WITH_COVERAGE"; then
1288            size=$((size+=250))
1289        fi
1290    fi
1291    if ! get_bool "$STRIP_BINARIES"; then
1292        size=$((4 * size))
1293    fi
1294
1295    echo "Setting up ${IMAGE_PUBLIC:?} (${size} MB)"
1296    rm -f "${IMAGE_PRIVATE:?}" "$IMAGE_PUBLIC"
1297
1298    # Create the blank file to use as a root filesystem
1299    truncate -s "${size}M" "$IMAGE_PUBLIC"
1300
1301    LOOPDEV=$(losetup --show -P -f "$IMAGE_PUBLIC")
1302    [ -b "$LOOPDEV" ] || return 1
1303    sfdisk "$LOOPDEV" <<EOF
1304,$((size - 50))M,L,*
1305,
1306EOF
1307
1308    udevadm settle
1309
1310    local label=(-L systemd_boot)
1311    # mkfs.reiserfs doesn't know -L. so, use --label instead
1312    [[ "$FSTYPE" == "reiserfs" ]] && label=(--label systemd_boot)
1313    if ! mkfs -t "${FSTYPE}" "${label[@]}" "${LOOPDEV}p1" -q; then
1314        dfatal "Failed to mkfs -t ${FSTYPE}"
1315        exit 1
1316    fi
1317}
1318
1319mount_initdir() {
1320    if [ -z "${LOOPDEV:=}" ]; then
1321        [ -e "${IMAGE_PRIVATE:?}" ] && image="$IMAGE_PRIVATE" || image="${IMAGE_PUBLIC:?}"
1322        LOOPDEV="$(losetup --show -P -f "$image")"
1323        [ -b "$LOOPDEV" ] || return 1
1324
1325        udevadm settle
1326    fi
1327
1328    if ! mountpoint -q "${initdir:?}"; then
1329        mkdir -p "$initdir"
1330        mount "${LOOPDEV}p1" "$initdir"
1331        TEST_SETUP_CLEANUP_ROOTDIR=1
1332    fi
1333}
1334
1335cleanup_initdir() {
1336    # only umount if create_empty_image_rootdir() was called to mount it
1337    get_bool "$TEST_SETUP_CLEANUP_ROOTDIR" && _umount_dir "${initdir:?}"
1338}
1339
1340umount_loopback() {
1341    # unmount the loopback device from all places. Otherwise we risk file
1342    # system corruption.
1343    for device in $(losetup -l | awk '$6=="'"${IMAGE_PUBLIC:?}"'" {print $1}'); do
1344        ddebug "Unmounting all uses of $device"
1345        mount | awk '/^'"${device}"'p/{print $1}' | xargs --no-run-if-empty umount -v
1346    done
1347}
1348
1349create_empty_image_rootdir() {
1350    create_empty_image
1351    mount_initdir
1352}
1353
1354check_asan_reports() {
1355    local ret=0
1356    local root="${1:?}"
1357
1358    if get_bool "$IS_BUILT_WITH_ASAN"; then
1359        ls -l "$root"
1360        if [[ -e "$root/systemd.asan.log.1" ]]; then
1361            cat "$root/systemd.asan.log.1"
1362            ret=$((ret+1))
1363        fi
1364
1365        journald_report="$(find "$root" -name "systemd-journald.*san.log*" -exec cat {} \;)"
1366        if [[ -n "$journald_report" ]]; then
1367            printf "%s\n" "$journald_report"
1368            cat "$root/systemd-journald.out" || :
1369            ret=$((ret+1))
1370        fi
1371
1372        pids="$(
1373            "$JOURNALCTL" -D "$root/var/log/journal" | perl -alne '
1374                 BEGIN {
1375                     %services_to_ignore = (
1376                         "dbus-daemon" => undef,
1377                         "dbus-broker-launch" => undef,
1378                     );
1379                 }
1380                 print $2 if /\s(\S*)\[(\d+)\]:\s*SUMMARY:\s+\w+Sanitizer/ && !exists $services_to_ignore{$1}'
1381        )"
1382        if [[ -n "$pids" ]]; then
1383            ret=$((ret+1))
1384            for pid in $pids; do
1385                "$JOURNALCTL" -D "$root/var/log/journal" _PID="$pid" --no-pager
1386            done
1387        fi
1388    fi
1389
1390    return $ret
1391}
1392
1393check_coverage_reports() {
1394    local root="${1:?}"
1395
1396    if get_bool "$NO_BUILD"; then
1397        return 0
1398    fi
1399    if ! get_bool "$IS_BUILT_WITH_COVERAGE"; then
1400        return 0
1401    fi
1402
1403    if [ -n "${ARTIFACT_DIRECTORY}" ]; then
1404        dest="${ARTIFACT_DIRECTORY}/${testname:?}.coverage-info"
1405    else
1406        dest="${TESTDIR:?}/coverage-info"
1407    fi
1408
1409    # Create a coverage report that will later be uploaded. Remove info about
1410    # system libraries/headers, as we don't really care about them.
1411    if [[ -f "$dest" ]]; then
1412        # If the destination report file already exists, don't overwrite it, but
1413        # dump the new report in a temporary file and then merge it with the already
1414        # present one - this usually happens when running both "parts" of a test
1415        # in one run (the qemu and the nspawn part).
1416        lcov --directory "${root}/${BUILD_DIR:?}" --capture --output-file "${dest}.new"
1417        lcov --remove "${dest}.new" -o "${dest}.new" '/usr/include/*' '/usr/lib/*'
1418        lcov --add-tracefile "${dest}" --add-tracefile "${dest}.new" -o "${dest}"
1419        rm -f "${dest}.new"
1420    else
1421        lcov --directory "${root}/${BUILD_DIR:?}" --capture --output-file "${dest}"
1422        lcov --remove "${dest}" -o "${dest}" '/usr/include/*' '/usr/lib/*'
1423    fi
1424
1425    # If the test logs contain lines like:
1426    #
1427    # ...systemd-resolved[735885]: profiling:/systemd-meson-build/src/shared/libsystemd-shared-250.a.p/base-filesystem.c.gcda:Cannot open
1428    #
1429    # it means we're possibly missing some coverage since gcov can't write the stats,
1430    # usually due to the sandbox being too restrictive (e.g. ProtectSystem=yes,
1431    # ProtectHome=yes) or the $BUILD_DIR being inaccessible to non-root users - see
1432    # `setfacl` stuff in install_compiled_systemd().
1433    if ! get_bool "${IGNORE_MISSING_COVERAGE:=}" && \
1434       "${JOURNALCTL:?}" -q --no-pager -D "${root:?}/var/log/journal" --grep "profiling:.+?gcda:[Cc]annot open"; then
1435        derror "Detected possibly missing coverage, check the journal"
1436        return 1
1437    fi
1438
1439    return 0
1440}
1441
1442save_journal() {
1443    # Default to always saving journal
1444    local save="yes"
1445
1446    if [ "${TEST_SAVE_JOURNAL}" = "no" ]; then
1447        save="no"
1448    elif [ "${TEST_SAVE_JOURNAL}" = "fail" ] && [ "$2" = "0" ]; then
1449        save="no"
1450    fi
1451
1452    if [ -n "${ARTIFACT_DIRECTORY}" ]; then
1453        dest="${ARTIFACT_DIRECTORY}/${testname:?}.journal"
1454    else
1455        dest="${TESTDIR:?}/system.journal"
1456    fi
1457
1458    for j in "${1:?}"/*; do
1459        if get_bool "$save"; then
1460            if [ "$SYSTEMD_JOURNAL_REMOTE" = "" ]; then
1461                cp -a "$j" "$dest"
1462            else
1463                "$SYSTEMD_JOURNAL_REMOTE" -o "$dest" --getter="$JOURNALCTL -o export -D $j"
1464            fi
1465        fi
1466
1467        if [ -n "${TEST_SHOW_JOURNAL}" ]; then
1468            echo "---- $j ----"
1469            "$JOURNALCTL" --no-pager -o short-monotonic --no-hostname --priority="${TEST_SHOW_JOURNAL}" -D "$j"
1470        fi
1471
1472        rm -r "$j"
1473    done
1474
1475    if ! get_bool "$save"; then
1476        return 0
1477    fi
1478
1479    if [ -n "${SUDO_USER}" ]; then
1480        setfacl -m "user:${SUDO_USER:?}:r-X" "$dest"*
1481    fi
1482
1483    # we want to print this sometime later, so save this in a variable
1484    JOURNAL_LIST="$(ls -l "$dest"*)"
1485}
1486
1487check_result_common() {
1488    local workspace="${1:?}"
1489    local ret
1490
1491    if [ -s "$workspace/failed" ]; then
1492        # Non-empty …/failed has highest priority
1493        cp -a "$workspace/failed" "${TESTDIR:?}/"
1494        if [ -n "${SUDO_USER}" ]; then
1495            setfacl -m "user:${SUDO_USER:?}:r-X" "${TESTDIR:?}/"failed
1496        fi
1497        ret=1
1498    elif get_bool "$TIMED_OUT"; then
1499        echo "(timeout)" >"${TESTDIR:?}/failed"
1500        ret=2
1501    elif [ -e "$workspace/testok" ]; then
1502        # …/testok always counts (but with lower priority than …/failed)
1503        ret=0
1504    elif [ -e "$workspace/skipped" ]; then
1505        # …/skipped always counts (a message is expected)
1506        echo "${TESTNAME:?} was skipped:"
1507        cat "$workspace/skipped"
1508        ret=0
1509    else
1510        echo "(failed; see logs)" >"${TESTDIR:?}/failed"
1511        ret=3
1512    fi
1513
1514    check_asan_reports "$workspace" || ret=4
1515
1516    check_coverage_reports "$workspace" || ret=5
1517
1518    save_journal "$workspace/var/log/journal" $ret
1519
1520    if [ -d "${ARTIFACT_DIRECTORY}" ] && [ -f "$workspace/strace.out" ]; then
1521        cp "$workspace/strace.out" "${ARTIFACT_DIRECTORY}/"
1522    fi
1523
1524    if [ ${ret:?} != 0 ] && [ -f "$TESTDIR/failed" ]; then
1525        echo -n "${TESTNAME:?}: "
1526        cat "$TESTDIR/failed"
1527    fi
1528    echo "${JOURNAL_LIST:-"No journals were saved"}"
1529
1530    return ${ret:?}
1531}
1532
1533check_result_nspawn() {
1534    local workspace="${1:?}"
1535    local ret
1536
1537    check_result_common "${workspace}"
1538    ret=$?
1539
1540    # Run additional test-specific checks if defined by check_result_nspawn_hook()
1541    if declare -F check_result_nspawn_hook >/dev/null; then
1542        if ! check_result_nspawn_hook; then
1543            derror "check_result_nspawn_hook() returned with EC > 0"
1544            ret=4
1545        fi
1546    fi
1547
1548    _umount_dir "${initdir:?}"
1549
1550    return $ret
1551}
1552
1553# can be overridden in specific test
1554check_result_qemu() {
1555    local ret
1556    mount_initdir
1557
1558    check_result_common "${initdir:?}"
1559    ret=$?
1560
1561    _umount_dir "${initdir:?}"
1562
1563    # Run additional test-specific checks if defined by check_result_qemu_hook()
1564    if declare -F check_result_qemu_hook >/dev/null; then
1565        if ! check_result_qemu_hook; then
1566            derror "check_result_qemu_hook() returned with EC > 0"
1567            ret=4
1568        fi
1569    fi
1570
1571    return $ret
1572}
1573
1574check_result_nspawn_unittests() {
1575    local workspace="${1:?}"
1576    local ret=1
1577
1578    [[ -e "$workspace/testok" ]] && ret=0
1579
1580    if [[ -s "$workspace/failed" ]]; then
1581        ret=$((ret + 1))
1582        echo "=== Failed test log ==="
1583        cat "$workspace/failed"
1584    else
1585        if [[ -s "$workspace/skipped" ]]; then
1586            echo "=== Skipped test log =="
1587            cat "$workspace/skipped"
1588            # We might have only skipped tests - that should not fail the job
1589            ret=0
1590        fi
1591        if [[ -s "$workspace/testok" ]]; then
1592            echo "=== Passed tests ==="
1593            cat "$workspace/testok"
1594        fi
1595    fi
1596
1597    get_bool "${TIMED_OUT:=}" && ret=1
1598    check_coverage_reports "$workspace" || ret=5
1599
1600    save_journal "$workspace/var/log/journal" $ret
1601
1602    _umount_dir "${initdir:?}"
1603
1604    return $ret
1605}
1606
1607check_result_qemu_unittests() {
1608    local ret=1
1609
1610    mount_initdir
1611    [[ -e "${initdir:?}/testok" ]] && ret=0
1612
1613    if [[ -s "$initdir/failed" ]]; then
1614        ret=$((ret + 1))
1615        echo "=== Failed test log ==="
1616        cat "$initdir/failed"
1617    else
1618        if [[ -s "$initdir/skipped" ]]; then
1619            echo "=== Skipped test log =="
1620            cat "$initdir/skipped"
1621            # We might have only skipped tests - that should not fail the job
1622            ret=0
1623        fi
1624        if [[ -s "$initdir/testok" ]]; then
1625            echo "=== Passed tests ==="
1626            cat "$initdir/testok"
1627        fi
1628    fi
1629
1630    get_bool "${TIMED_OUT:=}" && ret=1
1631    check_coverage_reports "$initdir" || ret=5
1632
1633    save_journal "$initdir/var/log/journal" $ret
1634
1635    _umount_dir "$initdir"
1636
1637    return $ret
1638}
1639
1640strip_binaries() {
1641    dinfo "Strip binaries"
1642    if ! get_bool "$STRIP_BINARIES"; then
1643        dinfo "STRIP_BINARIES == no, keeping binaries unstripped"
1644        return 0
1645    fi
1646    while read -r bin; do
1647        strip --strip-unneeded "$bin" |& grep -vi 'file format not recognized' | ddebug || :
1648    done < <(find "${initdir:?}" -executable -not -path '*/lib/modules/*.ko' -type f)
1649}
1650
1651create_rc_local() {
1652    dinfo "Create rc.local"
1653    mkdir -p "${initdir:?}/etc/rc.d"
1654    cat >"$initdir/etc/rc.d/rc.local" <<EOF
1655#!/usr/bin/env bash
1656exit 0
1657EOF
1658    chmod 0755 "$initdir/etc/rc.d/rc.local"
1659}
1660
1661install_execs() {
1662    ddebug "Install executables from the service files"
1663
1664    local pkg_config_path="${BUILD_DIR:?}/src/core/"
1665    local systemunitdir userunitdir exe
1666    systemunitdir="$(PKG_CONFIG_PATH="$pkg_config_path" pkg-config --variable=systemdsystemunitdir systemd)"
1667    userunitdir="$(PKG_CONFIG_PATH="$pkg_config_path" pkg-config --variable=systemduserunitdir systemd)"
1668    while read -r exe; do
1669        # some {rc,halt}.local scripts and programs are okay to not exist, the rest should
1670        # also, plymouth is pulled in by rescue.service, but even there the exit code
1671        # is ignored; as it's not present on some distros, don't fail if it doesn't exist
1672        dinfo "Attempting to install $exe (based on unit file reference)"
1673        inst "$exe" || [ "${exe%.local}" != "$exe" ] || [ "${exe%systemd-update-done}" != "$exe" ] || [ "${exe##*/}" == "plymouth" ]
1674    done < <(sed -r -n 's|^Exec[a-zA-Z]*=[@+!-]*([^ ]+).*|\1|gp' "${initdir:?}"/{"$systemunitdir","$userunitdir"}/*.service | sort -u)
1675}
1676
1677generate_module_dependencies() {
1678    dinfo "Generate modules dependencies"
1679    if [[ -d "${initdir:?}/lib/modules/${KERNEL_VER:?}" ]] && \
1680        ! depmod -a -b "$initdir" "$KERNEL_VER"; then
1681            dfatal "\"depmod -a $KERNEL_VER\" failed."
1682            exit 1
1683    fi
1684}
1685
1686install_depmod_files() {
1687    dinfo "Install depmod files"
1688    inst "/lib/modules/${KERNEL_VER:?}/modules.order"
1689    inst "/lib/modules/$KERNEL_VER/modules.builtin"
1690}
1691
1692install_plymouth() {
1693    dinfo "Install plymouth"
1694    # install plymouth, if found... else remove plymouth service files
1695    # if [ -x /usr/libexec/plymouth/plymouth-populate-initrd ]; then
1696    #     PLYMOUTH_POPULATE_SOURCE_FUNCTIONS="$TEST_BASE_DIR/test-functions" \
1697    #         /usr/libexec/plymouth/plymouth-populate-initrd -t $initdir
1698    #         image_install plymouth plymouthd
1699    # else
1700        rm -f "${initdir:?}"/{usr/lib,lib,etc}/systemd/system/plymouth* "$initdir"/{usr/lib,lib,etc}/systemd/system/*/plymouth*
1701    # fi
1702}
1703
1704install_haveged() {
1705    # If haveged is installed, it's probably included in initrd and needs to be
1706    # installed in the image too.
1707    if [ -x /usr/sbin/haveged ]; then
1708        dinfo "Install haveged files"
1709        inst /usr/sbin/haveged
1710        for u in /usr/lib/systemd/system/haveged*; do
1711            inst "$u"
1712        done
1713    fi
1714}
1715
1716install_ld_so_conf() {
1717    dinfo "Install /etc/ld.so.conf*"
1718    cp -a /etc/ld.so.conf* "${initdir:?}/etc"
1719    ldconfig -r "$initdir"
1720}
1721
1722install_testuser() {
1723    dinfo "Set up a test user"
1724    # create unprivileged user for user manager tests
1725    mkdir -p "${initdir:?}/etc/sysusers.d"
1726    cat >"$initdir/etc/sysusers.d/testuser.conf" <<EOF
1727u testuser    4711     "Test User" /home/testuser
1728EOF
1729
1730    mkdir -p "$initdir/home/testuser"
1731    chmod 0700 "$initdir/home/testuser"
1732    chown 4711:4711 "$initdir/home/testuser"
1733}
1734
1735install_config_files() {
1736    dinfo "Install config files"
1737    inst /etc/sysconfig/init || :
1738    inst /etc/passwd
1739    inst /etc/shadow
1740    inst_any /etc/login.defs /usr/etc/login.defs
1741    inst /etc/group
1742    inst /etc/shells
1743    inst_any /etc/nsswitch.conf /usr/etc/nsswitch.conf
1744    inst /etc/pam.conf || :
1745    inst_any /etc/os-release /usr/lib/os-release
1746    inst /etc/localtime
1747    # we want an empty environment
1748    : >"${initdir:?}/etc/environment"
1749    : >"$initdir/etc/machine-id"
1750    : >"$initdir/etc/resolv.conf"
1751
1752    # set the hostname
1753    echo 'H' >"$initdir/etc/hostname"
1754
1755    # let's set up just one image with the traditional verbose output
1756    if [ "${IMAGE_NAME:?}" != "basic" ]; then
1757        mkdir -p "$initdir/etc/systemd/system.conf.d"
1758        echo -e '[Manager]\nStatusUnitFormat=name' >"$initdir/etc/systemd/system.conf.d/status.conf"
1759    fi
1760}
1761
1762install_basic_tools() {
1763    dinfo "Install basic tools"
1764    image_install "${BASICTOOLS[@]}"
1765    image_install -o sushell
1766    # in Debian ldconfig is just a shell script wrapper around ldconfig.real
1767    image_install -o ldconfig.real
1768}
1769
1770install_debug_tools() {
1771    dinfo "Install debug tools"
1772    image_install -o "${DEBUGTOOLS[@]}"
1773
1774    if get_bool "$INTERACTIVE_DEBUG"; then
1775        # Set default TERM from vt220 to linux, so at least basic key shortcuts work
1776        local getty_override="${initdir:?}/etc/systemd/system/serial-getty@.service.d"
1777        mkdir -p "$getty_override"
1778        echo -e "[Service]\nEnvironment=TERM=linux" >"$getty_override/default-TERM.conf"
1779
1780        cat >"$initdir/etc/motd" <<EOF
1781To adjust the terminal size use:
1782    export COLUMNS=xx
1783    export LINES=yy
1784or
1785    stty cols xx rows yy
1786EOF
1787    fi
1788}
1789
1790install_libnss() {
1791    dinfo "Install libnss"
1792    # install libnss_files for login
1793    local NSS_LIBS
1794    mapfile -t NSS_LIBS < <(LD_DEBUG=files getent passwd 2>&1 >/dev/null | sed -n '/calling init: .*libnss_/ {s!^.* /!/!; p}')
1795    image_install "${NSS_LIBS[@]}"
1796}
1797
1798install_dbus() {
1799    dinfo "Install dbus"
1800    inst "${ROOTLIBDIR:?}/system/dbus.socket"
1801
1802    # Newer Fedora versions use dbus-broker by default. Let's install it if it's available.
1803    if [ -f "$ROOTLIBDIR/system/dbus-broker.service" ]; then
1804        inst "$ROOTLIBDIR/system/dbus-broker.service"
1805        inst_symlink /etc/systemd/system/dbus.service
1806        inst /usr/bin/dbus-broker
1807        inst /usr/bin/dbus-broker-launch
1808    elif [ -f "$ROOTLIBDIR/system/dbus-daemon.service" ]; then
1809        # Fedora rawhide replaced dbus.service with dbus-daemon.service
1810        inst "$ROOTLIBDIR/system/dbus-daemon.service"
1811        # Alias symlink
1812        inst_symlink /etc/systemd/system/dbus.service
1813    else
1814        inst "$ROOTLIBDIR/system/dbus.service"
1815    fi
1816
1817    while read -r file; do
1818        inst "$file"
1819    done < <(find /etc/dbus-1 /usr/share/dbus-1 -xtype f 2>/dev/null)
1820
1821    # setup policy for Type=dbus test
1822    mkdir -p "${initdir:?}/etc/dbus-1/system.d"
1823    cat >"$initdir/etc/dbus-1/system.d/systemd.test.ExecStopPost.conf" <<EOF
1824<?xml version="1.0"?>
1825<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
1826        "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
1827<busconfig>
1828    <policy user="root">
1829        <allow own="systemd.test.ExecStopPost"/>
1830    </policy>
1831</busconfig>
1832EOF
1833}
1834
1835install_user_dbus() {
1836    dinfo "Install user dbus"
1837    local userunitdir
1838    if ! userunitdir="$(pkg-config --variable=systemduserunitdir systemd)"; then
1839        dwarn "WARNING! Cannot determine userunitdir from pkg-config, assuming /usr/lib/systemd/user"
1840        userunitdir=/usr/lib/systemd/user
1841    fi
1842
1843    inst "$userunitdir/dbus.socket"
1844    inst_symlink "$userunitdir/sockets.target.wants/dbus.socket" || inst_symlink /etc/systemd/user/sockets.target.wants/dbus.socket
1845
1846    # Append the After= dependency on dbus in case it isn't already set up
1847    mkdir -p "${initdir:?}/etc/systemd/system/user@.service.d/"
1848    cat >"$initdir/etc/systemd/system/user@.service.d/dbus.conf" <<EOF
1849[Unit]
1850After=dbus.service
1851EOF
1852
1853    # Newer Fedora versions use dbus-broker by default. Let's install it if it's available.
1854    if [ -f "$userunitdir/dbus-broker.service" ]; then
1855        inst "$userunitdir/dbus-broker.service"
1856        inst_symlink /etc/systemd/user/dbus.service
1857    elif [ -f "${ROOTLIBDIR:?}/system/dbus-daemon.service" ]; then
1858        # Fedora rawhide replaced dbus.service with dbus-daemon.service
1859        inst "$userunitdir/dbus-daemon.service"
1860        # Alias symlink
1861        inst_symlink /etc/systemd/user/dbus.service
1862    else
1863        inst "$userunitdir/dbus.service"
1864    fi
1865}
1866
1867install_pam() {
1868    dinfo "Install PAM"
1869    local paths=()
1870
1871    if get_bool "$LOOKS_LIKE_DEBIAN" && type -p dpkg-architecture &>/dev/null; then
1872        paths+=("/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/security")
1873    else
1874        paths+=(/lib*/security)
1875    fi
1876
1877    for d in /etc/pam.d /{usr/,}etc/security /usr/{etc,lib}/pam.d; do
1878        [ -d "$d" ] && paths+=("$d")
1879    done
1880
1881    while read -r file; do
1882        inst "$file"
1883    done < <(find "${paths[@]}" -xtype f)
1884
1885    # pam_unix depends on unix_chkpwd.
1886    # see http://www.linux-pam.org/Linux-PAM-html/sag-pam_unix.html
1887    image_install -o unix_chkpwd
1888
1889    # set empty root password for easy debugging
1890    sed -i 's/^root:x:/root::/' "${initdir:?}/etc/passwd"
1891
1892    # And make sure pam_unix will accept it by making sure that
1893    # the PAM module has the nullok option.
1894    for d in /etc/pam.d /usr/{etc,lib}/pam.d; do
1895        [ -d "$initdir/$d" ] || continue
1896        sed -i '/^auth.*pam_unix.so/s/$/ nullok/' "$initdir/$d"/*
1897    done
1898}
1899
1900# shellcheck disable=SC2120
1901install_keymaps() {
1902    dinfo "Install keymaps"
1903    # The first three paths may be deprecated.
1904    # It seems now the last two paths are used by many distributions.
1905    for i in \
1906        /usr/lib/kbd/keymaps/include/* \
1907        /usr/lib/kbd/keymaps/i386/include/* \
1908        /usr/lib/kbd/keymaps/i386/qwerty/us.* \
1909        /usr/lib/kbd/keymaps/legacy/include/* \
1910        /usr/lib/kbd/keymaps/legacy/i386/qwerty/us.*; do
1911            [[ -f "$i" ]] || continue
1912            inst "$i"
1913    done
1914
1915    # When it takes any argument, then install more keymaps.
1916    if [[ $# -gt 1 ]]; then
1917        for i in \
1918        /usr/lib/kbd/keymaps/i386/*/* \
1919        /usr/lib/kbd/keymaps/legacy/i386/*/*; do
1920            [[ -f "$i" ]] || continue
1921            inst "$i"
1922        done
1923    fi
1924}
1925
1926install_zoneinfo() {
1927    dinfo "Install time zones"
1928    inst_any /usr/share/zoneinfo/Asia/Seoul
1929    inst_any /usr/share/zoneinfo/Asia/Vladivostok
1930    inst_any /usr/share/zoneinfo/Australia/Sydney
1931    inst_any /usr/share/zoneinfo/Europe/Berlin
1932    inst_any /usr/share/zoneinfo/Europe/Dublin
1933    inst_any /usr/share/zoneinfo/Europe/Kiev
1934    inst_any /usr/share/zoneinfo/Pacific/Auckland
1935    inst_any /usr/share/zoneinfo/Pacific/Honolulu
1936    inst_any /usr/share/zoneinfo/CET
1937    inst_any /usr/share/zoneinfo/EET
1938    inst_any /usr/share/zoneinfo/UTC
1939}
1940
1941install_fonts() {
1942    dinfo "Install system fonts"
1943    for i in \
1944        /usr/lib/kbd/consolefonts/eurlatgr* \
1945        /usr/lib/kbd/consolefonts/latarcyrheb-sun16*; do
1946            [[ -f "$i" ]] || continue
1947            inst "$i"
1948    done
1949}
1950
1951install_terminfo() {
1952    dinfo "Install terminfo files"
1953    local terminfodir
1954    for terminfodir in /lib/terminfo /etc/terminfo /usr/share/terminfo; do
1955        [ -f "${terminfodir}/l/linux" ] && break
1956    done
1957    image_install -o "${terminfodir}/l/linux"
1958}
1959
1960has_user_dbus_socket() {
1961    if [ -f /usr/lib/systemd/user/dbus.socket ] || [ -f /etc/systemd/user/dbus.socket ]; then
1962        return 0
1963    else
1964        echo "Per-user instances are not supported. Skipping..."
1965        return 1
1966    fi
1967}
1968
1969setup_nspawn_root_hook() { :;}
1970
1971setup_nspawn_root() {
1972    if [ -z "${initdir}" ]; then
1973        dfatal "\$initdir not defined"
1974        exit 1
1975    fi
1976
1977    rm -rf "${TESTDIR:?}/unprivileged-nspawn-root"
1978
1979    if get_bool "$RUN_IN_UNPRIVILEGED_CONTAINER"; then
1980        ddebug "cp -ar $initdir $TESTDIR/unprivileged-nspawn-root"
1981        cp -ar "$initdir" "$TESTDIR/unprivileged-nspawn-root"
1982    fi
1983
1984    setup_nspawn_root_hook
1985}
1986
1987setup_basic_dirs() {
1988    mkdir -p "${initdir:?}/run"
1989    mkdir -p "$initdir/etc/systemd/system"
1990    mkdir -p "$initdir/var/log/journal"
1991
1992
1993    for d in usr/bin usr/sbin bin etc lib "${libdir:?}" sbin tmp usr var var/log var/tmp dev proc sys sysroot root run run/lock run/initramfs; do
1994        if [ -L "/$d" ]; then
1995            inst_symlink "/$d"
1996        else
1997            inst_dir "/$d"
1998        fi
1999    done
2000
2001    ln -sfn /run "$initdir/var/run"
2002    ln -sfn /run/lock "$initdir/var/lock"
2003}
2004
2005mask_supporting_services() {
2006    # mask some services that we do not want to run in these tests
2007    ln -fsv /dev/null "${initdir:?}/etc/systemd/system/systemd-hwdb-update.service"
2008    ln -fsv /dev/null "$initdir/etc/systemd/system/systemd-journal-catalog-update.service"
2009    ln -fsv /dev/null "$initdir/etc/systemd/system/systemd-networkd.service"
2010    ln -fsv /dev/null "$initdir/etc/systemd/system/systemd-networkd.socket"
2011    ln -fsv /dev/null "$initdir/etc/systemd/system/systemd-resolved.service"
2012}
2013
2014inst_libs() {
2015    local bin="${1:?}"
2016    local so_regex='([^ ]*/lib[^/]*/[^ ]*\.so[^ ]*)'
2017    local file line
2018
2019    while read -r line; do
2020        [[ "$line" = 'not a dynamic executable' ]] && break
2021        # Ignore errors about our own stuff missing. This is most likely caused
2022        # by ldd attempting to use the unprefixed RPATH.
2023        [[ "$line" =~ libsystemd.*\ not\ found ]] && continue
2024
2025        if [[ "$line" =~ $so_regex ]]; then
2026            file="${BASH_REMATCH[1]}"
2027            [[ -e "${initdir:?}/$file" ]] && continue
2028            inst_library "$file"
2029            continue
2030        fi
2031
2032        if [[ "$line" =~ not\ found ]]; then
2033            dfatal "Missing a shared library required by $bin."
2034            dfatal "Run \"ldd $bin\" to find out what it is."
2035            dfatal "$line"
2036            dfatal "Cannot create a test image."
2037            exit 1
2038        fi
2039    done < <(LC_ALL=C ldd "$bin" 2>/dev/null)
2040}
2041
2042import_testdir() {
2043    # make sure we don't get a stale LOOPDEV value from old times
2044    local _LOOPDEV="${LOOPDEV:=}"
2045    # We don't want shellcheck to follow & check the $STATEFILE
2046    # shellcheck source=/dev/null
2047    [[ -e "$STATEFILE" ]] && . "$STATEFILE"
2048    LOOPDEV="$_LOOPDEV"
2049    if [[ ! -d "$TESTDIR" ]]; then
2050        if [[ -z "$TESTDIR" ]]; then
2051            TESTDIR="$(mktemp --tmpdir=/var/tmp -d -t systemd-test.XXXXXX)"
2052        else
2053            mkdir -p "$TESTDIR"
2054        fi
2055
2056        cat >"$STATEFILE" <<EOF
2057TESTDIR="$TESTDIR"
2058EOF
2059        export TESTDIR
2060    fi
2061
2062    IMAGE_PRIVATE="${TESTDIR}/${IMAGE_NAME:?}.img"
2063    IMAGE_PUBLIC="${IMAGESTATEDIR:?}/${IMAGE_NAME}.img"
2064}
2065
2066import_initdir() {
2067    initdir="${TESTDIR:?}/root"
2068    mkdir -p "$initdir"
2069    export initdir
2070}
2071
2072get_cgroup_hierarchy() {
2073    case "$(stat -c '%T' -f /sys/fs/cgroup)" in
2074        cgroup2fs)
2075            echo "unified"
2076            ;;
2077        tmpfs)
2078            if [[ -d /sys/fs/cgroup/unified && "$(stat -c '%T' -f /sys/fs/cgroup/unified)" == cgroup2fs ]]; then
2079                echo "hybrid"
2080            else
2081                echo "legacy"
2082            fi
2083            ;;
2084        *)
2085            dfatal "Failed to determine host's cgroup hierarchy"
2086            exit 1
2087    esac
2088}
2089
2090## @brief Converts numeric logging level to the first letter of level name.
2091#
2092# @param lvl Numeric logging level in range from 1 to 6.
2093# @retval 1 if @a lvl is out of range.
2094# @retval 0 if @a lvl is correct.
2095# @result Echoes first letter of level name.
2096_lvl2char() {
2097    case "$1" in
2098        1) echo F;;
2099        2) echo E;;
2100        3) echo W;;
2101        4) echo I;;
2102        5) echo D;;
2103        6) echo T;;
2104        *) return 1;;
2105    esac
2106}
2107
2108## @brief Internal helper function for _do_dlog()
2109#
2110# @param lvl Numeric logging level.
2111# @param msg Message.
2112# @retval 0 It's always returned, even if logging failed.
2113#
2114# @note This function is not supposed to be called manually. Please use
2115# dtrace(), ddebug(), or others instead which wrap this one.
2116#
2117# This function calls _do_dlog() either with parameter msg, or if
2118# none is given, it will read standard input and will use every line as
2119# a message.
2120#
2121# This enables:
2122# dwarn "This is a warning"
2123# echo "This is a warning" | dwarn
2124LOG_LEVEL="${LOG_LEVEL:-4}"
2125
2126dlog() {
2127    local lvl lvlc
2128
2129    [ -z "$LOG_LEVEL" ] && return 0
2130    lvl="${1:?}"; shift
2131    [ "$lvl" -le "$LOG_LEVEL" ] || return 0
2132    lvlc="$(_lvl2char "$lvl")" || return 0
2133
2134    if [ $# -ge 1 ]; then
2135        echo "$lvlc: $*"
2136    else
2137        while read -r line; do
2138            echo "$lvlc: " "$line"
2139        done
2140    fi
2141}
2142
2143## @brief Logs message at TRACE level (6)
2144#
2145# @param msg Message.
2146# @retval 0 It's always returned, even if logging failed.
2147dtrace() {
2148    set +x
2149    dlog 6 "$@"
2150    if get_bool "${debug:=}"; then
2151        set -x
2152    fi
2153}
2154
2155## @brief Logs message at DEBUG level (5)
2156#
2157# @param msg Message.
2158# @retval 0 It's always returned, even if logging failed.
2159ddebug() {
2160    dlog 5 "$@"
2161}
2162
2163## @brief Logs message at INFO level (4)
2164#
2165# @param msg Message.
2166# @retval 0 It's always returned, even if logging failed.
2167dinfo() {
2168    set +x
2169    dlog 4 "$@"
2170    if get_bool "${debug:=}"; then
2171        set -x
2172    fi
2173}
2174
2175## @brief Logs message at WARN level (3)
2176#
2177# @param msg Message.
2178# @retval 0 It's always returned, even if logging failed.
2179dwarn() {
2180    set +x
2181    dlog 3 "$@"
2182    if get_bool "${debug:=}"; then
2183        set -x
2184    fi
2185}
2186
2187## @brief Logs message at ERROR level (2)
2188#
2189# @param msg Message.
2190# @retval 0 It's always returned, even if logging failed.
2191derror() {
2192    dlog 2 "$@"
2193}
2194
2195## @brief Logs message at FATAL level (1)
2196#
2197# @param msg Message.
2198# @retval 0 It's always returned, even if logging failed.
2199dfatal() {
2200    set +x
2201    dlog 1 "$@"
2202    if get_bool "${debug:=}"; then
2203        set -x
2204    fi
2205}
2206
2207
2208# Generic substring function.  If $2 is in $1, return 0.
2209strstr() { [ "${1#*"$2"*}" != "$1" ]; }
2210
2211# normalize_path <path>
2212# Prints the normalized path, where it removes any duplicated
2213# and trailing slashes.
2214# Example:
2215# $ normalize_path ///test/test//
2216# /test/test
2217normalize_path() {
2218    shopt -q -s extglob
2219    set -- "${1//+(\/)//}"
2220    shopt -q -u extglob
2221    echo "${1%/}"
2222}
2223
2224# convert_abs_rel <from> <to>
2225# Prints the relative path, when creating a symlink to <to> from <from>.
2226# Example:
2227# $ convert_abs_rel /usr/bin/test /bin/test-2
2228# ../../bin/test-2
2229# $ ln -s $(convert_abs_rel /usr/bin/test /bin/test-2) /usr/bin/test
2230convert_abs_rel() {
2231    local __current __absolute __abssize __cursize __newpath
2232    local -i __i __level
2233
2234    set -- "$(normalize_path "${1:?}")" "$(normalize_path "${2:?}")"
2235
2236    # corner case #1 - self looping link
2237    [[ "$1" == "$2" ]] && { echo "${1##*/}"; return; }
2238
2239    # corner case #2 - own dir link
2240    [[ "${1%/*}" == "$2" ]] && { echo "."; return; }
2241
2242    IFS="/" read -ra __current <<< "$1"
2243    IFS="/" read -ra __absolute <<< "$2"
2244
2245    __abssize=${#__absolute[@]}
2246    __cursize=${#__current[@]}
2247
2248    while [[ "${__absolute[__level]}" == "${__current[__level]}" ]]
2249    do
2250        (( __level++ ))
2251        if (( __level > __abssize || __level > __cursize ))
2252        then
2253            break
2254        fi
2255    done
2256
2257    for ((__i = __level; __i < __cursize-1; __i++))
2258    do
2259        if ((__i > __level))
2260        then
2261            __newpath=$__newpath"/"
2262        fi
2263        __newpath=$__newpath".."
2264    done
2265
2266    for ((__i = __level; __i < __abssize; __i++))
2267    do
2268        if [[ -n $__newpath ]]
2269        then
2270            __newpath=$__newpath"/"
2271        fi
2272        __newpath=$__newpath${__absolute[__i]}
2273    done
2274
2275    echo "$__newpath"
2276}
2277
2278
2279# Install a directory, keeping symlinks as on the original system.
2280# Example: if /lib points to /lib64 on the host, "inst_dir /lib/file"
2281# will create ${initdir}/lib64, ${initdir}/lib64/file,
2282# and a symlink ${initdir}/lib -> lib64.
2283inst_dir() {
2284    local dir="${1:?}"
2285    local part="${dir%/*}"
2286    local file
2287
2288    [[ -e "${initdir:?}/${dir}" ]] && return 0  # already there
2289
2290    while [[ "$part" != "${part%/*}" ]] && ! [[ -e "${initdir}/${part}" ]]; do
2291        dir="$part $dir"
2292        part="${part%/*}"
2293    done
2294
2295    # iterate over parent directories
2296    for file in $dir; do
2297        [[ -e "${initdir}/$file" ]] && continue
2298        if [[ -L $file ]]; then
2299            inst_symlink "$file"
2300        else
2301            # create directory
2302            mkdir -m 0755 "${initdir}/$file" || return 1
2303            [[ -e "$file" ]] && chmod --reference="$file" "${initdir}/$file"
2304            chmod u+w "${initdir}/$file"
2305        fi
2306    done
2307}
2308
2309# $1 = file to copy to ramdisk
2310# $2 (optional) Name for the file on the ramdisk
2311# Location of the image dir is assumed to be $initdir
2312# We never overwrite the target if it exists.
2313inst_simple() {
2314    [[ -f "${1:?}" ]] || return 1
2315    strstr "$1" "/" || return 1
2316
2317    local src="$1"
2318    local target="${2:-$1}"
2319    if ! [[ -d ${initdir:?}/$target ]]; then
2320        [[ -e ${initdir}/$target ]] && return 0
2321        [[ -L ${initdir}/$target ]] && return 0
2322        [[ -d "${initdir}/${target%/*}" ]] || inst_dir "${target%/*}"
2323    fi
2324    # install checksum files also
2325    if [[ -e "${src%/*}/.${src##*/}.hmac" ]]; then
2326        inst "${src%/*}/.${src##*/}.hmac" "${target%/*}/.${target##*/}.hmac"
2327    fi
2328    ddebug "Installing $src"
2329    cp --sparse=always -pfL "$src" "${initdir}/$target"
2330}
2331
2332# find symlinks linked to given library file
2333# $1 = library file
2334# Function searches for symlinks by stripping version numbers appended to
2335# library filename, checks if it points to the same target and finally
2336# prints the list of symlinks to stdout.
2337#
2338# Example:
2339# rev_lib_symlinks libfoo.so.8.1
2340# output: libfoo.so.8 libfoo.so
2341# (Only if libfoo.so.8 and libfoo.so exists on host system.)
2342rev_lib_symlinks() {
2343    local fn="${1:?}"
2344    local links=""
2345    local orig
2346    orig="$(readlink -f "$1")"
2347
2348    [[ "${fn}" =~ .*\.so\..* ]] || return 1
2349
2350    until [[ "${fn##*.}" == so ]]; do
2351        fn="${fn%.*}"
2352        [[ -L "${fn}" && "$(readlink -f "${fn}")" == "${orig}" ]] && links+=" ${fn}"
2353    done
2354
2355    echo "${links}"
2356}
2357
2358# Same as above, but specialized to handle dynamic libraries.
2359# It handles making symlinks according to how the original library
2360# is referenced.
2361inst_library() {
2362    local src="${1:?}"
2363    local dest="${2:-$1}"
2364    local reallib symlink
2365
2366    strstr "$1" "/" || return 1
2367    [[ -e ${initdir:?}/$dest ]] && return 0
2368    if [[ -L $src ]]; then
2369        # install checksum files also
2370        if [[ -e "${src%/*}/.${src##*/}.hmac" ]]; then
2371            inst "${src%/*}/.${src##*/}.hmac" "${dest%/*}/.${dest##*/}.hmac"
2372        fi
2373        reallib="$(readlink -f "$src")"
2374        inst_simple "$reallib" "$reallib"
2375        inst_dir "${dest%/*}"
2376        [[ -d "${dest%/*}" ]] && dest="$(readlink -f "${dest%/*}")/${dest##*/}"
2377        ln -sfn -- "$(convert_abs_rel "${dest}" "${reallib}")" "${initdir}/${dest}"
2378    else
2379        inst_simple "$src" "$dest"
2380    fi
2381
2382    # Create additional symlinks.  See rev_symlinks description.
2383    for symlink in $(rev_lib_symlinks "$src") ${reallib:+$(rev_lib_symlinks "$reallib")}; do
2384        if [[ ! -e "$initdir/$symlink" ]]; then
2385            ddebug "Creating extra symlink: $symlink"
2386            inst_symlink "$symlink"
2387        fi
2388    done
2389}
2390
2391# find a binary.  If we were not passed the full path directly,
2392# search in the usual places to find the binary.
2393find_binary() {
2394    local bin="${1:?}"
2395    if [[ -z ${bin##/*} ]]; then
2396        if [[ -x "$bin" ]] || { strstr "$bin" ".so" && ldd "$bin" &>/dev/null; }; then
2397            echo "$bin"
2398            return 0
2399        fi
2400    fi
2401
2402    type -P "$bin"
2403}
2404
2405# Same as above, but specialized to install binary executables.
2406# Install binary executable, and all shared library dependencies, if any.
2407inst_binary() {
2408    local bin="${1:?}"
2409    local path target
2410
2411    # In certain cases we might attempt to install a binary which is already
2412    # present in the test image, yet it's missing from the host system.
2413    # In such cases, let's check if the binary indeed exists in the image
2414    # before doing any other checks. If it does, immediately return with
2415    # success.
2416    if [[ $# -eq 1 ]]; then
2417        for path in "" bin sbin usr/bin usr/sbin; do
2418            [[ -e "${initdir:?}${path:+/$path}/${bin}" ]] && return 0
2419        done
2420    fi
2421
2422    bin="$(find_binary "$bin")" || return 1
2423    target="${2:-$bin}"
2424    [[ -e "${initdir:?}/$target" ]] && return 0
2425    [[ -L "$bin" ]] && inst_symlink "$bin" "$target" && return 0
2426
2427    local file line
2428    local so_regex='([^ ]*/lib[^/]*/[^ ]*\.so[^ ]*)'
2429    # I love bash!
2430    while read -r line; do
2431        [[ "$line" = 'not a dynamic executable' ]] && break
2432
2433        # Ignore errors about our own stuff missing. This is most likely caused
2434        # by ldd attempting to use the unprefixed RPATH.
2435        [[ "$line" =~ libsystemd.*\ not\ found ]] && continue
2436
2437        if [[ "$line" =~ $so_regex ]]; then
2438            file="${BASH_REMATCH[1]}"
2439            [[ -e "${initdir}/$file" ]] && continue
2440            inst_library "$file"
2441            continue
2442        fi
2443
2444        if [[ "$line" =~ not\ found ]]; then
2445            dfatal "Missing a shared library required by $bin."
2446            dfatal "Run \"ldd $bin\" to find out what it is."
2447            dfatal "$line"
2448            dfatal "Cannot create a test image."
2449            exit 1
2450        fi
2451    done < <(LC_ALL=C ldd "$bin" 2>/dev/null)
2452    inst_simple "$bin" "$target"
2453}
2454
2455# same as above, except for shell scripts.
2456# If your shell script does not start with shebang, it is not a shell script.
2457inst_script() {
2458    local bin line shebang_regex
2459    bin="$(find_binary "${1:?}")" || return 1
2460    shift
2461
2462    read -r -n 80 line <"$bin"
2463    # If debug is set, clean unprintable chars to prevent messing up the term
2464    get_bool "${debug:=}" && line="$(echo -n "$line" | tr -c -d '[:print:][:space:]')"
2465    shebang_regex='(#! *)(/[^ ]+).*'
2466    [[ "$line" =~ $shebang_regex ]] || return 1
2467    inst "${BASH_REMATCH[2]}" && inst_simple "$bin" "$@"
2468}
2469
2470# same as above, but specialized for symlinks
2471inst_symlink() {
2472    local src="${1:?}"
2473    local target="${2:-$src}"
2474    local realsrc
2475
2476    strstr "$src" "/" || return 1
2477    [[ -L "$src" ]] || return 1
2478    [[ -L "${initdir:?}/$target" ]] && return 0
2479    realsrc="$(readlink -f "$src")"
2480    if ! [[ -e "$initdir/$realsrc" ]]; then
2481        if [[ -d "$realsrc" ]]; then
2482            inst_dir "$realsrc"
2483        else
2484            inst "$realsrc"
2485        fi
2486    fi
2487    [[ ! -e "$initdir/${target%/*}" ]] && inst_dir "${target%/*}"
2488    [[ -d "${target%/*}" ]] && target="$(readlink -f "${target%/*}")/${target##*/}"
2489    ln -sfn -- "$(convert_abs_rel "${target}" "${realsrc}")" "$initdir/$target"
2490}
2491
2492# attempt to install any programs specified in a udev rule
2493inst_rule_programs() {
2494    local rule="${1:?}"
2495    local prog bin
2496
2497    sed -rn 's/^.*?PROGRAM==?"([^ "]+).*$/\1/p' "$rule" | while read -r prog; do
2498        if [ -x "/lib/udev/$prog" ]; then
2499            bin="/lib/udev/$prog"
2500        else
2501            if ! bin="$(find_binary "$prog")"; then
2502                dinfo "Skipping program $prog used in udev rule $(basename "$rule") as it cannot be found"
2503                continue
2504            fi
2505        fi
2506
2507        #dinfo "Installing $_bin due to it's use in the udev rule $(basename $1)"
2508        image_install "$bin"
2509    done
2510}
2511
2512# udev rules always get installed in the same place, so
2513# create a function to install them to make life simpler.
2514inst_rules() {
2515    local target=/etc/udev/rules.d
2516    local found rule
2517
2518    inst_dir "/lib/udev/rules.d"
2519    inst_dir "$target"
2520    for rule in "$@"; do
2521        if [ "${rule#/}" = "$rule" ]; then
2522            for r in /lib/udev/rules.d /etc/udev/rules.d; do
2523                if [[ -f "$r/$rule" ]]; then
2524                    found="$r/$rule"
2525                    inst_simple "$found"
2526                    inst_rule_programs "$found"
2527                fi
2528            done
2529        fi
2530        for r in '' ./; do
2531            if [[ -f "${r}${rule}" ]]; then
2532                found="${r}${rule}"
2533                inst_simple "$found" "$target/${found##*/}"
2534                inst_rule_programs "$found"
2535            fi
2536        done
2537        [[ $found ]] || dinfo "Skipping udev rule: $rule"
2538        found=
2539    done
2540}
2541
2542# general purpose installation function
2543# Same args as above.
2544inst() {
2545    case $# in
2546        1) ;;
2547        2)
2548            [[ ! "$initdir" && -d "$2" ]] && export initdir="$2"
2549            [[ "$initdir" = "$2" ]] && set "$1"
2550            ;;
2551        3)
2552            [[ -z "$initdir" ]] && export initdir="$2"
2553            set "$1" "$3"
2554            ;;
2555        *)
2556            dfatal "inst only takes 1 or 2 or 3 arguments"
2557            exit 1
2558            ;;
2559    esac
2560
2561    local fun
2562    for fun in inst_symlink inst_script inst_binary inst_simple; do
2563        "$fun" "$@" && return 0
2564    done
2565
2566    dwarn "Failed to install '$1'"
2567    return 1
2568}
2569
2570# install any of listed files
2571#
2572# If first argument is '-d' and second some destination path, first accessible
2573# source is installed into this path, otherwise it will installed in the same
2574# path as source.  If none of listed files was installed, function return 1.
2575# On first successful installation it returns with 0 status.
2576#
2577# Example:
2578#
2579# inst_any -d /bin/foo /bin/bar /bin/baz
2580#
2581# Lets assume that /bin/baz exists, so it will be installed as /bin/foo in
2582# initramfs.
2583inst_any() {
2584    local dest file
2585
2586    [[ "${1:?}" = '-d' ]] && dest="${2:?}" && shift 2
2587
2588    for file in "$@"; do
2589        if [[ -e "$file" ]]; then
2590            [[ -n "$dest" ]] && inst "$file" "$dest" && return 0
2591            inst "$file" && return 0
2592        fi
2593    done
2594
2595    return 1
2596}
2597
2598# image_install [-o ] <file> [<file> ... ]
2599# Install <file> to the test image
2600# -o optionally install the <file> and don't fail, if it is not there
2601image_install() {
2602    local optional=no
2603    local prog="${1:?}"
2604
2605    if [[ "$prog" = '-o' ]]; then
2606        optional=yes
2607        shift
2608    fi
2609
2610    for prog in "$@"; do
2611        if ! inst "$prog" ; then
2612            if get_bool "$optional"; then
2613                dinfo "Skipping program $prog as it cannot be found and is" \
2614                    "flagged to be optional"
2615            else
2616                dfatal "Failed to install $prog"
2617                exit 1
2618            fi
2619        fi
2620    done
2621}
2622
2623# Install a single kernel module along with any firmware it may require.
2624# $1 = full path to kernel module to install
2625install_kmod_with_fw() {
2626    local module="${1:?}"
2627    # no need to go further if the module is already installed
2628    [[ -e "${initdir:?}/lib/modules/${KERNEL_VER:?}/${module##*"/lib/modules/$KERNEL_VER/"}" ]] && return 0
2629    [[ -e "$initdir/.kernelmodseen/${module##*/}" ]] && return 0
2630
2631    [ -d "$initdir/.kernelmodseen" ] && : >"$initdir/.kernelmodseen/${module##*/}"
2632
2633    inst_simple "$module" "/lib/modules/$KERNEL_VER/${module##*"/lib/modules/$KERNEL_VER/"}" || return $?
2634
2635    local modname="${module##*/}"
2636    local fwdir found fw
2637    modname="${modname%.ko*}"
2638
2639    while read -r fw; do
2640        found=
2641        for fwdir in /lib/firmware/updates /lib/firmware; do
2642            if [[ -d "$fwdir" && -f "$fwdir/$fw" ]]; then
2643                inst_simple "$fwdir/$fw" "/lib/firmware/$fw"
2644                found=yes
2645            fi
2646        done
2647        if ! get_bool "$found"; then
2648            if ! grep -qe "\<${modname//-/_}\>" /proc/modules; then
2649                dinfo "Possible missing firmware \"${fw}\" for kernel module" \
2650                    "\"${modname}.ko\""
2651            else
2652                dwarn "Possible missing firmware \"${fw}\" for kernel module" \
2653                    "\"${modname}.ko\""
2654            fi
2655        fi
2656    done < <(modinfo -k "$KERNEL_VER" -F firmware "$module" 2>/dev/null)
2657    return 0
2658}
2659
2660# Do something with all the dependencies of a kernel module.
2661# Note that kernel modules depend on themselves using the technique we use
2662# $1 = function to call for each dependency we find
2663#      It will be passed the full path to the found kernel module
2664# $2 = module to get dependencies for
2665# rest of args = arguments to modprobe
2666for_each_kmod_dep() {
2667    local func="${1:?}"
2668    local kmod="${2:?}"
2669    local found=0
2670    local cmd modpath
2671    shift 2
2672
2673    while read -r cmd modpath _; do
2674        [[ "$cmd" = insmod ]] || continue
2675        "$func" "$modpath" || return $?
2676        found=1
2677    done < <(modprobe "$@" --ignore-install --show-depends "$kmod")
2678
2679    ! get_bool "$found" && return 1
2680    return 0
2681}
2682
2683# instmods [-c] <kernel module> [<kernel module> ... ]
2684# instmods [-c] <kernel subsystem>
2685# install kernel modules along with all their dependencies.
2686# <kernel subsystem> can be e.g. "=block" or "=drivers/usb/storage"
2687# FIXME(?): dracutdevs/dracut@f4e38c0da8d6bf3764c1ad753d9d52aef63050e5
2688instmods() {
2689    local check=no
2690    if [[ $# -ge 0 && "$1" = '-c' ]]; then
2691        check=yes
2692        shift
2693    fi
2694
2695    inst1mod() {
2696        local mod="${1:?}"
2697        local ret=0
2698        local mod_dir="/lib/modules/${KERNEL_VER:?}/"
2699
2700        case "$mod" in
2701            =*)
2702                if [ -f "${mod_dir}/modules.${mod#=}" ]; then
2703                    (
2704                        [[ "$mpargs" ]] && echo "$mpargs"
2705                        cat "${mod_dir}/modules.${mod#=}"
2706                    ) | instmods
2707                else
2708                    (
2709                        [[ "$mpargs" ]] && echo "$mpargs"
2710                        find "$mod_dir" -path "*/${mod#=}/*" -name "*.ko*" -type f -printf '%f\n'
2711                    ) | instmods
2712                fi
2713                ;;
2714            --*)
2715                mpargs+=" $mod"
2716                ;;
2717            i2o_scsi)
2718                # Do not load this diagnostic-only module
2719                return
2720                ;;
2721            *)
2722                mod=${mod##*/}
2723                # if we are already installed, skip this module and go on
2724                # to the next one.
2725                [[ -f "${initdir:?}/.kernelmodseen/${mod%.ko}.ko" ]] && return
2726
2727                # We use '-d' option in modprobe only if modules prefix path
2728                # differs from default '/'.  This allows us to use Dracut with
2729                # old version of modprobe which doesn't have '-d' option.
2730                local mod_dirname=${mod_dir%%/lib/modules/*}
2731                [[ -n ${mod_dirname} ]] && mod_dirname="-d ${mod_dirname}/"
2732
2733                # ok, load the module, all its dependencies, and any firmware
2734                # it may require
2735                for_each_kmod_dep install_kmod_with_fw "$mod" \
2736                    --set-version "$KERNEL_VER" \
2737                    ${mod_dirname:+"$mod_dirname"} \
2738                    ${mpargs:+"$mpargs"}
2739                ((ret+=$?))
2740                ;;
2741        esac
2742        return "$ret"
2743    }
2744
2745    local mod mpargs
2746
2747    if [[ $# -eq 0 ]]; then  # filenames from stdin
2748        while read -r mod; do
2749            if ! inst1mod "${mod%.ko*}" && [ "$check" = "yes" ]; then
2750                dfatal "Failed to install $mod"
2751                return 1
2752            fi
2753        done
2754    fi
2755
2756    for mod in "$@"; do # filenames as arguments
2757        if ! inst1mod "${mod%.ko*}" && [ "$check" = "yes" ]; then
2758            dfatal "Failed to install $mod"
2759            return 1
2760        fi
2761    done
2762
2763    return 0
2764}
2765
2766_umount_dir() {
2767    local mountpoint="${1:?}"
2768    if mountpoint -q "$mountpoint"; then
2769        ddebug "umount $mountpoint"
2770        umount "$mountpoint"
2771    fi
2772}
2773
2774# can be overridden in specific test
2775test_setup_cleanup() {
2776    cleanup_initdir
2777}
2778
2779_test_cleanup() {
2780    # (post-test) cleanup should always ignore failure and cleanup as much as possible
2781    (
2782        set +e
2783        [[ -n "$initdir" ]] && _umount_dir "$initdir"
2784        [[ -n "$IMAGE_PUBLIC" ]] && rm -vf "$IMAGE_PUBLIC"
2785        # If multiple setups/cleans are ran in parallel, this can cause a race
2786        if [[ -n "$IMAGESTATEDIR" && $TEST_PARALLELIZE -ne 1 ]]; then
2787            rm -vf "${IMAGESTATEDIR}/default.img"
2788        fi
2789        [[ -n "$TESTDIR" ]] && rm -vfr "$TESTDIR"
2790        [[ -n "$STATEFILE" ]] && rm -vf "$STATEFILE"
2791    ) || :
2792}
2793
2794# can be overridden in specific test
2795test_cleanup() {
2796    _test_cleanup
2797}
2798
2799test_cleanup_again() {
2800    [ -n "$TESTDIR" ] || return
2801    rm -rf "$TESTDIR/unprivileged-nspawn-root"
2802    [[ -n "$initdir" ]] && _umount_dir "$initdir"
2803}
2804
2805test_create_image() {
2806    create_empty_image_rootdir
2807
2808    # Create what will eventually be our root filesystem onto an overlay
2809    (
2810        LOG_LEVEL=5
2811        setup_basic_environment
2812    )
2813}
2814
2815test_setup() {
2816    if get_bool "${TEST_REQUIRE_INSTALL_TESTS:?}" && \
2817            command -v meson >/dev/null && \
2818            [[ "$(meson configure "${BUILD_DIR:?}" | grep install-tests | awk '{ print $2 }')" != "true" ]]; then
2819        dfatal "$BUILD_DIR needs to be built with -Dinstall-tests=true"
2820        exit 1
2821    fi
2822
2823    if [ -e "${IMAGE_PRIVATE:?}" ]; then
2824        echo "Reusing existing image $IMAGE_PRIVATE$(realpath "$IMAGE_PRIVATE")"
2825        mount_initdir
2826    else
2827        if [ ! -e "${IMAGE_PUBLIC:?}" ]; then
2828            # default.img is the base that every test uses and optionally appends to
2829            if [ ! -e "${IMAGESTATEDIR:?}/default.img" ] || [ -n "${TEST_FORCE_NEWIMAGE:=}" ]; then
2830                # Create the backing public image, but then completely unmount
2831                # it and drop the loopback device responsible for it, since we're
2832                # going to symlink/copy the image and mount it again from
2833                # elsewhere.
2834                local image_old="${IMAGE_PUBLIC}"
2835                if [ -z "${TEST_FORCE_NEWIMAGE}" ]; then
2836                    IMAGE_PUBLIC="${IMAGESTATEDIR}/default.img"
2837                fi
2838                test_create_image
2839                test_setup_cleanup
2840                umount_loopback
2841                cleanup_loopdev
2842                IMAGE_PUBLIC="${image_old}"
2843            fi
2844            if [ "${IMAGE_NAME:?}" != "default" ] && ! get_bool "${TEST_FORCE_NEWIMAGE}"; then
2845                cp -v "$(realpath "${IMAGESTATEDIR}/default.img")" "$IMAGE_PUBLIC"
2846            fi
2847        fi
2848
2849        local hook_defined
2850        declare -f -F test_append_files >/dev/null && hook_defined=yes || hook_defined=no
2851
2852        echo "Reusing existing cached image $IMAGE_PUBLIC$(realpath "$IMAGE_PUBLIC")"
2853        if get_bool "$TEST_PARALLELIZE" || get_bool "$hook_defined"; then
2854            cp -v -- "$(realpath "$IMAGE_PUBLIC")" "$IMAGE_PRIVATE"
2855        else
2856            ln -sv -- "$(realpath "$IMAGE_PUBLIC")" "$IMAGE_PRIVATE"
2857        fi
2858
2859        mount_initdir
2860        # We want to test all services in TEST-01-BASIC, but mask them in
2861        # all other tests
2862        if [[ "${TESTID:?}" != "01" ]]; then
2863            dinfo "Masking supporting services"
2864            mask_supporting_services
2865        fi
2866
2867        if get_bool "$hook_defined"; then
2868            test_append_files "${initdir:?}"
2869        fi
2870    fi
2871
2872    setup_nspawn_root
2873}
2874
2875test_run() {
2876    local test_id="${1:?}"
2877    mount_initdir
2878
2879    if ! get_bool "${TEST_NO_QEMU:=}"; then
2880        if run_qemu "$test_id"; then
2881            check_result_qemu || { echo "qemu test failed"; return 1; }
2882        else
2883            dwarn "can't run qemu, skipping"
2884        fi
2885    fi
2886    if ! get_bool "${TEST_NO_NSPAWN:=}"; then
2887        mount_initdir
2888        if run_nspawn "${initdir:?}" "$test_id"; then
2889            check_result_nspawn "$initdir" || { echo "nspawn-root test failed"; return 1; }
2890        else
2891            dwarn "can't run systemd-nspawn, skipping"
2892        fi
2893
2894        if get_bool "${RUN_IN_UNPRIVILEGED_CONTAINER:=}"; then
2895            dir="$TESTDIR/unprivileged-nspawn-root"
2896            if NSPAWN_ARGUMENTS="-U --private-network ${NSPAWN_ARGUMENTS:-}" run_nspawn "$dir" "$test_id"; then
2897                check_result_nspawn "$dir" || { echo "unprivileged-nspawn-root test failed"; return 1; }
2898            else
2899                dwarn "can't run systemd-nspawn, skipping"
2900            fi
2901        fi
2902    fi
2903    return 0
2904}
2905
2906do_test() {
2907    if [[ $UID != "0" ]]; then
2908        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: not root" >&2
2909        exit 0
2910    fi
2911
2912    if get_bool "${TEST_NO_QEMU:=}" && get_bool "${TEST_NO_NSPAWN:=}"; then
2913        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: both qemu and nspawn disabled" >&2
2914        exit 0
2915    fi
2916
2917    if get_bool "${TEST_QEMU_ONLY:=}" && ! get_bool "$TEST_NO_NSPAWN"; then
2918        echo "TEST: $TEST_DESCRIPTION [SKIPPED]: qemu-only tests requested" >&2
2919        exit 0
2920    fi
2921
2922    if get_bool "${TEST_PREFER_NSPAWN:=}" && ! get_bool "$TEST_NO_NSPAWN"; then
2923        TEST_NO_QEMU=1
2924    fi
2925
2926    # Detect lib paths
2927    [[ "$libdir" ]] || for libdir in /lib64 /lib; do
2928        [[ -d $libdir ]] && libdirs+=" $libdir" && break
2929    done
2930
2931    [[ "$usrlibdir" ]] || for usrlibdir in /usr/lib64 /usr/lib; do
2932        [[ -d $usrlibdir ]] && libdirs+=" $usrlibdir" && break
2933    done
2934
2935    mkdir -p "$STATEDIR"
2936
2937    import_testdir
2938    import_initdir
2939
2940    if [ -n "${SUDO_USER}" ]; then
2941        ddebug "Making ${TESTDIR:?} readable for ${SUDO_USER} (acquired from sudo)"
2942        setfacl -m "user:${SUDO_USER:?}:r-X" "${TESTDIR:?}"
2943    fi
2944
2945    testname="$(basename "$PWD")"
2946
2947    while (($# > 0)); do
2948        case $1 in
2949            --run)
2950                echo "${testname} RUN: $TEST_DESCRIPTION"
2951                test_run "$TESTID"
2952                ret=$?
2953                if [ $ret -eq 0 ]; then
2954                    echo "${testname} RUN: $TEST_DESCRIPTION [OK]"
2955                else
2956                    echo "${testname} RUN: $TEST_DESCRIPTION [FAILED]"
2957                fi
2958                exit $ret
2959                ;;
2960            --setup)
2961                echo "${testname} SETUP: $TEST_DESCRIPTION"
2962                test_setup
2963                test_setup_cleanup
2964                ;;
2965            --clean)
2966                echo "${testname} CLEANUP: $TEST_DESCRIPTION"
2967                test_cleanup
2968                ;;
2969            --clean-again)
2970                echo "${testname} CLEANUP AGAIN: $TEST_DESCRIPTION"
2971                test_cleanup_again
2972                ;;
2973            --all)
2974                ret=0
2975                echo -n "${testname}: $TEST_DESCRIPTION "
2976                # Do not use a subshell, otherwise cleanup variables (LOOPDEV) will be lost
2977                # and loop devices will leak
2978                test_setup </dev/null >"$TESTLOG" 2>&1 || ret=$?
2979                if [ $ret -eq 0 ]; then
2980                    test_setup_cleanup </dev/null >>"$TESTLOG" 2>&1 || ret=$?
2981                fi
2982                if [ $ret -eq 0 ]; then
2983                    test_run "$TESTID" </dev/null >>"$TESTLOG" 2>&1 || ret=$?
2984                fi
2985                test_cleanup
2986                if [ $ret -eq 0 ]; then
2987                    rm "$TESTLOG"
2988                    echo "[OK]"
2989                else
2990                    echo "[FAILED]"
2991                    echo "see $TESTLOG"
2992                fi
2993                exit $ret
2994                ;;
2995            *)
2996                break
2997                ;;
2998        esac
2999        shift
3000    done
3001}
3002