Blog ·
When NetBox is the source of truth
Connecting an external DCIM is not a sync. It is a per field decision about who decides, plus a list of things you refuse to import and say so out loud.
By Provibr
A customer with a working NetBox does not want a second inventory. They want their racks and their machines to appear in the platform they operate from, and they want to keep editing them in the place their team already knows.
The word usually attached to that is "sync", and the word is the problem. Sync implies two systems converging on one truth. What you actually have is a per field question that has to be answered in advance and written down: for this column, on this object, who decides? Answer it once with a shrug and you have built a system that overwrites a deliberate local edit at three in the morning, or one that silently ignores a change the customer made an hour ago and thinks has landed.
This is how we answered it, including the fields where the answer is no.
Four values, and they are not degrees of trust
Every column of every linked object carries one of four ownership values, and they say four different things about who decides rather than four levels of confidence.
External. The other side decides. The field is read only here and an import overwrites it without asking. That is not a loss of control, it is the arrangement the administrator entered into when they connected the system.
Ours. We decide, and an import does not touch it. This is the state of every field of an object that is not linked at all, and of every field for which the external system has no counterpart.
Manually locked. The other side does have a value, and an administrator has deliberately overridden it here, with a reason. A difference is then a conflict that gets surfaced, never a silent overwrite. This is the only one of the four that cannot arise as a default: somebody sets it.
Derived. Nobody decides. It follows from something else: whether a machine is racked, whether it is bound to a service, whether it is ready, the capacity of a rack, the bookkeeping of the row itself. These are structurally not importable, and a test asserts that they never appear in any mapping.
That last one deserves a note. The bookkeeping fields, who created a row and when, are derived rather than ours. Nobody chooses those values, they follow from the action. An import path allowed to write a creation timestamp is an import path that can rewrite history.
The ownership hangs on the link rather than on the object. An object with no link is entirely ours, which means unlinking is a single delete rather than a migration of state across every column.
The register is per column, and the column list comes from the schema
Every column of the four supported object types, sites, locations, racks and machines, has an entry in a register. Including the ones that have nothing whatsoever to do with the external system.
That completeness is enforced rather than intended. A test derives the column list from the database schema and demands a decision for each one. A new column added in some later round is therefore a red test rather than a quiet gap, which is the same construction we use elsewhere: a test per case cannot, by its nature, find a case that is missing.
Some of the decisions are the interesting part.
The power budget of a rack stays ours. NetBox models power as feeds, not as a single budget figure. Deriving one number from several feeds would be a capacity claim assembled out of incomplete data, which is precisely the thing the capacity design forbids. So the field is not imported, with that sentence recorded next to it as the reason.
The process state of a machine stays ours. NetBox device status mixes lifecycle and operation in one field. A device marked as failed would, on a naive mapping, push a machine into a maintenance state and take it out of the sellable pool. Those are two different vocabularies that happen to share a name.
The kind of a machine stays ours. NetBox device role is free text. Guessing wrong there does not produce a cosmetic error, it changes what the platform will let you do with a machine.
Each of those is stored with a written reason, in the same structure as the decision. A rule with a reason attached survives the next person; a rule without one gets relaxed by somebody who assumes it was arbitrary.
Initial only, because there are stickers in a box
Two fields are imported once, at creation, and never updated: the code of a site, location or rack, and the asset tag of a machine.
The reason is physical. Those values are printed on labels, and the labels are in a box in a store cupboard. A slug renamed in NetBox next spring must not send a scan to the wrong place. Initial only is not a third ownership value, it is a property of the import: take it when adding, leave it alone after that.
The same fields are what an unlinked object is matched on when the connection is first made: the code for the other three, the asset tag for machines. Deliberately not the name, because a code is stable and case insensitively unique and a name is neither.
Placement is never imported, and there is no field that leads there
NetBox knows, for every device, its rack, its position and its face. Those three are refused.
Not ignored. Refused, by name, with a reason code, and shown in the preview.
The reasoning follows straight from the occupancy design: the only real limit on rack occupancy is a partial unique index on the slot table, and that index is only ever touched by one function. An import path that writes a rack and a unit number onto a machine is a second write path that does not know about that index, and then one day a machine is recorded in two racks with an external system confirming that this is correct.
The specification for this section says that a local placement or active claim must never be wiped out by an import. There are two ways to make that true. One is to write the import so that it leaves claims alone. The other is to make it impossible for the import to write a claim at all, and that is the one we took: there is no field that leads there.
Enforcing that is an abstract syntax tree guard rather than a test case. It reads the source of the import module and requires that the four protected tables, the claims, the slots, the service bindings and the labels, appear only inside a select. An insert, an update or a delete on any of them is red even if no code path ever reaches it, because the guarantee has to hold for code that does not exist yet. The walker fails loudly on a shape it does not recognise, which is a lesson learned the expensive way in an earlier guard that quietly skipped what it could not parse.
Refusals are listed in the preview by name. Silent omission is the dangerous version here: somebody connects their NetBox, reads "142 machines updated", and assumes their rack layout came with it.
An external delete is a tombstone
A linked object that the other side stops mentioning becomes a tombstone with an impact review, not a deletion.
That is not merely polite. A list that comes back short because of a filter mistake, a permissions change or a half fetched page would otherwise clear out a customer's inventory. The tombstone puts the object in front of a person, along with what depends on it: an object with an active claim cannot be removed at all, and after the claim is released the outcome is a retirement rather than a delete.
And if the fetch is known to be truncated, nothing happens at all. No tombstones are proposed for anything. What you did not see, you cannot conclude is missing. That is a two line condition and it is the difference between a connector and an incident.
What we would write down
- Decide ownership per column, not per object, and make the column list come from the schema so a new one cannot be forgotten.
- Store the reason with the decision. A field that is not imported needs a sentence, or somebody will import it later.
- Make the dangerous import impossible rather than careful. No field to it, plus a guard that reads the source.
- Name what you refuse in the preview, because a number of updated objects reads as completeness.
- Treat absence as a question, never as a delete, and treat a truncated fetch as no information at all.
On our own state: this is behind a rollout flag that is off by default with a per organisation setting on top, so it is not something to buy today, and it has not run against a real NetBox. What is said above about how NetBox models power, status and roles comes from its documentation and from the shape of its interface, not from a production instance of it. Everything above was measured against a stub that reproduces the paging behaviour, the version header and the nested objects of the real interface, pinned to the API version the production code sends. The stub genuinely offers a rack, a position and a face in its device payloads, and zero claims are written: the refusal is measured rather than assumed. The related question of why an external system reporting success is not a result of its own is in why a Proxmox 200 is not a result.
From the datacenter work on Provibr. The occupancy boundary this import is kept away from is described in the only real limit on rack occupancy, and the labels whose codes must stay stable in a label that survives a hostname change.