For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
    • NVIDIA Switch Infrastructure
    • I want to...
  • Quick Start
    • Start Here
    • Getting Started with Config Manager
    • TUI Wizard Reference
    • Configuration Samples
    • Interfaces
    • Local Development Quick Start
    • First Run Tour
  • Config Manager Overview
    • Config Manager Concepts
    • Getting Started with Nautobot
  • User Guides
    • New Site Bringup
    • Workflow Lifecycle
  • Deployment
    • Hosting Options
    • Network Topology Requirements
    • Firewall Ports
    • Airgapped Deployment
    • Troubleshooting
  • Services
      • Network Template Rendering System
      • Render Service
      • Filter quick reference
      • Template expansion
NVIDIANVIDIA
Developer-friendly docs for your API
Privacy Policy | Your Privacy Choices | Terms of Service | Accessibility | Corporate Policies | Product Security | Contact

Copyright © 2026, NVIDIA Corporation.

LogoLogo
On this page
  • Overview
  • Key Features
  • System Architecture
  • High-Level Flow
  • Components
  • Template Structure
  • Template Lookup
  • template-cli
  • Template Inheritance
  • Three-Tier Inheritance Model
  • Example: Cumulus Linux TAN-Leaf
  • Include Template Inheritance
  • Jinja2 Filters
  • Filter Organization
  • Device Filters
  • Basic Device Information
  • Routing & BGP
  • Interface Operations
  • Specialized Filters
  • BGP Filters
  • IP Filters
  • ISIS Filters
  • Location Filters
  • Vault Filters
  • Nautobot Data Model
  • Device Query Structure
  • Location Query Structure
  • Config Context
  • Creating Templates
  • Determine Template Location
  • Create or Extend Base Template
  • Create Include Templates
  • Create Entrypoint Template
  • Configure Nautobot
  • Test your template
  • Best Practices
  • Template Design
  • Filter Development
  • Nautobot Data Management
  • Version Management
  • Examples
  • Simple Interface Configuration
  • BGP Configuration with Peer Groups
  • Conditional Configuration Based on Tags
  • DHCP Server Configuration
  • Multi-VRF BGP Configuration
  • Secret Management
  • Interface with Connected Device Context
  • Conclusion
ServicesTemplate Rendering Service

Network Template Rendering System

||View as Markdown|
Previous

Load Firmware Checksum

Next

Config Manager Render Service

Overview

This template rendering system provides a flexible, inheritance-based approach to generating network device configurations. It queries structured data from Nautobot (a network source of truth) and renders device configurations using Jinja2 templates with custom filters.

Key Features

  • Multi-platform Support: Templates for Cumulus Linux, Arista EOS, Mellanox MLNX-OS, and NVIDIA NV-OS
  • Role-based Templates: Different device roles (leaf, spine, core, and so on) have customized configurations
  • Version Management: Support for multiple firmware versions per platform/role
  • Template Inheritance: DRY (Don’t Repeat Yourself) principle with multi-level template inheritance
  • Custom Filters: Powerful Jinja2 filters to extract and transform Nautobot data
  • Type Safety: Python dataclasses provide structured, validated data objects

System Architecture

High-Level Flow

Nautobot (Source of Truth)
↓ (GraphQL Query)
Device Data + Location Data
↓ (Pass to Renderer)
Jinja2 Template Engine
↓ (Apply Filters & Render)
Device Configuration Files

Components

  1. Renderer: Main orchestration class that:

    • Queries Nautobot through GraphQL
    • Loads and configures Jinja2 environment
    • Dynamically registers custom filters
    • Renders templates with device and location data
  2. Filters: Python functions that transform data within templates

    • Located in filters/ modules (bgp, device, ip, isis, location, vault)
    • Automatically loaded and registered as Jinja2 filters
    • Must have unique names across all modules
  3. Templates: Jinja2 templates organized by platform, role, and version

    • Entrypoint templates: Top-level templates that generate final configs
    • Base templates: Define configuration structure with named blocks
    • Include templates: Implement specific configuration sections
  4. Dataclasses: Structured Python objects for type-safe data handling

    • Interface: Network interface representation
    • BGPPeer: BGP peering information
    • VRF: Virtual routing and forwarding instance
    • ConnectedDevice: Details about connected neighbors

Template Structure

Templates are organized in a hierarchical directory structure:

