Skip to content

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.

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.

  • 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.

Service page: health and why, subscribed entities, member CIs, and open tickets with the route each took

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.

Members tab: core’s item picker, the member list, and the plugin’s log of what it changed in the graph

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.

The affected-service strip offering one suggested service with accept and dismiss

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.

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).

Census: counts per source across the estate, the caveats printed on the page, per-entity breakdown

The caveats above the numbers are each a way the number underneath can be wrong.

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.

Two records of one machine side by side, with the identity fields and a decision to record

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.

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.

One machine’s drift after a patch window: two upgrades as single changed rows, one package added, one removed

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.

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_itemtypes gains BusinessService; 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_id and 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.
Terminal window
# from the GLPI root
git clone https://github.com/bijstaan/glpi-service.git plugins/glpiservice
php bin/console plugin:install -u glpi glpiservice
php bin/console plugin:activate glpiservice

Installing 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.

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.

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

Settings page: status card, then one card and one Save per concern

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
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.

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.
  • 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, no last_inventory_update, and glpi_pdus does 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.
Terminal window
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:

Terminal window
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_detected that 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.

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.