Seeding Demo Data#
scripts/seed_demo_data.py builds a full ACI dataset for local
development: Fabrics, Pods, Nodes, Node Interfaces, VPC Protection
Groups, access policies, Leaf Switch Profiles, Tenants, VRFs, Bridge
Domains, L3Outs, Endpoint Groups, Endpoint Security Groups and
Contracts, plus the NetBox core objects (Sites, Devices, VRFs,
Prefixes, VLANs) they reference. Every plugin model gets at least one
row, so it doubles as a manual smoke test for the full object graph.
Run it with:
make seed
which pipes the script through NetBox's management shell against the
NETBOX_ROOT checkout (/opt/netbox by default, override with make
seed NETBOX_ROOT=/path/to/netbox). If the virtualenv is not active in
the current shell, spell out the interpreter explicitly:
cd /opt/netbox && /opt/netbox/venv/bin/python netbox/manage.py shell < /path/to/netbox-aci-plugin/scripts/seed_demo_data.py
Note that this is one of the few places that deliberately uses shell
rather than nbshell. Only shell reads piped stdin and runs it
through exec(). nbshell drops straight into code.interact(), which
compiles the input line by line and silently truncates every function
body at its first blank line.
Additive and convergent#
The seeder never deletes anything. Every object goes through an
ensure() helper that splits its arguments in two. The keyword
arguments are the row's identity, chosen to match the model's real
uniqueness constraint, falling back to its natural key for the few
core models with no constraint at all. The defaults dictionary is
the row's desired state. A row that already exists is brought to that
state rather than left alone, so editing a spec in the script takes
effect on the next run. Every row is validated with full_clean(),
whether it was just built or already existed, and written only when
something actually changed. Running the script twice is a no-op the
second time.
The whole run happens inside one transaction.atomic() block, so a
validation failure partway through leaves the database exactly as it
was before the run started.
The script writes to whichever database the active NetBox configuration points at. It has no database flag of its own, so check which configuration is active first.
What a run reports#
The closing summary counts only the rows this run touched, split into created, updated and unchanged, and covers the NetBox core objects alongside the plugin ones. A second run in a row should report every model as unchanged. A run also fails outright when a plugin model never received a row, which keeps the seeder honest as new models arrive.
Naming scheme#
Object names encode where they live in the Fabric and Tenant
hierarchy, so a name usually tells you its scope without following any
foreign keys. A few object types don't fit as cleanly: ACIContractFilterEntry
rows (tcp-app-syn-ack, arp-request) carry no scope prefix at all
and rely on their parent Contract Filter instead, and a fabric-scoped
name can still contain a hyphen of its own, as in the VPC Protection
Group F1VPC2101-2102.
| Scope | Pattern | Example |
|---|---|---|
| Fabric-scoped | fused fabric prefix, no separator | F1Pod1, F1Leaf2101, F1AAEP1 |
| Tenant-scoped | fused fabric and tenant prefix, then a hyphen, then the object token | F1T1-VRF1, F1T1-BD1-Sub1 |
| Reserved ACI Tenants | the Tenant keeps its exact APIC name, children take a capitalised short token | Tenant common, child F1Cmn-VRF1 |
Unlike the tenant-scoped children above, the Leaf Switch Profile tree
does not build a child's name from its parent's. A Leaf Switch Profile,
its Leaf Selectors and their Leaf Node Blocks are each named after the
Node IDs they cover, the same way a VPC Protection Group is, so
F1SwProf2101-2104 holds F1SwSel2101-2102 and F1SwSel2103-2104,
and the first of those holds F1NodeBlk2101-2102. A Leaf Node Block
covering a single Node drops the range, as in F2NodeBlk2101.
Fabrics themselves stay Fabric1 and Fabric2. Fabric1 carries all
three reserved Tenants, matching what the plugin's default data
migrations create. Fabric2 gets only common.
Node ID scheme#
Node IDs follow a four-digit abcd pattern: a is the switch type (1
for an RJ45 leaf, 2 for an SFP leaf, 3 for a spine), b is the Pod
ID, and cd counts up from 01 within that type and Pod. vPC pairs
always take an odd Node immediately followed by the next even Node,
for example 2101 paired with 2102, or 2103 paired with 2104.
APIC Nodes fall outside this scheme entirely. ACINode caps the Node
ID at 100 for the apic role, so APICs are simply numbered 1, 2,
3 and take no Pod or type digits.
The Fabrics#
Fabric1 is built out in full: Pods, both leaf switch types, spines, APICs, vPC pairs, a complete set of access policies and Tenants. Fabric2 stays deliberately thin, with a Pod and a vPC pair of its own and a much smaller slice of access policies and Tenants. A thin second Fabric gives fabric-scoped filtering and isolation checks something real to exclude, without duplicating the whole Fabric1 dataset.
Device Types mirror the NetBox Device Type Library, down to the port
layout of the real hardware, so a leaf carries its full complement of
downlinks and uplinks rather than a token handful. Every Device's
Interfaces are built from those Interface Templates rather than
hard-coded. NetBox only instantiates templates when a Device is first
created, so the seeder materializes them itself and a database whose
Devices predate the templates converges on the next run. The APICs are
the exception worth knowing: they are UCS appliances, so they carry no
Ethernet1/N ports at all and their management interface is the
CIMC, not an mgmt0.
NetBox VRF mapping#
Each ACI VRF that owns a Bridge Domain gets its own NetBox VRF, named
ACI-Demo- followed by the ACI VRF name and keyed on its route
distinguisher, since that is the only unique field NetBox gives a VRF.
Every gateway address, External Subnet prefix and uSeg Network
Attribute object is then created in the NetBox VRF mapped to its own
ACI VRF, derived from the object's place in the ACI hierarchy rather
than hard-coded. That keeps the demo data consistent with the rule
ACIExternalSubnet validates, which compares a linked prefix's NetBox
VRF against the one mapped to its ACI VRF.
F1Infra-VRF1 owns no Bridge Domain and is deliberately left
unmapped, so an ACI VRF without an nb_vrf stays in the dataset.
ACI-Demo-Underlay is the other exception: it carries the TEP pools
and the Node TEP addresses, and maps to no ACI VRF at all.
Extending the seeder#
Each domain has its own seed_*() function. Most blocks declare their
objects as a tuple of field values that the function iterates over,
and extending them means adding an entry to that spec tuple. Objects
too dissimilar to tabulate, such as the L3Outs or the Endpoint Group
AAEP Bindings, are instead spelled out one ensure() call at a time.
Domain order is fixed by the transaction.atomic() block at the
bottom of the script. Within a domain, respect the same ordering the
existing entries already follow, for example a Pod before its Nodes,
or both members of a vPC pair before the VPC Protection Group that
references them. The one that catches new entries most often: an
ACIEndpointGroupDomainBinding and an ACIAAEPDomainBinding must both
already be saved before an ACIEndpointGroupAAEPBinding for the same
EPG validates, because that binding's clean checks intersect the
persisted domain columns of the two binding types.
Four more traps aren't visible from reading the script but will catch
the next added row. Never key ensure()'s lookup on a field that
save() overwrites afterward: ACIEndpointGroupAAEPBinding.encap_vlan_id
is overwritten from nb_vlan.vid, and ACIExternalSubnet.matched_prefix
is overwritten from nb_prefix.prefix, so key those lookups on
(aci_endpoint_group, aci_aaep) and (aci_external_endpoint_group,
name) instead. A GenericForeignKey also cannot appear in a lookup
at all. Django raises FieldError if you try. That is the entire
reason the gfk() helper exists: it expands the GFK into its _type
/ _id column pair so the lookup can filter on those columns instead.
Assigning the GFK attribute directly in the model constructor, rather
than through gfk(), is fine, since only the lookup side needs the
expanded pair.
The third trap is that a model may refuse a field change after
creation. ACILeafInterfacePolicyGroup rejects a new group_type on
a row that already exists, so editing that column in a spec raises
rather than quietly creating a second group beside the first. The
fourth is the coverage check: adding a plugin model and forgetting to
seed it fails the run by name. EXEMPT_MODELS is the escape hatch for
a model that genuinely should not be seeded.
Seeding over existing objects
The seeder reconciles. An object whose identity fields match one
of its specs is adopted and then brought to the values the script
declares, so a hand-made object that collides by name loses its
own values, while objects the plugin's data migrations create,
such as Fabric1 and the reserved ACI Tenants, gain the demo
owner, scope and relations they would otherwise never get.
Nothing is ever deleted, and the whole run is one transaction, so
a collision that cannot be reconciled aborts cleanly rather than
leaving half a dataset behind.
Re-seeding a database built by an earlier version of the script
leaves three kinds of orphan behind, all harmless on a development
database. The previous Bridge Domain gateway addresses stay in
ACI-Demo-F1T1, unreferenced. Each APIC keeps a stale mgmt0,
since it now takes a CIMC instead. The Device Types the leaves
used to point at also remain, because the seeder repoints the
Devices rather than deleting the rows they left.