templates/
├── {platform}/ # e.g., "cumulus-linux", "arista-eos"
│ ├── role_common/ # Shared across all roles for this platform
│ │ ├── base/ # Base templates
│ │ │ └── startup.yaml.j2
│ │ └── include/ # Common include templates
│ │ ├── interface.j2
│ │ ├── service.j2
│ │ └── system.j2
│ └── {role}/ # e.g., "tan-leaf", "smn-spine", "wan"
│ ├── base/ # Role-specific base templates
│ │ └── startup.yaml.j2
│ ├── include/ # Role-specific includes
│ │ ├── interface.j2
│ │ ├── router.j2
│ │ └── qos.j2
│ └── {version}/ # e.g., "5.6.0", "4.29.3M"
│ └── entrypoint/ # Version-specific entrypoints
│ ├── startup.yaml.j2
│ └── boot-script.j2

Template Lookup

The renderer determines which entrypoint templates to use based on device attributes:

  1. Platform: Extracted from device’s platform field (e.g., “Cumulus Linux” → cumulus-linux)
  2. Role: Extracted from device’s role field (e.g., “TAN-Leaf” → tan-leaf)
  3. Version: Extracted from device’s config context intended-firmware.version

Example Path: cumulus-linux/tan-leaf/5.6.0/entrypoint/startup.yaml.j2

template-cli

template-cli is a local template development command, not a command exposed by a running Config Manager deployment or by the installer on the target host. It is installed into any Python environment that installs the nv-config-manager-templates package; from the Config Manager source tree, run it with uv run template-cli from components/network-templates.

Use template-cli to iterate on built-in templates or external template plugins before packaging them for the render service. Deployed environments use the same render engine through the Config Manager UI, render API, event consumers, and staged template plugin content.

Common commands:

CommandPurpose
cache-queryPull device and location GraphQL payloads once; write JSON fixtures for fast iteration and pytest. Supports --output-plugin-file when plugins add queries.
list-entrypointsShow which entrypoint templates match a device (live Nautobot, cached JSON, or test fixtures).
renderRender an --entrypoint name or a full --template path. Accepts --cached-data, --cached-location-data, --cached-plugin-data, optional --output-file, and --vault to perform real Vault lookups instead of dummy secrets.

Without --vault, encrypted fields in CLI output are placeholders. Use --vault before copying rendered configs to lab hardware. For render regression tests, add expected files under tests/resources/expected_config/ and matching Nautobot JSON under tests/resources/nautobot/, then run uv run pytest (see the nv-config-manager-templates README).

Template Inheritance

The system uses Jinja2’s template inheritance to enable code reuse and maintainability.

Three-Tier Inheritance Model

Level 1: Platform Common (role_common)
↓ extends
Level 2: Role-Specific Base
↓ extends
Level 3: Version-Specific Entrypoint

Example: Cumulus Linux TAN-Leaf

Level 1: cumulus-linux/role_common/base/startup.yaml.j2

1{% block header %}
2- set:
3{% endblock header %}
4
5{% block acl %}{% endblock acl %}
6{% block bridge %}{% endblock bridge %}
7{% block evpn %}{% endblock evpn %}
8{% block interfaces required %}{% endblock interfaces %}
9{% block nve %}{% endblock nve %}
10{% block qos required %}{% endblock qos %}
11{% block router required %}{% endblock router %}
12
13{% block service %}
14{% include "cumulus-linux/role_common/include/service.j2" %}
15{% endblock service %}
16
17{% block system %}
18{% include "cumulus-linux/role_common/include/system.j2" %}
19{% endblock system %}
20
21{% block vrf required %}{% endblock vrf %}

This defines the overall structure with named blocks. The required keyword ensures child templates must implement these blocks.

Level 2: cumulus-linux/tan-leaf/base/startup.yaml.j2

1{% extends "cumulus-linux/role_common/base/startup.yaml.j2" %}
2
3{% block bridge %}
4{% include "cumulus-linux/tan-leaf/include/bridge.j2" %}
5{% endblock bridge %}
6
7{% block interfaces %}
8{% include "cumulus-linux/tan-leaf/include/interface.j2" %}
9{% endblock interfaces %}
10
11{% block qos %}
12{% include "cumulus-linux/tan-leaf/include/qos.j2" %}
13{% endblock qos %}
14
15{% block router %}
16{% include "cumulus-linux/tan-leaf/include/router.j2" %}
17{% endblock router %}
18
19{% block vrf %}
20{% include "cumulus-linux/tan-leaf/include/vrf.j2" %}
21{% endblock vrf %}

