feat(cmdb): inventory reconcile + GLPI software catalog seeder [#705]

#307 s6 rewritten post-verification: zone was ALREADY clean (zero DNS
deletes); 12 stale CIs purged from GLPI, 3 alive off-cluster systems
given proper CIs (tsys-cloudron, ultix-field/highside), 75-row seed
regenerated. software-catalog.sh: 21 products + 59 installs (wazuh
agents linked from live manager); version lookup fixed to client-side
parent filter (GLPI ignores softwares_id search param).

Detail: https://projects.knownelement.com/issues/705
This commit is contained in:
2026-09-03 14:01:56 -05:00
parent 4c461c14e1
commit 056412a311
+169
View File
@@ -0,0 +1,169 @@
#!/bin/bash
#
# software-catalog.sh — seed the GLPI software catalog [#705]
#
# Usage: software-catalog.sh [--dry-run]
# Needs: ~/.creds/glpi.env (GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN)
# ~/.creds/wazuh.env optional (WAZUH_API_PASS) — for live agent installs
#
# Creates Software assets + SoftwareVersions and links installations to
# Computer CIs. Idempotent at all three levels (software, version, install).
# Versions are recorded ONLY where verified; unknowns say so — GLPI Agent
# auto-inventory (proposed) is the durable fix for version drift.
#
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
CREDS="${HOME}/.creds/glpi.env"
# shellcheck disable=SC1090
. "$CREDS"
: "${GLPI_URL:?}" "${GLPI_APP_TOKEN:?}" "${GLPI_USER_TOKEN:?}"
API="$GLPI_URL/apirest.php"
api() {
local method="$1" path="$2" body="${3:-}"
if [ -n "$body" ]; then
curl -sS --max-time 30 -X "$method" -H "App-Token: $GLPI_APP_TOKEN" \
-H "Session-Token: $SESSION" -H "Content-Type: application/json" \
-d "$body" "$API/$path"
else
curl -sS --max-time 30 -X "$method" -H "App-Token: $GLPI_APP_TOKEN" \
-H "Session-Token: $SESSION" "$API/$path"
fi
}
SESSION=$(curl -sS --max-time 20 -H "Content-Type: application/json" \
-H "App-Token: $GLPI_APP_TOKEN" \
-H "Authorization: user_token $GLPI_USER_TOKEN" \
"$API/initSession" | jq -re '.session_token')
echo "— session ok"
# software_id <name> — existing id or empty
sw_id() {
api GET "Software?searchText%5Bname%5D=$(printf %s "$1" | sed 's/ /%20/g')" \
| jq -r --arg n "$1" '[.[] | select(.name==$n)][0].id // empty'
}
# version_id <swid> <version> — GLPI ignores softwares_id as a search param,
# so pull the full list and filter client-side (parent AND name)
ver_id() {
api GET "SoftwareVersion?range=0-999" \
| jq -r --arg s "$1" --arg v "$2" \
'[.[] | select((.softwares_id|tostring)==$s and .name==$v)][0].id // empty'
}
# computer_id <ci-name>
pc_id() {
api GET "Computer?searchText%5Bname%5D=$(printf %s "$1" | sed 's/ /%20/g')" \
| jq -r --arg n "$1" '[.[] | select(.name==$n)][0].id // empty'
}
# install_exists <verid> <pcid>
install_exists() {
api GET "Item_SoftwareVersion?range=0-4999" \
| jq -e --arg v "$1" --arg p "$2" \
'[.[] | select(.softwareversions_id==($v|tonumber) and .items_id==($p|tonumber) and .itemtype=="Computer")] | length > 0' \
>/dev/null 2>&1
}
# ensure <swname> <version> <targets-comma-list|-> — '-' = catalog only
ensure() {
local sw="$1" ver="$2" targets="$3" sid vid cid
sid=$(sw_id "$sw")
if [ -z "$sid" ]; then
if [ "$DRY" = 1 ]; then echo " DRY: software '$sw'"; sid=0; else
sid=$(api POST Software "{\"input\":{\"name\":\"$sw\",\"manufacturers_id\":0,\"comment\":\"seeded by software-catalog.sh (#705)\"}}" \
| jq -r '.id // empty')
echo " + software '$sw' (id=$sid)"
fi
else
echo " = software '$sw' (id=$sid)"
fi
[ "$sid" = 0 ] && [ "$DRY" = 1 ] && return 0
vid=$(ver_id "$sid" "$ver")
if [ -z "$vid" ]; then
if [ "$DRY" = 1 ]; then echo " DRY: version '$ver'"; else
vid=$(api POST SoftwareVersion "{\"input\":{\"softwares_id\":\"$sid\",\"name\":\"$ver\",\"state\":0}}" \
| jq -r '.id // empty')
echo " + version '$ver' (id=$vid)"
fi
else
echo " = version '$ver' (id=$vid)"
fi
[ -z "$vid" ] && return 0
[ "$targets" = "-" ] && return 0
local IFS=','
for t in $targets; do
cid=$(pc_id "$t")
if [ -z "$cid" ]; then echo " ! no CI named '$t' (skipped)"; continue; fi
if install_exists "$vid" "$cid"; then echo " = installed on $t"; continue; fi
if [ "$DRY" = 1 ]; then echo " DRY: install on $t"; else
api POST Item_SoftwareVersion \
"{\"input\":{\"itemtype\":\"Computer\",\"items_id\":\"$cid\",\"softwareversions_id\":\"$vid\"}}" \
| jq -re 'if type=="array" or .id then empty else . end' >/dev/null 2>&1 \
&& echo " ! install POST failed on $t" || echo " + installed on $t"
fi
done
}
echo "— catalog rows"
# name | version | targets (comma list, '-' = catalog only)
while IFS='|' read -r sw ver targets; do
[ -z "$sw" ] && continue
ensure "$(echo "$sw" | xargs)" "$(echo "$ver" | xargs)" "$(echo "$targets" | xargs)"
done <<EOF
Proxmox VE|9.2.5|pfv-tsys1,pfv-tsys3,pfv-tsys4,pfv-tsys5,pfv-tsys6,pfv-tsys7,pfv-tsys9
Wazuh Manager|4.14.7|tsys-siem
Wazuh Indexer|4.14.7|tsys-siem
Wazuh Dashboard|4.14.7|tsys-siem
Wazuh Agent|4.14.7-1|-
Docker Engine|unknown (GLPI Agent pending)|pfv-netinfra-01,pfv-netinfra-02,tsys-librenms,tsys-cloudron
Pi-hole FTL (v6)|unknown (GLPI Agent pending)|pfv-netinfra-01,pfv-netinfra-02
Technitium DNS Server|unknown (GLPI Agent pending)|pfv-netinfra-01,pfv-netinfra-02
ISC dhcpd|unknown (GLPI Agent pending)|pfv-netinfra-01,pfv-netinfra-02
nginx|unknown (GLPI Agent pending)|tsys-ca,tsys-librenms
LibreNMS|unknown (GLPI Agent pending)|tsys-librenms
NetDisco|unknown (GLPI Agent pending)|tsys-librenms
Oxidized|unknown (GLPI Agent pending)|tsys-librenms
SmokePing|unknown (GLPI Agent pending)|tsys-librenms
unpoller|unknown (GLPI Agent pending)|tsys-librenms
NUT (Network UPS Tools)|unknown (GLPI Agent pending)|-
Proxmox Backup Server|4.x (dev, VM 5104)|-
k3s|unknown (k8s lane)|-
Uptime Kuma|unknown (Cloudron app)|tsys-cloudron
GLPI|unknown (Cloudron app)|tsys-cloudron
Vaultwarden|unknown (Cloudron app)|tsys-cloudron
EOF
# Wazuh Agent installs: from the LIVE manager (ground truth), CI matched by name
if [ -r "${HOME}/.creds/wazuh.env" ] && [ "${SKIP_AGENTS:-0}" != 1 ]; then
# shellcheck disable=SC1090
. "${HOME}/.creds/wazuh.env"
echo "— wazuh agent installs (live manager)"
sid=$(sw_id "Wazuh Agent")
[ -z "$sid" ] && { echo " ! Wazuh Agent software missing"; exit 0; }
vid=$(ver_id "$sid" "4.14.7-1")
[ -z "$vid" ] && { echo " ! version 4.14.7-1 missing"; exit 0; }
TOKEN=$(curl -sS --max-time 20 -u "wazuh:${WAZUH_API_PASS}" -k \
-X POST "https://tsys-wazuh.knel.net:55000/security/user/authenticate?raw=true")
curl -sS --max-time 30 -k -H "Authorization: Bearer $TOKEN" \
"https://tsys-wazuh.knel.net:55000/agents?status=active&limit=500" \
| jq -r '.data.affected_items[].name' > /tmp/wz-agents.txt
while IFS= read -r a; do
[ "$a" = "000" ] && continue
cid=$(pc_id "$a")
if [ -z "$cid" ]; then echo " ! agent '$a' has no CI (skipped)"; continue; fi
if install_exists "$vid" "$cid"; then echo " = $a"; continue; fi
if [ "$DRY" = 1 ]; then echo " DRY: install on $a"; else
api POST Item_SoftwareVersion \
"{\"input\":{\"itemtype\":\"Computer\",\"items_id\":\"$cid\",\"softwareversions_id\":\"$vid\"}}" >/dev/null \
&& echo " + $a"
fi
done < /tmp/wz-agents.txt
fi
echo "— done"