Asset CI Link Design - edmozley/freeitsm GitHub Wiki
Status: built - all of Parts A to E. The design was reviewed on issue #201 and the answers are folded in below. What changed after review lists them. How to use it: Linking an asset to its configuration item; how it is built: Developer Guide. It is built in the order in section 11, and Hyper-V and Failover Cluster discovery (#199) is designed to sit on the same rules (section 13).
The request, from issue #201 (raised by @Dr4inder):
"The same server can exist as an asset and a CI without an obvious connection for the analyst."
A server is usually two records in FreeITSM. The asset says what the box is: make, model, serial number, where it is, who bought it, what the agent last saw on it. The configuration item (CI) says what the box does: what runs on it, what depends on it, and what breaks if it fails. Today nothing joins the two, so an analyst looking at one has no way to reach the other.
This design adds one link between them, meaning "these two records are the same piece of equipment":
- Link and navigate both ways. A Link button on the asset and on the CI, each with a search picker. The other record then shows on both pages as a card you can click.
- Asset details on the CI, read from the asset. The CI's card shows the make, model, serial number, location and inventory freshness, labelled as coming from the asset. Nothing is copied, so there is nothing to keep in step.
- Create the CI from the asset. An administrator says which CMDB class an asset type becomes (servers become "Server" CIs, say). The asset then offers Create linked CI, which asks for anything the class requires and creates the CI and the link together.
- Impact from the asset. The asset's card shows what the linked CI's "If this failed" analysis already says, with a way into the CMDB and the diagram.
The link is an identity, not a dependency. It never appears in the CMDB graph, never spreads impact, and never deletes anything. It does not depend on FreeITSM's inventory agent either: an asset from Intune, a spreadsheet import, a REST tool or typed in by hand links the same way.
Dr4inder's answers to section 9 made the design stricter in six places:
| Point | Before | Now |
|---|---|---|
| Someone who cannot open the other record | Saw the asset's name, with no details | Sees only Linked asset - restricted: no name, no id, no details, no link. The same rule applies the other way round, and to REST, the history and conflict messages (Part A) |
| Who may link | Access to both modules | Write access to both modules and both records' company. A read-only analyst stays read-only |
| Replace | A confirmed action | Also refused if the link changed after the person looked at it. They see the new state and decide again |
| Merge with two different CIs | The kept asset's link wins, shown in the preview | The preview shows both links, and the person must choose which one ends. Cancelling leaves the merge undone. Neither CI and none of their relationships is touched |
| History | Who and how for the link | Who and how for both linking and unlinking, and the original asset is kept on every row when assets are merged |
| Display settings | Ticks for which details show | The same ticks, and they never widen access: details only ever show to someone who can open the asset |
A natural extension, as long as it keeps three lines the docs already draw:
- Assets are not CMDB items. Asset type profiles put it this way: "a link field is a fact about an asset, a CMDB relationship is a service dependency, a connection is a physical attachment." This design adds a fourth, narrow kind - the same equipment - and keeps it apart from the other three.
- The overlap is small, and that is fine. Linking equipment to tickets says "a broken mouse is an asset and will never be a configuration item⦠The only real overlap is a server, which can reasonably be both." The link is optional on both sides. An asset never needs a CI, and a service CI never needs an asset.
- No copying between modules. The asset stays the authority for inventory and lifecycle details. The CI keeps its own properties and relationships. The CI page reads the asset's values through the link and never stores them.
Mapped from the code at 3.16.0 and checked again at 4.1.0:
| The request needs | Today | Verdict |
|---|---|---|
| A link between an asset and a CI | None. Nothing on the CMDB side refers to an asset (cmdb_objects has no asset column, and no CMDB property type can point at one) |
β |
| An asset custom field that points at a CI | Exists: a Link to something else β A configuration item field, stored in asset_field_values.value_ref_id. Writable through REST and the importer, read-only on the asset page (the picker was never built). It is a fact about the asset, one-way, and nothing on the CI shows it |
|
| Navigation both ways | Neither page shows the other | β |
| A searchable CI picker |
api/cmdb/search_objects.php (company-scoped, can filter by class), with a picker inside the CI page's script |
β To reuse |
| A searchable asset picker | The Connect picker on the asset page, and api/tickets/search_linkable_assets.php for tickets |
β To reuse |
| History of link changes | Assets: asset_history, one row per change, with the analyst. CIs: no history at all - the CMDB developer guide lists "no versioning" as a known gap |
|
| Read-only access |
analystCanWriteModule() and requireModuleWriteJson() exist, and Changes, Forms and Tasks offer a Read only level. Assets and the CMDB do not offer one yet, so today their access is always full |
β To use - see Part A |
| Impact analysis for one CI |
cmdbBlastRadius(), behind GET /cmdb/objects/{id}/impact and the CI page's "If this failed" panel |
β To reuse |
| Create a CI with its required properties |
CmdbService::createObject(), used by the UI and by REST, checks required properties on create |
β To reuse |
| Map an asset type to a CMDB class | None. Asset types and CMDB classes only share an icon library | β |
| Company boundaries | Assets and CIs both belong to one company. A CI's links must stay inside its company (CmdbService::assertSameCompany()) |
β Same rule applies |
| Asset deletion | There is none, except a merge (a person merging two duplicates under Assets β Settings β Review). A test fails if a merge forgets any column that refers to an asset | β Covers a new table |
| CI deletion | A hard delete of the CI and everything inside it, done step by step in CmdbService::deleteObject()
|
On the asset, a Configuration item card on Key info:
- Not linked: No configuration item. with Link (a picker of CIs in the asset's company, searchable by name and filterable by class) and, if the asset's type has a class mapped (Part C), Create linked CI.
- Linked: the CI's icon, name and class as a link into the CMDB, with Unlink and Replace. Hovering the link shows who linked it, when and how.
On the CI, an Asset card, placed under Details:
- Not linked: No asset. with Link (a picker of assets in the CI's company).
- Linked: the asset's name as a link, the details from Part B, Unlink and Replace.
Both cards finish with a small Link history list: linked, unlinked, replaced, deleted, merged, each with who, when and how (a person, or which API key). This one list is the history for both records, so the CMDB gets an honest record of its links without first needing a general history of its own.
The asset card is a new Key info section (cmdb) in asset type layouts, so a team can hide it on types that will never be CIs, such as monitors and mice. Hiding it is presentation only, as with every other section.
Seeing the other record needs the same access as opening it: its module and its company. Both records are always in the same company, so in practice this is about the module.
| The reader can open | On the CI | On the asset |
|---|---|---|
| Both | The asset card in full | The CI card in full |
| The CI, not the asset | Linked asset - restricted. No name, id, details or link. "Linking is not available because you cannot open Assets." | - |
| The asset, not the CI | - | Linked configuration item - restricted, the same way |
The same rule applies everywhere the link appears:
-
REST:
linked_assetis{ "restricted": true }, with no id, when the key cannot read assets. The same goes forlinked_cithe other way round. - History: a row whose other end the reader cannot open says a restricted asset (or CI), with no name.
- Conflicts: "This configuration item is already linked to an asset you cannot open", with no name. A reader who can open it sees its name.
- The display setting in Part B chooses which details an authorised reader sees. It never shows anything to someone who could not open the asset.
One new table, asset_ci_links. Each row is one period of being linked:
| Column | Meaning |
|---|---|
asset_id, cmdb_object_id
|
The pair. asset_id follows a merge, so the kept asset sees the whole history |
original_asset_id, asset_name
|
The asset the link was made on, and its name, never changed - so a row that came through a merge still says which record it started on |
linked_at, linked_by_id, link_source
|
When, who (analyst), and how (ui, api:<key name>, or later integration:<name>) |
unlinked_at, unlinked_by_id, unlink_source, unlink_reason
|
NULL while the link is live. The reason is unlinked, replaced, ci_deleted or asset_merged
|
ci_name, ci_class_name
|
The CI's name and class, recorded when the link ends, so the history still reads properly after the CI is deleted |
- One authoritative record. Both pages read this one table, so there is no reverse reference to keep in step (acceptance criterion 2).
-
One live link per asset and one per CI. At most one row with
unlinked_atNULL for any asset, and at most one for any CI. The service checks this inside a transaction that locks both records, and tests cover it. -
Rows are never deleted. Ending a link closes the row. That is the history, and the asset's
asset_historyalso gets a line (cmdb_link). - No company column. The pair must be in the same company, checked on every write, and both records carry their own company.
- Linking, unlinking and replacing need write access to both sides: to change Assets, to change the CMDB, and to reach both records' company. Neither module has a Read only level today, so this is module access for now. If either gains one, read-only analysts are refused here with no change to this code. Someone who can only read either side sees the card but not the buttons.
- A CI already linked to another asset is refused. The message names that asset if the person can open it, and says an asset you cannot open if not. This applies the other way round too.
-
Replace is a separate, explicit action. It shows the link that will end and asks for confirmation. The request carries the link the person saw. If the live link has changed since then, because someone else linked, unlinked or replaced it, the replace is refused and the card reloads to show the new state. A newer link is never displaced silently. Replacing ends the old link with reason
replacedand starts the new one in the same transaction. - Linking the same pair again does nothing and says it is already linked. The same applies to retries over REST.
- Unlinking never deletes either record (acceptance criterion 4).
-
When a CI is deleted, its live link ends with reason
ci_deletedinside the same delete transaction, and the CI's name is kept on the row. The asset's card then reads No configuration item, and its history says the CI was deleted. The delete confirmation on the CI says "Linked to asset LON-SRV-01. The asset is kept; the link ends." (or "Linked to an asset you cannot open"). - Existing custom link fields are left exactly as they are. A Link to something else β configuration item field is never read as this link, never converted, and never changed (the issue's last point under section 5), apart from the delete fix in section 10. Teams that want to move to the native link can do it one asset at a time, and the docs will say which to use for what.
A merge is how an asset stops existing (Asset review), so the link has to come through it cleanly:
| Before the merge | What happens |
|---|---|
| Neither asset linked | Nothing |
| Only one asset linked | The link moves to the kept asset. Its row keeps original_asset_id, so the history still says it was made on the other record |
| Both linked to different CIs | The merge window shows both links and asks which one ends, with the kept asset's link selected. Merge stays disabled until the person has ticked I understand this link will end. The other ends with reason asset_merged. Neither CI is changed, merged or deleted, and neither CI's relationships are touched. Cancel leaves both assets and both links as they were |
The choice is part of the merge request. If the links have changed since the window opened, the merge is refused and the window reloads, as with Replace. The new table joins the merge's list of asset references (AssetMergeService::REFERENCES), which its guard test insists on.
The CI's Asset card shows, read live from the linked asset:
- the asset's name and type
- manufacturer and model
- serial number and asset tag
- status and location
- inventory freshness - when the asset was last seen, and the worst state in its Inventory sources panel (current, stale or failed), so a CI whose box has gone quiet shows it
Each value is labelled From the asset, and editing happens on the asset (the card links to it). Nothing is stored on the CI. That avoids keeping copies in step, and avoids any field-by-field sync between the modules (acceptance criterion 3).
- Which details show is a setting: CMDB β Settings β Asset card, a list of ticks with all of the above on by default. A CMDB team that only cares about the serial number can trim it. The ticks only narrow what an authorised reader sees. Someone who cannot open the asset gets the restricted marker whatever is ticked.
-
REST:
GET /cmdb/objects/{id}gains a read-onlylinked_assetblock (id,name), andGET /assets/{id}gainslinked_ci(id,name,class). Each is{ "restricted": true }when the key cannot read the other side. Both are additive, so existing consumers are unaffected. The details themselves are already available fromGET /assets/{id}.
Assets β Settings β Asset types β (a type) β CMDB class: a dropdown of active CMDB classes, empty by default. Setting it to Server on the Server type does one thing only: it makes Create linked CI appear on assets of that type. It never creates a CI by itself, and never touches existing assets (the issue's own words).
Create linked CI opens a short form:
- The name, filled in with the asset's name and editable.
- Any properties the class marks as required, using the same inputs as the CMDB's own New form, so they are checked by the same rules.
- Possible matches first. Before the form, FreeITSM looks for CIs of that class, in the asset's company, that are not already linked, whose name matches the asset's name (ignoring case). If any exist they are listed, each with Link this one instead. They are suggestions, never proof of identity, and a name match is never linked automatically (acceptance criterion 6). If none match, the form shows straight away.
Create makes the CI and the link in one transaction, through CmdbService::createObject(), so there is either a CI that is linked or nothing at all. The CI's company is the asset's. It needs write access to both modules, like Link.
- Repeated requests reuse the link. If the asset is already linked by the time the request lands, nothing is created, and the existing CI is returned and opened (acceptance criterion 5).
- Nothing hardware-shaped is copied into CI properties. The class may have its own "Serial number" property, but the design does not fill it, because Part B already shows the asset's serial on the CI. Filling it would create the second copy the issue asks to avoid.
Automatic linking by an integration (the issue's later, opt-in step) goes through the same service: same company rule, same one-live-link rule, same refusal on conflict, with link_source saying which integration did it. #199 is the first that will want it. It never links on a name alone. Ambiguous cases go to that integration's review list, never silently resolved.
When the asset is linked, its Configuration item card adds a line from the CI's existing analysis:
If this failed: 14 items affected, 3 of them services. Show Β· Open as diagram
- Show opens the CI page at its "If this failed" panel. Open as diagram uses the CI page's existing button (it needs Network Mapper access, as it does today).
- It calls the same
cmdbBlastRadius()the CI page does, with the CI's company scope, so it follows the CMDB's own relationship and impact settings and nothing else (acceptance criterion 7). When #199's redundancy-aware grades arrive, this line shows them too, because it reads the same result. - The asset is not in the graph. The link adds no edge, no containment, and nothing for the walk to follow. If the analysis finds no impact edges configured, the line says so, as the CI page does.
- Shown only to someone who can open the CI. Others see the restricted marker without the line.
The same service sits behind the UI and the API, so they cannot disagree (acceptance criterion 8).
| Operation | Permission | Behaviour |
|---|---|---|
GET /assets/{id}/ci-link |
asset_ci_links.read |
{ current: {...} or null, history: [...] } |
POST /assets/{id}/ci-link { "cmdb_object_id": 88 } (built as POST: the API takes no PUT) |
asset_ci_links.create |
Links. The same pair again returns 200 with "unchanged": true. 409 ci_already_linked / 409 asset_already_linked describe the other record (or restricted). To replace, send "replace": true and "replaces_link_id", the live link the caller saw. 409 link_changed if that is no longer the live link |
DELETE /assets/{id}/ci-link |
asset_ci_links.delete |
Ends the link. 404 if there is none |
GET /cmdb/objects/{id}/asset-link |
asset_ci_links.read |
The same record, seen from the CI |
POST /assets/{id}/ci-link/create-ci { "name", "properties" }
|
asset_ci_links.create and cmdb_objects.create
|
Part C. Uses the type's mapped class; 422 no_class_mapped if there is none. Possible matches come back as 409 possible_matches with the list, unless "ignore_matches": true
|
-
Company boundaries: the key must be able to see both records, and they must be in the same company. A record the key cannot see is a
404, as everywhere else in the API. -
Restricted blocks: a key with
asset_ci_links.readbut withoutassets.readgets{ "restricted": true }for the asset end, and withoutcmdb_objects.readfor the CI end. That covers the current link, the history and conflict bodies. -
Retries are safe: linking the same pair again changes nothing, and
create-cion an already-linked asset returns the existing CI rather than making a second. - A narrow permission of its own, so an integration can maintain links without being able to edit assets or CIs. That is the REST equivalent of the write rule: the key's permission and company scope are the boundary.
| # | Criterion | Where |
|---|---|---|
| 1 | Link from either module with a searchable picker | Part A |
| 2 | One association, visible and navigable both ways | Part A - one table, read by both pages |
| 3 | Asset information on the CI with provenance, no duplicate maintenance | Part B - read live, labelled From the asset |
| 4 | Changes recorded; unlinking keeps both records | Part A - rows closed, never deleted, actor and source at both ends; asset_history line |
| 5 | Creation honours the type β class mapping and required properties; no duplicates on retry | Part C |
| 6 | Ambiguous matches and conflicting links need an explicit choice | Part C (matches), Part A (Replace with fresh review, merge choice, 409) |
| 7 | Impact reachable from the asset, graph unchanged | Part D |
| 8 | UI and REST enforce the same permissions and company rules | Parts A and E - one service |
| 9 | Existing installs unchanged until used | Nothing is linked, mapped or shown until someone does it; existing custom link fields are untouched |
Agreed on the issue. The right-hand column is what is being built.
| # | Decision | Agreed |
|---|---|---|
| 1 | Where the history lives | In the link table, shown on both cards, plus a line in the asset's history. Actor and source for both linking and unlinking; the original asset kept through merges. A general CMDB history is separate work and does not block this. |
| 2 | One-to-one | One live link per asset and one per CI for the identity. Everything else stays an ordinary CMDB relationship. |
| 3 | A reader who cannot open the other side | A generic restricted marker - no name, id, details or navigation - in both directions, and in REST, history and conflict messages. |
| 4 | Which asset details the CI shows | Ticks in CMDB β Settings β Asset card, all on by default, and never widening access. |
| 5 | Replacing a link | A separate, confirmed Replace, refused if the live link changed after the person reviewed it. |
| 6 | A merge where both assets are linked to different CIs | Show both, require an explicit choice of the link that ends (the kept asset's selected), never touch either CI, cancel leaves the merge undone, the ended link stays in the history. |
| 7 | Possible matches for Create linked CI | Same class, same company, not already linked, same name ignoring case - a suggestion, never proof. |
| 8 | Who may link | Write access to both modules and both records' company; read-only analysts stay read-only. REST: the narrow permission and the key's company scope. |
| 9 | Existing custom link fields that point at CIs | Left alone, apart from the delete fix in section 10. |
-
Deleting a CI leaves custom link fields pointing at it.
CmdbService::deleteObject()clears CMDB references and ticket links, but an asset's Link to something else β configuration item value keeps the deleted CI's id and then shows as#88. This is separate from this design, but it is the same delete. Fixed alongside Part A by clearing those values in the same transaction, with a test. - The asset page's link fields are still read-only. Part A builds a CI picker for the asset page anyway, so the custom link fields could use the same picker. That stays a follow-up rather than widening this request.
-
Part A + Part B + the read half of Part E: the link, both cards, who sees what, the history, the CI delete and merge handling, and
GET. This is the issue's stated priority. -
The write half of Part E (
POST/DELETE). - Part D: impact on the asset card. Small, once A exists.
- Part C: the type β class mapping and Create linked CI, with its REST route.
Each step is usable on its own, and each ships in a release once it's done.
- Automatic linking or CI creation by integrations. It will use Part A's service and Part C's path, but each integration brings its own review flow (#199 first).
- A general CMDB history of property changes.
- Copying or syncing any asset value into CI properties, in either direction.
- Links from one asset to several CIs, or a CI made of several assets. Those are CMDB relationships.
- A picker for the existing custom link fields (see section 10).
#199 gives a CI a second kind of identity: discovered by a source (a Hyper-V VM, host or cluster). The two are deliberately the same shape, so neither has to be reworked for the other:
| Concern | This design (#201) | #199 | The shared rule |
|---|---|---|---|
| What the CI is tied to | One asset (asset_ci_links) |
One discovered object (cmdb_object_sources) |
Both are periods, never deleted, with actor and source at both ends. One live tie per side |
| Facts shown on the CI | From the asset card | From discovery card | Read live, labelled, never copied into CI properties |
| A reader without access | Restricted marker | Restricted marker | The association never reveals a record the reader cannot open |
| Ambiguity | Possible matches, Replace, merge choice | Review list, reviewed rebinding | Suggestions are never proof. A decision carries the state it was made on and is refused if that changed |
| Deletion | Deleting the CI ends the link | Deleting the CI ends the binding; the object goes to review | Nothing is deleted because of the other record |
| Impact | The asset shows the CI's analysis | Adds grades to the analysis | Neither the asset nor the link is ever in the graph |
A CI can have both: a Hyper-V VM's CI is bound to the VM and, separately, linked to the asset the same machine has from the agent, Intune or a REST tool. The two cards sit side by side. Neither requires the inventory agent.
- CMDB Β· CMDB - Developer Guide Β· CMDB impact analysis - Developer Guide
- Hyper-V and Failover Cluster discovery - design (#199)
- Asset type profiles - design (#200) Β· Asset type layouts Β· Connected assets Β· Asset review Β· Inventory freshness
- Custom asset fields Β· Linking equipment to tickets
- REST API - Assets Β· REST API - CMDB