GLPI Service
Business services over the CMDB, and the hygiene work that keeps the CMDB worth building them on. Answers what is broken for a given entity, and where the contents of the CMDB came from.
GLPI models the boxes well but has no word for the thing people actually depend on. “Managed Backup” is not a Computer, a Contract or a Location, so it ends up in a spreadsheet that is the only place knowing which four machines have to work.
Separately: point three collectors at one CMDB — GLPI’s inventory agent, an
osquery agent, a network scanner — and it cannot tell you which last spoke about
any given asset. All three feed core’s inventory pipeline and land on the same
GLPI Native Inventory auto-update system, leaving identical marks on the asset
row.
Where core comes close
Section titled “Where core comes close”| Core feature | What it gives | Where it stops |
|---|---|---|
Appliance |
A named group of items, with a type and an owner | One entity, not a subscription list. No health, no ticket dimension, no provenance |
| Impact analysis | A directed dependency graph across assets, with a graph view | The graph is a picture. No rollup, no state, no owner, no entities |
glpi_items_tickets |
Tickets linked to CIs | No service dimension, so SLA reporting cannot slice by service |
| Native inventory marks | “This asset came from an inventory” | Not which inventory |
glpi_rulematchedlogs |
Recent import events | Trimmed to 30 rows per asset; agents_id = 0 for anything not a Computer or Phone |
This plugin adds no collector of its own.
Business services
Section titled “Business services”- A BusinessService itemtype in the search engine, with an owner (person and/or team), a criticality, and an active/retired state.
- Subscriptions — which entities subscribe to the service. Many-to-many: “Managed Backup” is one service sold to forty entities.
- Members — the CIs the service is made of, stored in GLPI’s own impact graph rather than a table here.
- Health, derived and cached, with the reasoning shown next to it.
- A service page with all of it on one screen.

Members are edited on their own tab through core’s item picker. The itemtypes it offers are whatever the instance has enabled for impact analysis, and the tab says so rather than leaving a short list looking like a bug.

Ticket linkage
Section titled “Ticket linkage”A “service affected” association on tickets, shown as one strip in the fields panel. When the ticket’s linked item belongs to exactly one service, a suggestion chip offers it; accept and dismiss are both recorded. Nothing is written to a ticket without a click.

Plenty of service-affecting tickets never name a CI — “email is slow” arrives with no asset attached. Without the explicit link, reporting sliced by service would exclude exactly those.
CMDB hygiene
Section titled “CMDB hygiene”Provenance
Section titled “Provenance”A cron sweeps computers, network equipment, printers, phones and PDUs and records which source last asserted each one. Surfaced as a badge at the top of the asset form (source, age, whether more than one source claims it, which services depend on it) and a census page (counts per source per entity, multi-source assets, the stale list with ages).

The caveats above the numbers are each a way the number underneath can be wrong.
Reconciliation
Section titled “Reconciliation”Where two records attributed to different sources share an identity-relevant fact — same serial, overlapping MAC, same name — they are shown side by side and a human picks which source wins for that asset. The decision is recorded. Nothing is merged.

A merge is irreversible, and an automated merge between records that disagree is irreversible and uninformed: it destroys the evidence that would have shown which side was right.
Baselines and drift
Section titled “Baselines and drift”A configurable attribute set per CI class — installed software and OS for computers, ports and VLANs for network gear — captured as a normalised snapshot with a hash, on demand or on a per-entity schedule that is off by default. Drift is the difference between the baseline and now: added, removed, changed, with the date each was first seen.

A package upgrade is one changed row. Keyed by name-and-version instead, a
monthly patch cycle would produce a page of paired additions and removals.
Why services ride the impact graph
Section titled “Why services ride the impact graph”Membership is a row in core’s glpi_impactrelations saying this CI impacts this
service. There is no membership table in this plugin.
An estate should have one dependency graph. A parallel membership table would have been simpler and entity-aware out of the box, and would have produced a second graph core’s own impact analysis could not see. Riding the graph means a service appears in core’s impact view without core knowing this plugin exists.
The edges:
- Installing writes one core config key.
impact_enabled_itemtypesgainsBusinessService; uninstalling removes it. GLPI refuses to store an impact relation whose ends are not enabled, so until that key is written a service can have no members. Core’s own custom-asset impact capability writes the same key the same way. - Only impact-enabled itemtypes can be members. The picker offers what will be accepted and says why the list is short.
- The relation table has no entity column, and core applies no entity restriction when reading the graph. This plugin filters every read against the member CI’s entity and fails closed. It cannot fix core’s write path, which checks UPDATE on the starting node only.
- The relation table has no
users_idand no dates, so membership changes this plugin makes are mirrored into its own log. A membership edited through core’s impact editor changes the graph and will not appear in that log; the page says so. - A service with members in child entities must be recursive. Core’s relation coherency rule only accepts that shape for a recursive item, so the form defaults to recursive.
Install
Section titled “Install”# from the GLPI rootgit clone https://github.com/bijstaan/glpi-service.git plugins/glpiservicephp bin/console plugin:install -u glpi glpiservicephp bin/console plugin:activate glpiserviceInstalling adds BusinessService to Setup → General → Impact analysis and
registers five cron tasks. Uninstalling removes both, drops every table and
deletes the plugin’s impact relations.
Services are under Management → Business services, hygiene pages under Administration → CMDB hygiene, settings at Setup → Plugins → Business services.
Rights
Section titled “Rights”| Right | Grants | Given at install to |
|---|---|---|
plugin_glpiservice_service |
The catalogue: services, subscriptions, membership, ticket association | Profiles with config UPDATE (full), profiles with ticket UPDATE (READ) |
plugin_glpiservice_cmdb |
READ: census, conflicts, drift, badge. UPDATE: resolve a conflict, take a snapshot, set a baseline | Profiles with config UPDATE |
A service manager owns the catalogue; a CMDB owner owns the hygiene.
Settings
Section titled “Settings”| Setting | Default | Notes |
|---|---|---|
| Health cache TTL | 300s | Opening a service past this recomputes; the cron does the rest |
| Ticket priority → health level | 1:0,2:1,3:2,4:3,5:4,6:4 |
Unlisted priorities count as healthy |
| Suggest an affected service | on | Only when the ticket’s item is in exactly one service |
| Record asset provenance | on | Reads only; never writes to an asset |
| Itemtypes to sweep | Computer, NetworkEquipment, Printer, Phone, PDU | Phone and PDU matter where the scanner creates them |
| Stale after | 30 days | Assets with no dated claim count as stale |
| Scheduled snapshot entities | (none) | Empty means nowhere. Manual snapshots always work |
| Snapshots kept per item | 10 | The baseline is never expired |
| Attributes per itemtype | software+OS, ports+VLANs, (none) | From a fixed registry |
| Event retention | 30 days | The log exists so a late consumer can catch up |

