Feature Specification: Resource netbox
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/netbox/default.nix, core/resources/netbox/contract.nix, core/resources/netbox/self-hosted.nix, core/resources/netbox/external.nix, core/lib/coordinate-resource.nix, core/lib/coordinate-provider.nix, core/setups/wiring/netbox-routing.nix, core/setups/wiring/resource-routing.nix, core/setups/hosts-common.nix, core/components/hosting-provider/windmill/default.nix), pull request #1234.
User Scenarios & Testing (mandatory)
The netbox resource gives a node the coordinates of the NetBox IPAM of its
deployment: the server URL, a path to the API token, and the cluster, tenant
and pool to allocate from. It does not allocate addresses. OpenTofu on the
deployer allocates them. The shared resource rules are in the
resource family spec. The resource has the same shape as
dns: both use core/lib/coordinate-resource.nix.
User Story 1 - Find a self-hosted NetBox (Priority: P1)
A node needs to reach the NetBox node that its deployment stands up. The
netbox setup argument has no serverUrl. The routing row selects the
netbox provider and sets fediversity.netbox-self-hosted.host to the FQDN
of the node that provides netbox. The result is
serverUrl = "http://<host>:8001".
Why this priority: a deployment without an external IPAM allocates from its own NetBox.
Independent Test: nix-unit-setups-netbox-routing, tests
test-self-hosted-result and test-roster-keeps-self-hosted-node.
Acceptance Scenarios:
- Given a self-hosted route with
host = "netbox.example.com", When the result renders, ThenserverUrlishttp://netbox.example.com:8001andapiTokenFileis"". - Given a self-hosted route with
host = "", When the result renders, ThenserverUrlis"", and the incus graph finds the address at apply time.
User Story 2 - Use an external NetBox (Priority: P1)
A hosting provider already runs NetBox. The netbox setup argument has a
non-empty serverUrl. The routing row selects the external provider and
passes serverUrl and apiTokenFile. The roster drops the node that provides netbox.
Each external setting that is not empty replaces the matching request field.
Why this priority: the deployment must allocate from the IPAM that the hosting provider already uses.
Independent Test: nix-unit-setups-netbox-routing, tests
test-external-result, test-external-overrides-request,
test-external-overrides-range, test-external-overrides-tenant and
test-roster-drops-node-when-external.
Acceptance Scenarios:
- Given an external route, When the result renders, Then
serverUrlandapiTokenFilecome fromfediversity.netbox-external. - Given an external
prefixSelectorwith a non-emptykind, When the result renders, Then it replaces the requested selector. - Given an external route, When the roster is built, Then it has no node that provides
netbox.
User Story 3 - Name the address pool, or fail at eval (Priority: P2)
An operator states the pool through NETBOX_PREFIX_SELECTOR (<kind>:<value>)
or through effect-defaults.<tag>.netbox.prefixSelector in HOSTING_CONFIG.
requirePool returns the coordinates when they hold a prefixSelector. It
returns null for no coordinates. Coordinates without a prefixSelector fail
the evaluation with a message that names both inputs.
Why this priority: an allocation without a pool picks addresses from the wrong range.
Independent Test: nix-unit-setups-netbox-routing, tests
test-pool-missing-is-an-error, test-pool-alone-is-enough and
test-pool-absent-coordinates-are-null.
Acceptance Scenarios:
- Given coordinates without
prefixSelector, WhenrequirePoolevaluates, Then the evaluation fails. - Given only a
prefixSelector, WhenrequirePoolevaluates, Then it returns the coordinates.
User Story 4 - Pass the API token as a path (Priority: P3)
The deployer reads the NetBox API token at apply time. The secret
environment of the incus hosts graph carries netbox_api_token as the path
apiTokenFile. The token value never enters the Nix store.
Why this priority: the token is a credential.
Independent Test: nix-unit-setups-netbox-routing, tests
test-secret-env-carries-path and test-secret-env-empty-without-coordinates.
Acceptance Scenarios:
- Given
apiTokenFile = "/run/secrets/netbox-api-token", When the secret environment renders, Thennetbox_api_tokenis that path.
Edge Cases
- The external provider is inert when its
serverUrlis"". - An empty
rangeSelector.kindmeans the whole prefix. kindis a free string:cidr,id, or a NetBox prefix filter name.- The self-hosted token lives in OpenBao KV.
core/effects/tf/incus/hosts/main.tfreads it, which works only from the second apply.
Requirements (mandatory)
Functional Requirements
- FR-001: The
netboxcontract request MUST holdclusterName,tenant,prefixSelector,rangeSelectorandallocateIpv4. The result MUST holdserverUrl,apiTokenFileand those five fields. - FR-002: The policy MUST default
clusterNametoincus,tenantto"",prefixSelectorto{ kind = "tag"; value = "fediversity"; },rangeSelectorto an empty selector andallocateIpv4tofalse, and MUST copy the five into the request. - FR-003: The deploy MUST NOT create NetBox infrastructure. The cluster and the pool MUST already exist.
- FR-004: The
netboxprovider MUST fulfillserverUrl = "http://<host>:<port>", or""whenhostis empty, andapiTokenFile = "". The default port is the port thatnetbox.nixbinds, read fromcore/resources/netbox/gunicorn-port.nix. - FR-005: The
externalprovider MUST passserverUrlandapiTokenFilethrough and MUST replace each request field other thanallocateIpv4with its own value when that value is not empty. It MUST passallocateIpv4through. - FR-006:
netbox-routing.nixMUST treat NetBox as external only when thenetboxargument is notnulland has a non-emptyserverUrl. - FR-007:
requirePoolMUST fail the evaluation for coordinates withoutprefixSelector. - FR-008:
core/setups/hosts-common.nixMUST set"ipam".netboxwithenable = true, the setup coordinates where present, androuting = mkDefault providerModules.netbox, and MUST add theconsumerSideof itsprocessto each node. - FR-009: The
netboxrequest MUST hold only data:enableandconsumerName. TheconsumerSideMUST declare thewantof the request, so the node gets the contract result atcontracts.netbox.results.<consumerName>.default. A client module MUST importcore/resources/netbox/contract.nixto read it.
Key Entities
netboxcontract: local contract incore/resources/netbox/contract.nix.- Selector:
{ kind; value; }forprefixSelectorandrangeSelector. fediversity.netbox-self-hosted: optionshostandport.fediversity.netbox-external: optionsserverUrl,apiTokenFile,clusterName,tenant,prefixSelectorandrangeSelector.- Consumer
windmill: requests"ipam".netboxwithconsumerName = "windmill". A client module imports the contract module and, gated on the result, builds the operator curation fromcontracts.netbox.results.windmill.default.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-setups-netbox-routingpasses. - SC-002:
nix-unit-components-operator-curationpasses. - SC-003:
nix-unit-resources-coordinate-consumerandnix-unit-resources-plain-requestspass.
Assumptions
- The NetBox cluster, tenant and prefix exist before a deploy.
- The external API token is a file on the deployer.
Known Deviations
- The self-hosted route reads its token from OpenBao KV only from the second apply. The first apply of a cold deployment cannot reach the self-hosted NetBox. The
externalroute has no such asymmetry.