From a2b5e1958b88337d4d58207344c86603d855b875 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sat, 5 Sep 2026 12:16:13 -0500 Subject: [PATCH] [#801] CMDB drift reconcile (C3): nightly report-mode script + timer reconcile-report.sh diffs seed/systems.csv (canonical seed) vs live GLPI computers; report-only, never writes GLPI. First run: 77 seed CIs all present, 3 GLPI ghosts (tsys-cloudron, ultix-field, ultix-highside). Timer ukrrs-glpi-reconcile.timer 04:17 CT on ultix-streaming; exit 3 on drift = alert surface. --from-inventory mode experimental (s1-s4 parser). https://projects.knownelement.com/issues/801 --- scripts/reconcile-report.sh | 87 +++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 scripts/reconcile-report.sh diff --git a/scripts/reconcile-report.sh b/scripts/reconcile-report.sh new file mode 100755 index 0000000..9cf81d4 --- /dev/null +++ b/scripts/reconcile-report.sh @@ -0,0 +1,87 @@ +#!/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