#!/usr/bin/env bash # reconcile-report.sh — nightly CMDB drift report [#801 C3] # # Compares the CANONICAL seed CSV (seed/systems.csv, the rows that created # the GLPI computers) against the live GLPI CMDB and reports drift BOTH # ways. REPORT ONLY — this script never writes to GLPI. Apply-mode stays # manual (glpi-change.sh / to-glpi.sh) before CRs. # # Usage: # reconcile-report.sh # report to stdout # reconcile-report.sh --out FILE # also write the report # reconcile-report.sh --from-inventory # re-derive seed from t/307 post # # (EXPERIMENTAL: parser covers # # sections s1-s4 only today) # Env: discourse.env (DISCOURSE_*, only for --from-inventory), mglpi.env. # Exit: 0 no drift | 3 drift found | 1 error. set -uo pipefail POST_ID="${INV307_POST_ID:-385}" FROM_INVENTORY=0 OUT="" while [ $# -gt 0 ]; do case "$1" in --out) OUT="$2"; shift 2 ;; --from-inventory) FROM_INVENTORY=1; shift ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done # shellcheck disable=SC1091 [ -f "$HOME/.creds/discourse.env" ] && . "$HOME/.creds/discourse.env" # shellcheck disable=SC1091 [ -f "$HOME/.creds/mglpi.env" ] && . "$HOME/.creds/mglpi.env" : "${DISCOURSE_URL:?}"; : "${DISCOURSE_API_KEY:?}"; : "${DISCOURSE_API_USERNAME:?}" : "${MGLPI_URL:?}"; : "${MGLPI_APP_TOKEN:?}"; : "${MGLPI_USER_TOKEN:?}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT # 1. inventory side: canonical seed CSV (or re-derived from t/307) if [ "$FROM_INVENTORY" = 1 ]; then raw=$(curl -sS --max-time 30 -H "Api-Key: ${DISCOURSE_API_KEY}" \ -H "Api-Username: ${DISCOURSE_API_USERNAME}" "${DISCOURSE_URL}/posts/${POST_ID}.json" \ | jq -r '.raw // empty') [ -n "$raw" ] || { echo "FAIL: could not fetch t/307 post ${POST_ID}" >&2; exit 1; } printf '%s' "$raw" > "$TMP/inv.md" bash "$SCRIPT_DIR/../seed/from-inventory.sh" "$TMP/inv.md" > "$TMP/systems.csv" || exit 1 else cp "$SCRIPT_DIR/../seed/systems.csv" "$TMP/systems.csv" fi # 2. GLPI computers (read-only) S=$(curl -sS --max-time 30 -H "Content-Type: application/json" \ -H "App-Token: ${MGLPI_APP_TOKEN}" -H "Authorization: user_token ${MGLPI_USER_TOKEN}" \ "${MGLPI_URL}/initSession" | jq -r '.session_token // empty') [ -n "$S" ] || { echo "FAIL: GLPI session init" >&2; exit 1; } curl -sS --max-time 60 -H "App-Token: ${MGLPI_APP_TOKEN}" -H "Session-Token: $S" \ "${MGLPI_URL}/Computer?range=0-499&is_deleted=0" > "$TMP/glpi.json" curl -sS --max-time 30 -X DELETE -H "App-Token: ${MGLPI_APP_TOKEN}" \ -H "Session-Token: $S" "${MGLPI_URL}/killSession" >/dev/null # 3. name sets (lowercased) jq -r '.[] | select(type == "object") | (.name // "") | ascii_downcase' "$TMP/glpi.json" \ | sort -u > "$TMP/glpi.names" # ci_name is the 2nd field: rows are `category,"ci_name",...` — capture # between the first `,"` and the closing `"` (names never contain quotes) tail -n +2 "$TMP/systems.csv" | sed -n 's/^[^,]*,"\([^"]*\)".*/\1/p' \ | tr '[:upper:]' '[:lower:]' | sort -u > "$TMP/inv.names" comm -23 "$TMP/inv.names" "$TMP/glpi.names" > "$TMP/missing_in_glpi" comm -13 "$TMP/inv.names" "$TMP/glpi.names" > "$TMP/ghost_in_glpi" miss=$(grep -c . "$TMP/missing_in_glpi" || true) ghost=$(grep -c . "$TMP/ghost_in_glpi" || true) { echo "# CMDB drift report $(date -Is) (inventory t/307 post $POST_ID vs GLPI computers)" echo "inventory CIs: $(grep -c . "$TMP/inv.names") | GLPI computers: $(grep -c . "$TMP/glpi.names")" echo echo "## in inventory, MISSING in GLPI ($miss)" cat "$TMP/missing_in_glpi" echo echo "## in GLPI, not in inventory — GHOSTS ($ghost)" cat "$TMP/ghost_in_glpi" } | tee "${OUT:-/dev/null}" [ "$miss" -eq 0 ] && [ "$ghost" -eq 0 ] && exit 0 || exit 3