This implements role-specific blocks by including role-specific templates.

Level 3: cumulus-linux/tan-leaf/5.6.0/entrypoint/startup.yaml.j2

1{% extends "cumulus-linux/tan-leaf/base/startup.yaml.j2" %}

This simply extends the role base. If version 5.6.0 needs specific changes, blocks can be overridden here.

Include Template Inheritance

Include templates can also inherit from each other:

Common Interface Template: cumulus-linux/role_common/include/interface.j2

1 interface:
2{% block management %}
3{% set intf = device_data|interface_by_name("eth0") %}
4 eth0:
5 description: {{intf.description}}
6 ip:
7 address:
8 {{ intf.primary_ipv4 }}: {}
9 gateway:
10 {{ intf.primary_ipv4|gateway }}: {}
11 type: eth
12{% endblock management %}
13
14{% block loopback %}
15{% set intf = device_data|interface_by_name("lo") %}
16 lo:
17 ip:
18 address:
19 {{ intf.primary_ipv4 }}: {}
20 type: loopback
21{% endblock loopback %}
22
23{% block swp required %}{% endblock swp %}
24{% block vlan %}...{% endblock vlan %}

Role-Specific Interface Template: cumulus-linux/tan-leaf/include/interface.j2

1{% extends "cumulus-linux/role_common/include/interface.j2" %}
2
3{% block swp %}
4{% for intf in device_data|interfaces(prefix="swp") %}
5 {{ intf.name }}:
6 description: {{intf.description}}
7 qos:
8 congestion-control:
9 profile: tan-ecn-profile
10 type: swp
11{% endfor %}
12{% endblock swp %}

This inherits management and loopback blocks from common, only implementing swp-specific logic.

Jinja2 Filters

Filters are the primary mechanism for extracting and transforming Nautobot data within templates. All filters are pure Python functions that take data as input and return processed output.

Filter Organization

Filters are organized into modules by functionality:

  • device.py: Device-level attributes and interface operations
  • bgp.py: BGP-specific data transformations
  • ip.py: IP address and network calculations
  • isis.py: IS-IS protocol helpers
  • location.py: Site/location-level data (aggregates, ASNs, peers)
  • vault.py: Secret management and encryption

Device Filters

Extract device attributes from Nautobot GraphQL data.

Basic Device Information

FilterInputOutputDescriptionNautobot Field
hostnamedevice_datastringDevice hostnamedevice.name
site_namedevice_datastringSite name (handles nested locations)device.location hierarchy
platformdevice_datastringPlatform namedevice.platform.name
roledevice_datastringDevice roledevice.role.name
modeldevice_datastringDevice modeldevice.device_type.model
uuiddevice_datastringNautobot UUIDdevice.id
device_tagsdevice_datalist[str]List of device tagsdevice.tags[].name
has_tagdevice_data, tagboolCheck if device has specific tagdevice.tags[].name
desired_firmwaredevice_datastringTarget firmware versiondevice.config_context['intended-firmware']['version']

Example Usage:

1hostname {{ device_data|hostname }}
2! Running {{ device_data|platform }} {{ device_data|desired_firmware }}
3! Role: {{ device_data|role }}

Routing & BGP

FilterInputOutputDescriptionNautobot Field
router_iddevice_datastringRouter ID (IPv4 without mask)Loopback interface IP
asndevice_data, vrf=“default”stringBGP ASN for device/VRFdevice.bgp_routing_instances[].autonomous_system.asn or config context
local_asndevice_datastringLocal ASN for Azure devicesdevice.config_context['bgp']['local-asn']
bgp_peersdevice_data, vrf=“default”list[BGPPeer]List of BGP peersdevice.bgp_routing_instances[].endpoints

Example Usage:

1router bgp {{ device_data|asn }}
2 router-id {{ device_data|router_id }}
3 {% for peer in device_data|bgp_peers %}
4 neighbor {{ peer.peer_ipv4 }} remote-as {{ peer.asn }}
5 {% endfor %}

Interface Operations

