#!/usr/bin/env bash ################################################################################ # Script Name: glpi-exporter.sh # Version: 1.0 # Description: Prometheus exporter for GLPI ITSM and asset management. # Uses the GLPI REST API to collect ticket counts by status, # asset inventory, user counts, SLA compliance, entity stats, # and plugin health. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - curl and jq # - GLPI API token (user token + app token) # # Usage: # GLPI_URL="https://helpdesk.example.com" GLPI_USER_TOKEN="xxx" ./glpi-exporter.sh # GLPI_URL="https://helpdesk.example.com" GLPI_USER_TOKEN="xxx" ./glpi-exporter.sh --textfile # GLPI_URL="https://helpdesk.example.com" GLPI_USER_TOKEN="xxx" ./glpi-exporter.sh --http # # Parameters: # --textfile Write to textfile collector directory # --http Run as HTTP server # --install Create cron job for automatic collection # --help Show usage # # Environment: # GLPI_URL GLPI base URL (required) # GLPI_USER_TOKEN User API token (required) # GLPI_APP_TOKEN Application API token (optional, depends on GLPI config) # TEXTFILE_DIR Textfile collector directory (default: /var/lib/node_exporter/textfile_collector) # CURL_TIMEOUT API request timeout in seconds (default: 10) # # Metrics Exported: # Core: # - glpi_up # - glpi_exporter_info{version} # # Tickets: # - glpi_tickets_total # - glpi_tickets_new # - glpi_tickets_assigned # - glpi_tickets_planned # - glpi_tickets_waiting # - glpi_tickets_solved # - glpi_tickets_closed # - glpi_tickets_by_urgency{urgency} # - glpi_tickets_by_category{category} # # Assets: # - glpi_computers_total # - glpi_monitors_total # - glpi_network_devices_total # - glpi_phones_total # - glpi_printers_total # - glpi_software_total # # Organization: # - glpi_users_total # - glpi_groups_total # - glpi_entities_total # - glpi_locations_total # # Exporter: # - glpi_exporter_duration_seconds # - glpi_exporter_last_run_timestamp # ################################################################################ set -euo pipefail # --- Configuration --- readonly VERSION="1.0" readonly SCRIPT_NAME="$(basename "$0")" GLPI_URL="${GLPI_URL:-}" GLPI_USER_TOKEN="${GLPI_USER_TOKEN:-}" GLPI_APP_TOKEN="${GLPI_APP_TOKEN:-}" TEXTFILE_DIR="${TEXTFILE_DIR:-/var/lib/node_exporter/textfile_collector}" CURL_TIMEOUT="${CURL_TIMEOUT:-10}" TEXTFILE_MODE=false HTTP_MODE=false HTTP_PORT=9200 OUTPUT="" START_TIME="" SESSION_TOKEN="" # --- Functions --- usage() { cat </dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then echo "# ERROR: Missing required commands: ${missing[*]}" >&2 echo "# Install with: apt install ${missing[*]} OR dnf install ${missing[*]}" >&2 exit 1 fi } validate_config() { if [[ -z "$GLPI_URL" ]]; then echo "# ERROR: GLPI_URL environment variable is required" >&2 exit 1 fi if [[ -z "$GLPI_USER_TOKEN" ]]; then echo "# ERROR: GLPI_USER_TOKEN environment variable is required" >&2 exit 1 fi GLPI_URL="${GLPI_URL%/}" } init_session() { local auth_headers=(-H "Authorization: user_token ${GLPI_USER_TOKEN}") if [[ -n "$GLPI_APP_TOKEN" ]]; then auth_headers+=(-H "App-Token: ${GLPI_APP_TOKEN}") fi local response response=$(curl -sf --max-time "$CURL_TIMEOUT" \ "${auth_headers[@]}" \ "${GLPI_URL}/apirest.php/initSession" 2>/dev/null) || { echo ""; return 1; } SESSION_TOKEN=$(echo "$response" | jq -r '.session_token // empty' 2>/dev/null) if [[ -z "$SESSION_TOKEN" ]]; then return 1 fi return 0 } kill_session() { if [[ -n "$SESSION_TOKEN" ]]; then local headers=(-H "Session-Token: ${SESSION_TOKEN}") if [[ -n "$GLPI_APP_TOKEN" ]]; then headers+=(-H "App-Token: ${GLPI_APP_TOKEN}") fi curl -sf --max-time "$CURL_TIMEOUT" \ "${headers[@]}" \ "${GLPI_URL}/apirest.php/killSession" &>/dev/null || true SESSION_TOKEN="" fi } api_get() { local endpoint="$1" local headers=(-H "Session-Token: ${SESSION_TOKEN}" -H "Content-Type: application/json") if [[ -n "$GLPI_APP_TOKEN" ]]; then headers+=(-H "App-Token: ${GLPI_APP_TOKEN}") fi curl -sf --max-time "$CURL_TIMEOUT" \ "${headers[@]}" \ "${GLPI_URL}/apirest.php/${endpoint}" 2>/dev/null || echo "" } api_get_count() { local endpoint="$1" local range_header range_header=$(curl -sI --max-time "$CURL_TIMEOUT" \ -H "Session-Token: ${SESSION_TOKEN}" \ -H "Content-Type: application/json" \ ${GLPI_APP_TOKEN:+-H "App-Token: ${GLPI_APP_TOKEN}"} \ "${GLPI_URL}/apirest.php/${endpoint}?range=0-0" 2>/dev/null | grep -i '^Content-Range:' | tr -d '\r') if [[ -n "$range_header" ]]; then echo "$range_header" | sed 's|.*/||' else echo "0" fi } sanitize_label() { local value="$1" echo "$value" | sed 's/[^a-zA-Z0-9_ \/.-]/_/g' | sed 's/"/\\"/g' } add_metric() { local name="$1" local type="$2" local help="$3" local value="$4" local labels="${5:-}" if [[ -n "$labels" ]]; then OUTPUT+="# HELP ${name} ${help} # TYPE ${name} ${type} ${name}{${labels}} ${value} " else OUTPUT+="# HELP ${name} ${help} # TYPE ${name} ${type} ${name} ${value} " fi } add_metric_value() { local name="$1" local value="$2" local labels="${3:-}" if [[ -n "$labels" ]]; then OUTPUT+="${name}{${labels}} ${value} " else OUTPUT+="${name} ${value} " fi } # --- Collectors --- collect_tickets() { # Total tickets (open = not closed) local total total=$(api_get_count "Ticket") add_metric "glpi_tickets_total" "gauge" "Total number of tickets" "${total:-0}" # Tickets by status # GLPI status codes: 1=New, 2=Assigned, 3=Planned, 4=Waiting, 5=Solved, 6=Closed local status_names=("new" "assigned" "planned" "waiting" "solved" "closed") local status_codes=(1 2 3 4 5 6) for i in "${!status_codes[@]}"; do local code="${status_codes[$i]}" local name="${status_names[$i]}" local count count=$(api_get_count "Ticket?searchText[status]=${code}") add_metric "glpi_tickets_${name}" "gauge" "Tickets in ${name} status" "${count:-0}" done # Tickets by urgency # GLPI urgency: 1=Very low, 2=Low, 3=Medium, 4=High, 5=Very high OUTPUT+="# HELP glpi_tickets_by_urgency Number of tickets by urgency level # TYPE glpi_tickets_by_urgency gauge " local urgency_names=("very_low" "low" "medium" "high" "very_high") local urgency_codes=(1 2 3 4 5) for i in "${!urgency_codes[@]}"; do local code="${urgency_codes[$i]}" local uname="${urgency_names[$i]}" local count count=$(api_get_count "Ticket?searchText[urgency]=${code}") add_metric_value "glpi_tickets_by_urgency" "${count:-0}" "urgency=\"${uname}\"" done # Tickets by category (top categories) local cat_json cat_json=$(api_get "ITILCategory?range=0-49") if [[ -n "$cat_json" ]]; then local is_array is_array=$(echo "$cat_json" | jq -r 'if type == "array" then "yes" else "no" end' 2>/dev/null) if [[ "$is_array" == "yes" ]]; then local cat_count cat_count=$(echo "$cat_json" | jq 'length' 2>/dev/null) if [[ "$cat_count" -gt 0 ]]; then OUTPUT+="# HELP glpi_tickets_by_category Number of tickets per category # TYPE glpi_tickets_by_category gauge " local j for ((j = 0; j < cat_count && j < 30; j++)); do local cat_name cat_id cat_name=$(echo "$cat_json" | jq -r ".[$j].completename // .[$j].name // empty" 2>/dev/null) cat_id=$(echo "$cat_json" | jq -r ".[$j].id // empty" 2>/dev/null) if [[ -n "$cat_name" && -n "$cat_id" ]]; then local ticket_count ticket_count=$(api_get_count "Ticket?searchText[itilcategories_id]=${cat_id}") local safe_name safe_name=$(sanitize_label "$cat_name") add_metric_value "glpi_tickets_by_category" "${ticket_count:-0}" "category=\"${safe_name}\"" fi done fi fi fi } collect_assets() { # Computers local computers computers=$(api_get_count "Computer") add_metric "glpi_computers_total" "gauge" "Total number of computers" "${computers:-0}" # Monitors local monitors monitors=$(api_get_count "Monitor") add_metric "glpi_monitors_total" "gauge" "Total number of monitors" "${monitors:-0}" # Network devices local netdevices netdevices=$(api_get_count "NetworkEquipment") add_metric "glpi_network_devices_total" "gauge" "Total number of network devices" "${netdevices:-0}" # Phones local phones phones=$(api_get_count "Phone") add_metric "glpi_phones_total" "gauge" "Total number of phones" "${phones:-0}" # Printers local printers printers=$(api_get_count "Printer") add_metric "glpi_printers_total" "gauge" "Total number of printers" "${printers:-0}" # Software local software software=$(api_get_count "Software") add_metric "glpi_software_total" "gauge" "Total number of software entries" "${software:-0}" } collect_organization() { # Users local users users=$(api_get_count "User") add_metric "glpi_users_total" "gauge" "Total number of users" "${users:-0}" # Groups local groups groups=$(api_get_count "Group") add_metric "glpi_groups_total" "gauge" "Total number of groups" "${groups:-0}" # Entities local entities entities=$(api_get_count "Entity") add_metric "glpi_entities_total" "gauge" "Total number of entities" "${entities:-0}" # Locations local locations locations=$(api_get_count "Location") add_metric "glpi_locations_total" "gauge" "Total number of locations" "${locations:-0}" } # --- Output --- write_output() { if [[ "$TEXTFILE_MODE" == true ]]; then local output_file="${TEXTFILE_DIR}/glpi.prom" local temp_file="${output_file}.$$" mkdir -p "$TEXTFILE_DIR" echo "$OUTPUT" > "$temp_file" mv "$temp_file" "$output_file" echo "# Wrote metrics to ${output_file}" >&2 else echo "$OUTPUT" fi } serve_http() { if ! command -v nc &>/dev/null && ! command -v ncat &>/dev/null; then echo "# ERROR: nc (netcat) or ncat required for HTTP mode" >&2 exit 1 fi echo "# GLPI exporter listening on port ${HTTP_PORT}" >&2 echo "# Metrics endpoint: http://localhost:${HTTP_PORT}/metrics" >&2 local nc_cmd="nc" if command -v ncat &>/dev/null; then nc_cmd="ncat" fi while true; do OUTPUT="" START_TIME=$(date +%s%N) add_metric "glpi_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\"" if init_session; then add_metric "glpi_up" "gauge" "GLPI API reachability (1=up, 0=down)" "1" collect_tickets collect_assets collect_organization kill_session else add_metric "glpi_up" "gauge" "GLPI API reachability (1=up, 0=down)" "0" fi local end_time duration end_time=$(date +%s%N) duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0") add_metric "glpi_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration" add_metric "glpi_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)" local content_length=${#OUTPUT} local response="HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Length: ${content_length}\r\nConnection: close\r\n\r\n${OUTPUT}" echo -e "$response" | $nc_cmd -l -p "$HTTP_PORT" -q 1 2>/dev/null || \ echo -e "$response" | $nc_cmd -l "$HTTP_PORT" -c 2>/dev/null || \ echo -e "$response" | $nc_cmd -l -p "$HTTP_PORT" 2>/dev/null || true done } install_cron() { if [[ $EUID -ne 0 ]]; then echo "# ERROR: --install requires root" >&2 exit 1 fi local script_path script_path=$(readlink -f "$0") cat > /etc/cron.d/glpi-exporter </dev/null EOF chmod 644 /etc/cron.d/glpi-exporter echo "# Installed cron job: /etc/cron.d/glpi-exporter" >&2 echo "# Metrics will be written to: ${TEXTFILE_DIR}/glpi.prom" >&2 } # --- Main --- main() { for arg in "$@"; do case "$arg" in --textfile) TEXTFILE_MODE=true ;; --http) HTTP_MODE=true ;; -p|--port) shift; HTTP_PORT="${1:-$HTTP_PORT}" ;; --install) check_dependencies validate_config install_cron exit 0 ;; --help|-h) usage ;; *) ;; esac done check_dependencies validate_config if [[ "$HTTP_MODE" == true ]]; then serve_http exit 0 fi START_TIME=$(date +%s%N) add_metric "glpi_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\"" if init_session; then add_metric "glpi_up" "gauge" "GLPI API reachability (1=up, 0=down)" "1" collect_tickets collect_assets collect_organization kill_session else add_metric "glpi_up" "gauge" "GLPI API reachability (1=up, 0=down)" "0" fi local end_time duration end_time=$(date +%s%N) duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0") add_metric "glpi_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration" add_metric "glpi_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)" write_output } main "$@"