#!/bin/bash
# check_pve 1.3 - Proxmox Virtual Environment server check
# For NEMS Linux (Compatible with Nagios / Icinga / etc)
# By Robbie Ferguson // https://nemslinux.com/
#
# 1.0 - March 14, 2024 - Initial release.
# 1.1 - March 15, 2024 - Add cache for PVE ticket and PVE Version JSON
# 1.2 - March 17, 2024 - Invalidate ticket if password is changed in NConf, clean up old cache files after 2 hours,
#                        change ticket cache name to distinguish from version cache, use decimal as comparison for version (8.1 instead of 8.1.4)
# 1.3 - March 18, 2024 - Retry ticket generation 5 times before giving up, Rephrase output, loadavg is converted to percentage
#
# NConf checkcommand:
# $USER1$/check_pve ip=$HOSTADDRESS$ port=$ARG1$ node=$ARG2$ username="$ARG3$" password="$ARG4$" realm=$ARG5$ check=$ARG6$ warn=$ARG7$ crit=$ARG8$
# !8006!!!!!!80!95
# Port,Node Name,PVEAuditor Username,PVEAuditor Password,Realm (Eg. pve or pam),Check (load rootfs version memory swap),Warning %,Critical %

# Cleanup old ticket caches after 2 hour age
find /tmp/ -name pve_ticket_*.cache -type f -mmin +120 -delete