Four cards, four Save buttons, each form naming the section it saves. One form over all of them would mean a checkbox absent from the submitted form being indistinguishable from an unticked one — pressing Save under “Baselines and drift” would quietly switch provenance recording off.
| Task | Default | What it does |
|---|---|---|
Health |
5 min | Recompute cached health, emit state changes |
Provenance |
1 hour | Attribute assets, detect identity conflicts |
Snapshot |
daily | Scheduled snapshots, per-entity opt-in, off by default |
Drift |
6 hours | Compare CIs against their baselines |
Retention |
daily | Expire old events |
glpi-ai tools
Section titled “glpi-ai tools”| Tool | Answers |
|---|---|
service_status |
What this entity depends on, how healthy each service is, and what drove that verdict |
service_impact |
This machine is at fault — which business services depend on it |
The second turns “this host is down” into “their order processing is down”.
Health is served from the cached figure where it is fresh, never forced: a model asking about six services must not set off six health computations across a live CMDB. Every answer carries the time it was computed, and signals come back worst-first, capped at three.
Nothing writes. A model adding a member would put an unsourced edge into the map that every health calculation then trusts.
Extension points
Section titled “Extension points”Nothing leaves the estate — no outbound network calls and no setting that would enable one.
$PLUGIN_HOOKS['glpiservice_health']— contribute health signals. Composition is worst-of, so a contributor can only make a service look worse, which is what makes it safe to accept contributions from code this plugin did not write.$PLUGIN_HOOKS['glpiservice_events']— receive health changes, drift and new conflicts. Every event is written to a table before anything is told, so a consumer installed later loses nothing.$PLUGIN_HOOKS['glpiservice_asset_source']— answer for your own assets, and win over this plugin’s table sniffing.
Limitations
Section titled “Limitations”- Health is a proxy. The only contributor shipped reads open ticket priority on member items, so a service whose members are all down and whose users have not phoned reads as healthy. Stated on the service page, not just here.
- Attribution degrades when a collector is removed. Network equipment imported by the network scanner is indistinguishable from agent-imported gear on the core asset row; only the scanner’s own table separates them. Uninstall it and those assets are re-attributed to the GLPI agent.
- Some assets look hand-made and are not. The network scanner creates phones
and PDUs by a path that bypasses core’s inventory pipeline:
is_dynamic = 0, nolast_inventory_update, andglpi_pdusdoes not have those columns. - osquery and a stock agent are told apart by a string. Both use agent type
Core. - Conflicts are not per-field. Neither collector keeps a record of the values it asserted, so “osquery says the serial is X, netscan says Y” is a comparison that does not exist to be made. What is reported is two records from two sources sharing an identity fact.
- Drift needs a baseline. A CI with none is unmeasured, not unchanged, and the summary counts those separately.
- Printers have no default attribute set.
- Provenance is a photograph. No hook fires when another plugin updates its own tables, so the sweep is a periodic full pass. Every page showing it also shows when it last ran.
docker compose -p glpi exec glpi sh -c 'cd /var/www/glpi/plugins/glpiservice && tests/run.sh'Three pure suites, no database: the drift diff against fixture payloads, the source-attribution rules against synthetic fingerprints, and the health/suggestion arithmetic.
A fourth needs a live GLPI and is gated behind a flag, because it writes:
docker compose -p glpi exec -e GLPISERVICE_DB_LIVE=1 glpi \ sh -c 'cd /var/www/glpi/plugins/glpiservice && php tests/db-live.php'It creates a scratch entity tree, a service, a ticket, assets and their software rows, and purges all of it on the way out including from an exception. It covers:
- core’s
Impact::buildGraph()reading the relations this plugin writes, both directions, with the edge label intact; - the entity boundary holding when the session moves into a child entity, on every read that crosses one;
- a subscription, ticket link, accepted suggestion and dismissal each landing as the row the reporting depends on;
- the sweep attributing assets to the collectors that really own them, checked against each collector’s own tables;
- a baseline, a mutation and a re-detection producing one row per surviving
difference, with
first_detectedthat does not move; - two records sharing a serial opening a conflict, resolving it recording a decision and changing neither record, and a conflict that stops holding being closed;
- a purged asset or service taking every row this plugin owns with it.
Where an instance has no osquery or network-scanner tables, the assertions that need them report as skipped.
tests/browser/service-check.js drives all of this through the UI and takes the
screenshots above.
Licence
Section titled “Licence”GPL-3.0-or-later, the same licence as GLPI. The plugin is loaded into GLPI’s process and extends its classes, so it is a derivative work. See LICENSE.