Feature Specification: Effect netbox-seed
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f
(core/effects/tf/netbox/seed/effect.nix,
core/effects/tf/netbox/seed/main.tf,
core/effects/common/standalone-env.nix, core/render/catalog.nix,
core/setups/hosts-common.nix).
The shared behavior of a standalone effect is in spec.md. The
graph grammar, the verbs and the state rules are in
../004-deployment-programs/spec.md.
The incus deploy that reads the seeded objects is in
../009-deployment-methods/tf-incus.md.
This file does not repeat them.
User Scenarios & Testing (mandatory)
User Story 1 - Seed a fresh NetBox for the incus deploy (Priority: P1)
An operator has a NetBox instance with no fediversity objects. They run
netbox-seed once with NETBOX_SERVER_URL and a token file. The effect
creates the attribution tags, the cluster type and the cluster. The incus
deploy then finds these objects by name.
Why this priority: The incus deploy reads the cluster and the tags. It does not create them. Without the seed, the deploy has no cluster to put its virtual machines in.
Independent Test: No check applies the seed graph itself. Check
apps-tf-incus creates the equivalent objects through the tf-netbox-*
effects instead.
Acceptance Scenarios:
- Given an empty NetBox and
NETBOX_SERVER_URLset, When the operator runsnetbox-seed, Then NetBox holds tagfediversity, cluster typeIncusand clusterincus. - Given the seed has run, When the operator runs
netbox-seedagain with the same inputs, Thentofureports no changes. - Given the seed has run, When the operator reads the outputs,
Then they see
cluster_id,tenant_id,cluster_type_idandtag_slugs.
User Story 2 - Seed a second deployment in the same cluster (Priority: P2)
An operator runs two deployments against one NetBox cluster. They set
NETBOX_TENANT for each seed. Each seed creates one tenant. Each deploy
then keys its virtual machines on its own tenant.
Why this priority: NetBox keys a virtual machine's uniqueness on cluster, tenant and name. Two deployments without tenants collide, and NetBox returns a 400.
Independent Test: No check covers a tenant seed.
Acceptance Scenarios:
- Given
NETBOX_TENANT=a, When the operator runsnetbox-seed, Then NetBox holds tenantawith the attribution tags. - Given
NETBOX_TENANTreaches the deploy but not the seed, When the deploy runs, Then the deploy fails because the tenant does not exist.
User Story 3 - Seed against pre-existing objects (Priority: P3)
An operator's NetBox already holds the cluster type or the tags. They set
createClusterType = false or createTags = false. The effect then reads
the cluster type by name and does not manage the tags.
Why this priority: A shared NetBox can hold objects that other teams own. The seed must not take ownership of them.
Independent Test: No check covers the read-only modes.
Acceptance Scenarios:
- Given cluster type
Incusexists andcreateClusterType = false, When the seed applies, Then the cluster refers to the existing type and the state holds nonetbox_cluster_type. - Given
NETBOX_TAGS='[]', When the seed applies, Then it creates no tags and the cluster carries no tags.
User Story 4 - Plan without a change (Priority: P3)
An operator runs netbox-seed-plan against production NetBox. The plan
shows the changes and creates nothing.
Why this priority: A plan that applies changes production without review.
Independent Test: Check integrations-plan-does-not-apply asserts
that TF_VERB=plan holds, and that BOOTSTRAP_NO_HTTP_BACKEND=1 beats an
exported TF_HTTP_ADDRESS.
Acceptance Scenarios:
- Given an empty NetBox, When the operator runs
netbox-seed-plan, Then NetBox still holds no cluster.
Edge Cases
NETBOX_SERVER_URLis unset: the graph evaluates, andtofufails at run time on the empty URL.NETBOX_CLUSTER_NAMEis empty: the effect creates no cluster and does not read the cluster type.NETBOX_TAGSis not a JSON array: evaluation fails.NETBOX_SEED_STATEis unset: the state is a local file, keyed on the cluster name. A second workstation does not see it.- Two seeds use different cluster names: each gets its own state key, so the second seed does not rename the first cluster.
TF_VAR_netbox_api_tokenis exported: it wins overapiTokenFile.
Requirements (mandatory)
Functional Requirements
- FR-001: The effect MUST create a
netbox_tagfor each name intagNameswhencreateTagsis true, and none when it is false. - FR-002: The effect MUST create a
netbox_cluster_typenamedclusterTypeNamewhencreateClusterTypeis true andclusterNameis not empty. - FR-003: The effect MUST read the cluster type by name when
createClusterTypeis false andclusterNameis not empty. - FR-004: The effect MUST create a
netbox_clusternamedclusterName, tagged withtagNames, only whenclusterNameis not empty. - FR-005: The effect MUST create a
netbox_tenantnamedtenantName, tagged withtagNames, only whentenantNameis not empty. - FR-006: The cluster and the tenant MUST depend on the tags, because NetBox resolves tags by name.
- FR-007: The effect MUST read the API token at run time from
apiTokenFile, and an exportedTF_VAR_netbox_api_tokenMUST win. - FR-008: The derivation MUST hold only the token file path, never the token.
- FR-009: The state key MUST end in
clusterName, so that seeds for different clusters keep separate state. - FR-010: With
httpBackend = null, the state MUST be a local file, and an exportedTF_HTTP_ADDRESSMUST NOT change it. - FR-011:
NETBOX_SEED_STATEMUST set the HTTP backend address. The effect MUST NOT readTF_HTTP_ADDRESSfor it. - FR-012: The effect MUST read only its declared env keys:
NETBOX_SERVER_URL,NETBOX_SEED_STATE,NETBOX_API_TOKEN_FILE,NETBOX_CLUSTER_NAME,NETBOX_TENANT,NETBOX_CLUSTER_TYPE_NAMEandNETBOX_TAGS. - FR-013: An unset env key MUST keep the effect's default.
- FR-014: Each env key MUST have the same name as the key the incus deploy reads, so that seed and deploy agree.
- FR-015: The effect MUST expose the verbs of
tfVerbsasnetbox-seedandnetbox-seed-<verb>. - FR-016: The effect MUST use provider
e-breuninger/netboxat the version innpins.main.tfgives no version. The plugin mirror of the effect holds only the pinned version, and the lock file thattofu initwrites records it. - FR-017: The effect MUST declare no
effect-metaand MUST be inbuildingBlocks, so the operator form does not offer it. The operator runs it as the standalonenetbox-seed.
Key Entities
- Tag: an attribution marker, by default
fediversity. Its description names the group. - Cluster type: by default
Incus. The seed creates it or reads it. - Cluster: by default
incus. The incus deploy puts its virtual machines here. - Tenant: optional. It separates the virtual machines of two deployments in one cluster.
- Seed state: an OpenTofu state, keyed on the cluster name. It is a local file, or it lives at
NETBOX_SEED_STATE.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: After one apply on an empty NetBox, the incus deploy finds its cluster and tags by name.
- SC-002: A second apply with the same inputs reports zero changes.
- SC-003: A plan creates zero NetBox objects.
- SC-004:
nix-unit-env-configpasses, so the effect reads no undeclared env key. - SC-005:
nix-unit-effects-tf-provider-pinspasses, somain.tfhas no version literal, andtf-providerspasses, so the lock file of the effect selects the netbox version innpins. - SC-006:
nix-unit-effects-registrypasses, so the effect is inbuildingBlocksand declares noeffect-meta.
Assumptions
- The operator has a NetBox API token with write access to tags, cluster types, clusters and tenants.
- The NetBox instance is reachable from the machine that runs the effect.
- The operator runs the seed before the first incus deploy.
- Tags that the deploy names in
netboxTagsexist before the deploy. The seed creates them, or another party does.
Known Deviations
- No check applies the seed graph.
apps-tf-incuscreates the same kinds of objects through thetf-netbox-*effects.