Skip to content

CMDB Impact Analysis Developer Guide

Ed Mozley edited this page Aug 4, 2026 · 4 revisions

CMDB impact analysis β€” Developer Guide

How the blast radius is computed, why most links are deliberately ignored, and the two traps that make a naive graph walk wrong rather than merely slow.

Shipped as #971.

The user-facing page is CMDB. The module's design doc is docs/cmdb.md in the main repo β€” its UX principle 2 and V2 Β§1 both describe this panel, and the "prose impact Q&A" item there now has the graph walk it was waiting for. The company rules in Multi-tenancy are load-bearing in Β§5.


1. πŸ“ The files involved

Colour key: πŸ—„οΈ schema Β· βš™οΈ engine Β· πŸ”Œ API Β· πŸ–₯️ UI Β· 🎨 CSS Β· 🌍 i18n Β· πŸ§ͺ tests Β· πŸ“„ docs

🎨 File What it does
βš™οΈ includes/cmdb_impact.php new β€” the source of truth. cmdbBlastRadius() (the transitive walk), cmdbImpactEdgesFrom() (one hop, batched), cmdbDirectImpact() (the three legacy buckets), cmdbImpactTenantScope(), cmdbHasImpactEdgesConfigured(), and the CMDB_IMPACT_* direction constants
πŸ—„οΈ database/freeitsm.sql cmdb_relationship_types.impact_direction, cmdb_class_properties.spreads_impact, and the seed change so depends on ships as an impact edge
πŸ—„οΈ includes/db_verify_schema.php both columns, so an existing install gains them on verification
πŸ”Œ api/cmdb/get_object_impact.php now ~10 lines β€” calls the engine and returns impact + blast_radius. Previously carried its own copy of the walk
πŸ”Œ api/v1/resources/cmdb.php GET /cmdb/objects/{id}/impact β€” had a second copy of the same walk; now shares the engine. Its published keys are unchanged, blast_radius is additive
πŸ”Œ api/cmdb/save_relationship_type.php validates impact_direction against the allowed set
πŸ”Œ api/cmdb/get_relationship_types.php returns impact_direction
πŸ”Œ api/cmdb/save_class_property.php persists spreads_impact, and clears it when the type stops being object_ref
πŸ”Œ api/cmdb/get_class_properties.php returns spreads_impact
πŸ–₯️ cmdb/object.js renderBlastRadius() β€” grouping by hop, the "reached via" line, the two empty states
πŸ–₯️ cmdb/object.php the .blast-* styles, and the cog property modal's dependency tick
πŸ–₯️ cmdb/settings/index.php the When something fails control + the new list column
πŸ–₯️ cmdb/settings/settings.js syncRelTypeImpactOptions(), relTypeImpactCell(), and the settings property modal's tick
🌍 lang/en/cmdb.php + lang/pt-BR/cmdb.php impact.blast_*, impact.via_*, impact.hops_away_*, settings.rel_type_impact_*, settings.prop_spreads_impact*, prop_def.spreads_impact* β€” same commit, 547 keys each
πŸ“„ docs/cmdb.md, this wiki the design note under UX principle 2

⚠️ Two schema columns, so this needs a Database Verification on an existing install. Both default to "does not spread", so verifying changes no behaviour on its own.


2. πŸ”‘ The whole feature is "which edges do you follow?"

Walking a CMDB graph is trivial. Walking the right edges is the feature.

Follow everything and a server failure "affects" the building it is located in, the person who manages it, and every device on the same switch. The output is technically a reachable set and practically noise β€” which is how impact panels earn the reputation of being ignored.

So an edge is only followed if someone has said it carries a failure. Three kinds, and they opt in differently:

Edge Opts in via Direction
Containment (parent_id) always on parent β†’ children
Relationship cmdb_relationship_types.impact_direction either way β€” see Β§3
object_ref property cmdb_class_properties.spreads_impact referenced β†’ holder

Containment needs no switch because parent semantics in this module are ontological dependency β€” it is the same rule that makes delete cascade. If the parent is gone, the child is gone.

Object references need their own switch because plenty of estates record a dependency as a field rather than a relationship: a Database with a Host Server property is stating a dependency just as firmly as one with a depends on link. Ignoring those under-reports the blast radius exactly where a CMDB is best maintained.


3. βš™οΈ Direction is not a boolean

The trap that makes a single "carries impact" checkbox wrong:

  • to_from β€” "A depends on B". The row is from=A, to=B. B failing affects A. Impact travels against the arrow.
  • from_to β€” "A hosts B". The row is from=A, to=B. A failing affects B. Impact travels with the arrow.

Both are ordinary verbs an analyst would write. A boolean would silently get one of them backwards and report the blast radius of a host as empty.

Hence impact_direction VARCHAR(10) holding none / to_from / from_to, defaulting to none.

πŸ”‘ The UI never says "to_from". The settings control rewrites its own options from the live verb fields via syncRelTypeImpactOptions(), so both impact options share one sentence frame and differ only in which verb reads correctly from the affected object's side:

Stored Reads as
to_from "Everything that depends on it is affected"
from_to "Everything that is hosted by it is affected"

That is why from_to is labelled with the inverse verb. This follows the design doc's human-readable vocabulary from day one rule β€” the same strings feed the AI prompts.

⚠️ save_relationship_type.php coerces an unrecognised value to none rather than storing it. An unknown value would drop the edge from every blast radius with nothing on screen to explain why.


4. πŸ”‘ Breadth-first, because the hop count is the point

The panel groups by distance, so "2 steps away" has to mean shortest path β€” not "wherever the traversal happened to reach it". A depth-first walk would label the same object 2 or 5 hops depending on edge ordering.

