#!/bin/bash
################################################################################
# Script Name: nfs-server-exporter.sh
# Version: 1.0
# Description: Prometheus exporter for NFS server metrics — export status,
#              thread pool utilization, per-operation counters, RPC errors,
#              connected clients, and server health
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - NFS server running (nfs-kernel-server / nfs-utils)
#   - /proc/net/rpc/nfsd available
#   - /proc/fs/nfsd available
#   - exportfs command available
#   - netcat (nc) for HTTP mode
#
# Usage:
#   sudo ./nfs-server-exporter.sh
#   sudo ./nfs-server-exporter.sh --http -p 9588
#   sudo ./nfs-server-exporter.sh --textfile
#
# Metrics Exported:
#   - nfsd_up - Exporter status (1=up, 0=down)
#   - nfsd_exports_total - Number of active exports
#   - nfsd_export_info{export,client,options} - Export configuration
#   - nfsd_threads_total - Configured NFS server threads
#   - nfsd_threads_busy - Threads currently handling requests
#   - nfsd_pool_packets_total{pool} - Packets received per pool
#   - nfsd_pool_sockets_enqueued_total{pool} - Sockets enqueued per pool
#   - nfsd_pool_threads_woken_total{pool} - Threads woken per pool
#   - nfsd_pool_threads_timedout_total{pool} - Thread timeouts per pool
#   - nfsd_rpc_calls_total - Total RPC calls received
#   - nfsd_rpc_bad_calls_total - Total bad RPC calls
#   - nfsd_rpc_bad_auth_total - Total bad auth attempts
#   - nfsd_rpc_bad_client_total - Total bad client calls
#   - nfsd_v3_ops_total{op} - NFSv3 per-operation counters
#   - nfsd_v4_ops_total{op} - NFSv4 per-operation counters
#   - nfsd_io_read_bytes_total - Total bytes read by server
#   - nfsd_io_write_bytes_total - Total bytes written by server
#   - nfsd_clients_total - Connected NFSv4 clients
#   - nfsd_file_handles_stale_total - Total stale file handle replies
#   - nfsd_exporter_duration_seconds - Script execution time
#   - nfsd_exporter_last_run_timestamp - Last successful run time
#
# Configuration:
#   Default HTTP port: 9588
#   Textfile directory: /var/lib/node_exporter
#
################################################################################

set -o pipefail

# ============================================================================
# CONFIGURATION VARIABLES
# ============================================================================

TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT=9588
LOCK_FILE="/tmp/nfs-server-exporter.lock"
PROC_NFSD="/proc/net/rpc/nfsd"
NFSD_DIR="/proc/fs/nfsd"

# ============================================================================
# LOGGING FUNCTIONS
# ============================================================================

RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m'

log()  { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error(){ echo -e "${RED}[ERROR]${NC} $*" >&2; }

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

show_usage() {
    cat <<EOF
Usage: $0 [OPTIONS]

Export NFS server statistics as Prometheus metrics.

MODES:
    --textfile      Write to node_exporter textfile collector
    --http          Run HTTP server on port $HTTP_PORT

OPTIONS:
    -p, --port      HTTP port (default: 9588)
    -o, --output    Output file path

EXAMPLES:
    sudo $0 --textfile
    sudo $0 --http --port 9588
    sudo $0 -o /tmp/nfsd.prom

EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help) show_usage ;;
            --textfile) OUTPUT_FILE="$TEXTFILE_DIR/nfsd.prom"; shift ;;
            --http) HTTP_MODE=true; shift ;;
            -p|--port) HTTP_PORT="$2"; shift 2 ;;
            -o|--output) OUTPUT_FILE="$2"; shift 2 ;;
            *) error "Unknown option: $1"; exit 1 ;;
        esac
    done
}

acquire_lock() {
    if [ -f "$LOCK_FILE" ]; then
        local lock_pid
        lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
        if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
            error "Another instance is running (PID $lock_pid)"
            return 1
        fi
        rm -f "$LOCK_FILE"
    fi
    echo $$ > "$LOCK_FILE"
    trap 'rm -f "$LOCK_FILE"' EXIT
    return 0
}

