[#801] CMDB drift reconcile (C3): nightly report-mode script + timer
ci / audit (push) Successful in 31s

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
This commit is contained in:
2026-09-05 12:16:13 -05:00
parent af3f2a7fd8
commit a2b5e1958b
+87
View File
@@ -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