127 lines
5.8 KiB
Bash
Executable File
127 lines
5.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# ansipa_pve — CheckMK local check for a Proxmox VE hypervisor node.
|
|
#
|
|
# Install: copy to /usr/lib/check_mk_agent/local/ansipa_pve and chmod 755
|
|
# (ansipa-infra-checkmk-install.sh does this for you, along with installing
|
|
# the CheckMK agent itself and registering the host).
|
|
#
|
|
# Requires only `pvesh` (ships with pve-manager on every PVE node) — no jq
|
|
# or python3 dependency. JSON is requested as "json-pretty" (one key per
|
|
# line) and scraped with a small grep/awk parser so the script works on a
|
|
# bare PVE install. Any command that fails or returns nothing degrades that
|
|
# one service to CheckMK state 3 (UNKNOWN) rather than aborting the others.
|
|
#
|
|
# Emits (CheckMK local-check format: "<state> <service> <perfdata> <text>"):
|
|
# Ansipa_PVE_Cluster quorum / standalone status
|
|
# Ansipa_PVE_Storage worst storage utilisation on this node
|
|
# Ansipa_PVE_Guests VM/CT counts (running vs stopped)
|
|
# Ansipa_PVE_Backup freshness + outcome of the last vzdump job
|
|
#
|
|
# CPU/RAM/disk-root are already covered by CheckMK's stock Linux checks —
|
|
# not duplicated here.
|
|
|
|
command -v pvesh >/dev/null 2>&1 \
|
|
|| { echo "3 Ansipa_PVE - pvesh not found (not a Proxmox VE host?)"; exit 0; }
|
|
|
|
NODE=$(hostname -s 2>/dev/null || echo "")
|
|
|
|
# Extract every value of a JSON key from json-pretty text (one "key": value per line).
|
|
_jp() { grep -oP "^\s*\"$1\"\s*:\s*\"?\K[^\",]+" || true; }
|
|
|
|
# ── Cluster / quorum ────────────────────────────────────────────────────────
|
|
CSTAT=$(pvesh get /cluster/status --output-format json-pretty 2>/dev/null || true)
|
|
if [[ -z "$CSTAT" ]]; then
|
|
echo "3 Ansipa_PVE_Cluster - could not query /cluster/status"
|
|
else
|
|
mapfile -t CTYPES < <(_jp type <<< "$CSTAT")
|
|
QUORATE=$(_jp quorate <<< "$CSTAT" | head -1)
|
|
_clustered=false
|
|
for t in "${CTYPES[@]}"; do [[ "$t" == "cluster" ]] && _clustered=true && break; done
|
|
if [[ "$_clustered" == false ]]; then
|
|
echo "0 Ansipa_PVE_Cluster - standalone node (no cluster configured)"
|
|
elif [[ "$QUORATE" == "1" ]]; then
|
|
echo "0 Ansipa_PVE_Cluster - cluster quorate"
|
|
else
|
|
echo "2 Ansipa_PVE_Cluster - cluster NOT quorate"
|
|
fi
|
|
fi
|
|
|
|
# ── Storage usage (worst-case % across all storages on this node) ────────────
|
|
SSTAT=""
|
|
[[ -n "$NODE" ]] && SSTAT=$(pvesh get "/nodes/${NODE}/storage" --output-format json-pretty 2>/dev/null || true)
|
|
if [[ -z "$SSTAT" ]]; then
|
|
echo "3 Ansipa_PVE_Storage - could not query node storage"
|
|
else
|
|
mapfile -t SNAMES < <(_jp storage <<< "$SSTAT")
|
|
mapfile -t STOTAL < <(_jp total <<< "$SSTAT")
|
|
mapfile -t SUSED < <(_jp used <<< "$SSTAT")
|
|
WORST=0; WORST_NAME=""; PERF=""
|
|
for i in "${!SNAMES[@]}"; do
|
|
t="${STOTAL[$i]:-0}"; u="${SUSED[$i]:-0}"
|
|
[[ "$t" =~ ^[0-9]+$ && "$u" =~ ^[0-9]+$ && "$t" -gt 0 ]] || continue
|
|
pct=$(( u * 100 / t ))
|
|
PERF+=" ${SNAMES[$i]}_pct=${pct}%;80;90"
|
|
(( pct > WORST )) && { WORST=$pct; WORST_NAME="${SNAMES[$i]}"; }
|
|
done
|
|
if [[ -z "$WORST_NAME" ]]; then
|
|
echo "3 Ansipa_PVE_Storage - no usable storage entries found"
|
|
elif (( WORST >= 90 )); then
|
|
echo "2 Ansipa_PVE_Storage${PERF} CRIT — ${WORST_NAME} at ${WORST}%"
|
|
elif (( WORST >= 80 )); then
|
|
echo "1 Ansipa_PVE_Storage${PERF} WARN — ${WORST_NAME} at ${WORST}%"
|
|
else
|
|
echo "0 Ansipa_PVE_Storage${PERF} worst: ${WORST_NAME} at ${WORST}%"
|
|
fi
|
|
fi
|
|
|
|
# ── Guests (VMs + containers) ─────────────────────────────────────────────────
|
|
RSTAT=$(pvesh get /cluster/resources --output-format json-pretty 2>/dev/null || true)
|
|
if [[ -z "$RSTAT" ]]; then
|
|
echo "3 Ansipa_PVE_Guests - could not query /cluster/resources"
|
|
else
|
|
mapfile -t RTYPES < <(_jp type <<< "$RSTAT")
|
|
mapfile -t RSTATUS < <(_jp status <<< "$RSTAT")
|
|
VMS=0; CTS=0; RUNNING=0; STOPPED=0
|
|
for i in "${!RTYPES[@]}"; do
|
|
case "${RTYPES[$i]}" in
|
|
qm) VMS=$((VMS+1)) ;;
|
|
lxc) CTS=$((CTS+1)) ;;
|
|
*) continue ;;
|
|
esac
|
|
if [[ "${RSTATUS[$i]:-}" == "running" ]]; then RUNNING=$((RUNNING+1)); else STOPPED=$((STOPPED+1)); fi
|
|
done
|
|
TOTAL=$((VMS + CTS))
|
|
PERF="vms=${VMS};; cts=${CTS};; running=${RUNNING};; stopped=${STOPPED};;"
|
|
echo "0 Ansipa_PVE_Guests ${PERF} ${TOTAL} guest(s): ${RUNNING} running, ${STOPPED} stopped"
|
|
fi
|
|
|
|
# ── Most recent vzdump backup job (from cluster task history) ───────────────
|
|
TSTAT=$(pvesh get /cluster/tasks --output-format json-pretty 2>/dev/null || true)
|
|
if [[ -z "$TSTAT" ]]; then
|
|
echo "3 Ansipa_PVE_Backup - could not query /cluster/tasks"
|
|
else
|
|
mapfile -t TTYPE < <(_jp type <<< "$TSTAT")
|
|
mapfile -t TSTATUS < <(_jp status <<< "$TSTAT")
|
|
mapfile -t TSTART < <(_jp starttime <<< "$TSTAT")
|
|
LAST_TS=0; LAST_STATUS=""
|
|
for i in "${!TTYPE[@]}"; do
|
|
[[ "${TTYPE[$i]}" == "vzdump" ]] || continue
|
|
st="${TSTART[$i]:-0}"
|
|
[[ "$st" =~ ^[0-9]+$ ]] || continue
|
|
if (( st > LAST_TS )); then LAST_TS=$st; LAST_STATUS="${TSTATUS[$i]:-}"; fi
|
|
done
|
|
if [[ "$LAST_TS" -eq 0 ]]; then
|
|
echo "1 Ansipa_PVE_Backup - no vzdump backup job found in recent task history"
|
|
else
|
|
AGE_H=$(( ( $(date +%s) - LAST_TS ) / 3600 ))
|
|
PERF="last_backup_hours=${AGE_H}"
|
|
if [[ "$LAST_STATUS" != "OK" ]]; then
|
|
echo "2 Ansipa_PVE_Backup ${PERF} Last vzdump job FAILED (${AGE_H}h ago): ${LAST_STATUS}"
|
|
elif (( AGE_H >= 48 )); then
|
|
echo "1 Ansipa_PVE_Backup ${PERF} Last successful backup ${AGE_H}h ago (stale)"
|
|
else
|
|
echo "0 Ansipa_PVE_Backup ${PERF} Last backup OK, ${AGE_H}h ago"
|
|
fi
|
|
fi
|
|
fi
|