#!/usr/bin/env bash # glpi-change.sh — DEPRECATED shim: superseded by mglpi (KNEL connector # mopac-glpi-go — Go CLI + MCP, git.knownelement.com/UKRRS/mopac-glpi-go). # Charles ruling 2026-09-04: all custom tooling lives in ukrrs/connectors, # written in Go, MCP/CLI dual-surface. This bash script still WORKS (kept # until callers migrate) — new code must use mglpi: # echo "body" | mglpi --config ~/.creds/mglpi-agent.env change create \ # --title "[#NNN] summary" --content - # Exec-host: container mopac-cli-mglpi; shim: ~/.local/bin/mglpi. # # glpi-change.sh — create GLPI Change entries from the CLI [#769][#767] # # Charles rulings: questions-v10 OQ7 (2026-09-03) — GLPI Changes are logged # from NOW ON for converge/infra work, no Redmine-only exception path. # questions-09022206 Q1/Q2/Q3 (2026-09-04) — CRs live in GLPI as Changes; # the cmdb agent files CRs itself (least-privilege identity); prod (Tier 2) # agent ops carry .crush/active-cr with the Change id (t/325 §5 ladder). # # Two identities: # default ~/.creds/glpi.env — admin session (bootstrap, break-glass) # --agent ~/.creds/glpi-agent.env — the cmdb agent (user 7). GLPI's REST # API cannot write profile rights, and Self-Service cannot # create Changes, so the agent session switches to the built-in # Hotliner profile (verified: create+followup+transition OK, # CI writes still DENIED). Approved via Q2 ruling 2026-09-04. # # Ladder status mapping (GLPI Change statuses; ints per GLPI Change::getAllStatusArray): # 1 New (draft/filed) 2 Evaluation (review) 3 Approval 4 Acceptance # 5 Pending 6 Test 7 Qualification 8 closed 9 solved (verify label in UI # on first use — the REST API exposes ints only). # # Env: GLPI_URL, GLPI_USER_TOKEN, GLPI_APP_TOKEN in the chosen .env # # Usage: # echo "what changed, why, where" | \ # glpi-change.sh create --title "[#NNN] " [--urgency 2] [--impact 2] [--agent] # glpi-change.sh transition [--agent] # echo "note text" | glpi-change.sh followup [--agent] # glpi-change.sh show [--agent] # glpi-change.sh list [--status N] [--agent] set -euo pipefail die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } ENV_FILE="$HOME/.creds/glpi.env" IS_AGENT=0 # Pre-scan for --agent / --env before subcommand dispatch so both env files # must exist only when actually referenced. pre=("$@") for a in ${pre[@]+"${pre[@]}"}; do case "$a" in --agent) IS_AGENT=1 ;; esac done [ "$IS_AGENT" -eq 1 ] && ENV_FILE="$HOME/.creds/glpi-agent.env" [ -f "$ENV_FILE" ] || die "'$ENV_FILE' missing" set -a; . "$ENV_FILE"; set +a BASE="${GLPI_URL%/}" if [ -z "${GLPI_USER_TOKEN:-}" ] || [ -z "${GLPI_APP_TOKEN:-}" ]; then die "tokens missing in $ENV_FILE"; fi sess_request() { curl -s -m 30 -H "Content-Type: application/json" \ -H "App-Token: $GLPI_APP_TOKEN" \ -H "Authorization: user_token $GLPI_USER_TOKEN" \ "$BASE/apirest.php/initSession" } session="$(sess_request)" S_TOKEN="$(printf '%s' "$session" | jq -r '.session_token // empty')" [ -n "$S_TOKEN" ] || die "initSession failed: $session" cleanup() { curl -s -m 15 -o /dev/null -H "Session-Token: $S_TOKEN" \ -H "App-Token: $GLPI_APP_TOKEN" "$BASE/apirest.php/killSession" || true; } trap cleanup EXIT auth=(-H "Session-Token: $S_TOKEN" -H "App-Token: $GLPI_APP_TOKEN" -H "Content-Type: application/json") # api [body-file] api() { local method="$1" path="$2" body="${3:-}" if [ -n "$body" ]; then curl -s -m 30 -X "$method" "${auth[@]}" -d "@$body" "$BASE/apirest.php/$path" else curl -s -m 30 -X "$method" "${auth[@]}" "$BASE/apirest.php/$path" fi } # Activate the Hotliner profile for the agent identity (no-op cost for admin). if [ "$IS_AGENT" -eq 1 ]; then echo '{"profiles_id":5}' > "/tmp/glpi-change-cap.$$" api POST changeActiveProfile "/tmp/glpi-change-cap.$$" >/dev/null rm -f "/tmp/glpi-change-cap.$$" fi cmd="${1:-create}"; shift || true # Strip the shared flags from remaining args. args=() while [ $# -gt 0 ]; do case "$1" in --agent) shift ;; *) args+=("$1"); shift ;; esac done case "$cmd" in create) title=""; urgency=3; impact=3; content="" while [ ${#args[@]} -gt 0 ]; do case "${args[0]}" in --title) title="${args[1]}"; args=("${args[@]:2}") ;; --urgency) urgency="${args[1]}"; args=("${args[@]:2}") ;; --impact) impact="${args[1]}"; args=("${args[@]:2}") ;; *) die "unknown option ${args[0]}" ;; esac done [ -n "$title" ] || die "--title required" content="$(cat)" [ -n "$content" ] || die "change body required on stdin" body="$(mktemp)" jq -n --arg t "$title" --arg c "$content" \ --argjson u "$urgency" --argjson i "$impact" \ '{input: {name: $t, content: $c, urgency: $u, impact: $i}}' > "$body" # GLPI returns an ARRAY on create: [{"id":N,"message":...}] — handle both shapes. resp="$(api POST Change "$body")" rm -f "$body" cid="$(printf '%s' "$resp" | jq -r '.id // .[0].id // empty')" [ -n "$cid" ] || die "create failed: $resp" echo "created Change id=$cid — $BASE/front/change.form.php?id=$cid" ;; transition) cid="${args[0]:-}"; status="${args[1]:-}" if [ -z "" ] || [ -z "" ]; then die "usage: transition "; fi body="$(mktemp)" jq -n --argjson s "$status" '{input: {status: $s}}' > "$body" resp="$(api PUT "Change/$cid" "$body")" rm -f "$body" printf '%s' "$resp" | jq -e --arg id "$cid" '.[0][$id] == true' >/dev/null \ || die "transition failed: $resp" echo "Change $cid -> status $status" ;; followup) cid="${args[0]:-}" [ -n "$cid" ] || die "usage: followup (body on stdin)" content="$(cat)" [ -n "$content" ] || die "followup body required on stdin" body="$(mktemp)" jq -n --argjson c "$cid" --arg t "$content" \ '{input: [{itemtype: "Change", items_id: $c, content: $t}]}' > "$body" resp="$(api POST ITILFollowup "$body")" rm -f "$body" printf '%s' "$resp" | jq -e '.[0].id' >/dev/null || die "followup failed: $resp" echo "followup added to Change $cid" ;; show) cid="${args[0]:?change-id}" api GET "Change/$cid" | jq . ;; list) status="" while [ ${#args[@]} -gt 0 ]; do case "${args[0]}" in --status) status="${args[1]}"; args=("${args[@]:2}") ;; *) die "unknown option ${args[0]}" ;; esac done url="search/Change/?forcedisplay%5B0%5D=1&forcedisplay%5B1%5D=2&forcedisplay%5B2%5D=12&range=0-999" [ -n "$status" ] && url="$url&criteria%5B0%5D%5Bfield%5D=12&criteria%5B0%5D%5Bsearchtype%5D=equals&criteria%5B0%5D%5Bvalue%5D=$status" # Rows come back as objects keyed by field id: 1=name, 2=id, 12=status. api GET "$url" | jq -r '.data[]? | [."2", ."12", ."1"] | @tsv' ;; *) die "unknown command $cmd" ;; esac