cmdbBlastRadius() therefore works level by level, and the first path to reach an object claims it. That single rule also terminates cycles: A hosts B, B depends on A is a loop, and the seen map means the second visit is dropped. cmdbImpactEdgesFrom() issues four queries per level (containment, to_from, from_to, properties) with the whole frontier in an IN (…), so a wide estate costs a handful of queries rather than hundreds.

⚠️ Caps surface, they do not hide. maxNodes (1000) and maxDepth (20) bound pathological estates, and hitting either sets truncated, which the panel renders as "the estate is larger than this list". A capped list presented as the complete answer is worse than no list.


5. πŸ”’ The company gate, and the guard that would have failed open

The CMDB forbids cross-company links entirely (see CMDB β†’ Multi-company). So in principle a walk starting from an authorised object can never leave that company, and no filter is needed.

The walk filters anyway. The caller authorises the root only; every other object in the result is reached by transitive inference. One malformed row predating the rule would turn the blast-radius panel into a way to enumerate another company's estate, one hop at a time.

cmdbImpactTenantScope() resolves the root's company (NULL means the default company's β€” resolve it, or a single-company estate where every row is NULL matches nothing) and every query carries COALESCE(o.tenant_id, ?) = ?.

⚠️ This file requires includes/tenancy.php and does not guard with function_exists(). The first draft did, and it is worth spelling out why that was wrong: had tenancy.php not been loaded, function_exists('isMultiTenant') would be false, the scope would resolve to null, and the walk would run unfiltered across every company β€” the guard degrades silently, and not toward safety. A missing require should be a fatal error, not a quietly disabled security rule.


6. ⚠️ There were already two copies of this walk

Before this change the descendants traversal existed twice β€” once in api/cmdb/get_object_impact.php and once, copy-pasted, in apiCmdbObjectImpact() in api/v1/resources/cmdb.php. Adding transitive logic to one and not the other would have made the UI and the REST API disagree about what "impact" means.

Both now call includes/cmdb_impact.php. If you extend the walk, extend it there and both surfaces move together.

πŸ”‘ cmdbDirectImpact()'s return shape is frozen. GET /cmdb/objects/{id}/impact is published, and the REST resource maps it to its own key names (incoming_relationships, property). Add keys; never rename or remove.


7. ⚠️ An empty blast radius is two different states

"Nothing depends on this" and "nothing in this install is configured to spread impact yet" produce an identical empty list and call for opposite responses β€” reassurance versus go and configure it.

Every install that upgrades into this feature starts entirely in the second state, because both columns default to "does not spread". Without the distinction the feature would look broken on the very first object anyone opened after upgrading.

cmdbBlastRadius() therefore returns no_impact_edges_configured, computed only when the result is empty (it is a second pair of counts; there is no reason to pay for it otherwise), and the panel picks the wording from it.


8. πŸ§ͺ Verifying a change here

The engine is pure graph logic over seeded rows, so it tests well without a browser. scratchpad/test_impact.php in the shipping session built an estate shaped to catch the failure modes rather than to demonstrate success β€” reproduce that shape:

Case Why
A 4-link chain server β†’ vm β†’ app β†’ service β†’ customer the actual point β€” assert the customer lands at depth 4
An is located in relationship on the root must be absent; proves non-impact types are skipped
An object_ref property with spreads_impact = 0 must be absent; proves properties opt in
The root's parent must be absent β€” impact goes down the tree, not up
The root itself must never appear in its own blast radius
A relationship looping back to the root must terminate, and must not list the root
A from_to type walk from the host; the hosted object must appear

⚠️ A green suite is not a live run. After the assertions, hit api/cmdb/get_object_impact.php?id=… against real data with a real session β€” the shipping run found the local estate had every relationship type at none, which is exactly what a customer sees after upgrading and which no fixture would have shown.

⚠️ Rendered markup does not prove the JS parses. renderBlastRadius() is reached only after a fetch resolves, so a syntax error further down object.js shows up as a panel that never appears. Parse-check in headless Chrome (new Function(src) compiles without executing) and include a deliberately broken file as a negative control β€” the first attempt at this 404'd on the control and reported three cheerful PARSE-OKs that proved nothing.


9. ⚠️ Upgrading an existing install

  1. Run Database Verification β€” it adds both columns.
  2. Nothing changes yet. Every relationship type is none, so every blast radius is empty and the panel says "No relationship type or property is set to carry impact yet".
  3. In CMDB β†’ Settings β†’ Relationship Types, set the types that genuinely carry a failure. On a typical estate that is depends on and hosted on, both to_from.
  4. Optionally tick "This is a dependency" on object_ref properties like Host Server.

The seed only fires on an install with no relationship types, so an existing install keeps its own rows untouched and opts in deliberately. That is the conservative choice: silently deciding that someone's depends on means what we assume it means would change what the product tells them about their estate without asking.


10. ⬜ Not built yet

  • Prose summary of the blast radius. docs/cmdb.md V2 Β§1 β€” the walk now returns hop counts and how each object was reached, so this is a prompt change, not a traversal.
  • The graph visualisation. Explicitly V2 in the design doc; this ships as a grouped list.
  • Impact-weighted ticket priority. iTop derives priority from impact Γ— urgency fed by the CMDB. FreeITSM has no urgency concept at all β€” it would be a tickets-side change.
  • Impact on the change record. docs/cmdb.md V2 Β§6 wants a change scoped to CMDB objects to show what else it touches. The engine is now the missing half.
  • Status-page linkage. status_services and cmdb_objects know nothing about each other, so a CI outage cannot propose the status update.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally