Skip to content

Inventory Providers ​

Inventory providers connect Hegemony to external sources of truth - NetBox, a Git repository - and bring their devices and sites into your inventory alongside what you create by hand (the local source). Provider support ships as optional plugins, so the provider types offered depend on what this instance has installed.

Add a provider ​

  1. Open Settings → Inventory Providers and click to create one.
  2. Pick a provider type from the installed set and give the configuration a name.
  3. Fill in the type's own settings - the form comes from the provider plugin itself (server URL, credentials as secret references, filters). The configuration is validated when you save. Provider settings are platform configuration, so the API token may also read the platform's environment or secrets directory with {{ env('NAME') }} or {{ file('NAME') }}; the default device access refs stay secret() only.

Each configuration appears with its own detail page, where you can edit it or remove it. Its Synced Objects card links to the object lists the provider type can populate, each opened with the list's Provider filter set to this provider; a type you have unticked (below) is shown as not synced.

Test the connection ​

The detail page's Test button asks the provider to reach its source and report back. Beyond passing or failing, it shows whatever that provider considers worth knowing about the instance it reached - a NetBox provider reports the release it is running and every plugin installed on it.

What appears here is the provider's own account, so it differs by provider type, and a provider that reports nothing simply shows its result message. Nothing sensitive is included: a provider decides what is safe to display, and credentials never are.

It is worth a look when a sync returns something you did not expect. Two providers pointed at what you thought was the same system, or one pointed at a staging copy, both show up here immediately.

Discover what an instance serves ​

The object types offered above come from the provider's plugin, and are the same for every provider of that type. That is right for a source of truth with a fixed shape and wrong for one that can be extended: a NetBox with plugins installed serves object types the Hegemony plugin was never written to know about, every instance defines its own custom fields, and an older release does not serve everything a newer one does.

Discover on the provider's detail page asks that instance to describe itself. What comes back is stored against the provider and reported as three lists:

  • added — object types this instance serves that were not known before, including anything its own plugins add. Credential-store plugins are the exception: hegemony-inventory-netbox never offers or reads netbox-secrets or netbox-secretstore, because their API serves a hash of every stored secret's value.
  • updated — object types re-described by this run.
  • removed — object types it no longer serves, or that its plugin no longer offers. A NetBox provider that discovered netbox-secrets or netbox-secretstore types before hegemony-inventory-netbox 0.4.1 lists them here on its next run, even while the instance still serves them; the rows an earlier sync stored from them are marked stale, not deleted. Worth reading: if one of these is ticked in the provider's selection, that selection now syncs nothing.

After successful discovery, the instance's answer determines which object families its selection and sync offer. The plugin catalogue supplies metadata and is the fallback before discovery, not a list of endpoints to keep requesting after the instance says they are absent. Legacy device/site entry points remain available. An empty successful answer removes all generic families; a failed discovery leaves the previous answer intact. Removed selected families are reported and deselected, and their stored rows become stale. For example, NetBox 4.5+ exposes core.object_types instead of extras.object_types; discovery stops the latter being requested, but does not silently select the replacement. A 404 from an endpoint the instance still advertises remains a sync failure.

Discovery describes a whole API rather than importing inventory records. Run it after upgrading the instance or installing a plugin. NetBox also adopts the shared schema on editor loads and syncs as described below; explicit Discover refreshes it immediately.

Not every provider type can do this. One whose shape is fixed — a Git inventory, for instance — has nothing to describe and says so rather than pretending, and the Discover button is not offered for it at all.

What a discovered object type can then do ​

Once an instance has described itself, what it found is not merely recorded:

  • it can be ticked in that provider's object-type selection. The plugin's list is the same for every provider of that type, so on its own it can never offer the families that make discovering them worth doing;
  • it syncs like any other object type. The provider is built fresh for every operation and the family is absent from the catalogue it ships, so the platform hands back the mapping the instance returned — that is the only place that knowledge lives;
  • it lists and renders, because the object-type manifest the browse and detail pages read is the union of what plugins register and what instances discovered. Without that a discovered family would sync into rows no page could show.

