Deploy a Netbox design¤
design_deploy renders a YAML design with Jinja2, validates the rendered document, then creates or updates Netbox objects in dependency order. It never deletes objects omitted from the design. Existing objects receive the supplied fields on every run. The task does not calculate a diff before sending updates.
Design overview¤
Netbox Design Driven Modeling (NDDM) is an approach to building and maintaining the Netbox data model from reusable network designs. A design describes the objects and relationships needed for a site, service, or network pattern in YAML. Jinja2 templates let you reuse the same design for multiple deployments by supplying different context values, such as site names, device names, or address pools.
NorFab implements this approach through design_deploy: it turns the design into Netbox objects, resolves supported references, and processes collections in dependency order. Designs can include resource allocation requests for values that are not known in advance. Keeping designs and their context in version control makes the intended model reviewable and reusable.
NDDM designs are additive and idempotent. Deployment creates missing objects and updates matched objects according to each handler's rules. Reapplying the same design reuses those objects instead of creating duplicates. Objects omitted from a design remain in Netbox. Custom functions must implement their own idempotent behavior. The task models data in Netbox. Applying device configuration is a separate automation step.
design_input_schema:
type: object
properties:
site:
type: string
prefix:
type: string
roles:
- name: p2p
prefixes:
- prefix: {{ context.prefix }}
role: p2p
site: {{ context.site }}
devices:
{% for device_id in [1, 2] %}
- name: router{{ device_id }}
site: {{ context.site }}
role: router
platform: cisco_ios
device_type:
manufacturer: Cisco
model: ASR1001
status: active
interfaces:
GigabitEthernet0/0/0:
ip_addresses:
- create_ip:
prefix:
role: p2p
site: {{ context.site }}
mask_len: 30
{% if device_id == 1 %}
connection:
device: router2
interface: GigabitEthernet0/0/0
{% endif %}
bgp_peerings:
- local_as: 65000
remote_as: 65000
description: iBGP between Routers
{% endfor %}
For an agent-oriented authoring workflow, see Author a Netbox design with an agent.
A design is one YAML document with object collections such as sites and devices. It can also contain three optional control sections:
| Section | Purpose |
|---|---|
design_input_schema |
Validate values supplied through context before rendering. |
jinja_functions |
Load Python functions for use while Jinja2 renders the YAML. |
custom_functions |
Load Python functions called later, during collection deployment. |
| Remaining top-level keys | Define lists of Netbox objects dictionaries to create or update, such as sites, devices, and ip_addresses. See supported Netbox objects and deployment order. |
For YAML text or a file URL, put design_input_schema and jinja_functions before the first object collection. The loader reads only that initial metadata block to validate context and register Jinja2 functions before rendering the full document. custom_functions is loaded after rendering and does not have this placement requirement.
Keep pre-render metadata at the start
The loader parses only the initial YAML lines containing design_input_schema, jinja_functions, custom_functions, comments, and indented lines. It stops at the first other top-level line. This portion of document must be valid YAML on its own: keep Jinja2 expressions and top-level template directives out of it, and put design_input_schema and jinja_functions before any object collection. Use one YAML document. Do not insert --- between metadata and collections. A schema or Jinja2 function declared after a collection will not be available before rendering.
Design input schema¤
design_input_schema validates the supplied context before Jinja2 renders the design. Use inline JSON Schema or an nf:// / git:// Python file defining a Pydantic DesignInput model. The validated context is available as context in the template.
design_input_schema:
type: object
properties:
site: {type: string, minLength: 1}
required: [site]
additionalProperties: false
sites:
- name: "{{ context.site }}"
design_input_schema: nf://netbox/designs/site_input.py
sites:
- name: "{{ context.site }}"
status: active
The referenced nf://netbox/designs/site_input.py contains:
from pydantic import BaseModel, ConfigDict
class DesignInput(BaseModel):
model_config = ConfigDict(extra="forbid")
site: str
Jinja2 rendering¤
When design is YAML text or a file URL, Jinja2 renders it before YAML parsing, flattening, validation, or deployment. It receives context and a netbox pynetbox instance. File-based designs also support relative includes. jinja_functions loads named Python callables from nf:// or git:// files as both filters and template globals. When design is supplied as a Python dictionary, the task uses its values directly: Jinja2 expressions such as {{ context.site }} inside that dictionary are not evaluated.
Jinja2 runs before deployment
Jinja2 filters and global functions run while the design is rendered, before any object in that design is created. They may query objects already in Netbox, but must not depend on objects the same design plans to create: those objects do not exist yet. Use deployment-time custom_functions when a function needs objects created by an earlier collection.
devices:
{% for name in context.devices %}
- name: "{{ name }}"
site: "{{ context.site }}"
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
{% endfor %}
jinja_functions:
site_label: nf://netbox/designs/site_label.py
sites:
- name: "{{ context.site }}"
description: "{{ context.site | site_label('ACME') }}"
The referenced nf://netbox/designs/site_label.py contains:
def site_label(site: str, prefix: str) -> str:
return f"{prefix} site: {site}"
In context.site | site_label('ACME'), Jinja2 passes context.site as site and 'ACME' as prefix. The function does not receive context or netbox unless the template passes them explicitly.
jinja_functions:
site_label: nf://netbox/designs/site_label.py
sites:
- name: "{{ context.site }}"
description: "{{ site_label(context.site, 'ACME') }}"
The referenced nf://netbox/designs/site_label.py contains:
def site_label(site: str, prefix: str) -> str:
return f"{prefix} site: {site}"
In site_label(context.site, 'ACME'), the template passes site and prefix explicitly. Pass netbox explicitly too if the function needs to query it.
Jinja2 reads may query netbox, but object writes belong in collection handlers or deployment-time custom functions: rendering runs before design prerequisites are created.
Netbox objects and deployment order¤
Each design section contains a list of object records. The rows show the order in which Netbox objects are processed. Deployment stops at the first failure. For records with specified values, Match fields identify an existing object to update instead of creating another. Allocation tasks and custom functions use their own lookup rules.
| Order | Design top-level key | Netbox object | Match fields | Description and current limitations |
|---|---|---|---|---|
| 1 | tenants |
netbox.tenancy.tenants |
name |
- |
| 2 | regions |
netbox.dcim.regions |
name |
Parent can be a name. |
| 3 | manufacturers |
netbox.dcim.manufacturers |
name |
- |
| 4 | platforms |
netbox.dcim.platforms |
name |
Manufacturer can be a name. |
| 5 | device_types |
netbox.dcim.device_types |
manufacturer, model |
Default platform can be a name. |
| 6 | device_roles |
netbox.dcim.device_roles |
slug |
Uses the supplied slug or generates it from the name. |
| 7 | sites |
netbox.dcim.sites |
name |
Region and tenant can be names. |
| 8 | rack_roles |
netbox.dcim.rack_roles |
name |
- |
| 9 | racks |
netbox.dcim.racks |
site, name |
Site, role, and tenant can be names. |
| 10 | roles |
netbox.ipam.roles |
name |
- |
| 11 | rirs |
netbox.ipam.rirs |
name |
- |
| 12 | asn_ranges |
netbox.ipam.asn_ranges |
name |
Ranges are not site-scoped. |
| 13 | asns |
netbox.ipam.asns |
asn |
Explicit numbers are matched globally. create_asn allocates from a named range. Supports sites and role. |
| 14 | vlan_groups |
netbox.ipam.vlan_groups |
name |
Scope by rack, location, site, site group, region, cluster, or cluster group. |
| 15 | vlans |
netbox.ipam.vlans |
group, vid |
VLANs with a specified ID require both fields. Existing VLANs gain new tags and custom-field list items. create_vlan allocates a VID. Direct VLAN site is unsupported. |
| 16 | route_targets |
netbox.ipam.route_targets |
name |
VRF and L2VPN references can inline target dictionaries. |
| 17 | vrfs |
netbox.ipam.vrfs |
name, rd |
Import/export route targets accept named definitions or query filters. Existing target lists gain missing targets. |
| 18 | l2vpns |
netbox.vpn.l2vpns |
name |
Import/export route targets accept named definitions or query filters. Existing target lists gain missing targets. Nested terminations are flattened. Named VLAN terminations also populate vlans. |
| 19 | prefixes |
netbox.ipam.prefixes |
prefix, vrf |
Explicit prefixes can select location, site, site group, or region scope, and refer to a VLAN by {group, vid}. Existing prefixes gain new tags and custom-field list items. create_prefix supports site scope and VLAN association through vlan (VID) plus vlan_group (name). |
| 20 | devices |
netbox.dcim.devices |
name. tenant disambiguates duplicate names |
Nested components are flattened before writes. |
| 21 | interfaces |
netbox.dcim.interfaces |
device, name |
Independent interfaces are written before those with parent, lag, or bridge. |
| 22 | l2vpn_terminations |
netbox.vpn.l2vpn_terminations |
Attached interface or VLAN | Attachments require l2vpn and either device with interface, or VLAN group with vid. An attachment already used by another L2VPN is rejected. |
| 23 | power_ports |
netbox.dcim.power_ports |
device, name |
Nested connection is flattened. |
| 24 | console_ports |
netbox.dcim.console_ports |
device, name |
Nested connection is flattened. |
| 25 | power_outlets |
netbox.dcim.power_outlets |
device, name |
Nested connection is flattened. |
| 26 | console_server_ports |
netbox.dcim.console_server_ports |
device, name |
Nested connection is flattened. |
| 27 | connections |
netbox.dcim.cables |
Existing cable on both terminations | Creates interface, console, and power cables. Tenant can be a name or reference dictionary. Each end currently has one termination. An already-cabled port cannot be moved. |
| 28 | vrrp_groups |
netbox.ipam.fhrp_groups |
protocol, group_id |
Inline vip becomes an explicit IP address. Use top-level create_ip.vrrp_group for allocation. |
| 29 | ip_addresses |
netbox.ipam.ip_addresses |
address, vrf |
Addresses with specified values are bulk-written. create_ip allocates the next available address and can assign it to an interface or named VRRP group. |
| 30 | vrrp_group_assignments |
netbox.ipam.fhrp_group_assignments |
group, interface |
Associates an existing group and interface with priority. |
| 31 | primary_ip |
netbox.dcim.devices |
Device: site and name, plus tenant when supplied |
Sets an existing primary_ip4 or primary_ip6. It does not create the address. |
| 32 | bgp_communities |
netbox.plugins.bgp.community |
value, plus description when supplied |
A different description creates another community. Without description, the value must identify one object. |
| 33 | routing_policies |
netbox.plugins.bgp.routing_policy |
name |
Peering references can inline policy dictionaries or use policy names. |
| 34 | peer_groups |
netbox.plugins.bgp.peer_group |
name |
Peering references can provide a name or inline dictionary. |
| 35 | bgp_peerings |
netbox.plugins.bgp.session |
Supplied or generated name |
Uses create_bgp_peering to create missing sessions. Existing sessions are left unchanged. Design peerings default to create_reverse: false. Supports named or inline peer groups and import/export policies. |
| 36 | config_context |
netbox.extras.config_contexts |
name |
data is a dictionary. sites and tenants accept name lists for scope. |
| 37 | local_context_data |
netbox.dcim.devices |
Device: site and name, plus tenant when supplied |
Replaces each device's local context dictionary last. |
Ordinary records use Netbox API field names. Handlers bulk-create missing objects and bulk-update existing ones. Common name references, such as device.site, device.role, and vlan.group, are resolved by their handlers. Fields without a documented name shorthand must use a payload accepted by the target Netbox API. Unknown top-level collections are rejected.
Devices accept device_type: {manufacturer: ACME, model: AGG SWITCH} or device_type: AGG SWITCH. A string must match exactly one existing Netbox model, including during dry runs. Missing or ambiguous models fail. Use the dictionary form to disambiguate or preview types defined in the same design. New devices default to status: planned. Omitted status preserves existing device status. Explicit status applies to both creates and updates.
Set a device's config_template to an existing template's exact name or integer ID. Names must resolve to exactly one template, including during dry runs. Omit the field to preserve an existing association, or use null to clear it. Templates must exist before devices are deployed.
devices:
- name: branch-router-1
config_template: branch-router
When updating an existing record, design handlers add requested tags and custom-field list values without removing existing members. They also add VRF and L2VPN route targets, interface tagged VLANs, and config-context scope lists. Config-context sites and tenants accept name lists. Other scope lists use NetBox IDs. Scalar custom fields replace their old values. VLAN-group vid_ranges, local context data, and config-context data retain replacement behavior. Named object and multiobject custom-field values are resolved to NetBox IDs before writes.
Supported design verbs¤
Design verbs are YAML keys that request an operation, such as querying existing objects, allocating resources, or calling custom Python. Each verb is supported only in the locations listed below. Its value supplies the filters or arguments used during deployment.
| Verb | Supported objects | Description |
|---|---|---|
query |
VRF/L2VPN import and export route-target entries | Selects existing targets by Netbox filters after rendering. Attaches all matches. Fails if none match. |
custom_function |
All top-level collection records. Device local_context_data |
Calls registered Python with context, Netbox, dry-run mode, and supplied arguments. Functions handle their own writes. Local-context functions return a dictionary. |
create_asn |
asns. BGP peering local_as and remote_as, including nested peerings |
Allocates or reuses an ASN from a named range. See the ASN examples below. |
create_vlan |
vlans |
Allocates a VID from a VLAN group. |
create_prefix |
prefixes |
Allocates a child prefix. |
move_ip |
ip_addresses, including nested interface IPs |
Moves all source-interface addresses to the destination interface. |
create_ip |
ip_addresses, including nested interface IPs |
Allocates an IP, optionally assigning it to an interface or FHRP group. |
Each query entry contains only query with a nonempty filter dictionary.
Route-target lists accept queries and named definitions, not custom_function.
Put custom allocation calls in route_targets, then reference their targets.
Function return values are not automatically used as relationship references.
See Custom functions, L2VPNs and Terminations, and the allocation examples below.
Built-In Task Wrappers¤
Task wrappers call standalone NorFab NetBox service tasks during the relevant deployment stage. The wrapper contains that task's arguments, without renaming them. The allocation wrappers are useful when a design knows the pool and the object's purpose, but not its final ASN, VLAN ID, subnet, or IP address. Several branches can use the same design and receive different available values from their own pools.
| Task wrapper | How it works |
|---|---|
| create_asn | Allocates or reuses an ASN in asns or BGP peering local_as and remote_as. |
| create_vlan | Selects an available VID from an existing VLAN group's allowed ranges. |
| create_prefix | Allocates a child prefix of the requested length inside an existing parent prefix. Supports VLAN association by VID and group name. |
| move_ip | Moves all addresses from a source interface and optionally sets destination primaries. |
| create_ip | Allocates an address from an existing prefix and can assign it to a device interface or named VRRP group. |
| create_bgp_peering | Creates a BGP session under bgp_peerings. Can derive the local address from an interface and create a reverse session. |
Both forms belong under ip_addresses. Specify each address when its value is known:
To put the same VIP or anycast address on two interfaces, add the address under each interface with role: vip or role: anycast. NetBox creates a separate IP record for each interface. Running the design again keeps both records on their original interfaces.
ip_addresses:
- address: 192.0.2.10/24
description: ACME gateway 1
- address: 192.0.2.11/24
description: ACME gateway 2
- address: 192.0.2.12/24
description: ACME gateway 3
- address: 192.0.2.13/24
description: ACME gateway 4
- address: 192.0.2.14/24
description: ACME gateway 5
When the values are not known, ask the standalone create_ip task to select five available addresses from an existing prefix. Each request has a distinct description so it can find its address on a repeat run:
ip_addresses:
- create_ip:
prefix: 192.0.2.0/24
description: ACME gateway 1
- create_ip:
prefix: 192.0.2.0/24
description: ACME gateway 2
- create_ip:
prefix: 192.0.2.0/24
description: ACME gateway 3
- create_ip:
prefix: 192.0.2.0/24
description: ACME gateway 4
- create_ip:
prefix: 192.0.2.0/24
description: ACME gateway 5
For each object type, specified values are created or updated before next-available requests run. Allocation tasks look for an existing match on repeat deployment. Give each request a stable name or description and enough scope to identify it, otherwise a repeat run can allocate another value.
Entries under bgp_peerings call create_bgp_peering after the design's devices, interfaces, IP addresses, and ASNs have been deployed. Use a stable supplied or generated session name so repeat deployments find the existing session. Existing sessions are not updated by the design:
Design peerings create only the specified direction by default. Set create_reverse: true on a peering to create the reverse session as well. The standalone create_bgp_peering task retains its own default.
Peering records can specify an existing tenant name with tenant. Reverse sessions inherit it. Existing sessions are left unchanged.
For a dual-stack interface, use two peering entries with scoped local_ip_query filters containing family: 4 and family: 6. Each query must match one address. With local_interface supplied, an omitted remote address is derived from the first interface IP in the selected family, for IPv4 /30 or /31 and IPv6 /127 subnets.
bgp_peerings:
- name: ACME BRANCH PEER
device: acme-branch-rtr-1
local_interface: Ethernet1
local_as: 4200650001
remote_as: 4200650000
Allocation examples¤
The design creates an RIR, ASN range, VLAN group, and parent prefix. It then allocates an ASN, VLAN ID, child prefix, and IP address from those objects, in deployment order.
rirs:
- name: ACME PRIVATE
is_private: true
asn_ranges:
- name: ACME BRANCH ASNS
rir: ACME PRIVATE
start: 4200650001
end: 4200650099
asns:
- create_asn:
asn_range: ACME BRANCH ASNS
description: ACME branch ASN
vlan_groups:
- name: ACME BRANCH VLANS
vid_ranges: [[100, 199]]
vlans:
- create_vlan:
vlan_group: ACME BRANCH VLANS
name: ACME USERS
prefixes:
- prefix: 198.19.224.0/20
description: ACME allocation pool
- create_prefix:
parent: 198.19.224.0/20
prefixlen: 24
description: ACME user subnet
ip_addresses:
- create_ip:
prefix: {description: ACME user subnet}
description: ACME user gateway
Prerequisites: A text custom field named deployment_owner enabled for ASN objects (ipam.asn) in Netbox.
The design creates an ASN range under the RIR, then create_asn selects an available number from that range and sets its custom fields. Its description lets the task find the same ASN on a repeat run.
rirs:
- name: ACME PRIVATE
is_private: true
asn_ranges:
- name: ACME BRANCH ASNS
rir: ACME PRIVATE
start: 4200650001
end: 4200650099
asns:
- create_asn:
asn_range: ACME BRANCH ASNS
description: ACME branch ASN
custom_fields:
deployment_owner: ACME
You can also allocate an ASN directly in a BGP peering. This example reuses the branch ASN above and allocates a peer ASN from the same range:
bgp_peerings:
- name: branch-upstream
device: branch-router-1
local_address: 192.0.2.1
remote_address: 192.0.2.2
local_as:
create_asn:
asn_range: ACME BRANCH ASNS
description: ACME branch ASN
remote_as:
create_asn:
asn_range: ACME BRANCH ASNS
description: ACME peer ASN
The device and IP addresses must exist or be defined in the design. Wrappers
also work on peerings nested under devices or interfaces. Do not combine
a wrapper with the corresponding local_as_query or remote_as_query.
Use a stable description or role/site combination for repeatable allocation.
Dry runs do not reserve numbers, so allocations from the same range may
report the same available ASN.
The design creates a VLAN group allowing IDs 100-199, then create_vlan selects the next available ID for ACME USERS. Its name and group identify it on a repeat run.
vlan_groups:
- name: ACME BRANCH VLANS
vid_ranges: [[100, 199]]
vlans:
- create_vlan:
vlan_group: ACME BRANCH VLANS
name: ACME USERS
description: User access VLAN
The design creates the /20 parent prefix, then create_prefix allocates an available /24 within it. Its description helps the task find that child on a repeat run.
prefixes:
- prefix: 198.19.224.0/20
description: ACME allocation pool
- create_prefix:
parent: 198.19.224.0/20
prefixlen: 24
description: ACME user subnet
The design creates a /24 prefix, then create_ip selects an available address from the prefix identified by its description.
prefixes:
- prefix: 198.19.225.0/24
description: ACME user subnet
ip_addresses:
- create_ip:
prefix: {description: ACME user subnet}
description: ACME user gateway
Create ASN Next Available Allocation¤
The standalone create_asn task takes an asn_range name and selects an available ASN. Supply a stable description so repeat deployments can find the ASN already allocated within that range. The allocated ASN can use sites: [SITE-1, SITE-2] and role: ROLE NAME. sites assigns the ASN to sites, it does not select the range by site. ASN ranges are not site-scoped.
Create VLAN Next Available Allocation¤
The standalone create_vlan task takes vlan_group and name, then selects an available VID from the group's ranges. It reuses a VLAN with the same name in that group on repeat deployment.
VLANs require a group
Every VLAN in a design must belong to a VLAN group. Direct VLAN-to-site association is unsupported. Scope the group instead.
A VLAN group accepts one of rack, location, site, site_group, region, cluster, or cluster_group as a scope name. The referenced object must exist. For a rack or location, site may also be supplied to distinguish equal names at different sites.
vlan_groups:
- name: BRANCH FLOOR VLANS
location: Floor 2
site: BRANCH-1
vid_ranges: [[100, 199]]
Create Prefix Next Available Allocation¤
The standalone create_prefix task takes a parent prefix or parent filter dictionary and a prefixlen, then allocates an available child prefix. A stable description, optionally with site, role, or vrf, helps it find the same child on repeat deployment. create_prefix.parent can select the parent through a dictionary of filters. The task accepts site for the allocated child's scope and the pair vlan (VID) and vlan_group (group name) to associate an existing VLAN. Site cannot replace the group for VLAN identification. It has no location, site_group, or region argument for the child.
Both vlan and vlan_group must be supplied together. The VLAN must exist when allocation runs. VLANs defined in the same design are created before prefixes. A missing group or VLAN stops allocation. On repeat deployment, the association is updated if needed. Omitting both arguments preserves it.
vlan_groups:
- name: BRANCH VLANS
vid_ranges:
- [100, 199]
vlans:
- name: USERS
vid: 100
group: BRANCH VLANS
prefixes:
- prefix: 198.19.224.0/20
- create_prefix:
parent: 198.19.224.0/20
prefixlen: 24
description: Branch user subnet
vlan: 100
vlan_group: BRANCH VLANS
Create IP Next Available Allocation¤
IPv4 /32 and IPv6 /128 allocations work with the default create_peer_ip setting. The task automatically skips peer creation and peer-subnet reuse for these host prefixes. There is no need to set create_peer_ip: false explicitly.
The standalone create_ip task takes a prefix string or filter dictionary and allocates an available IP. It can assign that IP to a device and interface. If nested under an interface's ip_addresses, the design supplies those two arguments during flattening.
To allocate a VIP, supply vrrp_group with a unique existing VRRPv2 or VRRPv3 group name. Groups defined in the same design are deployed before IP allocations. The task reuses an address assigned to that group within the selected prefix, optionally matching description, and defaults the IP role to vrrp. Missing, ambiguous, or non-VRRP group names fail before allocation.
Put this wrapper in the top-level ip_addresses collection. Do not nest it under an interface: vrrp_group cannot accompany device, interface, or is_primary: true. Group allocation disables peer IP allocation automatically. See the allocated VIP example below.
A dry run does not create the VLANs, groups, or prefixes proposed by the design. Allocation lookups therefore require those prerequisites to exist already. The create_ip dry-run limitation still applies: mask_len is ignored and no child subnet is created.
Move Interface IP Addresses¤
ip_addresses accepts a move_ip wrapper containing from_device, from_interface, device, interface, and optional is_primary (default false). Nested wrappers inherit the destination device and interface. Moves run before explicit IP writes and allocations, after interfaces exist. Deployment controls instance, branch, and dry_run are inherited. Moved addresses appear in updated results.
Primary-IP behavior is documented in the Move IP task. Dry runs do not simulate chained moves.
ip_addresses:
- move_ip:
from_device: router-1
from_interface: Ethernet1
device: router-2
interface: Ethernet2
is_primary: true
Custom fields¤
Put a custom_fields dictionary on an object record. Its keys are Netbox custom-field names, not labels. Definitions must already exist and be enabled for that object type. The design passes values through to Netbox, which validates their types and choices. Updates PATCH supplied values without clearing omitted custom fields. Use null to clear a nullable scalar field.
When updating an ASN, a design adds new sites and tags without removing existing
ones. When updating a VLAN or prefix, it adds new tags. For these objects, if both the
existing and supplied values of a custom field are lists, the design adds only
new items. Supplying [] leaves an existing list unchanged. For custom fields,
null clears the value, and a new scalar value replaces the old one.
Object and multiobject custom fields accept numeric Netbox IDs or string references. The field's related object type determines how strings are matched:
| Related object type | String reference |
|---|---|
IP address (ipam.ipaddress) |
Address including prefix length, such as 192.0.2.10/32 |
Prefix (ipam.prefix) |
Prefix, such as 192.0.2.0/24 |
BGP community (netbox_bgp.community) |
Community value, such as 65100:100 |
Circuit (circuits.circuit) |
Circuit ID (cid) |
Device type (dcim.devicetype) |
Model |
| Other object types | Name |
Each reference must match exactly one existing object when its record is processed. Ambiguous matches cause an error. Use a numeric ID to distinguish objects with the same address, prefix, value, CID, model, or name. Referenced objects must already exist or be created earlier in the deployment order. Devices are processed before IP addresses, so device custom fields cannot reference IPs created later in the same design. Dry runs also require referenced objects to exist.
For example, with device custom fields management_ip (object) and
peer_ips (multiobject) both related to ipam.ipaddress:
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: BRANCH ROUTER
custom_fields:
management_ip: "192.0.2.10/32"
peer_ips: ["192.0.2.11/32", "192.0.2.12/32"]
The site, role, device type, custom-field definitions, and reference IPs must exist before this example is deployed. Multiobject updates add references without removing existing members.
FHRP group assignments and ConfigContext objects do not support custom fields. Device-local context is ordinary JSON data, not a custom-field definition. The design does not create custom-field definitions.
Prerequisites: the referenced device prerequisites and custom-field definitions enabled for devices and interfaces respectively.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
custom_fields:
deployment_owner: ACME
interfaces:
Loopback0:
type: virtual
custom_fields:
service_name: routing
Prerequisites: deployment_owner enabled for prefixes and IP addresses. Arguments belong inside the allocation wrapper.
prefixes:
- prefix: 192.0.2.0/24
custom_fields: {deployment_owner: ACME}
- create_prefix:
parent: 192.0.2.0/24
prefixlen: 28
description: ACME services
custom_fields: {deployment_owner: ACME}
ip_addresses:
- create_ip:
prefix: {description: ACME services}
description: ACME service IP
custom_fields: {deployment_owner: ACME}
Prerequisites: the prefix and a nullable deployment_owner custom field.
prefixes:
- prefix: 192.0.2.0/24
custom_fields: {deployment_owner: null}
Custom functions¤
Custom functions let a design run Python during deployment for use cases such as custom next-available allocation, Netbox lookups, or object creation that needs additional logic.
The following contract applies to custom_function on an object record. A function inside local_context_data has a different return contract: its dictionary becomes the device's context data.
When branch is supplied to design_deploy, the provided netbox client is already configured for that branch. Queries and writes through this client use the deployment branch. Custom functions do not need to initialize branching themselves.
| Argument | Type | Supplied by | Meaning |
|---|---|---|---|
context |
dict |
Deployment | Design input context after optional schema validation. |
netbox |
pynetbox.core.api.Api |
Deployment | Pynetbox client bound to the deployment's Netbox instance and branch. Use it for queries and writes. |
dry_run |
bool |
Deployment | Whether this is a preview. The function must avoid writes when True. Deployment does not intercept its API calls. |
| Other record fields | As defined in YAML | Design record | Every field except custom_function is passed unchanged as a keyword argument. For example, profile: foo becomes profile="foo". |
custom_function selects the registered function and is not passed as an argument. Do not supply context, netbox, or dry_run in the record, because deployment supplies them. No device object is passed automatically.
Every ordinary top-level object collection can mix object definitions and custom function records. Custom function records pass through flattening and lookup preparation without requiring object identity fields such as a route target name or a VLAN group. The collection handler processes object definitions first, then the custom functions run in record order. The function receives its record's remaining fields and handles its own Netbox writes. local_context_data uses the separate device-local contract.
For these object records, the return value is a report to the caller. Deployment appends it to the collection's custom list in the same order as the custom function records. It does not use the value to create or update an object, supply fields to another handler, or resolve references in later collections. The function must make any Netbox writes itself and honor dry_run. A string, number, dictionary, list, or None can be returned. Choose a JSON-serializable value that is useful to callers.
| Function outcome | Deployment behavior |
|---|---|
| Returns a value | Appends {"function": NAME, "result": VALUE} under the collection's custom results list, such as route_targets.custom. |
| Returns nothing | Records null as the function result. |
| Raises an exception | Reports the failure and stops deployment. Earlier writes are not rolled back. |
This example allocates a route target. Register the Python file under custom_functions, then use custom_function: allocate_bgp_route_target in a route_targets record. The function runs during the route-target deployment stage, after route-target records without custom_function have been processed. The record's other fields become keyword arguments, and deployment also supplies context, netbox, and dry_run.
custom_functions:
allocate_bgp_route_target: nf://netbox/designs/allocate_bgp_route_target.py
tenants:
- name: ACME
route_targets:
- custom_function: allocate_bgp_route_target
tenant: ACME
description: ACME branch route target
The referenced nf://netbox/designs/allocate_bgp_route_target.py contains:
def allocate_bgp_route_target(context, netbox, dry_run, tenant, description):
# Replace this fixed value with a real allocation rule when needed.
name = "65000:100"
existing = netbox.ipam.route_targets.get(name=name)
if not dry_run:
if existing:
existing.update({"tenant": {"name": tenant}, "description": description})
else:
netbox.ipam.route_targets.create(
{"name": name, "tenant": {"name": tenant}, "description": description}
)
return {"name": name}
The tenant and description fields become function arguments. Deployment supplies context, netbox, and dry_run, so do not put them in the record. This dummy allocator chooses 65000:100 and creates or updates that route target unless dry_run is true. Its return {"name": name} produces this excerpt of the deployment result:
{"route_targets": {"custom": [{"function": "allocate_bgp_route_target", "result": {"name": "65000:100"}}]}}
The dictionary describes what the function did. The returned name is not used for the Netbox write or for later VRF or L2VPN references. Returning name as a string would instead put "65000:100" in result. Custom functions must handle their own lookups and writes, including dry-run behavior. Only load trusted Python files.
This function calculates a VLAN name from the additional profile argument, then creates or updates the VLAN. The VLAN group must already exist when the vlans stage runs.
custom_functions:
create_profile_vlan: nf://netbox/designs/create_profile_vlan.py
vlan_groups:
- name: BRANCH VLANS
vlans:
- custom_function: create_profile_vlan
group: BRANCH VLANS
vid: 120
profile: foo
The referenced nf://netbox/designs/create_profile_vlan.py contains:
def create_profile_vlan(context, netbox, dry_run, group, vid, profile):
name = f"{profile.upper()}-{vid}"
vlan_group = netbox.ipam.vlan_groups.get(name=group)
if vlan_group is None:
raise ValueError(f"VLAN group not found: {group}")
existing = netbox.ipam.vlans.get(group_id=vlan_group.id, vid=vid)
if not dry_run:
if existing:
existing.update({"name": name})
else:
netbox.ipam.vlans.create(
{"group": vlan_group.id, "vid": vid, "name": name}
)
return {"group": group, "vid": vid, "name": name}
group, vid, and profile are passed unchanged from the design record. The calculated name is FOO-120. Because this is a custom-function record, the function performs its own lookup and write. Its return value appears under vlans.custom.
Jinja2 expands context.devices into one record per device. It does not allocate anything. During the later bgp_communities stage, each record calls the custom function with its device name. The function reuses a community identified by its description or takes the next free value in the example range.
custom_functions:
allocate_device_community: nf://netbox/designs/allocate_device_community.py
bgp_communities:
{% for device in context.devices %}
- custom_function: allocate_device_community
device: "{{ device }}"
{% endfor %}
Supply a context such as {"devices": ["branch-rtr-1", "branch-rtr-2"], "asn": 65100}. The referenced nf://netbox/designs/allocate_device_community.py contains:
def allocate_device_community(context, netbox, dry_run, device):
asn = context["asn"]
description = f"Community for {device}"
communities = list(netbox.plugins.bgp.community.all())
existing = next(
(item for item in communities
if item.description == description
and item.value in {f"{asn}:{number}" for number in range(100, 200)}),
None,
)
used = {item.value for item in communities}
value = existing.value if existing else next(
(f"{asn}:{number}" for number in range(100, 200)
if f"{asn}:{number}" not in used),
None,
)
if value is None:
raise ValueError(f"No free BGP community in {asn}:100-199")
if not dry_run and existing is None:
netbox.plugins.bgp.community.create(
{"value": value, "description": description}
)
return {"device": device, "value": value}
The design context passes asn=65100 to the function, so allocated values start at 65100:100. Each created community is identified on repeat deployment by Community for <device>. The returned dictionary appears under bgp_communities.custom. A dry run makes no reservations, so multiple records may propose the same free value.
This simplified allocator is intended for sequential deployments. It does not reserve values against concurrent allocations. The built-in handler for explicit community records matches value and, when supplied, description. A different description creates a separate community. If description is omitted, more than one matching value is an error.
Nested Device Records¤
Nesting keeps a device and its related objects together, making the design more natural to read and shorter to write. Interfaces, IP addresses, and peerings inherit their device or interface names instead of repeating them in separate top-level records.
Device records may contain interfaces, bgp_peerings, power_ports, console_ports, power_outlets, console_server_ports, and local_context_data. An interface may contain ip_addresses, bgp_peerings, connection, vrf, untagged_vlan, tagged_vlans, and vrrp. The task flattens these before validation or writes. It adds device and interface names to extracted records and merges them with records already at the top level. Inline VRFs, VLANs, VRRP groups, route targets, routing policies, and BGP peer groups are also collected into their earlier collections. Interface VRF identity dictionaries and VLAN group/VID dictionaries remain references. Nested route-target, routing-policy, and peer-group dictionaries are collected as definitions, even with only a name.
Prerequisites: Named sites, roles, device types, VLAN group, referenced VLANs/VRF, remote endpoints, peering IPs/ASNs, and the BGP plugin.
This shows every device-nested collection and every interface-nested field. Referenced sites, roles, device types, VLANs, and remote endpoints must exist or be defined earlier in the design. Use the focused tabs for complete examples of the less obvious fields.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Port-Channel1: {type: lag}
Ethernet1:
type: 10gbase-x-sfpp
lag: {name: Port-Channel1}
connection: {device: branch-router-1, interface: Ethernet1}
tagged_vlans: [{group: BRANCH VLANS, vid: 100}]
Vlan100:
type: virtual
vrf: BRANCH VRF
untagged_vlan: {group: BRANCH VLANS, vid: 100}
ip_addresses: [{address: 192.0.2.21/24}]
vrrp: {group_id: 10, vip: 192.0.2.1/24, priority: 200}
bgp_peerings:
- name: branch-agg-1-to-branch-router-1
remote_address: 192.0.2.11
local_as: 65100
remote_as: 65200
create_reverse: false
bgp_peerings:
- name: branch-agg-1-loopback-peer
local_address: 192.0.2.101
remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
power_ports:
PSU1: {type: iec-60320-c14}
console_ports:
Console: {type: rj-45}
power_outlets:
Outlet-1: {type: iec-60320-c13}
console_server_ports:
Line-1: {type: rj-45}
local_context_data: {profile: aggregation}
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: lag
name: Port-Channel1
device: branch-agg-1
- type: 10gbase-x-sfpp
lag:
name: Port-Channel1
tagged_vlans:
- group: BRANCH VLANS
vid: 100
name: BRANCH USERS
name: Ethernet1
device: branch-agg-1
- type: virtual
vrf: BRANCH VRF
untagged_vlan:
group: BRANCH VLANS
vid: 100
name: Vlan100
device: branch-agg-1
power_ports:
- type: iec-60320-c14
name: PSU1
device: branch-agg-1
console_ports:
- type: rj-45
name: Console
device: branch-agg-1
power_outlets:
- type: iec-60320-c13
name: Outlet-1
device: branch-agg-1
console_server_ports:
- type: rj-45
name: Line-1
device: branch-agg-1
connections:
- a_terminations:
- device: branch-agg-1
interface: Ethernet1
b_terminations:
- device: branch-router-1
interface: Ethernet1
vrrp_groups:
- group_id: 10
protocol: vrrp2
name: VRRP 10
ip_addresses:
- address: 192.0.2.21/24
device: branch-agg-1
interface: Vlan100
- address: 192.0.2.1/24
fhrp_group:
protocol: vrrp2
group_id: 10
vrrp_group_assignments:
- group_id: 10
protocol: vrrp2
device: branch-agg-1
interface: Vlan100
priority: 200
bgp_peerings:
- local_address: 192.0.2.101
remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
name: branch-agg-1-loopback-peer
device: branch-agg-1
- remote_address: 192.0.2.11
local_as: 65100
remote_as: 65200
create_reverse: false
name: branch-agg-1-to-branch-router-1
device: branch-agg-1
local_interface: Vlan100
local_context_data:
- device: branch-agg-1
site: BRANCH-1
local_context_data:
profile: aggregation
The peering under Vlan100 receives local_interface: Vlan100. The peering directly under the device does not. Interface creation uses two passes: independent interfaces first, then interfaces referencing parent, lag, or bridge.
Prerequisites: Named site, role, manufacturer, and device type.
interfaces is a dictionary keyed by interface name. The device name is added to every extracted interface. Independent interfaces are created before subinterfaces and LAG members.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Port-Channel1: {type: lag}
Ethernet1: {type: 10gbase-x-sfpp, lag: {name: Port-Channel1}}
Ethernet2.100: {type: virtual, parent: {name: Ethernet2}}
Ethernet2: {type: 10gbase-x-sfpp}
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: lag
name: Port-Channel1
device: branch-agg-1
- type: 10gbase-x-sfpp
lag:
name: Port-Channel1
name: Ethernet1
device: branch-agg-1
- type: virtual
parent:
name: Ethernet2
name: Ethernet2.100
device: branch-agg-1
- type: 10gbase-x-sfpp
name: Ethernet2
device: branch-agg-1
Prerequisites: Named site, role, device type, both IP addresses, both ASNs, and the BGP plugin.
Device-level bgp_peerings is a list. Flattening adds device. name is optional and is generated by create_bgp_peering when omitted. Supply addresses explicitly when no local interface is given.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
bgp_peerings:
- name: branch-agg-1-to-router-1
local_address: 192.0.2.101
remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
bgp_peerings:
- local_address: 192.0.2.101
remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
name: branch-agg-1-to-router-1
device: branch-agg-1
Prerequisites: Named site, role, manufacturer, and device type.
A compact interface vrrp record creates an FHRP group, its VIP IP address, and an assignment to the interface. The protocol defaults to vrrp2. The same group can be assigned to a second device with a different priority. This example shows both device-type reference forms. AGG SWITCH must uniquely identify an existing model.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Vlan100:
type: virtual
vrrp:
group_id: 10
name: BRANCH GATEWAY
vip: 192.0.2.1/24
priority: 200
- name: branch-agg-2
site: BRANCH-1
role: AGGREGATION
device_type: AGG SWITCH
interfaces:
Vlan100:
type: virtual
vrrp:
group_id: 10
name: BRANCH GATEWAY
vip: 192.0.2.1/24
priority: 100
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
- name: branch-agg-2
site: BRANCH-1
role: AGGREGATION
device_type: AGG SWITCH
interfaces:
- type: virtual
name: Vlan100
device: branch-agg-1
- type: virtual
name: Vlan100
device: branch-agg-2
vrrp_groups:
- group_id: 10
name: BRANCH GATEWAY
protocol: vrrp2
ip_addresses:
- address: 192.0.2.1/24
fhrp_group:
protocol: vrrp2
group_id: 10
vrrp_group_assignments:
- group_id: 10
protocol: vrrp2
device: branch-agg-1
interface: Vlan100
priority: 200
- group_id: 10
protocol: vrrp2
device: branch-agg-2
interface: Vlan100
priority: 100
Top-level vrrp_groups can instead provide a vip and an assignments list.
Prerequisites: Named site, role, device type, and allocation prefix.
An interface can contain explicit addresses and create_ip next-available allocations. Flattening adds the device and interface names to each address. primary_ip runs after address creation and selects an existing address.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Loopback0:
type: virtual
ip_addresses:
- address: 192.0.2.21/32
- create_ip:
prefix: 198.51.100.0/24
description: branch-agg-1 service IP
primary_ip:
- device: branch-agg-1
site: BRANCH-1
address: 192.0.2.21/32
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: virtual
name: Loopback0
device: branch-agg-1
ip_addresses:
- address: 192.0.2.21/32
device: branch-agg-1
interface: Loopback0
- create_ip:
prefix: 198.51.100.0/24
description: branch-agg-1 service IP
device: branch-agg-1
interface: Loopback0
primary_ip:
- device: branch-agg-1
site: BRANCH-1
address: 192.0.2.21/32
The create_ip prefix must already exist or be created earlier in the design. primary_ip.field defaults to primary_ip4. Set it to primary_ip6 for IPv6.
Prerequisites: Named site, role, device type, and remote device/interface.
connection under an interface specifies the remote device and interface. The local endpoint comes from its parent device and interface. Both interfaces must exist by the connections stage.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Ethernet1:
type: 10gbase-x-sfpp
connection: {device: branch-router-1, interface: Ethernet1}
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: 10gbase-x-sfpp
name: Ethernet1
device: branch-agg-1
connections:
- a_terminations:
- device: branch-agg-1
interface: Ethernet1
b_terminations:
- device: branch-router-1
interface: Ethernet1
Prerequisites: Named site, role, manufacturer, and device type.
A string names an existing VRF. A dictionary with fields beyond its identity is also collected as a VRF definition and deployed before interfaces.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Vlan100:
type: virtual
vrf: {name: BRANCH VRF, rd: "65100:100", description: Branch routing}
Equivalent using top-level keys:
vrfs:
- name: BRANCH VRF
rd: 65100:100
description: Branch routing
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: virtual
vrf:
name: BRANCH VRF
rd: 65100:100
name: Vlan100
device: branch-agg-1
Prerequisites: Named site, role, device type, and VLAN group.
untagged_vlan identifies one VLAN by group and VID. If its dictionary also contains name, it becomes a VLAN definition deployed before interfaces.
devices:
- name: branch-access-1
site: BRANCH-1
role: ACCESS
device_type: {manufacturer: ACME, model: ACCESS SWITCH}
interfaces:
Ethernet1:
type: 1000base-t
untagged_vlan: {group: BRANCH VLANS, vid: 100, name: USERS}
Equivalent using top-level keys:
vlans:
- group: BRANCH VLANS
vid: 100
name: USERS
devices:
- name: branch-access-1
site: BRANCH-1
role: ACCESS
device_type:
manufacturer: ACME
model: ACCESS SWITCH
interfaces:
- type: 1000base-t
untagged_vlan:
group: BRANCH VLANS
vid: 100
name: Ethernet1
device: branch-access-1
Prerequisites: Named site, role, device type, VLAN group, and VLAN 200.
tagged_vlans is a list. Entries with name and vid define VLANs. Entries with just group and vid refer to VLANs already defined elsewhere.
devices:
- name: branch-access-1
site: BRANCH-1
role: ACCESS
device_type: {manufacturer: ACME, model: ACCESS SWITCH}
interfaces:
Ethernet48:
type: 10gbase-x-sfpp
mode: tagged
tagged_vlans:
- {group: BRANCH VLANS, vid: 100, name: USERS}
- {group: BRANCH VLANS, vid: 200}
Equivalent using top-level keys:
vlans:
- group: BRANCH VLANS
vid: 100
name: USERS
devices:
- name: branch-access-1
site: BRANCH-1
role: ACCESS
device_type:
manufacturer: ACME
model: ACCESS SWITCH
interfaces:
- type: 10gbase-x-sfpp
mode: tagged
tagged_vlans:
- group: BRANCH VLANS
vid: 100
- group: BRANCH VLANS
vid: 200
name: Ethernet48
device: branch-access-1
Prerequisites: Named site, role, device type, remote IP, both ASNs, and the BGP plugin.
Nesting a peering under an interface supplies device and local_interface to create_bgp_peering. The task resolves the local address from IPAM on that interface. Define the interface IP before the peering stage.
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type: {manufacturer: ACME, model: AGG SWITCH}
interfaces:
Loopback0:
type: virtual
ip_addresses: [{address: 192.0.2.101/32}]
bgp_peerings:
- name: branch-agg-1-to-branch-router-1
remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
Equivalent using top-level keys:
devices:
- name: branch-agg-1
site: BRANCH-1
role: AGGREGATION
device_type:
manufacturer: ACME
model: AGG SWITCH
interfaces:
- type: virtual
name: Loopback0
device: branch-agg-1
ip_addresses:
- address: 192.0.2.101/32
device: branch-agg-1
interface: Loopback0
bgp_peerings:
- remote_address: 192.0.2.102
local_as: 65100
remote_as: 65200
create_reverse: false
name: branch-agg-1-to-branch-router-1
device: branch-agg-1
local_interface: Loopback0
Prerequisites: Named site, role, device type, and remote PDU outlet.
power_ports is keyed by port name. A nested connection specifies a remote power outlet. The PDU and outlet must exist by the connections stage.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
power_ports:
PSU1:
type: iec-60320-c14
connection: {device: branch-pdu-1, power_outlet: Outlet-1}
Equivalent using top-level keys:
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
power_ports:
- type: iec-60320-c14
name: PSU1
device: branch-router-1
connections:
- a_terminations:
- device: branch-router-1
power_port: PSU1
b_terminations:
- device: branch-pdu-1
power_outlet: Outlet-1
Prerequisites: Named site, role, device type, and remote console server port.
console_ports is keyed by port name. Its connection can name a console server port.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
console_ports:
Console:
type: rj-45
connection: {device: branch-console-1, console_server_port: Line-1}
Equivalent using top-level keys:
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
console_ports:
- type: rj-45
name: Console
device: branch-router-1
connections:
- a_terminations:
- device: branch-router-1
console_port: Console
b_terminations:
- device: branch-console-1
console_server_port: Line-1
Prerequisites: Named site, role, device type, and remote device power port.
power_outlets is keyed by outlet name. A nested connection can point to a device power port.
devices:
- name: branch-pdu-1
site: BRANCH-1
role: PDU
device_type: {manufacturer: ACME, model: PDU}
power_outlets:
Outlet-1:
type: iec-60320-c13
connection: {device: branch-router-1, power_port: PSU1}
Equivalent using top-level keys:
devices:
- name: branch-pdu-1
site: BRANCH-1
role: PDU
device_type:
manufacturer: ACME
model: PDU
power_outlets:
- type: iec-60320-c13
name: Outlet-1
device: branch-pdu-1
connections:
- a_terminations:
- device: branch-pdu-1
power_outlet: Outlet-1
b_terminations:
- device: branch-router-1
power_port: PSU1
Prerequisites: Named site, role, device type, and remote device console port.
console_server_ports is keyed by port name. A nested connection can point to a device console port.
devices:
- name: branch-console-1
site: BRANCH-1
role: CONSOLE SERVER
device_type: {manufacturer: ACME, model: TERMINAL SERVER}
console_server_ports:
Line-1:
type: rj-45
connection: {device: branch-router-1, console_port: Console}
Equivalent using top-level keys:
devices:
- name: branch-console-1
site: BRANCH-1
role: CONSOLE SERVER
device_type:
manufacturer: ACME
model: TERMINAL SERVER
console_server_ports:
- type: rj-45
name: Line-1
device: branch-console-1
connections:
- a_terminations:
- device: branch-console-1
console_server_port: Line-1
b_terminations:
- device: branch-router-1
console_port: Console
Define a cable at one end only. Existing cables are patched on repeat deployment. A port already cabled to a different endpoint causes an error. The design does not disconnect it.
Prerequisites: Named site, role, manufacturer, and device type.
local_context_data is a device-local dictionary. It is processed last, after the other nested records have been deployed. It replaces the device's previous local context.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
local_context_data:
routing: {profile: core}
Equivalent using top-level keys:
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
local_context_data:
- device: branch-router-1
site: BRANCH-1
local_context_data:
routing:
profile: core
The configuration context section also shows how to calculate this dictionary with a custom function.
Interface dependency limits
All parent, LAG, and bridge interfaces must be defined in the design or already exist in Netbox. A newly created dependent interface cannot itself be the parent of another new interface in the same deployment, because only two passes are performed.
Device deployment searches by name only. A single match is updated, with site and tenant treated as update attributes. When multiple devices share a name, the supplied tenant must select exactly one. Missing tenant, no tenant match, or multiple tenant matches cause an ambiguity error. Site does not disambiguate devices. Interfaces and IP assignments refer to a device by name, so use unambiguous device names for designs that include those nested objects. The separate primary-IP and local-context stages retain their site/name and optional tenant matching.
Nested VRRP Group Records¤
vrrp_groups supports an assignments list and an optional vip. Each assignment supplies device, interface, and priority. The design extracts these into vrrp_group_assignments, adding protocol and group_id from the parent group. The optional vip becomes an IP address assigned to the group, not to an individual interface.
For a top-level group, specify both protocol and group_id. Unlike the compact interface vrrp form, this form does not default the protocol. Groups are created before their VIP addresses and interface assignments.
Prerequisites: both devices and their Vlan100 interfaces. The VIP is created as an IP address assigned to the group. Assignments attach that group to each interface with a priority.
vrrp_groups:
- protocol: vrrp2
group_id: 10
vip: 192.0.2.1/24
assignments:
- {device: branch-agg-1, interface: Vlan100, priority: 200}
- {device: branch-agg-2, interface: Vlan100, priority: 100}
Equivalent using top-level keys:
vrrp_groups:
- protocol: vrrp2
group_id: 10
ip_addresses:
- address: 192.0.2.1/24
fhrp_group:
protocol: vrrp2
group_id: 10
vrrp_group_assignments:
- device: branch-agg-1
interface: Vlan100
priority: 200
protocol: vrrp2
group_id: 10
- device: branch-agg-2
interface: Vlan100
priority: 100
protocol: vrrp2
group_id: 10
Prerequisites: both devices and their Vlan100 interfaces. This design creates the pool and group before allocating the VIP. Interface assignments remain separate from the VIP's assignment to the group.
prefixes:
- prefix: 192.0.2.0/24
description: Branch management subnet
vrrp_groups:
- protocol: vrrp2
group_id: 20
name: BRANCH MANAGEMENT GATEWAY
assignments:
- device: branch-agg-1
interface: Vlan100
priority: 200
- device: branch-agg-2
interface: Vlan100
priority: 100
ip_addresses:
- create_ip:
prefix:
description: Branch management subnet
description: Branch management VIP
vrrp_group: BRANCH MANAGEMENT GATEWAY
The group's vip field accepts an explicit CIDR address, not a create_ip wrapper. Omit vip when allocating the address through ip_addresses as above. The stable group name, prefix selection, and description let repeated deployments reuse the same VIP.
L2VPNs and Terminations¤
Define L2VPNs in l2vpns. Like VRFs, they accept import_route_targets and
export_route_targets as lists of dictionaries with name. These definitions
are extracted into route_targets and deployed before the L2VPN. Repeating the
same target dictionary in both lists creates it once.
Import/export lists also accept query to select existing route targets using
Netbox filters, including custom fields. Queries run after template rendering
and attach all matches. They do not create or update targets. A query matching
nothing causes an error, including in dry runs. Queries can match targets created
earlier in the same deployment. During a dry run those targets must already exist.
Duplicate matches are attached once. Use either query or a named definition
in each entry.
l2vpns:
- name: BRANCH EVPN
type: vxlan
import_route_targets:
- query:
name: "65100:100"
export_route_targets:
- query:
cf_customer_id: "{{ context.customer_id }}"
The same syntax works under vrfs. Supported filter keys depend on your Netbox
instance and its custom-field definitions.
terminations under an L2VPN uses the same record shape as the top-level
l2vpn_terminations collection, except that the parent supplies l2vpn.
Each termination attaches one device interface (device and interface) or
one VLAN (group and vid). Add name to a VLAN termination to create or
update that VLAN before attaching it. Additional fields on that entry describe
the VLAN. Flattening retains only l2vpn, group, and vid on the termination.
This works for nested and top-level terminations. VLAN definitions are deduplicated
by group and VID: definitions already in vlans take precedence, followed by
the first extracted termination definition. Entries without name
remain references. The interface or referenced VLAN must already exist or be
defined earlier in the design. NetBox permits only one L2VPN termination per
attached object. Deployment reports a conflict rather than moving an object
from another L2VPN. Omitted terminations are not deleted.
vlan_groups:
- name: BRANCH VLANS
l2vpns:
- name: BRANCH EVPN
type: vxlan
identifier: 10100
import_route_targets:
- name: "65100:100"
export_route_targets:
- name: "65100:100"
terminations:
- device: branch-router-1
interface: Ethernet1
- group: BRANCH VLANS
vid: 100
name: BRANCH USERS
status: active
description: User access VLAN created before attachment
custom_fields:
customer_id: ACME
l2vpn_terminations:
- l2vpn: BRANCH EVPN
device: branch-router-2
interface: Ethernet1
The example creates or updates VLAN 100 in BRANCH VLANS, then attaches it
to BRANCH EVPN. The routers and their interfaces must already exist or be
defined elsewhere in the design. The customer_id custom field must already
be defined in Netbox. Omit custom_fields if it is not available.
During flattening, the named VLAN termination produces two records:
vlans:
- group: BRANCH VLANS
vid: 100
name: BRANCH USERS
status: active
description: User access VLAN created before attachment
custom_fields:
customer_id: ACME
l2vpn_terminations:
- l2vpn: BRANCH EVPN
group: BRANCH VLANS
vid: 100
To attach an existing VLAN without updating its properties, supply only
group and integer vid in the nested termination. That reference does not
create a missing VLAN. Deployment fails if it cannot resolve the VLAN.
Named entries apply all additional fields, including tags and custom fields,
to the VLAN, rather than to the termination. VLAN groups must exist or be
defined in vlan_groups. Termination extraction does not create groups.
The l2vpn_terminations stage runs after interfaces. A dry run can report
planned terminations even when their interfaces or VLANs are also planned and
therefore have no IDs yet.
Nested VRF Records¤
VRFs accept import_route_targets and export_route_targets as lists of dictionaries. These route targets become top-level definitions and are deployed before their VRF, even when only name is supplied. A VRF can be defined at the top level or nested under a device interface.
Prerequisites: none for these IPAM objects. Route targets are created before the VRF and associated through its import/export fields.
vrfs:
- name: BRANCH VRF
rd: "65100:100"
import_route_targets:
- name: "65100:100"
description: Branch routes
export_route_targets:
- name: "65100:100"
description: Branch routes
Equivalent using top-level keys:
route_targets:
- name: 65100:100
description: Branch routes
vrfs:
- name: BRANCH VRF
rd: 65100:100
import_route_targets:
- name: 65100:100
export_route_targets:
- name: 65100:100
Prerequisites: the named site, device role, manufacturer, and device type. The nested VRF and its route targets are created before the interface.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
interfaces:
Vlan100:
type: virtual
vrf:
name: BRANCH VRF
rd: "65100:100"
import_route_targets:
- name: "65100:100"
export_route_targets:
- name: "65100:100"
Equivalent using top-level keys:
route_targets:
- name: 65100:100
vrfs:
- name: BRANCH VRF
rd: 65100:100
import_route_targets:
- name: 65100:100
export_route_targets:
- name: 65100:100
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
interfaces:
- type: virtual
vrf:
name: BRANCH VRF
rd: 65100:100
name: Vlan100
device: branch-router-1
Nested BGP Peering Records¤
BGP peerings accept import_policies and export_policies as lists of routing-policy names or dictionaries. The design extracts these into top-level routing_policies, creates or updates them before BGP sessions, and associates them with the peering. A policy name or a dictionary containing only name is also treated as a policy definition.
peer_group accepts a name or a dictionary with name and other peer-group fields. Both forms are collected into top-level peer_groups and created or updated before the BGP session. The peering then passes the group name to create_bgp_peering.
This works for top-level bgp_peerings and peerings nested under devices or interfaces.
Prerequisites: the device, both IP addresses, both ASNs, and the BGP plugin. Inline peer groups and policies are deployed before the peering. create_reverse: false creates only the specified direction.
bgp_peerings:
- name: branch-to-core
device: branch-router-1
local_address: 192.0.2.1
remote_address: 192.0.2.2
local_as: 65100
remote_as: 65200
create_reverse: false
peer_group:
name: BRANCH PEERS
description: Branch BGP neighbors
import_policies:
- name: BRANCH IMPORT
description: Branch inbound policy
export_policies:
- BRANCH EXPORT
Equivalent using top-level keys:
routing_policies:
- name: BRANCH IMPORT
description: Branch inbound policy
- name: BRANCH EXPORT
peer_groups:
- name: BRANCH PEERS
description: Branch BGP neighbors
bgp_peerings:
- name: branch-to-core
device: branch-router-1
local_address: 192.0.2.1
remote_address: 192.0.2.2
local_as: 65100
remote_as: 65200
create_reverse: false
peer_group: BRANCH PEERS
import_policies:
- BRANCH IMPORT
export_policies:
- BRANCH EXPORT
Configuration context¤
Top-level config_context creates or updates reusable Netbox ConfigContext objects. Its data is a dictionary, and its scope determines which devices receive it. Device-specific values belong in device local context.
A custom_function on a config_context record uses the same arguments and return handling as any ordinary custom function: deployment supplies context, netbox, and dry_run, and the record supplies additional keyword arguments. It receives no device object. Its return value is recorded under config_context.custom, not saved as context data automatically. The function must perform its own writes. The Jinja calculation example below instead renders data for the normal handler to save.
config_context matches an existing ConfigContext by name, creating it if absent or updating it if present. sites and tenants scope the context by name to objects created earlier or already in Netbox. Unknown tenant names fail. A dry run cannot resolve tenants that are only planned. Updates add scope associations without removing existing ones. Without a scope, the context applies globally, so specify one when the data is site-specific.
sites:
- name: BRANCH-1
slug: branch-1
config_context:
- name: BRANCH DEFAULTS
sites: [BRANCH-1]
data:
routing:
bgp:
hold_time: 90
ntp:
servers: [192.0.2.10, 192.0.2.11]
The data dictionary is sent to Netbox as supplied. The handler does not merge it with the previous data value.
This Jinja function calculates DNS and NTP server addresses from a site services subnet. It only returns a dictionary. The normal config_context handler creates or updates the site-scoped ConfigContext after rendering. The subnet must be supplied in context, because Jinja functions run before the design creates any Netbox objects.
jinja_functions:
site_services_context: nf://netbox/designs/site_services_context.py
sites:
- name: BRANCH-1
slug: branch-1
config_context:
- name: BRANCH SERVICES
sites: [BRANCH-1]
data: {{ site_services_context(context.services_prefix) | tojson }}
Supply a context such as {"services_prefix": "192.0.2.0/24"}. The referenced nf://netbox/designs/site_services_context.py contains:
from ipaddress import ip_network
def site_services_context(services_prefix):
subnet = ip_network(services_prefix)
if subnet.num_addresses < 8:
raise ValueError("Services subnet needs at least eight addresses")
return {
"dns": {"servers": [str(subnet[2]), str(subnet[3])]},
"ntp": {"servers": [str(subnet[4]), str(subnet[5])]},
}
For 192.0.2.0/24, the function returns DNS servers 192.0.2.2 and 192.0.2.3, plus NTP servers 192.0.2.4 and 192.0.2.5. tojson renders the returned dictionary as valid YAML content. The function does not query or mutate Netbox. Calculation from objects created by the same design would require support for a deployment-time calculator inside config_context.data, which the handler does not currently provide.
Device local context¤
local_context_data updates a local_context_data field on one device, not a reusable ConfigContext object. Both nested and top-level local context definitions run in the final deployment stage, after all other design objects have been created or updated. Custom functions can query those objects through the provided netbox client to calculate device context. Local context replaces the previous dictionary rather than merging with it.
A custom_function inside local_context_data must return the complete context-data dictionary to store on the matched device. For example, return {"routing": {"router_id": "192.0.2.1"}} sets the device's local_context_data to exactly that dictionary. The handler performs the device update. The function does not need to call device.update() or netbox.dcim.devices.update(). The return value is not placed under a custom result list or passed to another design collection.
Final-stage queries and branching
During a normal deployment, objects from all preceding stages are available in Netbox when device-local context functions run. If branch was supplied to design_deploy, the provided netbox client already targets that branch. During a dry run, planned objects are not created and may be unavailable.
Device-local context functions receive the device object
A function referenced inside local_context_data receives device, context, netbox, and dry_run, plus any arguments beside custom_function. device is the matched pynetbox device object, not its name. The function can inspect device.id, device.name, or other device fields. It must return a dictionary, which the handler writes to the device's local context.
The function receives the full device record so it can inspect fields beyond its identity. Related objects such as interfaces can be queried through netbox.
Local Context Custom Function Input
| Argument | Type | Supplied by | Meaning |
|---|---|---|---|
device |
pynetbox.core.response.Record |
Deployment | Matched Netbox device object, available after earlier deployment stages. Inspect fields such as device.id and device.name. |
context |
dict |
Deployment | Design input context after optional schema validation. |
netbox |
pynetbox.core.api.Api |
Deployment | Pynetbox client bound to the deployment's Netbox instance and branch. Use it to query the modeled device state. |
dry_run |
bool |
Deployment | Whether this is a preview. The handler skips its write when True. The function should calculate and return data without making writes itself. |
Other fields inside local_context_data |
As defined in YAML | Design record | Fields beside custom_function are passed unchanged as keyword arguments, such as interface and area in the example below. |
custom_function selects the function and is not passed to it. Do not supply device, context, netbox, or dry_run inside the local-context dictionary. Outer device identity fields, such as site and tenant, select the device and are not forwarded as function arguments.
| Function outcome | Deployment behavior |
|---|---|
| Returns a dictionary | Writes that dictionary as the device's entire local_context_data, replacing the previous value. The result lists the device name under local_context_data.updated, not under custom. |
| Returns a dictionary during a dry run | Does not write it. The function runs only if the device already exists. Earlier planned objects may still be absent. Missing devices are skipped and omitted from local_context_data.updated. |
Returns another type, including None |
Reports an error and stops deployment. A dictionary is required. |
| Raises an exception | Reports the failure and stops deployment. Earlier writes are not rolled back. |
Prerequisites: Named site, role, manufacturer, and device type.
Put local_context_data under a device for values specific to that device. The design writes it after all other object types, even though it appears inside the device record.
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
local_context_data:
routing:
profile: core
router_id: 192.0.2.101
Equivalent using top-level keys:
devices:
- name: branch-router-1
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
local_context_data:
- device: branch-router-1
site: BRANCH-1
local_context_data:
routing:
profile: core
router_id: 192.0.2.101
The dictionary is sent as-is. It replaces the device's previous local context.
Prerequisites: Named site, role, manufacturer, and device type.
Allocate a loopback IP first, then calculate the IS-IS NET ID from the address stored in Netbox. Pass the loopback interface name and IS-IS area beside custom_function. Local context runs last, so the function can read the address allocated earlier in the deployment.
custom_functions:
calculate_isis_context: nf://netbox/designs/calculate_isis_context.py
prefixes:
- prefix: 192.0.2.0/24
description: Branch loopback pool
devices:
- name: branch-router-2
site: BRANCH-1
role: ROUTER
device_type: {manufacturer: ACME, model: ROUTER}
interfaces:
Loopback0:
type: virtual
ip_addresses:
- create_ip:
prefix: 192.0.2.0/24
mask_len: 32
description: branch-router-2 loopback
local_context_data:
custom_function: calculate_isis_context
interface: Loopback0
area: "49.0001"
Equivalent using top-level keys:
custom_functions:
calculate_isis_context: nf://netbox/designs/calculate_isis_context.py
prefixes:
- prefix: 192.0.2.0/24
description: Branch loopback pool
devices:
- name: branch-router-2
site: BRANCH-1
role: ROUTER
device_type:
manufacturer: ACME
model: ROUTER
interfaces:
- type: virtual
name: Loopback0
device: branch-router-2
ip_addresses:
- create_ip:
prefix: 192.0.2.0/24
mask_len: 32
description: branch-router-2 loopback
device: branch-router-2
interface: Loopback0
local_context_data:
- device: branch-router-2
site: BRANCH-1
local_context_data:
custom_function: calculate_isis_context
interface: Loopback0
area: '49.0001'
The referenced nf://netbox/designs/calculate_isis_context.py contains:
from ipaddress import ip_interface
from typing import Any
from pynetbox.core.api import Api
from pynetbox.core.response import Record
def calculate_isis_context(
device: Record,
context: dict,
netbox: Api,
dry_run: bool,
interface: str,
area: str,
) -> dict[str, Any]:
loopback = netbox.dcim.interfaces.get(
device_id=device.id, name=interface, fields="id,name"
)
if loopback is None:
raise ValueError(f"Interface not found: {device.name}:{interface}")
addresses = list(netbox.ipam.ip_addresses.filter(
interface_id=loopback.id, family=4, fields="id,address"
))
if len(addresses) != 1:
raise ValueError(
f"Expected one IPv4 address on {device.name}:{interface}, "
f"found {len(addresses)}"
)
address = ip_interface(addresses[0].address).ip
digits = "".join(f"{octet:03d}" for octet in address.packed)
system_id = ".".join(digits[index:index + 4] for index in (0, 4, 8))
return {
"device": device.name,
"routing": {
"router_id": str(address),
"isis": {"net": f"{area}.{system_id}.00"},
}
}
create_ip allocates a /32 from the pool and assigns it to Loopback0. The function reads that interface's IPv4 address and converts it into a twelve-digit system ID. For example, if the allocated address is 192.0.2.102/32, the NET ID is 49.0001.1920.0000.2102.00. It rejects missing or multiple IPv4 addresses rather than selecting one arbitrarily.
Deployment supplies device, context, netbox, and dry_run. YAML supplies interface and area, not a predetermined router ID. The function only returns a dictionary. The local-context handler writes it to the device.
During a dry run, the function is skipped if the device does not yet exist. If the device exists but its loopback or IP is only planned, the lookup fails because dry runs do not create them.
Prerequisites: The specified device, site, and tenant.
Use a top-level local_context_data list when the device is not defined under devices in this design. site and device identify it. Add tenant when needed to distinguish devices with the same name and site.
local_context_data:
- device: branch-router-3
site: BRANCH-1
tenant: ACME
local_context_data:
routing: {profile: edge}
A missing or ambiguous device fails outside dry-run mode. The update replaces its prior local context.
Markdown designs¤
A Markdown design keeps network notes, input data and Netbox object definitions in one readable file. Reviewers can understand the design and check the records it will create or update. Use it for branch networks, repeatable site builds and service definitions. Store the file in version control to review notes and data changes together.
Key features:
- Keep headings, diagrams and explanations alongside YAML/Jinja design blocks.
- Define input data with YAML, CSV or visible Markdown tables.
- Split collections across sections. Repeated collections are combined.
- Validate inputs with a schema and reuse designs with different task context.
- Define shared Jinja variables and use file-backed Jinja or deployment functions.
- Set shared Netbox configuration context and device-local context.
- Preview rendered YAML or deploy the file directly.
Pass a .md or .markdown file URL to design_deploy. Only marked blocks and
tables supply data. Other text and unmarked examples are ignored. Preview with
dry_run_render=True before deploying.
Split a design across yaml design fences. Repeated collections are joined
in document order. Records are not deduplicated. Keep each loop inside one
collection section and indent records. Inline collections such as sites: []
are unsupported.
```yaml design
{% set prefix = 'branch-' %}
```
```yaml design
tenants:
- name: "{{ prefix }}{{ context.site }}"
```
Assembly order is schema, custom functions, Jinja functions, standalone Jinja blocks, then collections. Put each metadata key in one block. Standalone Jinja blocks retain document order and must each contain complete syntax. Relative includes resolve from the Markdown file's directory.
Use yaml context.KEY, csv context.KEY, or a marked Markdown table. Each
key must be unique and top-level. YAML preserves types. CSV/table cells stay
strings. Use YAML | scalars for multiline text. JSON and text fences are
unsupported.
```yaml context.settings
enabled: true
note: |
Branch rollout
```
```csv context.links
name,description
{% for n in range(context.count) %}
r{{ n }},"Router {{ n }}"
{% endfor %}
```
<!-- context.regions -->
| name |
| --- |
| {{ context.site }}-region |
YAML/CSV blocks render before parsing. Markdown cells render individually.
Only explicit task context is available, not document values, schema defaults,
shared Jinja variables or custom functions. Rendering is single-pass and
undefined variables fail. Escape source table pipes as \|. Quote generated
YAML/CSV values appropriately. Table headers stay literal.
Document context supplies defaults. Task context overrides whole top-level keys. Overridden definitions are not rendered. Context must be JSON-serializable.
Save as nf://netbox/designs/branch.md. This creates a tenant, region and site
using all three context formats and shared Jinja logic:
# Branch rollout
```yaml context.settings
note: Branch rollout
```
```csv context.tenants
name
{{ context.site }}-tenant
```
<!-- context.regions -->
| name |
| --- |
| {{ context.site }}-region |
```yaml design
design_input_schema:
type: object
properties:
site: {type: string}
settings: {type: object}
tenants: {type: array}
regions: {type: array}
required: [site]
```
```yaml design
{% set description = 'Approved: ' ~ context.settings.note %}
```
```yaml design
tenants:
{% for tenant in context.tenants %}
- name: {{ tenant.name | tojson }}
{% endfor %}
regions:
{% for region in context.regions %}
- name: {{ region.name | tojson }}
{% endfor %}
sites:
- name: {{ context.site | tojson }}
tenant: {{ context.tenants[0].name | tojson }}
region: {{ context.regions[0].name | tojson }}
description: {{ description | tojson }}
```
Preview through Python. Remove dry_run_render to deploy:
result = client.run_job(
"netbox", "design_deploy", workers="any",
kwargs={
"design": "nf://netbox/designs/branch.md",
"context": {"site": "BRISBANE"},
"dry_run_render": True,
},
)
Or use NFCLI:
nf# netbox design deploy design nf://netbox/designs/branch.md context '{"site":"BRISBANE"}' dry-run-render
Authoring rules¤
- Mark design blocks with
yaml designand close each fence explicitly. - Define each context key once. Use top-level names such as
context.settings. Put nested values inside a YAML block instead of usingcontext.settings.name. - Keep YAML context JSON-serializable. Quote numeric mapping keys. Avoid binary values and sets.
- Keep CSV and Markdown table headers unique and nonempty. Check table widths. Markdown tables can pad missing cells or ignore extra cells.
- Put each schema or function metadata key in one design block.
- Indent collection records and multiline values. Do not use inline collections
such as
devices: []. - Put shared Jinja assignments in separate design blocks. Keep each loop or conditional complete within one block and one collection section.
- Render context from explicit task input only. Context blocks cannot reference other document context, schema defaults or shared design variables.
- Make rendered YAML and CSV valid for their format. Escape source pipes in
Markdown table cells as
\|. - Preview with
dry_run_render=Trueto check the assembled design before deployment.
Design Deployment Output¤
With dry_run_render=True, result contains the rendered text and the result's dry_run flag is True. Dictionary designs return the supplied dictionary unchanged. This option takes precedence over dry_run and returns before YAML parsing, flattening, design validation, lookups, or deployment-time custom functions. Context and initial metadata parsing, input-schema validation, Netbox client setup, and Jinja2 functions still run.
Deployment and ordinary dry-run results are keyed by collection. Each collection reports object identities in created and updated. No-op allocation results are omitted. Repeat deployments can report updates because existing records are sent to Netbox without a local diff.
{
"tenants": {"created": ["ACME"], "updated": []},
"sites": {"created": ["BRANCH-1"], "updated": []}
}
Errors identify the collection that stopped deployment. Earlier collections are not rolled back.
Examples¤
nf# netbox design deploy design nf://netbox/designs/branch.yaml context '{"site":"BRANCH-1"}'
nf# netbox design deploy design nf://netbox/designs/branch.yaml context '{"site":"BRANCH-1"}' dry-run-render
from norfab.core.nfapi import NorFab
with NorFab(inventory="inventory.yaml") as nf:
result = nf.client.run_job(
service="netbox",
task="design_deploy",
workers="any",
kwargs={
"design": "nf://netbox/designs/branch.yaml",
"context": {"site": "BRANCH-1"},
"dry_run_render": True, # Preview the rendered design.
},
)
Enable the Netbox service endpoints on the NorFab FastAPI worker and configure a bearer token. Send task arguments directly in the JSON body, without a kwargs wrapper. Replace the API address and token with your environment's values. This example deploys to branch branch-review, which requires Netbox branching.
curl -X POST "http://localhost:8000/api/netbox/design_deploy/" \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"design":"nf://netbox/designs/branch.yaml","context":{"site":"BRANCH-1"},"branch":"branch-review","workers":"any"}'
See FastAPI setup and authentication for service configuration. Inspect the response's worker results for deployment errors.
Connect your agent to the NorFab MCP service with the Netbox design_deploy tool exposed. Ask it to deploy using a chat message such as:
Use the NorFab Netbox design_deploy tool to instantiate the design at
nf://netbox/designs/branch.yaml with context {"site": "BRANCH-1"}.
Deploy to Netbox branch `branch-review`. If branching is unavailable,
stop and ask before writing to main data. Report the deployment result
and any errors.
The agent should pass the file path as design, the dictionary as context, and the branch name as branch. The file must be accessible to the NorFab worker.
Notes¤
- A dry run does not create prerequisites, so it cannot fully resolve references to objects proposed earlier in the same design.
- Explicit ASN, VLAN, and prefix records can supply object and multiobject custom-field references by string reference or ID, using the matching fields described in Custom fields. String references must identify exactly one existing object when that record is processed. Updates add multiobject references and other list values without removing current values. Scalar and
nullvalues replace them. Custom-field definitions must already exist in NetBox. - Check that named parents, device types, sites, interfaces, and allocation pools exist or are created earlier in the design order.
- Jinja filters, input models, and custom functions execute Python inside the worker. Use trusted files.
- See the ACME design example for a full nested design.
Task command shell reference¤
nf#man tree netbox.design.deploy
R - required field, M - supports multiline input, D - dynamic key
root
└── netbox: Netbox service
└── design: Deploy NetBox designs
└── deploy: Deploy an additive NetBox design
├── design (R): NetBox design as YAML text, file URL, or parsed dictionary
├── context: Template context validated by design_input_schema, default '{}'
├── instance: NetBox instance name to target
├── dry-run: Validate design without writing to NetBox, default 'False'
├── dry-run-render: Return rendered design before parsing or deployment, default 'False'
├── branch: NetBox branching plugin branch name to use
├── timeout: Job timeout
├── workers: Filter worker to target, default 'any'
├── verbose-result: Control output details, default 'False'
└── nowait: Do not wait for job to complete, default 'False'
nf#
Python API reference¤
Render and flatten a design, validate it, then deploy ordered collections.
Fetches Jinja and custom function files in separate batches, executing each group in its declared order.
Updates add tags, custom-field list entries, and the supported native relationship lists. Scalar fields and context-data dictionaries replace their current values. Object custom-field names resolve to NetBox IDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
Job
|
NorFab job injected by the task framework. |
required |
design
|
Union[str, dict]
|
YAML text, NorFab file URL, or parsed design dictionary. File URLs ending in .md or .markdown are converted from Markdown first. Device-type references accept manufacturer/model dictionaries or model strings matching exactly one existing NetBox type. String references must exist in NetBox even during dry runs. New devices default to planned status; omitted status preserves existing device status. |
required |
context
|
Union[str, dict]
|
Jinja2 variables, optionally checked by design_input_schema. Explicit values override Markdown context by top-level key. |
{}
|
instance
|
str
|
NetBox instance name; defaults to the worker's instance. |
None
|
dry_run
|
bool
|
Return planned writes without changing NetBox. |
False
|
branch
|
str
|
NetBox Branching plugin branch name. |
None
|
dry_run_render
|
bool
|
Return rendered text before YAML parsing, flattening, validation, or deployment. Dictionary designs return unchanged. Takes precedence over dry_run; context validation and Jinja2 functions still run. |
False
|
Returns:
| Type | Description |
|---|---|
Result
|
Object identities grouped by collection and creation/update action, |
Result
|
or rendered text (the original dictionary for dictionary designs) |
Result
|
with dry_run=True when dry_run_render is enabled. Preparation and |
Result
|
deployment errors, including job timeout expiry, are reported in a |
Result
|
failed Result. Timeout checks stop subsequent operations; in-flight |
Result
|
calls are not interrupted. |
Source code in norfab\workers\netbox_worker\design_tasks.py
4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 | |