#!/usr/bin/env bash
# =============================================================================
#  start-nfs.sh - install (offline) and start the NFS server daemon on
#                 RHEL 9.6, with a test export mounted and ready for
#                 performance testing
# =============================================================================
#
#  USAGE (as root)
#      ./start-nfs.sh            install if needed, configure, start, mount
#      ./start-nfs.sh start      same as above
#      ./start-nfs.sh stop       unmount the test export, stop the NFS server
#      ./start-nfs.sh restart    stop, then start again
#      ./start-nfs.sh status     show the server, the export and the mount
#      ./start-nfs.sh reset      delete the test data files on the export
#      ./start-nfs.sh cleanup    stop and remove everything the kit created
#
#  WHAT "start" DOES, STEP BY STEP
#      1. Checks the operating system (RHEL 9 expected).
#      2. Installs nfs-utils and fio if missing, WITHOUT internet:
#           a) from RPM files in ./rpms/  (see download-rpms.sh), or
#           b) from a local dnf repository already set up on the host
#              (for example the mounted RHEL 9.6 DVD / ISO).
#      3. Creates the shared folder /srv/nfs-perf-test.
#      4. Writes the test export (for 127.0.0.1 only) and the nfsd thread count.
#      5. Checks the SELinux settings NFS needs.
#      6. Starts nfs-server.service (or reloads the exports if it runs).
#      7. Mounts the test export on /mnt/nfs-perf-test and checks a write.
#
#  SAFE FOR A HOST THAT ALREADY SERVES NFS
#      Existing exports in /etc/exports are not changed; the kit adds its own
#      file /etc/exports.d/nfs-perf-test.exports. The test export can be
#      mounted only from 127.0.0.1, so it cannot be reached from the network.
#      "stop" keeps the server running when it exports other folders.
#      "cleanup" removes everything from steps 3-7.
# =============================================================================

set -euo pipefail
source "$(dirname -- "${BASH_SOURCE[0]}")/lib/common.sh"


# ----------------------------------------------------------------------------
#  Step 1 - operating system check
# ----------------------------------------------------------------------------
check_operating_system() {
    # /etc/os-release defines NAME, VERSION_ID, ...
    source /etc/os-release

    if [[ ${VERSION_ID%%.*} != 9 ]]; then
        die "This kit is made for RHEL 9. Found: $PRETTY_NAME"
    fi
    if [[ $ID != rhel || $VERSION_ID != 9.6 ]]; then
        warn "Target is RHEL 9.6; this host is '$PRETTY_NAME'. Continuing."
    fi
    log "Operating system: $PRETTY_NAME"
}


