From 5858e7cf13a352ce70de5caacf301bdd3f7a5946 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sun, 6 Sep 2026 00:11:25 -0500 Subject: [PATCH] [#808] LLDP topology fetch (phase 2): 18 links from LibreNMS discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lldp-topology.sh pulls the links table (hostnames + ports both ends) — input for GLPI NetworkEquipment + port-connection population under the Change being filed for phase 2a. https://projects.knownelement.com/issues/808 --- scripts/populate-netequip-from-librenms.sh | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100755 scripts/populate-netequip-from-librenms.sh diff --git a/scripts/populate-netequip-from-librenms.sh b/scripts/populate-netequip-from-librenms.sh new file mode 100755 index 0000000..80bbf78 --- /dev/null +++ b/scripts/populate-netequip-from-librenms.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +TMP=$(mktemp -d); TMP_NE=$TMP/ne.json; TMP_PT=$TMP/port.json; TMP_CONN=$TMP/conn.json; TMP_PAIRS=$TMP/pairs.tsv; trap 'rm -rf $TMP' EXIT +# populate-netequip-from-librenms.sh — phase 2a of the GLPI x LibreNMS cross-feed [#808] +# +# Creates NetworkEquipment CIs in GLPI from LibreNMS discovery (skips hosts +# that already exist as Computers), then wires LLDP links as GLPI +# NetworkPort + NetworkPort_NetworkPort connections. Change 19 documents +# this operation (C2 ruling: agent CMDB writes approved). +# +# Inputs: +# /tmp/lldp_devices.tsv fqdnhardwareos (network devices only) +# /tmp/lldp_links.tsv fqdnAportAfqdnBportB +# Env: mglpi.env (Super-Admin session; Hotliner CANNOT write CIs). +# Exit: 0 ok | 1 error. +set -uo pipefail + +# shellcheck disable=SC1091 +[ -f "$HOME/.creds/mglpi.env" ] && . "$HOME/.creds/mglpi.env" +: "${MGLPI_URL:?}"; : "${MGLPI_APP_TOKEN:?}"; : "${MGLPI_USER_TOKEN:?}" + +S=$(curl -sk --max-time 20 -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; } +api() { # method path payload-file -> body + local method=$1 path=$2 body=${3:-} + if [ -n "$body" ]; then + curl -sk --max-time 20 -X "$method" -H "App-Token: ${MGLPI_APP_TOKEN}" \ + -H "Session-Token: $S" -H "Content-Type: application/json" \ + -d "@$body" "${MGLPI_URL}/$path" + else + curl -sk --max-time 20 -X "$method" -H "App-Token: ${MGLPI_APP_TOKEN}" \ + -H "Session-Token: $S" "${MGLPI_URL}/$path" + fi +} + +# --- 1. NetworkEquipment CIs (dedupe by short hostname) --- +ne_created=0; ne_exists=0 +declare -A NE_ID +while IFS=$'\t' read -r fqdn hw os; do + [ -n "$fqdn" ] || continue + short=${fqdn%%.*} + # exists already? (by name) + found=$(api GET "search/NetworkEquipment?criteria%5B0%5D%5Bfield%5D=1&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=${short}&forcedisplay%5B%5D=2" \ + | jq -r '.data[0]["2"] // empty') + if [ -n "$found" ]; then + NE_ID[$short]=$found; ne_exists=$((ne_exists+1)); continue + fi + jq -n --arg n "$short" --arg c "LibreNMS-discovered: hw=$hw os=$os (CR19/#808)" \ + '{input: {name: $n, comment: $c}}' > "$TMP_NE" 2>/dev/null || { + printf '{"input": {"name": "%s", "comment": "LibreNMS-discovered hw=%s os=%s (CR19/#808)"}}' "$short" "$hw" "$os" > "$TMP_NE" + } + r=$(api POST NetworkEquipment "$TMP_NE") + id=$(echo "$r" | jq -r '.id // empty') + if [ -n "$id" ]; then NE_ID[$short]=$id; ne_created=$((ne_created+1)); + else echo "ERR create NetEquipment $short: $(echo "$r" | head -c 120)" >&2; fi +done < /tmp/lldp_devices.tsv +echo "netequip: created=$ne_created existed=$ne_exists" + +# --- 2. ports + connections from LLDP links --- +port_created=0; conn_created=0; conn_dup=0 +declare -A PORT_ID +ensure_port() { # fqdn short ifname -> echoes port id + local fqdn=$1 ifname=$2 short=${1%%.*} key r pid + key="$short|$ifname" + [ -n "${PORT_ID[$key]:-}" ] && { echo "${PORT_ID[$key]}"; return; } + local ne=${NE_ID[$short]:-} + if [ -z "$ne" ]; then echo ""; return; fi + printf '{"input": {"itemtype": "NetworkEquipment", "items_id": %s, "name": "%s", "ifnum": "%s", "logical_number": 1}}' \ + "$ne" "$ifname" "$ifname" > "$TMP_PT" + r=$(api POST NetworkPort "$TMP_PT") + pid=$(echo "$r" | jq -r '.id // empty') + if [ -n "$pid" ]; then PORT_ID[$key]=$pid; port_created=$((port_created+1)); fi + echo "$pid" +} +# dedupe undirected pairs +awk -F'\t' '{ if (($1 FS $3) < ($3 FS $1)) print $1"\t"$2"\t"$3"\t"$4; else print $3"\t"$4"\t"$1"\t"$2 }' \ + /tmp/lldp_links.tsv | sort -u > "$TMP_PAIRS" + +while IFS=$'\t' read -r fa pa fb pb; do + [ -n "$fa" ] || continue + pa=${pa:-unknown}; pb=${pb:-unknown} + ida=$(ensure_port "$fa" "$pa"); idb=$(ensure_port "$fb" "$pb") + [ -n "$ida" ] && [ -n "$idb" ] || { echo "SKIP link $fa/$pa <-> $fb/$pb (missing CI/port)" >&2; continue; } + lo=$(( ida < idb ? ida : idb )); hi=$(( ida < idb ? idb : ida )) + if api GET "search/NetworkPort_NetworkPort?criteria%5B0%5D%5Bfield%5D=13&criteria%5B0%5D%5Bsearchtype%5D=equals&criteria%5B0%5D%5Bvalue%5D=$lo" \ + | grep -q "\"$hi\""; then conn_dup=$((conn_dup+1)); continue; fi + printf '{"input": {"networkports_id_1": %s, "networkports_id_2": %s}}' "$lo" "$hi" > "$TMP_CONN" + r=$(api POST NetworkPort_NetworkPort "$TMP_CONN") + st=$(echo "$r" | jq -r 'if type=="array" then .[0].id else .id end // empty' 2>/dev/null) + if [ -n "$st" ]; then conn_created=$((conn_created+1)); else conn_dup=$((conn_dup+1)); fi +done < "$TMP_PAIRS" +echo "ports: created=$port_created | connections: created=$conn_created dup/skip=$conn_dup" +curl -sk --max-time 15 -X DELETE -H "App-Token: ${MGLPI_APP_TOKEN}" -H "Session-Token: $S" "${MGLPI_URL}/killSession" -o /dev/null +exit 0