FilterInputOutputDescriptionNautobot Field
interfacesdevice_data, prefix=None, role=None, tags=Nonelist[Interface]Filter interfaces by prefix/role/tagsdevice.interfaces[]
interface_by_namedevice_data, name, fail_if_missing=TrueInterface or NoneGet specific interface; set fail_if_missing=False to avoid raising when missingdevice.interfaces[] where name matches
breakout_countdevice_data, interface_nameintNumber of breakout portsDerived from child interfaces
loopback_prefixdevice_datastringParent prefix of loopbackinterfaces[name='lo'].ip_addresses[0].parent.parent.prefix

Interface Object Properties:

1Interface(
2 name: str # Interface name (e.g., "swp1", "Ethernet1")
3 primary_ipv4: str | None # Primary IPv4 address with prefix
4 primary_ipv6: str | None # Primary IPv6 address with prefix
5 enabled: bool # Interface admin state
6 mtu: int # MTU size
7 tags: list[str] # Interface tags
8 untagged_vlan: int | None # Untagged VLAN ID
9 tagged_vlans: list[int] # List of tagged VLAN IDs
10 vrf: str # VRF name (default if none)
11 description: str # Interface description
12 role: str # Interface role
13 connected_interface: ConnectedInterface # Details about connected device
14)

Example Usage:

1{% for intf in device_data|interfaces(prefix="swp", role="Uplink") %}
2interface {{ intf.name }}
3 description {{ intf.description }}
4 mtu {{ intf.mtu }}
5 {% if intf.primary_ipv4 %}
6 ip address {{ intf.primary_ipv4 }}
7 {% endif %}
8{% endfor %}

Specialized Filters

FilterInputOutputDescription
spx_subnetsdevice_data, ip_version=4list[dict]Spectrum-X downlink /31 subnets with rail prefixes
tenant_vrfsdevice_datalist[VRF]Tenant VRFs configured on device
console_server_portsdevice_datalist[ConsoleServerPort]Console connections
helper_addresses_by_vlandevice_data, location_datadictDHCP helper addresses keyed by VLAN ID
helper_addresses_by_vrfdevice_data, location_datadictHelper configuration grouped by VRF
usersdevice_datalist[dict]Usernames, optional roles, and password key references for templated local or AAA users

BGP Filters

Transform BGP ASN formats.

FilterInputOutputDescription
asplainasdot_stringintConvert ASDOT notation to ASPLAIN (e.g., “1.1” → 65537)

Example:

