217 lines
8.9 KiB
Markdown
217 lines
8.9 KiB
Markdown
# mopac-glpi-go
|
|
|
|
A 100% Go client library, CLI (`mglpi`), and MCP server (`mglpi-mcp`)
|
|
for the GLPI REST API (ITSM/CMDB). Replaces python glue for every CMDB
|
|
update: one static binary, zero third-party modules (stdlib only),
|
|
machine-parseable JSON output, and tokens that never appear in flags,
|
|
logs, or error strings. Clones the architecture of
|
|
[mopac-redmine-go](https://git.knownelement.com/ukrrs/mopac-redmine-go)
|
|
file for file.
|
|
|
|
Status: 2026-09-03 — v0 complete and green: changes (create / show /
|
|
list / transition / followup), CI search + raw item fetch, profiles +
|
|
agent-profile auto-switch, session lifecycle (init / kill), 0600
|
|
env-file config, `-o json` on every command, exit codes 0/1/2 with
|
|
one-line API-error stderr, and a stdio MCP server (initialize /
|
|
tools-list / 6 tools). Built and tested entirely against a fake GLPI
|
|
(unit suite + end-to-end smoke); the live CMDB
|
|
(cmdb.knownelement.com) attaches with zero code change via
|
|
`MGLPI_URL`/`MGLPI_APP_TOKEN`/`MGLPI_USER_TOKEN`. [#767]
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph host[Host]
|
|
M[mglpi CLI\nbin/mglpi] --config 0600 env --> M
|
|
P[mglpi-mcp\nstdio JSON-RPC] --> M
|
|
S[smoke/smoke.sh]
|
|
end
|
|
subgraph docker[Digest-pinned Docker builder — ALL dev]
|
|
B[go build / vet / test]
|
|
F[smoke/fakeglpi\nstateful in-memory GLPI]
|
|
end
|
|
subgraph wire[Library: package glpi]
|
|
C[Client\nApp-Token + Session-Token headers only]
|
|
end
|
|
M --> C
|
|
P --> C
|
|
C --> F
|
|
C -.-> G[(real GLPI\ncmdb.knownelement.com)]
|
|
S --> B
|
|
S --> F
|
|
```
|
|
|
|
All compilation, vetting, and testing runs inside the digest-pinned
|
|
`golang:1.26-bookworm` builder (`./dev.sh`); the host never runs a Go
|
|
toolchain. Tests and smoke talk only to the in-process/containerized
|
|
fake; the real CMDB is never contacted by the test suite.
|
|
|
|
## GLPI quirks modeled (and tested)
|
|
|
|
- **Create endpoints return an ARRAY**: `[{"id":N,"message":"..."}]` —
|
|
never an object. `CreateChange` / `AddFollowup` parse it.
|
|
- **Input shapes differ per endpoint**: Change create takes
|
|
`{"input":{...}}` (object); ITILFollowup REQUIRES
|
|
`{"input":[{...}]}` (array of objects) — the fake rejects the wrong
|
|
shape with a 400, and the client sends each faithfully.
|
|
- **Search rows are keyed by field-id string** under forcedisplay:
|
|
`{"data":[{"1":"name","2":7,"12":3}]}` — parsed into typed rows
|
|
(fields 1 name, 2 id, 12 status).
|
|
- **Sessions**: `POST /initSession` (App-Token + `Authorization:
|
|
user_token ...`) returns `{"session_token":"..."}`; every later call
|
|
carries `Session-Token`; `killSession` cleans up (the client re-inits
|
|
transparently afterwards).
|
|
- **Profiles**: `GET /getMyProfiles` lists the user's profiles;
|
|
`POST /changeActiveProfile` with `{"profiles_id":N}` switches the
|
|
session's active profile. Agent mode uses this to move into the right
|
|
role (e.g. 5 = Hotliner) before working.
|
|
- **initSession auth failures are auth failures**: GLPI answers 400
|
|
(ERROR_GLPI_LOGIN / ERROR_APP_TOKEN_...) for bad credentials — the
|
|
client maps that to `ErrAuth`, not `ErrValidation`.
|
|
|
|
## Quickstart (verified against the fake)
|
|
|
|
This is the exact flow the smoke run (`./dev.sh smoke`) executes:
|
|
|
|
```sh
|
|
./dev.sh check # build + vet + test inside the Docker builder
|
|
./dev.sh smoke # boots fake GLPI on 127.0.0.1:8602, drives bin/mglpi
|
|
|
|
# against the real CMDB instead (0600 env file, see env.example):
|
|
export MGLPI_URL=https://cmdb.knownelement.com/apirest.php
|
|
export MGLPI_APP_TOKEN=<app token>
|
|
export MGLPI_USER_TOKEN=<user token>
|
|
|
|
mglpi whoami # profiles, [active] marker
|
|
mglpi change create --title "Disk pressure" --urgency 4 <<'EOF'
|
|
- array05 on pfv-tsys5 at 92%
|
|
EOF
|
|
mglpi change list --status new
|
|
mglpi change show 104
|
|
mglpi change transition 104 solved
|
|
mglpi change followup 104 <<'EOF'
|
|
REPORT delivered: array expanded
|
|
EOF
|
|
mglpi ci search Computer web
|
|
mglpi ci show Computer 101 -o json
|
|
```
|
|
|
|
Agent mode: put `MGLPI_PROFILE_ID=5` in the env file (or pass
|
|
`--profile 5`) — the CLI switches to that profile right after
|
|
InitSession, so profile-gated servers accept the calls. `whoami` never
|
|
switches. Add `-o json` to any command for machine output.
|
|
|
|
## Command reference
|
|
|
|
| Command | Effect | Key flags |
|
|
|---|---|---|
|
|
| `whoami` | the session's GLPI profiles | `-o json` |
|
|
| `change create --title T` | create change (body from stdin) | `--content FILE\|-`, `--urgency 1..5`, `--impact 1..5`, `--profile N`, `-o json` |
|
|
| `change show ID` | one change with content | `-o json` |
|
|
| `change list` | search (forcedisplay 1/2/12) | `--status N\|NAME`, `-o json` |
|
|
| `change transition ID STATUS` | move status (name or number) | `-o json` (re-fetches) |
|
|
| `change followup ID` | append ITILFollowup (body from stdin) | `--content FILE\|-`, `--profile N` |
|
|
| `ci search TYPE TERM` | name-substring search | `-o json` |
|
|
| `ci show TYPE ID` | raw CI object | `-o json` (default shape) |
|
|
| `help` | usage | |
|
|
|
|
Flags may appear before or after positionals (`mglpi change show 104
|
|
-o json` parses).
|
|
|
|
## Configuration
|
|
|
|
| Source | Keys | Discipline |
|
|
|---|---|---|
|
|
| environment | `MGLPI_URL`, `MGLPI_APP_TOKEN`, `MGLPI_USER_TOKEN`, `MGLPI_PROFILE_ID` (optional) | env wins over file |
|
|
| `--config PATH` (also `$MGLPI_CONFIG`, default `~/.config/mglpi/env`) | same keys | file must be 0600 or stricter; refused before any read; parsed in pure Go (no sourcing/expansion) |
|
|
|
|
Tokens travel only in headers: `App-Token` on every request,
|
|
`Authorization: user_token ...` on initSession, `Session-Token` after.
|
|
Response bodies are never surfaced in error strings — the fake GLPI
|
|
deliberately echoes the presented token in error bodies, and the test
|
|
suite proves nothing leaks (`TestTokensNeverLeak`, plus the smoke
|
|
redaction pass).
|
|
|
|
## Exit codes and stderr
|
|
|
|
| Code | Meaning | stderr shape |
|
|
|---|---|---|
|
|
| 0 | ok | — |
|
|
| 1 | usage / config error | one line, names the problem (never values) |
|
|
| 2 | API error | one line: `mglpi: glpi: <sentinel>: http NNN` — parseable |
|
|
|
|
Sentinels (library): `glpi.ErrAuth`, `ErrNotFound`, `ErrValidation`,
|
|
`ErrServer`, `ErrUnreachable`, `ErrMalformedResponse` — all comparable
|
|
with `errors.Is`.
|
|
|
|
## Library surface (what the harness calls)
|
|
|
|
```go
|
|
import "git.knownelement.com/ukrrs/mopac-glpi-go/glpi"
|
|
|
|
c := glpi.New(glpi.Config{BaseURL: url, AppToken: app, UserToken: user})
|
|
|
|
_ = c.InitSession(ctx) // also lazy on first call
|
|
_ = c.ChangeActiveProfile(ctx, 5) // agent mode (5 = Hotliner)
|
|
profs, _ := c.GetMyProfiles(ctx) // id/name/is_active
|
|
id, _ := c.CreateChange(ctx, "title", "<p>content</p>", 3, 4)
|
|
ch, _ := c.GetChange(ctx, id) // full read shape
|
|
rows, _ := c.ListChanges(ctx, glpi.StatusNew) // search, fields 1/2/12
|
|
_ = c.TransitionChange(ctx, id, glpi.StatusSolved)
|
|
_ = c.AddFollowup(ctx, id, "note") // {"input":[{...}]} shape
|
|
cis, _ := c.SearchCI(ctx, "Computer", "web")
|
|
obj, _ := c.GetItem(ctx, "Computer", 101) // raw map for ci show
|
|
_ = c.KillSession(ctx)
|
|
```
|
|
|
|
Status helpers: `glpi.StatusNew`…`StatusClosed` constants,
|
|
`StatusName(id)`, `StatusID(name)`. All methods take a
|
|
`context.Context`.
|
|
|
|
## MCP server
|
|
|
|
`cmd/mglpi-mcp` is a minimal stdio JSON-RPC MCP server (stdlib only):
|
|
`initialize` (echoes protocol_version + serverInfo), `tools/list` (six
|
|
tools), `tools/call`, `ping`. Tool failures are results with
|
|
`isError: true`; only unknown methods are JSON-RPC errors (-32601).
|
|
|
|
```sh
|
|
docker exec -i mopac-cli-mglpi mglpi-mcp # from the compose exec-host
|
|
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
|
|
| mglpi-mcp --config ~/.creds/mglpi.env
|
|
```
|
|
|
|
Tools: `change_create`, `change_list`, `change_transition`,
|
|
`change_followup`, `ci_search`, `ci_show`.
|
|
|
|
## Development
|
|
|
|
```sh
|
|
./dev.sh build|vet|test|check|smoke|mglpi|mcp|shell # everything runs in the digest-pinned builder
|
|
make build|vet|test|check|smoke # same, Makefile front door
|
|
```
|
|
|
|
Test suite: table-driven round-trips against the stateful fake
|
|
(`internal/fakeglpi`): session/header discipline, create/array-reply
|
|
parsing, keyed search rows, transition, followup input-shape contract,
|
|
CI search, profiles + the RequireProfile rejection path (agent mode),
|
|
error mapping (400/401/403/404/422/5xx → sentinels),
|
|
unreachable/malformed handling, and token redaction. Shell scripts are
|
|
shellcheck-clean (including info level).
|
|
|
|
## Status
|
|
|
|
| Area | State |
|
|
|---|---|
|
|
| Library (changes, followups, search, profiles, sessions) | done, green |
|
|
| CLI surface (`mglpi`) | done, green |
|
|
| MCP server (`mglpi-mcp`, 6 tools) | done, green (handshake + calls tested) |
|
|
| Fake-GLPI test suite | done, green (unit + smoke) |
|
|
| Token redaction | done, tested (fake echoes all three tokens; nothing leaks) |
|
|
| Dev harness (Docker-only builds) | done (`dev.sh`/`Makefile`) |
|
|
| Live-CMDB verification | pending first live attach via `MGLPI_URL`/`MGLPI_APP_TOKEN`/`MGLPI_USER_TOKEN` |
|
|
|
|
License: AGPLv3 (see `LICENSE`). Pattern source:
|
|
https://git.knownelement.com/ukrrs/mopac-redmine-go
|