feat: Add Infrahub Jinja2 transform for VRF/L3VNI configuration (#21)

- Add GraphQL query vrf_intent.gql: queries InfraVRFDeviceAssignment by
  device name, returning device-specific RD, VRF attributes (name,
  description, l3vni), and both device-level and VRF-level import/export
  route targets for fallback logic
- Add Jinja2 template vrf_yang.j2: renders a JSON array of VRF config
  objects with RD fallback (device → VRF-level) and RT merge logic
  (device-level if present, otherwise VRF-level); all optional
  relationships guarded with 'is defined and is not none' checks
- Update .infrahub.yml: register vrf_intent query and vrf_yang_transform
- Add unit test fixtures for leaf3: input.json (mock GraphQL response)
  and output.json (expected rendered JSON array)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Damien
2026-03-01 11:30:39 +01:00
parent 668e9cbada
commit 7b40c5fbe1
6 changed files with 246 additions and 0 deletions

View File

@@ -24,6 +24,8 @@ queries:
file_path: infrahub/transforms/queries/interface_intent.gql
- name: vxlan_intent
file_path: infrahub/transforms/queries/vxlan_intent.gql
- name: vrf_intent
file_path: infrahub/transforms/queries/vrf_intent.gql
jinja2_transforms:
- name: vlan_yang_transform
@@ -38,3 +40,7 @@ jinja2_transforms:
description: "Generate VXLAN/VTEP configuration payload from Infrahub intent"
query: vxlan_intent
template_path: infrahub/transforms/templates/vxlan_yang.j2
- name: vrf_yang_transform
description: "Generate VRF/L3VNI configuration payload from Infrahub intent"
query: vrf_intent
template_path: infrahub/transforms/templates/vrf_yang.j2

View File

@@ -0,0 +1,50 @@
query VrfIntent($device_name: String!) {
InfraVRFDeviceAssignment(device__name__value: $device_name) {
edges {
node {
route_distinguisher { value }
vrf {
node {
name { value }
description { value }
route_distinguisher { value }
l3vni {
node {
vni { value }
vni_type { value }
}
}
import_targets {
edges {
node {
target { value }
}
}
}
export_targets {
edges {
node {
target { value }
}
}
}
}
}
import_targets {
edges {
node {
target { value }
}
}
}
export_targets {
edges {
node {
target { value }
}
}
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
{#
vrf_yang.j2 — Produce a JSON array of VRF configuration objects.
Input: GraphQL response from vrf_intent query.
For each VRFDeviceAssignment, produce one VRF config entry with:
- device-specific RD (falls back to VRF-level RD if not set on assignment)
- L3VNI from the VRF's l3vni relationship
- import/export targets: device-level if present, otherwise VRF-level
#}
{%- set vrf_list = [] -%}
{%- for assignment_edge in data.InfraVRFDeviceAssignment.edges -%}
{%- set assignment = assignment_edge.node -%}
{#— Resolve VRF node —#}
{%- set vrf_name = none -%}
{%- set vrf_description = none -%}
{%- set l3vni = none -%}
{%- set vrf_level_import_rts = [] -%}
{%- set vrf_level_export_rts = [] -%}
{%- if assignment.vrf is defined and assignment.vrf is not none and assignment.vrf.node is not none -%}
{%- set vrf_node = assignment.vrf.node -%}
{%- set vrf_name = vrf_node.name.value -%}
{%- set vrf_description = vrf_node.description.value | default(none) -%}
{%- if vrf_node.l3vni is defined and vrf_node.l3vni is not none and vrf_node.l3vni.node is not none -%}
{%- set l3vni = vrf_node.l3vni.node.vni.value -%}
{%- endif -%}
{%- for rt_edge in vrf_node.import_targets.edges -%}
{%- set _ = vrf_level_import_rts.append(rt_edge.node.target.value) -%}
{%- endfor -%}
{%- for rt_edge in vrf_node.export_targets.edges -%}
{%- set _ = vrf_level_export_rts.append(rt_edge.node.target.value) -%}
{%- endfor -%}
{%- endif -%}
{#— Resolve route_distinguisher: device-specific first, fall back to VRF-level —#}
{%- set rd = none -%}
{%- if assignment.route_distinguisher is defined and assignment.route_distinguisher is not none and assignment.route_distinguisher.value is not none and assignment.route_distinguisher.value != "" -%}
{%- set rd = assignment.route_distinguisher.value -%}
{%- elif assignment.vrf is defined and assignment.vrf is not none and assignment.vrf.node is not none and assignment.vrf.node.route_distinguisher is defined and assignment.vrf.node.route_distinguisher is not none -%}
{%- set rd = assignment.vrf.node.route_distinguisher.value | default(none) -%}
{%- endif -%}
{#— Resolve import targets: device-level if present, otherwise VRF-level —#}
{%- set import_rts = [] -%}
{%- if assignment.import_targets is defined and assignment.import_targets is not none and assignment.import_targets.edges | length > 0 -%}
{%- for rt_edge in assignment.import_targets.edges -%}
{%- set _ = import_rts.append(rt_edge.node.target.value) -%}
{%- endfor -%}
{%- else -%}
{%- set import_rts = vrf_level_import_rts -%}
{%- endif -%}
{#— Resolve export targets: device-level if present, otherwise VRF-level —#}
{%- set export_rts = [] -%}
{%- if assignment.export_targets is defined and assignment.export_targets is not none and assignment.export_targets.edges | length > 0 -%}
{%- for rt_edge in assignment.export_targets.edges -%}
{%- set _ = export_rts.append(rt_edge.node.target.value) -%}
{%- endfor -%}
{%- else -%}
{%- set export_rts = vrf_level_export_rts -%}
{%- endif -%}
{%- set _ = vrf_list.append({
"vrf_name": vrf_name,
"description": vrf_description,
"route_distinguisher": rd,
"l3vni": l3vni,
"import_targets": import_rts,
"export_targets": export_rts,
"redistribute_connected": true
}) -%}
{%- endfor -%}
{{ vrf_list | tojson(indent=2) }}

View File

@@ -0,0 +1,82 @@
{
"data": {
"InfraVRFDeviceAssignment": {
"edges": [
{
"node": {
"route_distinguisher": {
"value": "10.0.250.13:1"
},
"vrf": {
"node": {
"name": {
"value": "gold"
},
"description": {
"value": "VRF gold - L3 VXLAN with symmetric IRB"
},
"route_distinguisher": {
"value": null
},
"l3vni": {
"node": {
"vni": {
"value": 100001
},
"vni_type": {
"value": "l3vni"
}
}
},
"import_targets": {
"edges": [
{
"node": {
"target": {
"value": "1:100001"
}
}
}
]
},
"export_targets": {
"edges": [
{
"node": {
"target": {
"value": "1:100001"
}
}
}
]
}
}
},
"import_targets": {
"edges": [
{
"node": {
"target": {
"value": "1:100001"
}
}
}
]
},
"export_targets": {
"edges": [
{
"node": {
"target": {
"value": "1:100001"
}
}
}
]
}
}
}
]
}
}
}

View File

@@ -0,0 +1,15 @@
[
{
"description": "VRF gold - L3 VXLAN with symmetric IRB",
"export_targets": [
"1:100001"
],
"import_targets": [
"1:100001"
],
"l3vni": 100001,
"redistribute_connected": true,
"route_distinguisher": "10.0.250.13:1",
"vrf_name": "gold"
}
]

View File

@@ -0,0 +1,15 @@
---
version: "1.0"
infrahub_tests:
- resource: Jinja2Transform
resource_name: vrf_yang_transform
tests:
- name: smoke_check
spec:
kind: jinja2-transform-smoke
- name: render_leaf3
spec:
kind: jinja2-transform-unit-render
directory: infrahub/transforms/tests/vrf_yang
input: input.json
output: output.json