1router bgp {{ "1.100"|asplain }} {# Results in: router bgp 65636 #}

IP Filters

Network calculations and IP address manipulations.

FilterInputOutputDescription
gatewaycidrstringFirst usable IP in subnet
subnetcidr, prefix_lenlist[str]Subdivide network into smaller subnets
supernetcidr, prefix_lenstringGet parent supernet
ipscidrlist[str]List all IPs in network
netmask_notationcidrtuple[str, str]Convert CIDR to (address, netmask)
get_peer_ipcidrstringGet the other IP in a /31 point-to-point link
network_addresscidrstringExtract network address from host IP
host_rangecidrtuple[str, str]Get (first_host, last_host) for DHCP ranges
rfc3442_classless_static_routecidr, next_hopstringFormat RFC3442 static route for DHCP

Example Usage:

1{# Gateway calculation #}
2ip route 0.0.0.0/0 {{ intf.primary_ipv4|gateway }}
3
4{# Subnet division #}
5{% for subnet in "10.0.0.0/16"|subnet(24) %}
6ip prefix-list SUBNETS permit {{ subnet }}
7{% endfor %}
8
9{# Peer IP for /31 links #}
10{% set peer_ip = intf.primary_ipv4|get_peer_ip %}
11neighbor {{ peer_ip }} remote-as {{ peer_asn }}
12
13{# DHCP host range #}
14{% set first, last = subnet|host_range %}
15range {{ first }} {{ last }};

ISIS Filters

IS-IS protocol helpers.

FilterInputOutputDescription
isis_system_idloopback_ipstringGenerate IS-IS system ID from loopback (e.g., “1.2.3.4” → “49.0039.8037.0001.0002.0003.0004.00”)

Example:

1router isis CORE
2 net {{ device_data|router_id|isis_system_id }}

Location Filters

Extract site/location-level data (operates on location_data, not device_data).

FilterInputOutputDescriptionNautobot Field
site_aggregateslocation_data, role_name, tags=None, exclude_tags=Nonelist[str]Prefixes with specific role; optional tag include/exclude listslocation.prefixes[] filtered by role
site_asnlocation_datastringSite-level BGP ASNlocation.config_contexts[0].data.site_asn
route_server_peerslocation_datalist[BGPPeer]BGP route server peers at sitelocation.route_servers[]
wan_loopbackslocation_datalist[tuple]WAN router names and loopbackslocation.wan_devices[].interfaces[]
uc_jumphost_prefixeslocation_datalist[str]UC jumphost prefixeslocation.uc_jumphost_prefixes[]

Example Usage:

1{# Get site aggregates for advertisement #}
2{% for aggregate in location_data|site_aggregates("Aggregate", tags=["bgp-advertise"]) %}
3network {{ aggregate }}
4{% endfor %}
5
6{# Peer with route servers #}
7{% for rs in location_data|route_server_peers %}
8neighbor {{ rs.peer_ipv4 }} remote-as {{ rs.asn }}
9neighbor {{ rs.peer_ipv4 }} peer-group {{ rs.peer_group }}
10{% endfor %}

Vault Filters

Secret management and encryption (for passwords, keys, and so on).

FilterInputOutputDescription
load_secretkey, region=None, site=NonestringLoad secret from Hashicorp Vault
encryptplaintext, algo, site=NonestringEncrypt using “sha512”, “md5”, or “ciscot7”; optional site for salt lookup
get_password_keydevice_data, usernamestringGet password key for user from config context

Secret Loading Modes:

  1. Production: Queries Hashicorp Vault directly
  2. Kubernetes: Reads from injected secret files
  3. Development: Returns dummy value ({path}:{key}) when NV_CONFIG_MANAGER_SKIP_VAULT=1

Example Usage:

1{# Load and encrypt password #}
2{% set admin_key = device_data|get_password_key("admin") %}
3{% set admin_pass = admin_key|load_secret(site=device_data|site_name) %}
4username admin secret {{ admin_pass|encrypt("sha512") }}
5
6{# Load TACACS key #}
7{% set tacacs_key = "tacacs_key"|load_secret(region="US-WEST") %}
8tacacs-server key {{ tacacs_key|encrypt("ciscot7") }}

Nautobot Data Model

The system queries Nautobot using GraphQL to retrieve comprehensive device and location data.

Device Query Structure

The device query (query_config_data_by_device_id_v2.graphql) retrieves:

device
├── Basic Attributes: id, name, serial, role, platform, device_type, tenant
├── tags[]: List of device tags
├── location: Site/location hierarchy
├── config_context: Custom JSON data (intended firmware, BGP config, and so on)
├── interfaces[]
│ ├── Basic: name, type, mtu, enabled, description, mgmt_only
│ ├── role: Interface role
│ ├── tags[]: Interface tags
│ ├── vrf: VRF assignment with RD and route targets
│ ├── ip_addresses[]: IPv4/IPv6 addresses with parent prefix hierarchy
│ ├── member_interfaces[]: LAG members
│ ├── parent_interface: Parent for sub-interfaces
│ ├── untagged_vlan: Access VLAN
│ ├── tagged_vlans[]: Trunk VLANs
│ └── connected_interface
│ ├── name, ip_addresses[]
│ └── device: Connected device details (name, role, tenant, tags, config_context)
├── bgp_routing_instances[]
│ ├── status, autonomous_system (ASN)
│ ├── router_id (with interface and VRF)
│ └── endpoints[] (BGP peers)
│ ├── peer_group
│ ├── source_interface
│ └── peer: Remote device routing instance
├── console_server_ports[]
│ └── connected_console_port
│ └── device: Connected device
└── nvlink_domain: NVLink topology data

Location Query Structure

The location query (query_location_data.graphql) retrieves site-level data:

location
├── name, location_type
├── config_contexts[]: Site-level configuration (ASN, and so on)
├── prefixes[]: IP prefixes at this site
│ ├── prefix, role
│ └── tags[]
├── route_servers[]: BGP route servers
│ ├── name, config_context
│ └── interfaces[].ip_addresses[]
├── wan_devices[]: WAN routers
│ ├── name
│ └── interfaces[] (for loopback IPs)
└── uc_jumphost_prefixes[]: Jumphost prefixes

Config Context

The config_context field contains custom JSON data defined in Nautobot:

1{
2 "intended-firmware": {
3 "version": "5.6.0",
4 "image": "cumulus-linux-5.6.0.bin"
5 },
6 "bgp": {
7 "asn": 65100,
8 "local-asn": 65200
9 },
10 "password_mappings": {
11 "default": {
12 "admin": {
13 "password": "admin_password",
14 "rotation": "v1",
15 "role": "admin"
16 }
17 }
18 }
19}

Allowed password-mapping roles are admin, ro, and rw.

Creating Templates

Determine Template Location

Based on your device, identify:

  1. Platform: cumulus-linux, arista-eos, mlnx-os, nv-os
  2. Role: tan-leaf, smn-spine, wan, oob-switch, and so on
  3. Version: Firmware version like 5.6.0, 4.29.3M

Create or Extend Base Template

If creating a new role, start with a base template:

File: {platform}/{role}/base/{config-file}.j2

1{% extends "{platform}/role_common/base/{config-file}.j2" %}
2
3{% block interfaces %}
4{% include "{platform}/{role}/include/interface.j2" %}
5{% endblock interfaces %}
6
7{% block router %}
8{% include "{platform}/{role}/include/router.j2" %}
9{% endblock router %}
10
11{# Override other blocks as needed #}

Create Include Templates

Implement specific configuration sections:

File: {platform}/{role}/include/interface.j2

1{# Can extend common if desired #}
2{% extends "{platform}/role_common/include/interface.j2" %}
3
4{% block swp %}
5{% for intf in device_data|interfaces(prefix="swp") %}
6interface {{ intf.name }}
7 description {{ intf.description }}
8 {% if intf.primary_ipv4 %}
9 ip address {{ intf.primary_ipv4 }}
10 {% endif %}
11 {% if not intf.enabled %}
12 shutdown
13 {% endif %}
14{% endfor %}
15{% endblock swp %}

Create Entrypoint Template

File: {platform}/{role}/{version}/entrypoint/{config-file}.j2

1{% extends "{platform}/{role}/base/{config-file}.j2" %}
2
3{# Override blocks if version-specific changes needed #}
4{% block new_feature %}
5{% if device_data|has_tag("enable-new-feature") %}
6! New feature configuration for version {{ device_data|desired_firmware }}
7new-feature enable
8{% endif %}
9{% endblock new_feature %}

Configure Nautobot

Ensure the device in Nautobot has:

  1. Platform set correctly
  2. Role set correctly
  3. Config Context with intended-firmware.version matching your template path
  4. Required interface, IP, and BGP data populated

Test your template

  1. Cache device data:

    $uv run template-cli cache-query --hostname my-device \
    > --output-file tests/resources/nautobot/my-device.json \
    > --output-location-file tests/resources/nautobot/MY-SITE.json
  2. Render locally:

    $uv run template-cli render \
    > --cached-data tests/resources/nautobot/my-device.json \
    > --cached-location-data tests/resources/nautobot/MY-SITE.json \
    > --entrypoint startup.yaml.j2
  3. Add expected output: save the rendered output to tests/resources/expected_config/my-device_startup.yaml (naming convention is {hostname}_{entrypoint}).

  4. Run tests: uv run pytest tests/nv_config_manager_templates/test_render.py.

Best Practices

Template Design

  1. Use Inheritance: Do not duplicate configuration. Extend base templates and override only what differs.

  2. Keep Includes Focused: Each include template should handle one logical section (interfaces, routing, QoS, and so on).

  3. Fail Explicitly: Use filters with fail_if_missing=True (default) to catch data issues early.

  4. Handle Optional Data: Check for None before using optional fields:

    1{% if intf.description %}
    2description {{ intf.description }}
    3{% endif %}
  5. Use Filters for Logic: Move complex logic into filters rather than templates:

    1{# Bad - complex logic in template #}
    2{% if intf.connected_interface and not intf.primary_ipv4 and not intf.untagged_vlan %}
    3
    4{# Good - logic in filter #}
    5{% if intf.has_bgp_peer() %}
  6. Whitespace Management: Jinja2 trim_blocks=True is enabled. Use {%- and -%} for additional control:

    1{%- for item in items %}
    2line {{ item }}
    3{%- endfor %}

Filter Development

  1. Single Responsibility: Each filter should do one thing well.

  2. Type Hints: Use type hints for clarity:

    1def my_filter(value: dict[str, Any], option: str = "default") -> list[str]:
    2 """Clear docstring explaining purpose."""
    3 ...
  3. Error Handling: Raise FilterException with clear messages:

    1if not result:
    2 raise FilterException(f"No data found for {identifier}")
  4. Test Coverage: Every filter must have unit tests:

    1def test_my_filter(device_fixture):
    2 result = my_filter(device_fixture, "option")
    3 assert result == expected_value
  5. Immutable Data: Use frozen=True dataclasses to prevent accidental mutations.

Nautobot Data Management

  1. Consistent Naming: Follow consistent naming conventions for devices, interfaces, VLANs, and so on.

  2. Config Context: Use config context for:

    • Intended firmware versions
    • BGP ASNs and configuration
    • Password key mappings
    • Feature flags
  3. Interface Roles: Define and use interface roles consistently (Uplink, Downlink, Management, and so on)

  4. Tags: Use tags for:

    • Feature enablement (enable-feature-x)
    • Grouping devices (production, staging)
    • Interface classification
  5. IP Hierarchy: Maintain proper prefix parent-child relationships:

    • Rail aggregates → Device prefixes → /31 point-to-point links

Version Management

  1. Version-Specific Changes Only: Only create version-specific templates when absolutely necessary.

  2. Feature Detection: When possible, use tags rather than version checks:

    1{# Better #}
    2{% if device_data|has_tag("supports-feature-x") %}
    3
    4{# Avoid #}
    5{% if device_data|desired_firmware >= "5.0.0" %}
  3. Deprecation Path: When deprecating old versions, keep templates for graceful migration.

Examples

Simple Interface Configuration

Template: my-platform/my-role/include/interface.j2

1{% for intf in device_data|interfaces(prefix="Ethernet") %}
2interface {{ intf.name }}
3 description {{ intf.description }}
4 mtu {{ intf.mtu }}
5 {% if intf.primary_ipv4 %}
6 ip address {{ intf.primary_ipv4 }}
7 {% endif %}
8 {% if intf.enabled %}
9 no shutdown
10 {% else %}
11 shutdown
12 {% endif %}
13{% endfor %}

BGP Configuration with Peer Groups

Template: my-platform/my-role/include/bgp.j2

1router bgp {{ device_data|asn }}
2 router-id {{ device_data|router_id }}
3
4 {# Define peer groups #}
5 neighbor SPINE peer-group
6 neighbor SPINE remote-as {{ location_data|site_asn }}
7 neighbor SPINE send-community extended
8
9 {# Configure individual peers #}
10 {% for peer in device_data|bgp_peers %}
11 {% if peer.peer_group == "SPINE" %}
12 neighbor {{ peer.peer_ipv4 }} peer-group SPINE
13 neighbor {{ peer.peer_ipv4 }} description {{ peer.description }}
14 {% endif %}
15 {% endfor %}
16
17 {# Address families #}
18 address-family ipv4 unicast
19 {% for aggregate in location_data|site_aggregates("Aggregate") %}
20 network {{ aggregate }}
21 {% endfor %}
22 exit-address-family

Conditional Configuration Based on Tags

Template: my-platform/my-role/include/features.j2

1{# Enable LLDP only on devices with the tag #}
2{% if device_data|has_tag("enable-lldp") %}
3lldp run
4{% for intf in device_data|interfaces(prefix="Ethernet") %}
5interface {{ intf.name }}
6 lldp transmit
7 lldp receive
8{% endfor %}
9{% endif %}
10
11{# Configure QoS on tagged interfaces #}
12{% for intf in device_data|interfaces(tags=["qos-enabled"]) %}
13interface {{ intf.name }}
14 qos trust dscp
15 qos cos {{ intf.tags|select("match", "^cos-\\d+$")|first|replace("cos-", "") }}
16{% endfor %}

DHCP Server Configuration

Template: cumulus-linux/cin-leaf/base/dhcpd.conf.j2

1default-lease-time 600;
2max-lease-time 7200;
3ddns-update-style none;
4authoritative;
5
6{% for intf in device_data|interfaces(role="Tenant") %}
7{% if intf.primary_ipv4 %}
8subnet {{ intf.primary_ipv4|network_address|netmask_notation|first }}
9 netmask {{ intf.primary_ipv4|network_address|netmask_notation|last }} {
10 {% set first_host, last_host = intf.primary_ipv4|network_address|host_range %}
11 range {{ first_host }} {{ last_host }};
12 option routers {{ intf.primary_ipv4|gateway }};
13 option domain-name-servers 8.8.8.8, 8.8.4.4;
14
15 {# Static routes via RFC3442 #}
16 {% set routes = [] %}
17 {% for aggregate in location_data|site_aggregates("Aggregate") %}
18 {% set _ = routes.append(aggregate|rfc3442_classless_static_route(intf.primary_ipv4|gateway)) %}
19 {% endfor %}
20 option rfc3442-classless-static-routes {{ routes|join(', ') }};
21}
22{% endif %}
23{% endfor %}

Multi-VRF BGP Configuration

Template: my-platform/my-role/include/bgp-vrf.j2

1{% for vrf in device_data|tenant_vrfs %}
2router bgp {{ device_data|asn("default") }}
3 vrf {{ vrf.name }}
4 router-id {{ device_data|router_id }}
5
6 {# VRF-specific peers #}
7 {% for peer in device_data|bgp_peers(vrf=vrf.name) %}
8 neighbor {{ peer.peer_ipv4 }} remote-as {{ peer.asn }}
9 neighbor {{ peer.peer_ipv4 }} description {{ peer.description }}
10 {% endfor %}
11
12 {# L3 VNI for EVPN #}
13 address-family ipv4 unicast
14 redistribute connected
15 redistribute static
16 exit-address-family
17
18 address-family l2vpn evpn
19 advertise ipv4 unicast
20 exit-address-family
21 exit-vrf
22{% endfor %}

Secret Management

Template: my-platform/my-role/include/management.j2

1{# Load site-specific secrets #}
2{% set site = device_data|site_name %}
3
4{# Admin user with encrypted password #}
5{% set admin_key = device_data|get_password_key("admin") %}
6{% set admin_password = admin_key|load_secret(site=site) %}
7username admin privilege 15 secret {{ admin_password|encrypt("sha512") }}
8
9{# TACACS configuration #}
10{% set tacacs_key = "tacacs_shared_key"|load_secret(site=site) %}
11tacacs-server host 10.0.0.10 key {{ tacacs_key|encrypt("ciscot7") }}
12tacacs-server host 10.0.0.11 key {{ tacacs_key|encrypt("ciscot7") }}
13
14{# SNMP community string #}
15{% set snmp_community = "snmp_ro_community"|load_secret(site=site) %}
16snmp-server community {{ snmp_community }} ro

Interface with Connected Device Context

Template: my-platform/my-role/include/interface-advanced.j2

1{% for intf in device_data|interfaces(prefix="Ethernet") %}
2interface {{ intf.name }}
3 description {{ intf.description }}
4
5 {% if intf.connected_interface %}
6 {# We know what's on the other end #}
7 {% set peer = intf.connected_interface.device %}
8
9 {# Configure based on peer role #}
10 {% if peer.role == "Spine" %}
11 {# Uplink to spine #}
12 no switchport
13 ip address {{ intf.primary_ipv4 }}
14
15 {% elif peer.role == "GPU" %}
16 {# Downlink to GPU server #}
17 switchport mode access
18 switchport access vlan {{ intf.untagged_vlan }}
19 spanning-tree portfast
20
21 {% elif peer.role in ["Storage-Server", "Control-Server"] %}
22 {# Downlink to server with LAG #}
23 channel-group {{ intf.name|regex_replace('Ethernet(\\d+)', '\\1') }} mode active
24
25 {% endif %}
26 {% endif %}
27{% endfor %}

Conclusion

This template rendering system provides a powerful, maintainable approach to network configuration management. By leveraging:

  • Jinja2 inheritance for code reuse
  • Custom filters for data transformation
  • Nautobot as source of truth for structured data
  • Python dataclasses for type safety

You can build scalable configuration templates that adapt to diverse network environments while maintaining consistency and reliability.

For support or questions about template development, contact the CFA team or the nv-config-manager-templates README.

  • Filter quick reference
  • Template expansion walkthrough
  • Render Service
  • API Reference