#!/bin/bash
# ---------------------------------------------------------------------------
# Install the bundle on the offline machine.
#
# Puts the KUnit test modules and the kselftests in place from the RPMs in
# bundle/rpms, unpacks LTP from bundle/ltp-*.tar.gz, and then checks that
# what it installed actually works on this kernel.
#
#   ./install-offline.sh              install everything in bundle/
#   ./install-offline.sh --check      report what is present, change nothing
#
# Nothing here reaches the network.
# ---------------------------------------------------------------------------
set -uo pipefail

KT_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=lib/common.sh
. "$KT_ROOT/lib/common.sh"
kt_load_settings

BUNDLE=$KT_ROOT/bundle
CHECK_ONLY=0
FORCE=0

usage() { sed -n '3,12p' "$0" | sed 's/^# \{0,1\}//'; cat <<'EOF'

Options:
  -b, --bundle DIR   where the RPMs and the LTP tarball are (default: ./bundle)
  -c, --check        report only, install nothing
  -f, --force        install the RPMs even if they were built for a different
                     kernel than the one running
  -h, --help         this text
EOF
}

while [ $# -gt 0 ]; do
    case $1 in
        -b|--bundle) BUNDLE=$2; shift 2 ;;
        -c|--check)  CHECK_ONLY=1; shift ;;
        -f|--force)  FORCE=1; shift ;;
        -h|--help)   usage; exit 0 ;;
        *) kt_err "unknown option: $1"; usage; exit 2 ;;
    esac
done

[ "$CHECK_ONLY" = 1 ] || kt_require_root
KVER=$(kt_kver)

kt_head "This machine"
kt_log "kernel   : $KVER"
kt_log "os       : $(kt_osrel)"
kt_log "selinux  : $(kt_selinux_mode)"
kt_log "$(kt_secureboot)"
if kt_module_sig_enforced; then
    kt_warn "This kernel only loads modules signed by a key it trusts."
    kt_warn "KUnit modules that came from anywhere but this machine's own"
    kt_warn "vendor will be refused.  LTP and kselftest are not affected."
fi

PROBLEMS=0

# --- RPMs ------------------------------------------------------------------
kt_head "KUnit and kselftest RPMs"
RPMDIR=$BUNDLE/rpms
if [ ! -d "$RPMDIR" ]; then
    kt_warn "no $RPMDIR -- run download-rpms.sh on a connected machine first"