# Check if NFS server is running and proc files are available
check_nfsd() {
    if [ ! -f "$PROC_NFSD" ]; then
        error "$PROC_NFSD not found — is the NFS server running?"
        return 1
    fi
    if [ ! -d "$NFSD_DIR" ]; then
        error "$NFSD_DIR not found"
        return 1
    fi
    return 0
}

# Get number of configured NFS threads
# Returns: Thread count
get_thread_count() {
    if [ -f "$NFSD_DIR/threads" ]; then
        cat "$NFSD_DIR/threads" 2>/dev/null | head -1
    else
        echo "0"
    fi
}

# Get pool stats from /proc/fs/nfsd/pool_stats
# Returns: Lines with "pool packets sockets_enqueued threads_woken threads_timedout"
get_pool_stats() {
    if [ -f "$NFSD_DIR/pool_stats" ]; then
        # Skip header line
        tail -n +2 "$NFSD_DIR/pool_stats" 2>/dev/null
    fi
}

# Get RPC stats from /proc/net/rpc/nfsd
# The "rc" line: hits misses nocache
# The "io" line: read write
# The "rpc" line: calls badcalls badfmt badauth badclnt
get_rpc_calls() {
    awk '/^rpc / { print $2 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

get_rpc_bad_calls() {
    awk '/^rpc / { print $3 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

get_rpc_bad_auth() {
    awk '/^rpc / { print $5 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

get_rpc_bad_client() {
    awk '/^rpc / { print $6 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

# Get server I/O bytes from /proc/net/rpc/nfsd "io" line
get_io_read_bytes() {
    awk '/^io / { print $2 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

get_io_write_bytes() {
    awk '/^io / { print $3 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

# Get stale file handle count from /proc/net/rpc/nfsd "fh" line
# fh: stale total_lookups anonlookups dirnocache nondir_nocache
get_stale_handles() {
    awk '/^fh / { print $2 }' "$PROC_NFSD" 2>/dev/null || echo "0"
}

# Get NFSv3 per-operation counters from /proc/net/rpc/nfsd
# proc3 line: num_ops null getattr setattr lookup access readlink read write create mkdir symlink ...
get_v3_ops() {
    awk '/^proc3 / {
        ops[1]="null"; ops[2]="getattr"; ops[3]="setattr"; ops[4]="lookup"
        ops[5]="access"; ops[6]="readlink"; ops[7]="read"; ops[8]="write"
        ops[9]="create"; ops[10]="mkdir"; ops[11]="symlink"; ops[12]="mknod"
        ops[13]="remove"; ops[14]="rmdir"; ops[15]="rename"; ops[16]="link"
        ops[17]="readdir"; ops[18]="readdirplus"; ops[19]="fsstat"; ops[20]="fsinfo"
        ops[21]="pathconf"; ops[22]="commit"
        n = $2 + 0
        for (i = 1; i <= n && i <= 22; i++) {
            printf "%s %s\n", ops[i], $(i + 2)
        }
    }' "$PROC_NFSD" 2>/dev/null
}

# Get NFSv4 per-operation counters from /proc/net/rpc/nfsd
# proc4ops line: num_ops op0 op1 op2 ...
get_v4_ops() {
    awk '/^proc4ops / {
        ops[1]="op0-unused"; ops[2]="op1-unused"; ops[3]="access"; ops[4]="close"
        ops[5]="commit"; ops[6]="create"; ops[7]="delegpurge"; ops[8]="delegreturn"
        ops[9]="getattr"; ops[10]="getfh"; ops[11]="link"; ops[12]="lock"
        ops[13]="lockt"; ops[14]="locku"; ops[15]="lookup"; ops[16]="lookupp"
        ops[17]="nverify"; ops[18]="open"; ops[19]="openattr"; ops[20]="open_confirm"
        ops[21]="open_downgrade"; ops[22]="putfh"; ops[23]="putpubfh"; ops[24]="putrootfh"
        ops[25]="read"; ops[26]="readdir"; ops[27]="readlink"; ops[28]="remove"
        ops[29]="rename"; ops[30]="renew"; ops[31]="restorefh"; ops[32]="savefh"
        ops[33]="secinfo"; ops[34]="setattr"; ops[35]="setclientid"; ops[36]="setclientid_confirm"
        ops[37]="verify"; ops[38]="write"; ops[39]="release_lockowner"
        n = $2 + 0
        for (i = 1; i <= n && i <= 39; i++) {
            if (ops[i] !~ /unused/) {
                printf "%s %s\n", ops[i], $(i + 2)
            }
        }
    }' "$PROC_NFSD" 2>/dev/null
}

# Get active exports
# Tries /proc/fs/nfsd/exports first, then /var/lib/nfs/etab, then exportfs -v
# Returns: Lines with "export client(options)" — one per line
get_exports() {
    local raw=""

    # /proc/fs/nfsd/exports is most reliable (kernel export table)
    if [ -f "$NFSD_DIR/exports" ]; then
        raw=$(grep '^/' "$NFSD_DIR/exports" 2>/dev/null)
    fi

    # Fall back to etab (userspace export table)
    if [ -z "$raw" ] && [ -f /var/lib/nfs/etab ]; then
        raw=$(grep '^/' /var/lib/nfs/etab 2>/dev/null)
    fi

    # Fall back to exportfs command
    if [ -z "$raw" ]; then
        raw=$(/usr/sbin/exportfs -v 2>/dev/null || exportfs -v 2>/dev/null)
        # Merge continuation lines
        raw=$(echo "$raw" | awk '
            /^\// {
                if (line) print line
                line = $0
                next
            }
            { line = line " " $0 }
            END { if (line) print line }
        ')
    fi

    echo "$raw" | sed 's/[[:space:]]\+/ /g' | sed 's/^ //' | grep -v '^$'
}

# Count active exports
get_export_count() {
    local count
    count=$(get_exports | wc -l)
    echo "${count:-0}"
}

# Count connected NFSv4 clients
get_client_count() {
    if [ -d "$NFSD_DIR/clients" ]; then
        local count
        count=$(ls -1 "$NFSD_DIR/clients" 2>/dev/null | wc -l)
        echo "${count:-0}"
    else
        echo "0"
    fi
}

# ============================================================================
# METRIC GENERATION
# ============================================================================

generate_metrics() {
    local script_start
    script_start=$(date +%s)

    if ! check_nfsd; then
        cat <<EOF
# HELP nfsd_up NFS server exporter status
# TYPE nfsd_up gauge
nfsd_up 0
EOF
        return
    fi

    cat <<EOF
# HELP nfsd_up NFS server exporter status
# TYPE nfsd_up gauge
nfsd_up 1

EOF

    # Export count and info
    local export_count
    export_count=$(get_export_count)

    cat <<EOF
# HELP nfsd_exports_total Number of active NFS exports
# TYPE nfsd_exports_total gauge
nfsd_exports_total $export_count

# HELP nfsd_export_info Export configuration information
# TYPE nfsd_export_info gauge
EOF

    while IFS= read -r line; do
        [ -z "$line" ] && continue
        local export_path client_opts client opts
        export_path=$(echo "$line" | awk '{print $1}')
        client_opts=$(echo "$line" | awk '{print $2}')
        client="${client_opts%%(*}"
        opts="${client_opts#*(}"
        opts="${opts%)}"
        [ -z "$export_path" ] && continue
        echo "nfsd_export_info{export=\"$export_path\",client=\"$client\",options=\"$opts\"} 1"
    done < <(get_exports)

    echo ""

    # Thread pool metrics
    local threads
    threads=$(get_thread_count)

    cat <<EOF
# HELP nfsd_threads_total Configured NFS server threads
# TYPE nfsd_threads_total gauge
nfsd_threads_total ${threads:-0}

EOF

    # Pool stats
    local has_pool_stats=false
    if [ -f "$NFSD_DIR/pool_stats" ]; then
        has_pool_stats=true
    fi

    if [ "$has_pool_stats" = true ]; then
        cat <<EOF
# HELP nfsd_pool_packets_total Packets received per thread pool
# TYPE nfsd_pool_packets_total counter
EOF
        # pool_stats format: pool_num packets sockets_enqueued threads_woken threads_timedout
        while read -r pool_num packets sockets_enqueued threads_woken threads_timedout; do
            [ -z "$pool_num" ] && continue
            echo "nfsd_pool_packets_total{pool=\"$pool_num\"} $packets"
        done < <(get_pool_stats)

        echo ""

        cat <<EOF
# HELP nfsd_pool_sockets_enqueued_total Sockets enqueued per thread pool
# TYPE nfsd_pool_sockets_enqueued_total counter
EOF
        while read -r pool_num packets sockets_enqueued threads_woken threads_timedout; do
            [ -z "$pool_num" ] && continue
            echo "nfsd_pool_sockets_enqueued_total{pool=\"$pool_num\"} $sockets_enqueued"
        done < <(get_pool_stats)

        echo ""

        cat <<EOF
# HELP nfsd_pool_threads_woken_total Threads woken per thread pool
# TYPE nfsd_pool_threads_woken_total counter
EOF
        while read -r pool_num packets sockets_enqueued threads_woken threads_timedout; do
            [ -z "$pool_num" ] && continue
            echo "nfsd_pool_threads_woken_total{pool=\"$pool_num\"} $threads_woken"
        done < <(get_pool_stats)

        echo ""

        cat <<EOF
# HELP nfsd_pool_threads_timedout_total Thread timeouts per thread pool
# TYPE nfsd_pool_threads_timedout_total counter
EOF
        while read -r pool_num packets sockets_enqueued threads_woken threads_timedout; do
            [ -z "$pool_num" ] && continue
            echo "nfsd_pool_threads_timedout_total{pool=\"$pool_num\"} $threads_timedout"
        done < <(get_pool_stats)

        echo ""
    fi

    # RPC stats
    local rpc_calls rpc_bad rpc_bad_auth rpc_bad_client
    rpc_calls=$(get_rpc_calls)
    rpc_bad=$(get_rpc_bad_calls)
    rpc_bad_auth=$(get_rpc_bad_auth)
    rpc_bad_client=$(get_rpc_bad_client)

    cat <<EOF
# HELP nfsd_rpc_calls_total Total RPC calls received
# TYPE nfsd_rpc_calls_total counter
nfsd_rpc_calls_total ${rpc_calls:-0}

# HELP nfsd_rpc_bad_calls_total Total bad RPC calls
# TYPE nfsd_rpc_bad_calls_total counter
nfsd_rpc_bad_calls_total ${rpc_bad:-0}

# HELP nfsd_rpc_bad_auth_total Total bad authentication attempts
# TYPE nfsd_rpc_bad_auth_total counter
nfsd_rpc_bad_auth_total ${rpc_bad_auth:-0}

# HELP nfsd_rpc_bad_client_total Total bad client calls
# TYPE nfsd_rpc_bad_client_total counter
nfsd_rpc_bad_client_total ${rpc_bad_client:-0}

EOF

    # Server I/O bytes
    local io_read io_write
    io_read=$(get_io_read_bytes)
    io_write=$(get_io_write_bytes)

    cat <<EOF
# HELP nfsd_io_read_bytes_total Total bytes read by NFS server
# TYPE nfsd_io_read_bytes_total counter
nfsd_io_read_bytes_total ${io_read:-0}

# HELP nfsd_io_write_bytes_total Total bytes written by NFS server
# TYPE nfsd_io_write_bytes_total counter
nfsd_io_write_bytes_total ${io_write:-0}

EOF

    # Stale file handles
    local stale
    stale=$(get_stale_handles)

    cat <<EOF
# HELP nfsd_file_handles_stale_total Total stale file handle replies
# TYPE nfsd_file_handles_stale_total counter
nfsd_file_handles_stale_total ${stale:-0}

EOF

    # NFSv3 per-operation counters
    local v3_data
    v3_data=$(get_v3_ops)

    if [ -n "$v3_data" ]; then
        cat <<EOF
# HELP nfsd_v3_ops_total NFSv3 per-operation call count
# TYPE nfsd_v3_ops_total counter
EOF
        while read -r op count; do
            [ -z "$op" ] && continue
            echo "nfsd_v3_ops_total{op=\"$op\"} ${count:-0}"
        done <<< "$v3_data"

        echo ""
    fi

    # NFSv4 per-operation counters
    local v4_data
    v4_data=$(get_v4_ops)

    if [ -n "$v4_data" ]; then
        cat <<EOF
# HELP nfsd_v4_ops_total NFSv4 per-operation call count
# TYPE nfsd_v4_ops_total counter
EOF
        while read -r op count; do
            [ -z "$op" ] && continue
            echo "nfsd_v4_ops_total{op=\"$op\"} ${count:-0}"
        done <<< "$v4_data"

        echo ""
    fi

    # Connected clients
    local clients
    clients=$(get_client_count)

    cat <<EOF
# HELP nfsd_clients_total Connected NFSv4 clients
# TYPE nfsd_clients_total gauge
nfsd_clients_total ${clients:-0}

EOF

    # Exporter runtime
    local script_end script_duration
    script_end=$(date +%s)
    script_duration=$((script_end - script_start))

    cat <<EOF
# HELP nfsd_exporter_duration_seconds Time to generate all metrics
# TYPE nfsd_exporter_duration_seconds gauge
nfsd_exporter_duration_seconds $script_duration

# HELP nfsd_exporter_last_run_timestamp Unix timestamp of last successful run
# TYPE nfsd_exporter_last_run_timestamp gauge
nfsd_exporter_last_run_timestamp $script_end
EOF

    echo ""
}

# ============================================================================
# HTTP SERVER MODE
# ============================================================================

run_http_server() {
    log "Starting NFS server exporter on port $HTTP_PORT..."

    if ! command -v nc >/dev/null 2>&1; then
        error "netcat (nc) required for HTTP mode"
        exit 1
    fi

    while true; do
        {
            read -r request
            if [[ "$request" =~ ^GET\ /metrics ]]; then
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r"
                generate_metrics
            else
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r"
                echo "<html><head><title>NFS Server Exporter</title></head><body><h1>NFS Server Prometheus Exporter</h1><p><a href=\"/metrics\">Metrics</a></p></body></html>"
            fi
        } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
    done
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

main() {
    parse_args "$@"

    if ! acquire_lock; then
        exit 1
    fi

    if [ "$HTTP_MODE" = true ]; then
        run_http_server
    elif [ -n "$OUTPUT_FILE" ]; then
        local output_dir
        output_dir="$(dirname "$OUTPUT_FILE")"
        mkdir -p "$output_dir"

        local temp_file
        temp_file=$(mktemp "${output_dir}/.nfsd_metrics.XXXXXX")

        if ! generate_metrics > "$temp_file" 2>/dev/null; then
            rm -f "$temp_file"
            error "Failed to generate metrics"
            exit 1
        fi

        local file_lines
        file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0)

        if [ "$file_lines" -lt 5 ]; then
            rm -f "$temp_file"
            error "Metrics file too small ($file_lines lines), keeping previous"
            exit 1
        fi

        chmod 644 "$temp_file"
        mv -f "$temp_file" "$OUTPUT_FILE"

        log "Metrics written to $OUTPUT_FILE ($file_lines lines)"
    else
        generate_metrics
    fi
}

main "$@"