Where an object type is both registered and discovered, the two are merged rather than duplicated: the plugin's display name, grouping and columns are kept, and the fields are the union of both. Two instances of one provider type genuinely differ — a NetBox 4.1 serves a prefix's site where 4.3 serves its scope — and declaring only one would leave a dead column for whichever half of the estate is on the other release.

The read is merged the same way, for the same reason. A sync of such a family hands the provider the mapping the instance returned as well, so the fields only that instance has — its custom fields above all — are actually filled in rather than shown empty on every row. Which half wins where the two disagree is the provider's call: the plugin knows how it reaches a family and what key its rows are stored under, and a stored answer from a discovery that ran months ago should not overrule a plugin that has been upgraded since.

Devices and sites from the same read ​

A provider used to be asked for its devices and sites separately from everything else, and that second read is where a whole class of quiet wrongness lived: a NetBox device arrived with its site nested inside it, keyed one way, while the site listing emitted the same site keyed another. Both were stored, so one NetBox site became two rows in Hegemony — and the devices attached to the copy with no parent, which disconnected the site tree from the devices hanging off it without anything failing.

A provider that describes itself can say instead which of its object families are the devices and sites. Hegemony then writes those two tables from the rows it already stored, so there is one read, one key and nothing left to disagree.

Two things change that you can see:

  • The site tree is made of real things. Where the NetBox provider used to invent a site for each region so the tree had something to hang from, a region is now an object you can list and open like any other, and a site's parent is a link to it.
  • A device carries everything NetBox holds about it, not only the fields the device table has columns for, because the object row is still there beside the projection.
  • A source's own tags stay targetable. A tag in the source is a flat label and device search matches on a key=value map, so a binding can name the list and each entry becomes a key you can select on — the same tag: keys a run form already filters by.
  • The source says what its values mean. NetBox states an address as 10.0.0.1/24, which is the truth about the interface and useless to anything dialling it. The object keeps what the source said; the device column gets what a transport can use, because the binding names the conversion. Hegemony performs only conversions it was asked for, from a fixed list it publishes — host, lower, upper, strip, first — so a provider can never hand the platform logic to run, only a name to look up.

Nothing changes for a provider whose shape is fixed — Git keep answering for devices and sites the way they always have.

A NetBox provider needs discovery data before it can sync devices and sites. Its separate device and site reads are gone, so sync automatically obtains or adopts the instance's schema before reading records. You do not need to press Discover first. If discovery cannot complete, sync reports the failure rather than silently importing nothing. Use Discover for an explicit schema refresh and to inspect the projection plan before the next sync; existing provider settings and record identities are retained.

Discover tells you what the next sync will do ​

Pressing Discover stores what the instance said, and the next scheduled sync starts writing devices and sites from it. Between those two moments there is nothing to see — which matters, because a binding that keys rows differently from the ones already there does not fail. It writes a second set beside the first, and the first goes stale on the pass after. Nothing turns red; the estate is just replaced overnight.

So Discover reports the plan alongside what it found: per table, how many existing rows keep their identity, how many arrive new, and how many are superseded — keyed differently from what the provider now describes, so the next sync stops reporting them. A large superseded count is the signal to check the binding before letting the sync run.

The plan is read-only and computed from the objects stored at that moment. A family that has never synced has nothing to plan from and counts zero rather than guessing.

The same is true after a rollback. Unwinding the database to before this release drops what discovery recorded, and rolling forward again recreates it empty rather than restoring it. The next NetBox sync automatically obtains or adopts discovery data again; press Discover if you want an explicit refresh and projection preview beforehand. Its object rows, devices and sites are untouched either way; only the description of what the instance serves has to be obtained again.

The old invented region rows are not migrated. Nothing could migrate them honestly — they are keyed by a name where the real ones are keyed by an identifier, and no upgrade step can turn one into the other without asking the source. The first sync after discovery simply stops reporting them, they go stale like any other site that disappeared, and your real sites keep their own rows and gain the right parent.

Read only what changed ​