for ARGUMENT in "$@"
do
   KEY=$(echo $ARGUMENT | cut -f1 -d=)

   KEY_LENGTH=${#KEY}
   VALUE="${ARGUMENT:$KEY_LENGTH+1}"

   export "$KEY"="$VALUE"
done

# Check optional variables and set defaults if not set or invalid
if [[ -z "$warn" || $warn -lt 0 || $warn -gt 100 ]]; then
    warn=80
fi
if [[ -z "$crit" || $crit -lt 0 || $crit -gt 100 ]]; then
    crit=95
fi
if [[ -z "$port" || ! "$port" =~ ^[0-9]+$ ]]; then
    port=8006
fi

# Create a new ticket cache every 90 minutes (tickets are valid for 2 hours)
# Use the same ticket for all checks based on username+password (hashed into the cache filename)
hash=$(echo -n "${username}${password}" | md5sum | cut -d' ' -f1)
ticket_cache_file="/tmp/pve_ticket_${hash}.cache"
cache_max_age=$(date -d "now - 90 minutes" +%s)
if [ ! -e "$ticket_cache_file" ] || [ "$ticket_cache_file" -ot "$cache_max_age" ]; then
  retries=5
  attempt=1
  while [ $attempt -le $retries ]; do
    # Get a ticket from the Proxmox VE host
    auth=`curl -s -k -d "username=${username}@${realm}" --data-urlencode "password=${password}" https://${ip}:${port}/api2/json/access/ticket`
    # Check if the JSON response contains {"data":null}
    if [[ $(echo "$auth" | jq '.data == null') == true ]]; then
      # A null response was received, so try again
      ((attempt++))
    else
      # A valid response was received, so proceed
      break
    fi

    # If this is the last attempt, give up, otherwise, pause before retrying
    if [ $attempt -gt $retries ]; then
      echo "Proxmox rejected Username/Password."
      exit 3 # Unknown state
    else
      sleep 5
    fi
  done
  ticket=`echo $auth | jq --raw-output '.data.ticket'`
  echo $ticket | sed 's/^/PVEAuthCookie=/' > $ticket_cache_file
fi
ticket=$(< "$ticket_cache_file")
if [[ $ticket == 'PVEAuthCookie=null' ]]; then
  echo "Proxmox rejected Username/Password."
  exit 3 # Unknown State
fi

# Connect to the API using our current ticket
json_data=`curl -s --insecure --cookie "${ticket}" https://${ip}:${port}/api2/json/nodes/${node}/status | jq "."`
if [[ -z $json_data ]]; then
  echo "Proxmox did not send a valid response."
  exit 3 # Unknown State
fi

check_load() {
    # Parse JSON data and extract relevant fields
    cpus=$(echo "$json_data" | jq -r '.data.cpuinfo.cpus')
    loadavg=$(echo "$json_data" | jq -r '.data.loadavg[0]')
    loadavg5=$(echo "$json_data" | jq -r '.data.loadavg[1]')
    loadavg15=$(echo "$json_data" | jq -r '.data.loadavg[2]')

    # Initialize variables to store highest and lowest values
    loadhigh=$loadavg
    loadlow=$loadavg

    # Comparing loadavg5 with highest and lowest
    if (( $(echo "$loadavg5 > $loadhigh" | bc -l) )); then
      loadhigh=$loadavg5
    fi
    if (( $(echo "$loadavg5 < $loadlow" | bc -l) )); then
      loadlow=$loadavg5
    fi

    # Comparing loadavg15 with highest and lowest
    if (( $(echo "$loadavg15 > $loadhigh" | bc -l) )); then
      loadhigh=$loadavg15
    fi
    if (( $(echo "$loadavg15 < $loadlow" | bc -l) )); then
      loadlow=$loadavg15
    fi

    # Calculate the percentage of loadavg against cpus
    loadavg_percentage=$(bc <<< "scale=2; $loadavg / $cpus * 100")
    loadavg_low_percentage=$(bc <<< "scale=2; $loadlow / $cpus * 100")
    loadavg_high_percentage=$(bc <<< "scale=2; $loadhigh / $cpus * 100")

    # 1 decimal place or a whole number if the decimal is zero
    loadavg_percentage=$(printf "%.1f" "$loadavg_percentage")
    if [[ "$loadavg_percentage" == *\.0 ]]; then
        loadavg_percentage=${loadavg_percentage%\.0}
    fi
    loadavg_low_percentage=$(printf "%.1f" "$loadavg_low_percentage")
    if [[ "$loadavg_low_percentage" == *\.0 ]]; then
        loadavg_low_percentage=${loadavg_low_percentage%\.0}
    fi
    loadavg_high_percentage=$(printf "%.1f" "$loadavg_high_percentage")
    if [[ "$loadavg_high_percentage" == *\.0 ]]; then
        loadavg_high_percentage=${loadavg_high_percentage%\.0}
    fi

    # Compare loadavg percentage against thresholds
    if (( $(bc <<< "$loadavg_percentage >= $crit") )); then
        echo "CPU Load: $loadavg_percentage% | Load=${loadavg_percentage}%;$warn;$crit;$loadavg_low_percentage;$loadavg_high_percentage; Threads=$cpus LoadAvg1m=$loadavg LoadAvg5m=$loadavg5 LoadAvg15m=$loadavg15;;;;"
        exit 2 # Critical State
    elif (( $(bc <<< "$loadavg_percentage >= $warn") )); then
        echo "CPU Load: $loadavg_percentage% | Load=${loadavg_percentage}%;$warn;$crit;$loadavg_low_percentage;$loadavg_high_percentage; Threads=$cpus LoadAvg1m=$loadavg LoadAvg5m=$loadavg5 LoadAvg15m=$loadavg15;;;;"
        exit 1 # Warning State
    else
        echo "CPU Load: $loadavg_percentage% | Load=${loadavg_percentage}%;$warn;$crit;$loadavg_low_percentage;$loadavg_high_percentage; Threads=$cpus LoadAvg1m=$loadavg LoadAvg5m=$loadavg5 LoadAvg15m=$loadavg15;;;;"
        exit 0 # OK State
    fi
}

check_rootfs() {
    # Parse JSON data and extract relevant fields
    total_rootfs=$(echo "$json_data" | jq -r '.data.rootfs.total')
    used_rootfs=$(echo "$json_data" | jq -r '.data.rootfs.used')

    # Calculate the percentage of rootfs used
    rootfs_percentage=$(bc <<< "scale=2; $used_rootfs / $total_rootfs * 100")

    # Check if rootfs usage exceeds thresholds
    if (( $(bc <<< "$rootfs_percentage >= $crit") )); then
        echo "Root filesystem usage: $rootfs_percentage% | Usage=${rootfs_percentage}%;$warn;$crit;;"
        exit 2 # Critical State
    elif (( $(bc <<< "$rootfs_percentage >= $warn") )); then
        echo "Root filesystem usage $rootfs_percentage% | Usage=${rootfs_percentage}%;$warn;$crit;;"
        exit 1 # Warning State
    else
        echo "Root filesystem usage $rootfs_percentage% | Usage=${rootfs_percentage}%;$warn;$crit;;"
        exit 0 # OK State
    fi
}

check_proxmox_version() {

    proxmox_version_cache_file="/tmp/pve_version.cache"
    cache_max_age=$(date -d "now - 6 hours" +%s)
    if [ ! -e "$proxmox_version_cache_file" ] || [ "$proxmox_version_cache_file" -ot "$cache_max_age" ]; then
      # Create or update the Proxmox Version Cache from remote JSON data
      curl -sS https://endoflife.date/api/proxmox-ve.json > $proxmox_version_cache_file
    fi
    proxmox_json=$(< "$proxmox_version_cache_file" )


    # Check if proxmox_json is valid JSON containing [.[].latest]
    if ! jq -e '.[].latest' <<< "$proxmox_json" >/dev/null; then
        echo "Unable to access Proxmox version data from remote server."
        exit 3 # Unknown State
    fi

    # Extract our Proxmox version
    our_version=$(echo "$json_data" | jq -r '.data.pveversion' | cut -d'/' -f2)
    our_version_decimal=$(echo "$our_version" | sed 's/\.[0-9]*$//')

    # Check if our_version is a valid float number
    if ! [[ "$our_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
        echo "Proxmox did not send a valid response."
        exit 3 # Unknown State
    fi

    # Extract the latest version from the remote JSON data
    latest_version=$(echo "$proxmox_json" | jq -r '[.[].latest] | map(split(".") | map(tonumber)) | max | join(".")')

    # Check if our version is not the latest
    if [ "$our_version_decimal" != "$latest_version" ]; then
        echo "Your Proxmox version ($our_version) is not the latest ($latest_version) | Version=$our_version Latest=$latest_version;;;;"
        exit 1 # Warning State
    fi

    # Find the corresponding entry for our version in the remote JSON
    entry=$(echo "$proxmox_json" | jq --arg latest_version "$latest_version" '.[] | select(.latest == $latest_version)')

    # Extract EOL date
    eol_date=$(echo "$entry" | jq -r '.eol')

    # Check if we are within 90 days of EOL
    current_date=$(date +%Y-%m-%d)
    days_to_eol=$(( ($(date -d "$eol_date" +%s) - $(date -d "$current_date" +%s)) / 86400 ))
    if [ "$eol_date" != false ] && [ "$days_to_eol" -le 90 ]; then
        echo "Your Proxmox version ($our_version) has reached End-of-Life ($eol_date) | Version=$our_version Latest=$latest_version;;;;"
        exit 2 # Critical State
    fi

    echo "Your Proxmox version ($our_version) is up to date | Version=$our_version Latest=$latest_version;;;;"
    exit 0 # OK State
}

check_memory_usage() {
    # Parse JSON data and extract relevant fields
    total_memory=$(echo "$json_data" | jq -r '.data.memory.total')
    used_memory=$(echo "$json_data" | jq -r '.data.memory.used')

    # Calculate the percentage of memory used
    memory_percentage=$(bc <<< "scale=2; $used_memory / $total_memory * 100")

    # 1 decimal place or a whole number if the decimal is zero
    memory_percentage=$(printf "%.1f" "$memory_percentage")
    if [[ "$memory_percentage" == *\.0 ]]; then
        memory_percentage=${memory_percentage%\.0}
    fi

    # Check if memory usage exceeds thresholds
    if (( $(bc <<< "$memory_percentage >= $crit") )); then
        echo "Memory usage: $memory_percentage% | Usage=$memory_percentage%;$warn;$crit;;"
        exit 2 # Critical State
    elif (( $(bc <<< "$memory_percentage >= $warn") )); then
        echo "Memory usage: $memory_percentage% | Usage=$memory_percentage%;$warn;$crit;;"
        exit 1 # Warning State
    else
        echo "Memory usage: $memory_percentage% | Usage=$memory_percentage%;$warn;$crit;;"
        exit 0 # OK State
    fi
}

check_swap_usage() {
    # Parse JSON data and extract relevant fields
    total_swap=$(echo "$json_data" | jq -r '.data.swap.total')
    used_swap=$(echo "$json_data" | jq -r '.data.swap.used')

    # Calculate the percentage of swap used
    swap_percentage=$(bc <<< "scale=2; $used_swap / $total_swap * 100")

    # Check if swap usage exceeds thresholds
    if (( $(bc <<< "$swap_percentage >= $crit") )); then
        echo "Swap usage: $swap_percentage% | Usage=${swap_percentage}%;$warn;$crit;; Total=${total_swap} Used=${used_swap};;"
        exit 2 # Critical State
    elif (( $(bc <<< "$swap_percentage >= $warn") )); then
        echo "Swap usage: $swap_percentage% | Usage=${swap_percentage}%;$warn;$crit;; Total=${total_swap} Used=${used_swap};;"
        exit 1 # Warning State
    else
        echo "Swap usage: $swap_percentage% | Usage=${swap_percentage}%;$warn;$crit;; Total=${total_swap} Used=${used_swap};;"
        exit 0 # OK State
    fi
}

# Check if any required variables are unset
if [[ -z "$ip" || -z "$port" || -z "$node" || -z "$username" || -z "$password" || -z "$realm" ]]; then
    WHITE='\033[0;97m'
    CYAN='\033[0;36m'
    YELLOW='\033[1;33m'
    RED='\033[0;31m'
    NC='\033[0m' # No Color

    echo -e "${WHITE}"
    echo "check_pve 1.3 by Robbie Ferguson // https://nemslinux.com/"
    echo -e "${NC}"

    echo -e "${YELLOW}Usage:${NC}"
    echo "$0 variable=value"
    echo

    echo -e "${YELLOW}Variables:${NC}"
    echo -e "  - ${CYAN}ip:${NC} IP address of the Proxmox server ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}port:${NC} Port number Proxmox is accessible on (default: 8006)"
    echo -e "  - ${CYAN}node:${NC} The name of the node you wish to check ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}username:${NC} Username of user with PVEAuditor permission set ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}password:${NC} Password for that user ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}realm:${NC} Authentication realm (Eg., pve or pam) ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}check:${NC} Specify the check to perform (load, rootfs, version, memory, swap) ${WHITE}[${RED}Required${WHITE}]${NC}"
    echo -e "  - ${CYAN}warn:${NC} Warning threshold [int] percentage (default: 80)"
    echo -e "  - ${CYAN}crit:${NC} Critical threshold [int] percentage (default: 95)"
    echo

    echo -e "${YELLOW}Example:${NC}"
    echo "$0 ip=10.0.0.5 port=8006 node=myserver username=auditor password=Str0ngP4ssw0rd realm=pve check=load warn=80 crit=95"
    echo
    exit 1
fi

case "$check" in
    load)
        check_load
        ;;
    rootfs)
        check_rootfs
        ;;
    version)
        check_proxmox_version
        ;;
    memory)
        check_memory_usage
        ;;
    swap)
        check_swap_usage
        ;;
    *)
        echo "Unknown check: $check"
        exit 3
        ;;
esac
