#!/usr/bin/env bash
# =============================================================================
#  start-dhcp.sh - install (offline) and start the ISC DHCP server daemon
#                  (dhcpd) on RHEL 9.6, ready for performance testing
# =============================================================================
#
#  USAGE (as root)
#      ./start-dhcp.sh            install if needed, configure, start
#      ./start-dhcp.sh start      same as above
#      ./start-dhcp.sh stop       stop the test DHCP server
#      ./start-dhcp.sh restart    stop, then start again
#      ./start-dhcp.sh status     show whether the DHCP server is running
#      ./start-dhcp.sh reset      stop, empty the lease database, start
#      ./start-dhcp.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 the dhcp-server RPM 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 a private test network: a virtual cable (veth pair) from
#         the host to a network namespace where the simulated clients live.
#      4. Writes the test configuration /etc/dhcp/dhcpd-perf-test.conf and
#         an empty lease database /var/lib/dhcpd/dhcpd-perf-test.leases.
#      5. Checks the configuration with "dhcpd -t".
#      6. Creates the systemd service dhcpd-perf-test.service.
#      7. Starts the daemon, then waits until it answers a DHCP DISCOVER.
#
#  SAFE FOR A HOST THAT ALREADY RUNS DHCP
#      The normal dhcpd.service and /etc/dhcp/dhcpd.conf are not changed.
#      The test server listens only on the virtual cable, so it never
#      answers a real client. "cleanup" removes everything from steps 3-6.
# =============================================================================

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 DHCP server without internet access
# ----------------------------------------------------------------------------
install_dhcp_offline() {
    if rpm -q dhcp-server >/dev/null 2>&1; then
        log "DHCP server is already installed: $(rpm -q dhcp-server)"
    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 dhcp-server 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).
            log "Installing dhcp-server from the local dnf repositories"
            if ! dnf install -y dhcp-server; then
                die "Could not install dhcp-server offline.
       Either copy RPMs into $rpm_dir (see download-rpms.sh),
       or mount the RHEL 9.6 DVD and configure it as a local repository."
            fi
        fi
        ok "Installed $(rpm -q dhcp-server)"
    fi

    # The load generator needs Python 3 and the "ip" command. Both are part
    # of every RHEL 9 installation (dnf itself needs Python 3).
    command -v python3 >/dev/null || die "python3 not found (package python3)."
    command -v ip      >/dev/null || die "'ip' command not found (package iproute)."
}


# ----------------------------------------------------------------------------
#  Step 3 - private test network
# ----------------------------------------------------------------------------
#
#     host (dhcpd listens here)                 namespace "dhcp-perf"
#     +-------------------------+   virtual   +--------------------------+
#     | dhcp-srv   10.199.0.1   |<===========>| dhcp-cli  (test clients) |
#     +-------------------------+    cable    +--------------------------+
#
create_test_network() {
    if test_network_exists; then
        log "Test network already exists ($SERVER_IFACE <-> $TEST_NETNS/$CLIENT_IFACE)"
        return
    fi
    log "Creating test network: $SERVER_IFACE ($SERVER_IP) <-> namespace $TEST_NETNS"

    # Remove half-created leftovers from an earlier failed attempt.
    ip link delete "$SERVER_IFACE" 2>/dev/null || true
    ip netns delete "$TEST_NETNS" 2>/dev/null || true

    # The namespace: a separate network stack for the simulated clients.
    ip netns add "$TEST_NETNS"

    # The virtual cable. One end stays on the host, the other goes into the namespace.
    ip link add "$SERVER_IFACE" type veth peer name "$CLIENT_IFACE" netns "$TEST_NETNS"

    # Server end: fixed address, like the network card of a real DHCP server.
    ip addr add "$SERVER_IP/$PREFIX_LENGTH" dev "$SERVER_IFACE"
    ip link set "$SERVER_IFACE" up

    # Client end: an address only so that the clients can send packets.
    ip -n "$TEST_NETNS" addr add "$CLIENT_IP/$PREFIX_LENGTH" dev "$CLIENT_IFACE"
    ip -n "$TEST_NETNS" link set "$CLIENT_IFACE" up
    ip -n "$TEST_NETNS" link set lo up

    # Tell NetworkManager (if running) to leave the test interface alone.
    if command -v nmcli >/dev/null && nmcli -t general status >/dev/null 2>&1; then
        nmcli device set "$SERVER_IFACE" managed no 2>/dev/null || true
    fi
}