A sync reads everything a provider serves, every time. For fifty thousand devices on a half-hour cadence that is the whole table pulled forty-eight times a day to learn that almost none of it moved. Where a provider can answer what changed since X — NetBox can, through last_updated — Delta sync asks it that instead.

It is off by default, because reading deltas brings one failure a full read does not have: a delta read cannot see a deletion. A record someone deleted appears in no answer at all, so a platform reading only deltas would keep a decommissioned device for ever and go on letting runs target it.

So delta never runs alone. Two rules hold it up:

  • A full pass is forced on a cadence. full_reconcile_interval_seconds is how long a provider may go without reading a family in full, and therefore the longest a deleted record can linger before anything notices. A day by default; 0 reconciles every pass, which is the same as switching delta off.
  • A delta pass never marks anything stale. Staleness means "absent from the listing", and absence from a delta listing means "did not change". Marking on one would stale the whole family except the handful of records that moved.

Everything ambiguous reads in full: no marker recorded yet, the provider does not declare it can answer, the operator has not enabled it, the full pass is overdue. A full read costs time and returns the truth; a delta read taken when it should not have been leaves the platform confidently wrong.

Switching delta off forgets what was recorded, so it takes effect on the next sync rather than whenever the markers happen to expire.

Configure NetBox inventory filters ​

Each provider has one Inventory filters configuration: an optional NetBox tenant slug and visible rules for individual families. Tune this configuration over time; create another provider instance when an organization needs a different inventory. The editor offers fields, operators and values from the instance's discovered schema. Existing raw queries and the active saved scope migrate into this single rule set when read, preserving their combined restrictions. Saving persists the new format.

The tenant slug applies tenant=<slug> to selected families advertising that filter and slug=<slug> to the tenant family. Families without a tenant filter are suppressed until explicitly allowed. Allow them only for shared reference data or when the NetBox account enforces the boundary. For contacts, allow the family and add a group equals gondor rule. Editing filters forces a full reconciliation; objects that leave the filter become stale. Filters control synchronization; source permissions enforce authorization.

Configure the same NetBox URL in each Hegemony organization with that organization's tenant slug and preferably a separate least-privilege token. API schemas are cached for 24 hours across connections to the same instance. Opening a saved provider's object-type editor or starting synchronization automatically discovers or reuses the schema. Each connection still authenticates and reads custom fields using its own credentials. Inventory objects, selected families, projection bindings and sync state stay separate.

Discover explicitly refreshes the shared schema. Other connections adopt it on their next editor load or sync. For URL aliases pointing to one NetBox, set discovery_url to the same canonical base URL; schema requests use that URL. Discovery device/site counts only describe projections of already imported records. Run Sync to import inventory.

Set public_url to the browser-accessible NetBox URL if it differs from the API connection URL. IP-address details include the assigned interface and its device or virtual machine; relationships use names and link directly to target objects when those objects are synced.

Choose which object types to sync ​

Devices and sites are always synced - they are the platform's own tables, with their own pages, targeting and site tree - and the form shows them for that reason, ticked and not deselectable. Sites covers more than the sites themselves: a provider also stores whatever it places above them, so that the site tree has parents to hang from. A NetBox provider reads its regions (or its site groups, per that plugin's site_tree_source) on every sync and stores them as sites, which is why the form says so beside the entry.

Everything else the provider plugin serves is listed under Object types to sync, grouped into the families the plugin declares: a NetBox provider shows DCIM, IPAM, Circuits, Tenancy and the rest, each holding its own types. A family's own checkbox takes all of it, and shows a dash when only some of it is ticked. A new provider starts with nothing ticked, so it syncs devices and sites and no more until you say otherwise: a plugin can serve fifty families, and reading all of them from a large source system is a decision rather than a default.

Untick a type to stop fetching it: nothing of that type is read from the provider any more, and the rows an earlier sync stored are marked stale on the next sync rather than left looking current. Tick it again and the next sync restores the rows the provider still reports; anything it has since dropped stays stale.

