Documentation Index
Fetch the complete documentation index at: https://mintlify.com/OpsMill/infrahub/llms.txt
Use this file to discover all available pages before exploring further.
Jinja2 templates provide a powerful way to generate text-based artifacts from Infrahub data. They’re ideal for creating device configurations, documentation, and structured text output.
What is Jinja2?
Jinja2 is a modern templating engine for Python that allows you to generate text output with:
- Variable substitution
- Control flow (loops, conditionals)
- Filters and functions
- Template inheritance
- Macros for reusable logic
Infrahub uses Jinja2 to transform GraphQL query results into configuration files and other text-based artifacts.
1. Define a GraphQL Query
First, create a GraphQL query to retrieve the data you need:
# templates/device_startup_info.gql
query device_startup_info ($device: String!) {
InfraDevice(name__value: $device) {
edges {
node {
id
name { value }
asn {
node {
asn { value }
}
}
interfaces {
edges {
node {
id
name { value }
description { value }
enabled { value }
mtu { value }
role { value }
... on InfraInterfaceL3 {
ip_addresses {
edges {
node {
address { value }
}
}
}
}
}
}
}
}
}
}
}
2. Create the Jinja2 Template
Create a template file that uses the GraphQL query data:
{# templates/device_startup_config.tpl.j2 #}
hostname {{ data.InfraDevice.edges[0].node.name.value }}
!
username admin privilege 15 secret admin123
!
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
interface {{ intf.node.name.value }}
{% if intf.node.description.value %}
description {{ intf.node.description.value }}
{% else %}
description role: {{ intf.node.role.value }}
{% endif %}
{% if intf.node.mtu.value %}
mtu {{ intf.node.mtu.value }}
{% endif %}
{% if not intf.node.enabled.value %}
shutdown
{% endif %}
{% if intf.node.ip_addresses %}
{% for ip in intf.node.ip_addresses.edges %}
ip address {{ ip.node.address.value }}
no switchport
{% endfor %}
{% endif %}
!
{% endfor %}
3. Register in .infrahub.yml
Add the query and transform to your repository configuration:
# .infrahub.yml
queries:
- name: device_startup_info
file_path: "templates/device_startup_info.gql"
jinja2_transforms:
- name: device_startup
description: "Template to generate startup configuration for network devices"
query: device_startup_info
template_path: "templates/device_startup_config.tpl.j2"
Data Structure
The GraphQL query response is passed to the template as a data dictionary with the exact structure returned by GraphQL:
data = {
"InfraDevice": {
"edges": [
{
"node": {
"id": "...",
"name": {"value": "edge-router-01"},
"interfaces": {
"edges": [
{
"node": {
"name": {"value": "GigabitEthernet0/0"},
"enabled": {"value": true},
"role": {"value": "uplink"}
}
}
]
}
}
}
]
}
}
Access data in templates using dot notation:
{{ data.InfraDevice.edges[0].node.name.value }}
Template Syntax Reference
Variables
Access and output variables:
{{ data.InfraDevice.edges[0].node.name.value }}
{{ intf.node.description.value }}
{{ ip.node.address.value }}
Conditionals
Control flow with if/elif/else:
{% if intf.node.description.value %}
description {{ intf.node.description.value }}
{% else %}
description role: {{ intf.node.role.value }}
{% endif %}
{% if intf.node.role.value == "loopback" %}
ip ospf network loopback
{% elif intf.node.role.value == "peer" %}
ip ospf network point-to-point
{% endif %}
Loops
Iterate over lists:
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
interface {{ intf.node.name.value }}
!
{% endfor %}
{% for ip in intf.node.ip_addresses.edges %}
ip address {{ ip.node.address.value }}
{% endfor %}
Namespace Variables
Use namespace to share variables across scopes:
{% set ns = namespace(loopback_ip=none, management_ip=none) %}
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{% if intf.node.role.value == "loopback" %}
{% set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{% elif intf.node.role.value == "management" %}
{% set ns.management_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{% endif %}
{% endfor %}
router ospf 1
router-id {{ ns.loopback_ip }}
Filters
Modify variables with filters:
{# String operations #}
{{ device_name | upper }}
{{ description | lower }}
{{ text | replace('old', 'new') }}
{# String manipulation #}
{{ ip_address | split('/')[0] }}
{# List operations #}
{{ interfaces | length }}
{{ interfaces | first }}
{{ interfaces | last }}
Add comments that won’t appear in output:
{# This is a comment #}
{#
Multi-line comment
Won't appear in output
#}
Real-World Example: Arista Device Configuration
Complete example from Infrahub’s demo repository:
{% set ns = namespace(loopback_intf_name=none, loopback_ip=none, management_intf_name=none, management_ip=none) %}
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{% if intf.node.role.value == "loopback" %}
{% set ns.loopback_intf_name = intf.node.name.value %}
{% set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{% elif intf.node.role.value == "management" %}
{% set ns.management_intf_name = intf.node.name.value %}
{% set ns.management_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{% endif %}
{% endfor %}
no aaa root
!
username admin privilege 15 role network-admin secret sha512 $6$...
!
transceiver qsfp default-mode 4x10G
!
service routing protocols model multi-agent
!
hostname {{ data.InfraDevice.edges[0].node.name.value }}
!
spanning-tree mode mstp
!
management api http-commands
no shutdown
!
management api gnmi
transport grpc default
!
management api netconf
transport ssh default
!
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{% if intf.node.name.value != ns.management_intf_name and intf.node.name.value != ns.loopback_intf_name %}
interface {{ intf.node.name.value }}
{% if intf.node["description"]["value"] %}
description {{ intf.node["description"]["value"] }}
{% else %}
description role: {{ intf.node.role.value }}
{% endif %}
{% if "mtu" in intf.node and intf.node["mtu"]["value"] %}
mtu {{ intf.node["mtu"]["value"] }}
{% endif %}
{% if not intf.node["enabled"]["value"] %}
shutdown
{% endif %}
{% if intf.node["ip_addresses"] %}
{% for ip in intf.node["ip_addresses"]["edges"] %}
ip address {{ ip.node["address"]["value"] }}
no switchport
{% if intf.node.role.value == "peer" or intf.node.role.value == "backbone" %}
ip ospf network point-to-point
{% endif %}
{% endfor %}
{% endif %}
!
{% endif %}
{% endfor %}
!
interface {{ ns.management_intf_name }}
{% for intf in data.InfraDevice.edges[0]["interfaces"] %}
{% if intf.node.name.value == ns.management_intf_name %}
{% for ip in intf["ip_addresses"] %}
ip address {{ ip["address"]["value"] }}
{% endfor %}
{% endif %}
{% endfor %}
!
interface {{ ns.loopback_intf_name }}
{% for intf in data.InfraDevice.edges[0]["interfaces"] %}
{% if intf.node.name.value == ns.loopback_intf_name %}
{% for ip in intf["ip_addresses"] %}
ip address {{ ip["address"]["value"] }}
{% endfor %}
{% endif %}
{% endfor %}
!
ip routing
!
{% if data.InfraDevice.edges[0].node.asn %}
router bgp {{ data.InfraDevice.edges[0].node.asn.node.asn.value }}
router-id {{ ns.loopback_ip }}
!
{% endif %}
!
router ospf 1
router-id {{ ns.loopback_ip }}
redistribute connected
max-lsa 12000
passive-interface Loopback0
network 0.0.0.0/0 area 0.0.0.0
!
end
For simpler use cases like data extraction:
{# templates/person_with_cars.j2 #}
Name: {{ data.TestingPerson.edges[0].node.name.value }}
With configuration:
jinja2_transforms:
- name: person_with_cars
description: "Template to a report card showing a person and the cars they own"
query: "person_with_cars"
template_path: "templates/person_with_cars.j2"
Using with Artifacts
Combine Jinja2 transforms with artifact definitions:
artifact_definitions:
- name: "Startup Config for Edge devices"
artifact_name: "startup-config"
parameters:
device: "name__value"
content_type: "text/plain"
targets: "edge_router"
transformation: "device_startup" # References jinja2_transforms name
When the artifact is generated:
- Target devices are identified from the
edge_router group
- For each device, the
device parameter is extracted
- GraphQL query executes with
device variable
- Jinja2 template renders with query response
- Result is stored as an artifact
Error Handling
Infrahub catches and reports template errors:
from infrahub_sdk.template.exceptions import JinjaTemplateError
try:
result = await jinja2_template.render(variables=data)
except JinjaTemplateError as exc:
# Template syntax error, undefined variable, etc.
raise TransformError(
repository_name=repo_name,
commit=commit,
location=template_path,
message=exc.message
)
Common errors:
- Undefined variables: Accessing data that doesn’t exist
- Template syntax errors: Invalid Jinja2 syntax
- Type errors: Attempting invalid operations on data
Best Practices
-
Check for existence: Always verify data exists before accessing
{% if intf.node.description.value %}
description {{ intf.node.description.value }}
{% endif %}
-
Use meaningful variable names: Make templates readable
{% for interface in device.interfaces.edges %}
{# Clear what 'interface' represents #}
{% endfor %}
-
Add comments: Explain complex logic
{# Extract loopback IP for OSPF router-id #}
{% set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
-
Keep templates focused: One template per configuration type
-
Test with sample data: Validate templates before production
-
Handle missing data gracefully: Provide defaults or skip sections
{% if intf.node.mtu.value %}
mtu {{ intf.node.mtu.value }}
{% endif %}
-
Use consistent indentation: Match output format requirements
-
Optimize GraphQL queries: Request only needed fields
Template Execution
Templates are rendered using the Infrahub SDK’s Jinja2Template class:
from infrahub_sdk.template import Jinja2Template
from pathlib import Path
jinja2_template = Jinja2Template(
template=Path("template.j2"),
template_directory=Path("/repo/templates")
)
rendered = await jinja2_template.render(variables={"data": query_result})
The render process:
- Loads template from repository worktree
- Validates template syntax
- Passes GraphQL query response as
data variable
- Renders template with Jinja2 engine
- Returns rendered string
Timeout Configuration
Jinja2 transforms have configurable timeouts:
jinja2_transforms:
- name: device_startup
query: device_startup_info
template_path: "templates/device_startup_config.tpl.j2"
timeout: 30 # 30 seconds (default: 10)
Next Steps