# ----------------------------------------------------------------------------
#  Step 4 - dhcpd configuration and lease database
# ----------------------------------------------------------------------------
write_test_config() {
    log "Writing $TEST_CONF"
    cat > "$TEST_CONF" <<EOF
# =============================================================================
#  Created by $KIT_DIR/start-dhcp.sh
#  Values come from settings.conf. "./start-dhcp.sh cleanup" removes this file.
#  Used only by $TEST_SERVICE.service, never by the normal dhcpd.service.
# =============================================================================

# This server is the only DHCP server on the test network, so it may answer
# "no" (DHCPNAK) to clients that ask for a wrong address.
authoritative;

# Normally dhcpd first "pings" an address before offering it, and waits up
# to 1 second for an answer. That protects against address conflicts on a
# real network, but on the private test network it would only add delay.
ping-check false;

# Write every renewal to the lease database. (By default dhcpd skips the
# write if a client renews very soon after the last write; real clients
# renew at half the lease time, so they always cause a write.)
dhcp-cache-threshold 0;

default-lease-time $LEASE_TIME;
max-lease-time     $MAX_LEASE_TIME;

# The test subnet. dhcpd listens on $SERVER_IFACE ($SERVER_IP), which is in it.
subnet $SUBNET netmask $NETMASK {
    range $POOL_START $POOL_END;
    option routers             $SERVER_IP;
    option domain-name-servers $SERVER_IP;
    option domain-name         "perf-test.example";
}
EOF

    # dhcpd will not start without a lease database file, so create an empty one.
    if [[ ! -f $TEST_LEASES ]]; then
        log "Creating empty lease database $TEST_LEASES"
        touch "$TEST_LEASES"
    fi
    chown dhcpd:dhcpd "$TEST_LEASES"

    # Give the files the SELinux labels dhcpd is allowed to use.
    if command -v restorecon >/dev/null; then
        restorecon "$TEST_CONF" "$TEST_LEASES"
    fi
}


# ----------------------------------------------------------------------------
#  Step 5 - configuration check
# ----------------------------------------------------------------------------
check_config() {
    log "Checking dhcpd configuration (dhcpd -t)"
    if ! /usr/sbin/dhcpd -4 -t -q -cf "$TEST_CONF"; then
        die "The dhcpd configuration has errors (see messages above)."
    fi
}


# ----------------------------------------------------------------------------
#  Step 6 - systemd service
# ----------------------------------------------------------------------------
#  A copy of RHEL's own dhcpd.service, with the test config, the test lease
#  file and the test interface. It has no [Install] section on purpose: the
#  test network disappears at reboot, so the service must not start at boot.
write_systemd_unit() {
    has_systemd || return 0

    log "Writing systemd service $TEST_UNIT_FILE"
    cat > "$TEST_UNIT_FILE" <<EOF
# Created by $KIT_DIR/start-dhcp.sh - "./start-dhcp.sh cleanup" removes it.
[Unit]
Description=DHCPv4 server for performance testing (DHCP perf kit)
Documentation=man:dhcpd(8) man:dhcpd.conf(5)
After=network.target

[Service]
Type=notify
# Fail with a clear message if the test network was not created.
ExecStartPre=/usr/sbin/ip link show $SERVER_IFACE
ExecStart=/usr/sbin/dhcpd -f -4 -cf $TEST_CONF -lf $TEST_LEASES -user dhcpd -group dhcpd --no-pid $SERVER_IFACE
StandardError=null
EOF

    if command -v restorecon >/dev/null; then
        restorecon "$TEST_UNIT_FILE"
    fi
    systemctl daemon-reload
}


