#!/bin/bash ################################################################################ # Script Name: keepalived-exporter.sh # Version: 1.0 # Description: Prometheus exporter for Keepalived VRRP — instance state, # priority, VIP assignment, advertisement and transition counters # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - Keepalived installed and running # - Root/sudo access for sending signals and reading state files # - netcat (nc) for HTTP mode # # Usage: # sudo ./keepalived-exporter.sh # sudo ./keepalived-exporter.sh --http -p 9588 # sudo ./keepalived-exporter.sh --textfile # # Configuration: # Default HTTP port: 9588 # Textfile directory: /var/lib/node_exporter # # Metrics exported: # keepalived_up Exporter status (1=up, 0=down) # keepalived_process_running Keepalived process status # keepalived_vrrp_state{instance} VRRP state (2=MASTER, 1=BACKUP, 0=FAULT) # keepalived_vrrp_vip_status{instance,vip} VIP assigned (1) or not (0) # keepalived_vrrp_priority{instance} Configured base priority # keepalived_vrrp_effective_priority{instance} Current effective priority # keepalived_vrrp_advert_interval{instance} Advertisement interval in seconds # keepalived_vrrp_virtual_router_id{instance} VRID # keepalived_vrrp_became_master_total{instance} Transitions to MASTER # keepalived_vrrp_released_master_total{instance} Transitions from MASTER # keepalived_vrrp_adverts_received_total{instance} Advertisements received # keepalived_vrrp_adverts_sent_total{instance} Advertisements sent # keepalived_vrrp_garp_sent_total{instance} Gratuitous ARPs sent # keepalived_vrrp_last_transition_timestamp{instance} Last state change timestamp # keepalived_exporter_duration_seconds Script execution time # keepalived_exporter_last_run_timestamp Last run timestamp # ################################################################################ # ────────────────────────────────────────────────────────────────────────────── # CONFIGURATION VARIABLES # ────────────────────────────────────────────────────────────────────────────── TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9588 DATA_FILE="/tmp/keepalived.data" STATS_FILE="/tmp/keepalived.stats" SIGNAL_WAIT=0.5 # ────────────────────────────────────────────────────────────────────────────── # HELPER FUNCTIONS # ────────────────────────────────────────────────────────────────────────────── show_usage() { cat <&2; exit 1 ;; esac done } get_keepalived_pid() { local pid="" if [ -f /run/keepalived.pid ]; then pid=$(cat /run/keepalived.pid 2>/dev/null) elif [ -f /var/run/keepalived.pid ]; then pid=$(cat /var/run/keepalived.pid 2>/dev/null) fi if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then pid=$(pgrep -f "keepalived" | head -1) fi echo "$pid" } check_keepalived() { if pgrep keepalived >/dev/null 2>&1; then return 0 fi if systemctl is-active keepalived >/dev/null 2>&1; then return 0 fi return 1 } map_state() { case "$1" in MASTER) echo 2 ;; BACKUP) echo 1 ;; FAULT) echo 0 ;; INIT) echo 0 ;; *) echo 0 ;; esac } dump_keepalived_data() { local pid pid=$(get_keepalived_pid) [ -z "$pid" ] && return 1 kill -USR1 "$pid" 2>/dev/null || return 1 sleep "$SIGNAL_WAIT" [ -f "$DATA_FILE" ] || return 1 return 0 } dump_keepalived_stats() { local pid pid=$(get_keepalived_pid) [ -z "$pid" ] && return 1 kill -USR2 "$pid" 2>/dev/null || return 1 sleep "$SIGNAL_WAIT" [ -f "$STATS_FILE" ] || return 1 return 0 } check_vip_assigned() { local vip="$1" local ip_only="${vip%%/*}" if ip addr show 2>/dev/null | grep -qw "$ip_only"; then echo 1 else echo 0 fi } # ────────────────────────────────────────────────────────────────────────────── # METRIC GENERATION # ────────────────────────────────────────────────────────────────────────────── parse_data_file() { [ -f "$DATA_FILE" ] || return local current_instance="" local in_vip_block=false while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*VRRP\ Instance:\ (.+)$ ]]; then current_instance="${BASH_REMATCH[1]}" current_instance=$(echo "$current_instance" | xargs) in_vip_block=false continue fi [ -z "$current_instance" ] && continue if [[ "$line" =~ ^[[:space:]]*State\ =\ (.+)$ ]]; then local state="${BASH_REMATCH[1]}" state=$(echo "$state" | xargs) local state_val state_val=$(map_state "$state") echo "keepalived_vrrp_state{instance=\"$current_instance\"} $state_val" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Wantstate\ =\ (.+)$ ]]; then in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Last\ transition\ =\ ([0-9]+) ]]; then local ts="${BASH_REMATCH[1]}" echo "keepalived_vrrp_last_transition_timestamp{instance=\"$current_instance\"} $ts" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Base\ priority\ =\ ([0-9]+) ]]; then echo "keepalived_vrrp_priority{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Effective\ priority\ =\ ([0-9]+) ]]; then echo "keepalived_vrrp_effective_priority{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Advert\ interval\ =\ ([0-9]+) ]]; then echo "keepalived_vrrp_advert_interval{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Virtual\ Router\ ID\ =\ ([0-9]+) ]]; then echo "keepalived_vrrp_virtual_router_id{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" in_vip_block=false elif [[ "$line" =~ ^[[:space:]]*Virtual\ IP ]]; then in_vip_block=true elif $in_vip_block && [[ "$line" =~ ^[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?) ]]; then local vip="${BASH_REMATCH[1]}" vip=$(echo "$vip" | xargs) local assigned assigned=$(check_vip_assigned "$vip") echo "keepalived_vrrp_vip_status{instance=\"$current_instance\",vip=\"$vip\"} $assigned" elif $in_vip_block && [[ ! "$line" =~ ^[[:space:]] ]]; then in_vip_block=false fi done < "$DATA_FILE" } parse_stats_file() { [ -f "$STATS_FILE" ] || return local current_instance="" local in_adverts=false while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*VRRP\ Instance:\ (.+)$ ]]; then current_instance="${BASH_REMATCH[1]}" current_instance=$(echo "$current_instance" | xargs) in_adverts=false continue fi [ -z "$current_instance" ] && continue if [[ "$line" =~ ^[[:space:]]*Advertisements: ]]; then in_adverts=true elif $in_adverts && [[ "$line" =~ ^[[:space:]]*Received:\ ([0-9]+) ]]; then echo "keepalived_vrrp_adverts_received_total{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" elif $in_adverts && [[ "$line" =~ ^[[:space:]]*Sent:\ ([0-9]+) ]]; then echo "keepalived_vrrp_adverts_sent_total{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" in_adverts=false elif [[ "$line" =~ ^[[:space:]]*Became\ master:\ ([0-9]+) ]]; then echo "keepalived_vrrp_became_master_total{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" elif [[ "$line" =~ ^[[:space:]]*Released\ master:\ ([0-9]+) ]]; then echo "keepalived_vrrp_released_master_total{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" elif [[ "$line" =~ ^[[:space:]]*Gratuitous\ ARP:\ ([0-9]+) ]]; then echo "keepalived_vrrp_garp_sent_total{instance=\"$current_instance\"} ${BASH_REMATCH[1]}" fi done < "$STATS_FILE" } generate_metrics() { local script_start script_start=$(date +%s) if ! check_keepalived; then echo "# HELP keepalived_up Keepalived exporter status" echo "# TYPE keepalived_up gauge" echo "keepalived_up 0" echo "" echo "# HELP keepalived_process_running Keepalived process status (1=running, 0=stopped)" echo "# TYPE keepalived_process_running gauge" echo "keepalived_process_running 0" return fi echo "# HELP keepalived_up Keepalived exporter status" echo "# TYPE keepalived_up gauge" echo "keepalived_up 1" echo "" echo "# HELP keepalived_process_running Keepalived process status (1=running, 0=stopped)" echo "# TYPE keepalived_process_running gauge" echo "keepalived_process_running 1" echo "" # Signal keepalived to dump data and stats local has_data=false local has_stats=false if dump_keepalived_data; then has_data=true fi if dump_keepalived_stats; then has_stats=true fi if ! $has_data && ! $has_stats; then echo "# No VRRP data available — could not signal keepalived or read dump files" echo "" 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 "Keepalived Exporter

Keepalived Prometheus Exporter

Metrics

" fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } # ────────────────────────────────────────────────────────────────────────────── # MAIN # ────────────────────────────────────────────────────────────────────────────── 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}/.keepalived_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 chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE" >&2 else generate_metrics fi } main "$@"