From 333ad9d1eb0026b00ba078d64a6e340c6afe1068 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sat, 5 Sep 2026 16:20:41 -0500 Subject: [PATCH] =?UTF-8?q?[#705]=20LibreNMS=E2=86=92GLPI=20network-layer?= =?UTF-8?q?=20drift=20report=20(phase=201=20of=20CMDB=20cross-feed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit librenms-glpi-drift.sh: LibreNMS discovery vs GLPI NetworkEquipment, report-only. First run: 11 devices discovered, 0 in GLPI. Location data needs normalization (sysLocation jokes) before location sync. Phase 2 = LLDP links → GLPI connections. https://projects.knownelement.com/issues/705 --- scripts/librenms-glpi-drift.sh | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 scripts/librenms-glpi-drift.sh diff --git a/scripts/librenms-glpi-drift.sh b/scripts/librenms-glpi-drift.sh new file mode 100755 index 0000000..6a9c28e --- /dev/null +++ b/scripts/librenms-glpi-drift.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# librenms-glpi-drift.sh — network-layer CMDB drift report [#705 x #343 x #461] +# +# Compares LibreNMS-discovered network devices (the network discovery truth) +# against GLPI NetworkEquipment CIs (the CMDB). Report-only; apply-mode is a +# separate approved change. +# +# Env: librenms.env (LIBRENMS_URL/LIBRENMS_API_TOKEN), mglpi.env (MGLPI_*). +# Exit: 0 no drift | 3 drift found | 1 error. +set -uo pipefail + +# shellcheck disable=SC1091 +[ -f "$HOME/.creds/librenms.env" ] && . "$HOME/.creds/librenms.env" +# shellcheck disable=SC1091 +[ -f "$HOME/.creds/mglpi.env" ] && . "$HOME/.creds/mglpi.env" +: "${LIBRENMS_URL:?}"; : "${LIBRENMS_API_TOKEN:?}" +: "${MGLPI_URL:?}"; : "${MGLPI_APP_TOKEN:?}"; : "${MGLPI_USER_TOKEN:?}" + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT + +# 1. LibreNMS devices +curl -sS --max-time 30 -H "X-Auth-Token: ${LIBRENMS_API_TOKEN}" \ + "${LIBRENMS_URL}/api/v0/devices" > "$TMP/lnms.json" +jq -r '.devices[] | [.hostname, (.sysName // ""), (.location // ""), (.hardware // ""), (.os // "")] | @tsv' \ + "$TMP/lnms.json" > "$TMP/lnms.tsv" +[ -s "$TMP/lnms.tsv" ] || { echo "FAIL: LibreNMS returned no devices" >&2; exit 1; } + +# 2. GLPI NetworkEquipment CIs +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}/NetworkEquipment?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 +jq -r '.[] | select(type == "object") | (.name // "") | ascii_downcase' "$TMP/glpi.json" \ + | sort -u > "$TMP/glpi.names" + +# 3. diff by hostname (short name match: glpi uses short names, lnms uses FQDN) +cut -f1 "$TMP/lnms.tsv" | sed 's/\..*//' | tr '[:upper:]' '[:lower:]' | sort -u > "$TMP/lnms.names" +comm -23 "$TMP/lnms.names" "$TMP/glpi.names" > "$TMP/missing_in_glpi" +comm -13 "$TMP/lnms.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 "# Network-layer CMDB drift report $(date -Is) (LibreNMS discovery vs GLPI NetworkEquipment)" + echo "LibreNMS devices: $(grep -c . "$TMP/lnms.names") | GLPI NetworkEquipment CIs: $(grep -c . "$TMP/glpi.names")" + echo + echo "## discovered by LibreNMS, MISSING in GLPI ($miss)" + while IFS=$'\t' read -r fqdn _sys loc hw os; do + short=${fqdn%%.*} + grep -qx "$short" "$TMP/missing_in_glpi" && echo "$short (lnms: $fqdn, loc=$loc, hw=$hw, os=$os)" + done < "$TMP/lnms.tsv" + echo + echo "## in GLPI as NetworkEquipment, NOT in LibreNMS ($ghost)" + cat "$TMP/ghost_in_glpi" + echo + echo "# phase 2 (queued): LLDP neighbor links → GLPI port connections" +} | tee "${OUT:-/dev/null}" + +[ "$miss" -eq 0 ] && [ "$ghost" -eq 0 ] && exit 0 || exit 3