else
    [ -r "$RPMDIR/MANIFEST.txt" ] && sed 's/^/  /' "$RPMDIR/MANIFEST.txt"
    shopt -s nullglob
    rpms=( "$RPMDIR"/*.rpm )
    shopt -u nullglob
    [ ${#rpms[@]} -gt 0 ] || kt_warn "no .rpm files in $RPMDIR"

    to_install=()
    for r in "${rpms[@]}"; do
        nevra=$(rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "$r" 2>/dev/null)
        name=$(rpm -qp --qf '%{NAME}' "$r" 2>/dev/null)
        vendor=$(rpm -qp --qf '%{VENDOR}' "$r" 2>/dev/null)
        [ -n "$nevra" ] || { kt_warn "cannot read $r"; continue; }

        # Handing dnf a directory of RPMs is an upgrade request, and it fails
        # outright on anything already installed at that exact version.  Ask
        # package by package instead.
        if rpm -q "$nevra" >/dev/null 2>&1; then
            kt_ok "$nevra already installed"
            continue
        fi

        # kernel-modules-internal carries "Requires: kernel-uname-r = <exact>",
        # because its modules only load into that one kernel build.  Installing
        # a mismatched one gives modules that refuse to load, with a vermagic
        # error that is easy to misread as a bug.  kernel-selftests-internal
        # has no such requirement -- it is userspace -- so a version skew there
        # is worth a warning, not a refusal.
        want_k=$(rpm -qp --qf '%{VERSION}-%{RELEASE}.%{ARCH}' "$r" 2>/dev/null)
        if [ "$want_k" != "$KVER" ]; then
            if rpm -qpR "$r" 2>/dev/null | grep -q '^kernel-uname-r'; then
                if [ "$FORCE" = 0 ]; then
                    kt_err "$nevra is built for $want_k but this machine runs $KVER"
                    kt_err "  Its modules load into that kernel only.  Fetch the RPMs"
                    kt_err "  for $KVER, or boot the kernel they were built for."
                    PROBLEMS=$(( PROBLEMS + 1 ))
                    continue
                fi
                kt_warn "$nevra does not match $KVER; installing anyway (--force)"
            else
                kt_warn "$nevra was built alongside $want_k, not $KVER."
                kt_warn "  It holds userspace programs and does not depend on the"
                kt_warn "  running kernel, so it will work; tests for features this"
                kt_warn "  kernel does not have will report themselves as skipped."
            fi
        fi

        case $vendor in
            *Red*Hat*) ;;
            *)
                kt_warn "$nevra was built by '$vendor', not Red Hat."
                # Only a package that actually ships modules is affected by
                # whose key signed them.
                if rpm -qpl "$r" 2>/dev/null | grep -q '\.ko'; then
                    kt_warn "  Its modules are signed by that vendor's key.  They load"
                    kt_warn "  on a machine with Secure Boot off; with Secure Boot on"
                    kt_warn "  they do not."
                else
                    kt_warn "  It holds no kernel modules, so no signature applies."
                fi ;;
        esac
        to_install+=("$r")
    done

    if [ ${#to_install[@]} -eq 0 ]; then
        kt_log "nothing to install"
    elif [ "$CHECK_ONLY" = 1 ]; then
        kt_log "would install: ${to_install[*]##*/}"
    else
        # --disablerepo='*' keeps dnf from trying to reach a repository that
        # is not there.  --nogpgcheck is needed because the bundle is not
        # signed by a key this machine has imported.
        opts=(-y --disablerepo='*' --nogpgcheck --setopt=install_weak_deps=False)
        if dnf "${opts[@]}" install "${to_install[@]}" >/dev/null 2>&1; then
            kt_ok "installed ${#to_install[@]} package(s)"
        else
            kt_warn "installing them together failed; trying one at a time"
            for r in "${to_install[@]}"; do
                if dnf "${opts[@]}" install "$r" >/dev/null 2>&1; then
                    kt_ok "  $(basename "$r")"
                else
                    kt_err "  $(basename "$r") would not install"
                    PROBLEMS=$(( PROBLEMS + 1 ))
                fi
            done
        fi
    fi
fi

