#!/bin/bash ################################################################################ # Script Name: proxmox-metrics.sh # Version: 1.0 # Description: Prometheus exporter for Proxmox VE — cluster health, per-node # CPU/memory/load, VM and container resource usage, storage pools, # backup job status, HA state, replication lag, and task queue depth # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - curl, jq # - Proxmox API token with PVEAuditor role # - netcat (nc) for HTTP mode # # Usage: # ./proxmox-metrics.sh # ./proxmox-metrics.sh --http -p 9221 # ./proxmox-metrics.sh --textfile # # Configuration: # Default HTTP port: 9221 # Textfile directory: /var/lib/node_exporter # ################################################################################ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9221 PVE_HOST="${PVE_HOST:-localhost}" PVE_PORT="${PVE_PORT:-8006}" PVE_TOKEN_ID="${PVE_TOKEN_ID:-}" PVE_TOKEN_SECRET="${PVE_TOKEN_SECRET:-}" PVE_VERIFY_SSL="${PVE_VERIFY_SSL:-false}" show_usage() { cat <&2; exit 1 ;; esac done } pve_api() { local endpoint="$1" local ssl_flag="" [ "$PVE_VERIFY_SSL" = "false" ] && ssl_flag="-k" curl -s $ssl_flag \ -H "Authorization: PVEAPIToken=${PVE_TOKEN_ID}=${PVE_TOKEN_SECRET}" \ "https://${PVE_HOST}:${PVE_PORT}/api2/json${endpoint}" 2>/dev/null } check_proxmox() { if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then echo "ERROR: curl and jq are required" >&2 return 1 fi if [ -z "$PVE_TOKEN_ID" ] || [ -z "$PVE_TOKEN_SECRET" ]; then echo "ERROR: PVE_TOKEN_ID and PVE_TOKEN_SECRET required" >&2 return 1 fi if ! pve_api "/version" | jq -e '.data.version' >/dev/null 2>&1; then echo "ERROR: Cannot connect to Proxmox API" >&2 return 1 fi return 0 } generate_metrics() { local script_start script_start=$(date +%s) if ! check_proxmox; then cat < 0) | "pve_storage_total_bytes{node=\"\($node)\",storage=\"\(.storage)\",type=\"\(.type // "unknown")\"} \(.total // 0)\npve_storage_used_bytes{node=\"\($node)\",storage=\"\(.storage)\",type=\"\(.type // "unknown")\"} \(.used // 0)\npve_storage_usage_ratio{node=\"\($node)\",storage=\"\(.storage)\",type=\"\(.type // "unknown")\"} \(if .total > 0 then (.used / .total) else 0 end)"' 2>/dev/null done echo "" # Exporter runtime local script_end script_duration script_end=$(date +%s) script_duration=$((script_end - script_start)) cat <&2 if ! command -v nc >/dev/null 2>&1; then echo "ERROR: netcat (nc) required for HTTP mode" >&2 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 "Proxmox Exporter

Proxmox VE Prometheus Exporter

Metrics

" fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } main() { parse_args "$@" 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}/.proxmox_metrics.XXXXXX") if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 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" echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2 exit 1 fi chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else generate_metrics fi } main "$@"