Network Template Rendering System
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
Components
-
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
-
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
- Located in
-
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
-
Dataclasses: Structured Python objects for type-safe data handling
Interface: Network interface representationBGPPeer: BGP peering informationVRF: Virtual routing and forwarding instanceConnectedDevice: Details about connected neighbors
Template Structure
Templates are organized in a hierarchical directory structure:
Template Lookup
The renderer determines which entrypoint templates to use based on device attributes:
- Platform: Extracted from device’s platform field (e.g., “Cumulus Linux” →
cumulus-linux) - Role: Extracted from device’s role field (e.g., “TAN-Leaf” →
tan-leaf) - 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:
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
Example: Cumulus Linux TAN-Leaf
Level 1: cumulus-linux/role_common/base/startup.yaml.j2
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
This implements role-specific blocks by including role-specific templates.
Level 3: cumulus-linux/tan-leaf/5.6.0/entrypoint/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
Role-Specific Interface Template: cumulus-linux/tan-leaf/include/interface.j2
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
Example Usage:
Routing & BGP
Example Usage:
Interface Operations
Interface Object Properties:
Example Usage:
Specialized Filters
BGP Filters
Transform BGP ASN formats.
Example:
IP Filters
Network calculations and IP address manipulations.
Example Usage:
ISIS Filters
IS-IS protocol helpers.
Example:
Location Filters
Extract site/location-level data (operates on location_data, not device_data).
Example Usage:
Vault Filters
Secret management and encryption (for passwords, keys, and so on).
Secret Loading Modes:
- Production: Queries Hashicorp Vault directly
- Kubernetes: Reads from injected secret files
- Development: Returns dummy value (
{path}:{key}) whenNV_CONFIG_MANAGER_SKIP_VAULT=1
Example Usage:
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:
Location Query Structure
The location query (query_location_data.graphql) retrieves site-level data:
Config Context
The config_context field contains custom JSON data defined in Nautobot:
Allowed password-mapping roles are admin, ro, and rw.
Creating Templates
Determine Template Location
Based on your device, identify:
- Platform:
cumulus-linux,arista-eos,mlnx-os,nv-os - Role:
tan-leaf,smn-spine,wan,oob-switch, and so on - 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
Create Include Templates
Implement specific configuration sections:
File: {platform}/{role}/include/interface.j2
Create Entrypoint Template
File: {platform}/{role}/{version}/entrypoint/{config-file}.j2
Configure Nautobot
Ensure the device in Nautobot has:
- Platform set correctly
- Role set correctly
- Config Context with
intended-firmware.versionmatching your template path - Required interface, IP, and BGP data populated
Test your template
-
Cache device data:
-
Render locally:
-
Add expected output: save the rendered output to
tests/resources/expected_config/my-device_startup.yaml(naming convention is{hostname}_{entrypoint}). -
Run tests:
uv run pytest tests/nv_config_manager_templates/test_render.py.
Best Practices
Template Design
-
Use Inheritance: Do not duplicate configuration. Extend base templates and override only what differs.
-
Keep Includes Focused: Each include template should handle one logical section (interfaces, routing, QoS, and so on).
-
Fail Explicitly: Use filters with
fail_if_missing=True(default) to catch data issues early. -
Handle Optional Data: Check for None before using optional fields:
-
Use Filters for Logic: Move complex logic into filters rather than templates:
-
Whitespace Management: Jinja2
trim_blocks=Trueis enabled. Use{%-and-%}for additional control:
Filter Development
-
Single Responsibility: Each filter should do one thing well.
-
Type Hints: Use type hints for clarity:
-
Error Handling: Raise
FilterExceptionwith clear messages: -
Test Coverage: Every filter must have unit tests:
-
Immutable Data: Use
frozen=Truedataclasses to prevent accidental mutations.
Nautobot Data Management
-
Consistent Naming: Follow consistent naming conventions for devices, interfaces, VLANs, and so on.
-
Config Context: Use config context for:
- Intended firmware versions
- BGP ASNs and configuration
- Password key mappings
- Feature flags
-
Interface Roles: Define and use interface roles consistently (Uplink, Downlink, Management, and so on)
-
Tags: Use tags for:
- Feature enablement (
enable-feature-x) - Grouping devices (
production,staging) - Interface classification
- Feature enablement (
-
IP Hierarchy: Maintain proper prefix parent-child relationships:
- Rail aggregates → Device prefixes → /31 point-to-point links
Version Management
-
Version-Specific Changes Only: Only create version-specific templates when absolutely necessary.
-
Feature Detection: When possible, use tags rather than version checks:
-
Deprecation Path: When deprecating old versions, keep templates for graceful migration.
Examples
Simple Interface Configuration
Template: my-platform/my-role/include/interface.j2
BGP Configuration with Peer Groups
Template: my-platform/my-role/include/bgp.j2
Conditional Configuration Based on Tags
Template: my-platform/my-role/include/features.j2
DHCP Server Configuration
Template: cumulus-linux/cin-leaf/base/dhcpd.conf.j2
Multi-VRF BGP Configuration
Template: my-platform/my-role/include/bgp-vrf.j2
Secret Management
Template: my-platform/my-role/include/management.j2
Interface with Connected Device Context
Template: my-platform/my-role/include/interface-advanced.j2
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.