# ----------------------------------------------------------------------------
#  Step 7 - start the daemon
# ----------------------------------------------------------------------------
start_daemon() {
    if dhcp_is_running; then
        log "The test DHCP server is running - restarting it to apply the configuration"
        dhcp_restart
    else
        log "Starting the DHCP server daemon"
        dhcp_start
    fi

    if ! wait_until_serving 30; then
        if has_systemd; then
            die "The DHCP server does not answer within 30 s.
       Look at:  journalctl -u $TEST_SERVICE"
        else
            die "The DHCP server does not answer within 30 s.
       Run it in the foreground to see why:  $(dhcpd_direct_command) -d"
        fi
    fi
}

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

    echo
    ok "The DHCP server is running and handing out addresses."
    echo "    Version      : $(dhcpd_version)"
    echo "    Main PID     : $(dhcp_main_pid)"
    echo "    Listens on   : $SERVER_IFACE ($SERVER_IP), test network only"
    echo "    Address pool : $POOL_START - $POOL_END"
    echo "    Config file  : $TEST_CONF"
    echo "    Lease file   : $TEST_LEASES ($(du -h "$TEST_LEASES" | cut -f1))"
    echo "    SELinux      : $selinux"
    if has_systemd; then
        echo "    Managed by   : systemd  (systemctl status $TEST_SERVICE)"
    else
        echo "    Managed by   : PID file $TEST_PIDFILE (no systemd, e.g. inside a container)"
    fi
    echo
    echo "    Next step    : ./test-dhcp.sh"
}


# ----------------------------------------------------------------------------
#  Other actions
# ----------------------------------------------------------------------------
show_status() {
    if dhcp_is_serving; then
        ok "The test DHCP server is running (PID $(dhcp_main_pid)) and answers on $SERVER_IFACE"
        echo "    Lease file : $TEST_LEASES ($(du -h "$TEST_LEASES" | cut -f1))"
        echo "    Leases     : $(grep -c '^lease ' "$TEST_LEASES" 2>/dev/null || true) records in the file"
    elif dhcp_is_running; then
        warn "dhcpd is running, but does not answer. Run: ./start-dhcp.sh"
        exit 1
    else
        warn "The test DHCP server is not running."
        exit 1
    fi
}

# Empty the lease database: every test run adds records to it, and a big
# lease file makes dhcpd start slower.
reset_leases() {
    log "Emptying the lease database $TEST_LEASES"
    empty_lease_database
    ok "Lease database emptied and DHCP server restarted."
}

cleanup_test_setup() {
    log "Stopping the test DHCP server"
    dhcp_stop

    log "Removing $TEST_UNIT_FILE, $TEST_CONF and $TEST_LEASES"
    rm -f "$TEST_UNIT_FILE" "$TEST_CONF" "$TEST_LEASES" "$TEST_LEASES~" "$TEST_PIDFILE"
    has_systemd && systemctl daemon-reload

    log "Removing the test network ($SERVER_IFACE and namespace $TEST_NETNS)"
    ip link delete "$SERVER_IFACE" 2>/dev/null || true
    ip netns delete "$TEST_NETNS" 2>/dev/null || true

    ok "Cleanup finished. (The dhcp-server RPM stays installed; remove it with: dnf remove dhcp-server)"
}


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

    case "$action" in
        start)
            check_operating_system
            install_dhcp_offline
            create_test_network
            write_test_config
            check_config
            write_systemd_unit
            start_daemon
            print_summary
            ;;
        stop)
            dhcp_stop
            ok "The test DHCP server is stopped."
            ;;
        restart)
            dhcp_restart
            wait_until_serving 30 || die "The DHCP server did not come back. Run ./start-dhcp.sh"
            ok "The test DHCP server restarted."
            ;;
        status)
            show_status
            ;;
        reset)
            reset_leases
            ;;
        cleanup)
            cleanup_test_setup
            ;;
        *)
            echo "Usage: $0 [start|stop|restart|status|reset|cleanup]"
            exit 2
            ;;
    esac
}

main "$@"