With every type ticked the provider is stored as syncing "everything it serves", so a type a plugin upgrade adds is synced without a visit to the form. In the API the selection is object_types: null for everything, a list to restrict, [] for devices and sites only; a type the plugin does not serve is rejected by name. Configuration Exchange carries the same field, and a bundle without it means everything.

The list can only offer what the plugin says it serves, so a plugin that declares few types shows a short list. Nothing in a provider's advanced configuration overrides the selection: this form is the only place the choice is made.

Where provider data shows up ​

Objects imported from a provider appear in Devices and Sites with their Source column naming the provider, next to your local objects. Provider-sourced objects are refreshed from the external system, so treat that system as the place to edit them - the sites guide describes how provider-sourced sites behave in the tree. Other object types a provider serves (prefixes, VLANs, and so on) are listed under Inventory with a Provider column, since one type page holds every provider's rows - see the inventory objects guide.

Syncing ​

Each provider is synced on its own schedule, and the provider's detail page also has a Sync now button. Either way the run is recorded in the provider's sync history with what it did: how many items it added, updated, left unchanged (the provider sent exactly what is already stored, so the row was not rewritten), marked stale (the provider stopped reporting them) and restored (it started reporting them again). Nothing is deleted on the strength of a provider going quiet - a source of truth that cannot see a device for a few minutes is not the same as the device being gone.

Sites are counted separately from devices, because the two mean different things: a stale site can relabel or hide every device beneath it. A manual sync reports site changes alongside the device counts when there are any.

The provider list shows each provider's last sync outcome and when its next automatic sync is due (Manual only when the interval is 0, Due now once the scheduled time has passed). The detail page shows the same beside the run history, where a failed run's error message is its own column.

A running sync holds a lease on its provider rather than a flag. While the lease is live, the schedule skips the provider and Sync now answers that a sync is already in progress. If the process running the sync dies, the lease expires on its own, the next scheduled tick (or the next Sync now) records the interrupted run as an error and takes the provider over. The provider's calls (listing sites, devices and objects) share one time budget, HEGEMONY_INVENTORY_SYNC_DEADLINE_SECONDS (15 minutes by default); a sync that runs past it is recorded as a timeout error and its lease released.

Sync history is kept for 30 days by default (HEGEMONY_INVENTORY_SYNC_HISTORY_RETENTION_DAYS; 0 keeps every run), and each provider's newest 20 runs (HEGEMONY_INVENTORY_SYNC_HISTORY_KEEP_LAST) stay whatever their age, so a provider that has not synced in months still shows what happened last.

A provider that serves more than devices and sites (a NetBox provider also syncs prefixes, addresses and VLANs) syncs each object type on its own. If one type fails - an endpoint that times out, say - the devices, sites and the other types are still brought up to date, the failed type's objects are left exactly as the previous sync had them (never marked stale on the strength of a listing that did not finish), and the run is recorded as partial, with the failure for each type shown on the provider page. A partial run retries on the same schedule as a failed one.

Projection follows the same rule, one record at a time. An object without a management address (a PDU, a planned device) is not a device Hegemony can reach, so it is skipped and counted, not stored as a device and not reported as a failure: the run is still ok and the family's delta checkpoint is recorded. A device that loses its address is marked stale in the pass that saw it lose it. A record that should project and cannot -- a credential field whose value is not a secret() reference -- is a failure of that record only: the other records project, the failed record's existing device row is left exactly as the last successful pass wrote it (not stale), the run is partial naming the record's external id and never the value, and the family's checkpoint is withheld so the next pass reads the record again and projects it once it is repaired. The same secret()-only rule applies to a provider's default_access_config when the provider is saved, so a credential reference the platform would refuse per device is refused on the form instead.

A failed sync is not parked for the provider's whole interval. It retries after 60 seconds, then 2 minutes, then 4, doubling per consecutive failure and never waiting longer than the interval itself, so a rotated token or a source of truth that was briefly down is back within minutes. The first successful sync resets the schedule to the interval.

Building your own provider ​

Provider plugins are developed against the inventory SDK; see Inventory Plugin Development on the documentation site.

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].