# ----------------------------------------------------------------------------
#  Step 2 - install the NFS server and fio without internet access
# ----------------------------------------------------------------------------
#  nfs-utils  the NFS server programs (the server itself is in the kernel)
#  fio        the standard Linux storage benchmark, used by test-nfs.sh
install_packages_offline() {
    local packages=(nfs-utils fio)
    local missing=() package
    for package in "${packages[@]}"; do
        rpm -q "$package" >/dev/null 2>&1 || missing+=("$package")
    done

    if (( ${#missing[@]} == 0 )); then
        log "Already installed: $(rpm -q "${packages[@]}" | tr '\n' ' ')"
    else
        local rpm_dir="$KIT_DIR/rpms"

        if compgen -G "$rpm_dir/*.rpm" >/dev/null; then
            # a) RPM files shipped next to this script. All other repositories
            #    are disabled so dnf never tries to reach the internet.
            log "Installing ${missing[*]} from RPM files in $rpm_dir"
            dnf install -y --disablerepo='*' "$rpm_dir"/*.rpm
        else
            # b) A local repository already configured on this host (RHEL DVD).
            #    nfs-utils is in BaseOS, fio in AppStream.
            log "Installing ${missing[*]} from the local dnf repositories"
            if ! dnf install -y "${missing[@]}"; then
                die "Could not install ${missing[*]} offline.
       Either copy RPMs into $rpm_dir (see download-rpms.sh),
       or mount the RHEL 9.6 DVD and configure BaseOS + AppStream as local repositories."
            fi
        fi
        ok "Installed $(rpm -q "${packages[@]}" | tr '\n' ' ')"
    fi

    # The small-file test tool needs Python 3, part of every RHEL 9 installation.
    command -v python3 >/dev/null || die "python3 not found (package python3)."
}


# ----------------------------------------------------------------------------
#  Step 3 - the shared folder
# ----------------------------------------------------------------------------
#  With "root_squash" (the default), root on the client works as the user
#  "nobody" on the server, so the folder belongs to nobody.
create_export_dir() {
    install -d -o nobody -g nobody -m 755 "$EXPORT_DIR"
    command -v restorecon >/dev/null && restorecon "$EXPORT_DIR"

    # Some file systems cannot be exported without an "fsid=" option.
    local fs_type
    fs_type="$(df --output=fstype "$EXPORT_DIR" | tail -1)"
    case "$fs_type" in
        overlay)
            die "$EXPORT_DIR is on an overlay file system (a container layer),
       which NFS cannot export. Choose another EXPORT_DIR in settings.conf." ;;
        tmpfs)
            [[ $EXPORT_OPTIONS == *fsid=* ]] ||
                die "$EXPORT_DIR is on tmpfs; add an option like fsid=1234 to EXPORT_OPTIONS." ;;
    esac

    local free_mb need_mb
    free_mb="$(df --output=avail -m "$EXPORT_DIR" | tail -1 | tr -d ' ')"
    need_mb=$(( DATA_FILE_MB * SEQ_STREAMS + 1024 ))
    if (( free_mb < need_mb )); then
        die "Only $free_mb MB free in $EXPORT_DIR; the tests need about $need_mb MB.
       Lower DATA_FILE_MB or choose another EXPORT_DIR in settings.conf."
    fi
    log "Shared folder: $EXPORT_DIR ($fs_type, $free_mb MB free)"
}


# ----------------------------------------------------------------------------
#  Step 4 - the export and the server settings
# ----------------------------------------------------------------------------
write_server_config() {
    # Remember (once) whether the NFS server was running before the kit
    # started it, so that "cleanup" does not stop a server others use.
    if [[ ! -f $TEST_STATE_FILE ]]; then
        if nfs_server_is_running; then
            echo "server_was_running=yes" > "$TEST_STATE_FILE"
        else
            echo "server_was_running=no" > "$TEST_STATE_FILE"
        fi
    fi

    log "Writing $TEST_EXPORTS_FILE"
    mkdir -p "$(dirname "$TEST_EXPORTS_FILE")"
    cat > "$TEST_EXPORTS_FILE" <<EOF
# Created by $KIT_DIR/start-nfs.sh - "./start-nfs.sh cleanup" removes it.
# Test export for the NFS performance kit, for this machine only.
# Format: <folder> <client>(<options>)      see: man 5 exports
$EXPORT_DIR $EXPORT_CLIENT($EXPORT_OPTIONS)
EOF

    # /etc/nfs.conf.d/*.conf files are read after /etc/nfs.conf and
    # override it, so /etc/nfs.conf itself stays unchanged.
    log "Writing $TEST_NFS_CONF (nfsd threads = $NFSD_THREADS)"
    mkdir -p "$(dirname "$TEST_NFS_CONF")"
    cat > "$TEST_NFS_CONF" <<EOF
# Created by $KIT_DIR/start-nfs.sh - "./start-nfs.sh cleanup" removes it.
# Overrides /etc/nfs.conf for the NFS performance kit.  See: man 5 nfs.conf
[nfsd]
threads=$NFSD_THREADS
EOF

    command -v restorecon >/dev/null && restorecon "$TEST_EXPORTS_FILE" "$TEST_NFS_CONF"
    return 0
}


# ----------------------------------------------------------------------------
#  Step 5 - SELinux
# ----------------------------------------------------------------------------
#  The kernel NFS server may share any folder as long as the SELinux
#  booleans nfs_export_all_rw / nfs_export_all_ro are on (RHEL default).
check_selinux() {
    if ! selinux_is_on; then
        log "SELinux is disabled - nothing to check"
        return
    fi
    local boolean
    for boolean in nfs_export_all_rw nfs_export_all_ro; do
        if [[ $(getsebool "$boolean" | awk '{ print $3 }') != on ]]; then
            warn "SELinux boolean $boolean is off; the export may be refused.
       Switch it on with:  setsebool $boolean on"
        fi
    done
    log "SELinux mode: $(getenforce)"
}


# ----------------------------------------------------------------------------
#  Step 6 - start the NFS server
# ----------------------------------------------------------------------------
start_server() {
    if nfs_server_is_running; then
        log "The NFS server is running - reloading the exports"
        reload_exports
        # Change the number of threads without a restart.
        rpc.nfsd "$NFSD_THREADS"
    else
        log "Starting the NFS server daemon"
        nfs_start
    fi

    if ! wait_until_nfs_answers 30; then
        if has_systemd; then
            die "The NFS server does not answer within 30 s.
       Look at:  journalctl -u $NFS_SERVICE -u nfs-mountd"
        else
            die "The NFS server does not answer within 30 s. Look at: dmesg | tail"
        fi
    fi

    if ! exportfs -s | awk '{ print $1 }' | grep -qxF "$EXPORT_DIR"; then
        die "The NFS server runs, but does not export $EXPORT_DIR.
       Check the export line with:  exportfs -rv"
    fi
    log "NFS server answers; $(nfsd_thread_count) nfsd threads"
}


# ----------------------------------------------------------------------------
#  Step 7 - mount the test export (as a client on this same machine)
# ----------------------------------------------------------------------------
mount_test_export() {
    # Mount again every time, so changed mount options are used.
    unmount_test_export
    mkdir -p "$MOUNT_POINT"

    log "Mounting $(mount_source) on $MOUNT_POINT"
    if ! timeout 60 mount -t nfs -o "$(mount_options)" "$(mount_source)" "$MOUNT_POINT"; then
        die "Mounting the test export failed.
       Try it by hand:  mount -v -t nfs -o $(mount_options) $(mount_source) $MOUNT_POINT"
    fi

    # Write, read and delete one file, to be sure the whole setup works.
    local probe="$MOUNT_POINT/.start-nfs-probe"
    if ! { echo "hello" > "$probe" && grep -q hello "$probe" && rm -f "$probe"; }; then
        die "The export is mounted, but a file cannot be written.
       Check that $EXPORT_DIR belongs to 'nobody' (root_squash), and with SELinux:
       ausearch -m avc -ts recent"
    fi
    log "Test write through NFS works"
}

unmount_test_export() {
    if test_mount_is_mounted; then
        log "Unmounting $MOUNT_POINT"
        # "-l" (lazy) as a fallback, so a hanging server cannot block us.
        umount "$MOUNT_POINT" 2>/dev/null || umount -l "$MOUNT_POINT"
    fi
}

print_summary() {
    local selinux
    selinux="$(getenforce 2>/dev/null || echo 'not available')"

    echo
    ok "The NFS server is running and the test export is mounted."
    echo "    Version        : $(nfs_utils_version) (server in kernel $(uname -r))"
    echo "    nfsd threads   : $(nfsd_thread_count)"
    echo "    Export         : $EXPORT_DIR  ->  $EXPORT_CLIENT($EXPORT_OPTIONS)"
    echo "    Mounted on     : $MOUNT_POINT"
    echo "    Mount options  : $(active_mount_options)"
    echo "    Config files   : $TEST_EXPORTS_FILE, $TEST_NFS_CONF"
    echo "    SELinux        : $selinux"
    if has_systemd; then
        echo "    Managed by     : systemd  (systemctl status $NFS_SERVICE)"
    else
        echo "    Managed by     : this script (no systemd, e.g. inside a container)"
    fi
    echo
    echo "    Try it         : ls -l $MOUNT_POINT ; nfsstat -m"
    echo "    Next step      : ./test-nfs.sh"
}


# ----------------------------------------------------------------------------
#  Other actions
# ----------------------------------------------------------------------------
stop_server() {
    unmount_test_export

    local others
    others="$(other_exports)"
    if [[ -n $others ]]; then
        # Other folders are shared too: only take the test export away.
        exportfs -u "$EXPORT_CLIENT:$EXPORT_DIR" 2>/dev/null || true
        warn "The NFS server also exports other folders, so it keeps running:"
        sed 's/^/           /' <<< "$others" >&2
        ok "The test export is unmounted and no longer shared."
    else
        nfs_stop
        ok "The NFS server is stopped."
    fi
}

show_status() {
    if ! nfs_server_is_running; then
        warn "The NFS server is not running. Run: ./start-nfs.sh"
        exit 1
    fi
    if ! nfs_answers; then
        warn "The NFS server has threads, but does not answer. Run: ./start-nfs.sh restart"
        exit 1
    fi

    ok "The NFS server is running ($(nfsd_thread_count) nfsd threads)"
    echo "    Exports:"
    exportfs -s | sed 's/^/        /'
    local clients
    clients="$(find /proc/fs/nfsd/clients -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)"
    echo "    NFSv4 clients connected : $clients"
    if test_mount_is_mounted; then
        echo "    Test mount              : $MOUNT_POINT"
        echo "    Mount options           : $(active_mount_options)"
    else
        echo "    Test mount              : NOT mounted (run ./start-nfs.sh)"
    fi
    echo "    Test data on the export : $(du -sh "$EXPORT_DIR" 2>/dev/null | cut -f1)"
}

# Delete the test data files (they are created again by the next test run).
reset_test_data() {
    log "Deleting the test data in $EXPORT_DIR"
    find "$EXPORT_DIR" -mindepth 1 -delete 2>/dev/null || true
    ok "Test data deleted."
}

cleanup_test_setup() {
    unmount_test_export
    rmdir "$MOUNT_POINT" 2>/dev/null || true

    log "Removing the test export and the server settings"
    rm -f "$TEST_EXPORTS_FILE" "$TEST_NFS_CONF"
    if nfs_server_is_running; then
        reload_exports                # the kernel forgets the test export
    fi

    # Stop the server only if the kit started it and it shares nothing else.
    if grep -q '^server_was_running=no' "$TEST_STATE_FILE" 2>/dev/null &&
       nfs_server_is_running && [[ -z $(other_exports) ]]; then
        log "Stopping the NFS server (the kit started it)"
        nfs_stop
    elif nfs_server_is_running; then
        log "The NFS server keeps running (it was running before the kit, or it shares other folders)."
        log "It uses $NFSD_THREADS threads until its next restart."
    fi
    rm -f "$TEST_STATE_FILE"

    log "Removing the shared folder $EXPORT_DIR"
    find "$EXPORT_DIR" -mindepth 1 -delete 2>/dev/null || true
    rmdir "$EXPORT_DIR" 2>/dev/null || true       # stays if it is a mount point

    ok "Cleanup finished. (The RPMs stay installed; remove them with: dnf remove fio)"
}


# ----------------------------------------------------------------------------
#  Main
# ----------------------------------------------------------------------------
main() {
    local action="${1:-start}"
    require_root "$@"

    case "$action" in
        start)
            check_operating_system
            install_packages_offline
            create_export_dir
            write_server_config
            check_selinux
            start_server
            mount_test_export
            print_summary
            ;;
        stop)
            stop_server
            ;;
        restart)
            stop_server
            "$0" start
            ;;
        status)
            show_status
            ;;
        reset)
            reset_test_data
            ;;
        cleanup)
            cleanup_test_setup
            ;;
        *)
            echo "Usage: $0 [start|stop|restart|status|reset|cleanup]"
            exit 2
            ;;
    esac
}

main "$@"