# --- LTP -------------------------------------------------------------------
kt_head "LTP"
shopt -s nullglob
ltp_tars=( "$BUNDLE"/ltp-*.tar.gz )
shopt -u nullglob
if [ -d "$LTP_DIR/runtest" ] && [ ${#ltp_tars[@]} -eq 0 ]; then
    kt_ok "already installed at $LTP_DIR (version $(cat "$LTP_DIR/Version" 2>/dev/null))"
elif [ ${#ltp_tars[@]} -eq 0 ]; then
    kt_warn "no ltp-*.tar.gz in $BUNDLE -- run build-ltp.sh on a connected machine"
else
    tar=${ltp_tars[0]}
    [ ${#ltp_tars[@]} -gt 1 ] && kt_warn "several LTP tarballs here; using $(basename "$tar")"
    if [ -r "$tar.sha256" ]; then
        if ( cd "$BUNDLE" && sha256sum -c "$(basename "$tar").sha256" >/dev/null 2>&1 ); then
            kt_ok "checksum verified"
        else
            kt_err "checksum does not match for $(basename "$tar") -- refusing to unpack"
            PROBLEMS=$(( PROBLEMS + 1 ))
            tar=""
        fi
    else
        kt_warn "no .sha256 beside the tarball; unpacking unverified"
    fi

    if [ -n "$tar" ] && [ "$CHECK_ONLY" = 1 ]; then
        kt_log "would unpack $(basename "$tar") over $LTP_DIR"
    elif [ -n "$tar" ]; then
        if [ -d "$LTP_DIR" ]; then
            kt_log "$LTP_DIR exists; the tarball is unpacked over it"
        fi
        tar xzf "$tar" -C / || { kt_err "unpacking failed"; PROBLEMS=$(( PROBLEMS + 1 )); }
        [ -d "$LTP_DIR/runtest" ] && kt_ok "LTP $(cat "$LTP_DIR/Version" 2>/dev/null) at $LTP_DIR"
    fi
fi

# LTP runs some tests as unprivileged users and creates them itself.  The
# build machine skipped this (SKIP_IDCHECK=1) so it happens here.
if [ "$CHECK_ONLY" = 0 ] && [ -x "$LTP_DIR/IDcheck.sh" ]; then
    if yes | "$LTP_DIR/IDcheck.sh" >/dev/null 2>&1; then
        kt_ok "LTP users and groups in place"
    else
        kt_warn "IDcheck.sh reported a problem; some LTP tests will be skipped"
    fi
fi

if [ "$CHECK_ONLY" = 0 ]; then
    mkdir -p "$LTP_TMPDIR" 2>/dev/null
    chmod 1777 "$LTP_TMPDIR" 2>/dev/null
    avail=$(df -Pm "$LTP_TMPDIR" 2>/dev/null | awk 'NR==2{print $4}')
    kt_log "scratch  : $LTP_TMPDIR (${avail:-?}MB free)"
    [ "${avail:-0}" -lt 4096 ] && kt_warn "under 4GB free; filesystem tests will report failures"
fi

# --- does it actually work? ------------------------------------------------
kt_head "Checking what was installed"

# KUnit: the module has to match this kernel build, not just its version
# string, so the real test is loading it.
if find "/lib/modules/$KVER" -name 'kunit.ko*' -print -quit 2>/dev/null | grep -q .; then
    vm=$(modinfo -F vermagic kunit 2>/dev/null)
    kt_log "kunit vermagic: $vm"
    if [ "$CHECK_ONLY" = 0 ]; then
        if modprobe kunit enable=1 2>/dev/null; then
            kt_ok "KUnit loads"
            lsmod | grep -q '^kunit ' && modprobe -r kunit 2>/dev/null
        else
            kt_err "kunit.ko is installed but will not load."
            kt_err "  Usually this is a module built for a different kernel, or"
            kt_err "  Secure Boot refusing a signature this machine does not trust."
            kt_err "  'dmesg | tail' says which."
            PROBLEMS=$(( PROBLEMS + 1 ))
        fi
    fi
    # Counting files named *kunit* undercounts badly: many KUnit modules are
    # called things like time_test or lib_test.
    kt_log "counting KUnit test modules (walks every module, a few seconds)"
    n=$(kt_list_kunit_modules | grep -vc '^kunit$')
    kt_log "KUnit test modules available: $n"
else
    kt_warn "KUnit: kernel-modules-internal is not installed for $KVER"
fi

if [ -x "$KSELFTEST_DIR/run_kselftest.sh" ]; then
    n=$(grep -cE '^[A-Za-z0-9_./-]+:[^[:space:]]+$' "$KSELFTEST_DIR/kselftest-list.txt" 2>/dev/null)
    c=$(grep -oE '^[A-Za-z0-9_./-]+:' "$KSELFTEST_DIR/kselftest-list.txt" 2>/dev/null | sort -u | wc -l)
    kt_ok "kselftest: $n tests in $c collections at $KSELFTEST_DIR"
else
    kt_warn "kselftest: kernel-selftests-internal is not installed"
fi

if [ -d "$LTP_DIR/runtest" ]; then
    n=$(find "$LTP_DIR/testcases/bin" -maxdepth 1 -type f 2>/dev/null | wc -l)
    s=$(find "$LTP_DIR/runtest" -maxdepth 1 -type f 2>/dev/null | wc -l)
    kt_ok "LTP: $n test programs, $s scenarios at $LTP_DIR"
    if [ -f "$LTP_DIR/kirk" ]; then
        if "$KT_PYTHON" "$LTP_DIR/kirk" --version >/dev/null 2>&1; then
            kt_ok "LTP runner (kirk) works under $KT_PYTHON"
        else
            kt_warn "kirk will not start under $KT_PYTHON; runltp will be used instead"
        fi
    fi
else
    kt_warn "LTP: not installed"
fi

kt_head "Result"
if [ "$PROBLEMS" = 0 ]; then
    kt_ok "ready.  Try:  ./test-kernel.sh --profile smoke"
    exit 0
fi
kt_err "$PROBLEMS problem(s) above.  Engines whose files are missing are"
kt_err "skipped rather than failing, so a partial install still runs."
exit 1
