diff --git a/.infrahub.yml b/.infrahub.yml index f1dce06..fc0b43d 100644 --- a/.infrahub.yml +++ b/.infrahub.yml @@ -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 diff --git a/infrahub/transforms/queries/vrf_intent.gql b/infrahub/transforms/queries/vrf_intent.gql new file mode 100644 index 0000000..0d48b2e --- /dev/null +++ b/infrahub/transforms/queries/vrf_intent.gql @@ -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 } + } + } + } + } + } + } +} diff --git a/infrahub/transforms/templates/vrf_yang.j2 b/infrahub/transforms/templates/vrf_yang.j2 new file mode 100644 index 0000000..cae8f91 --- /dev/null +++ b/infrahub/transforms/templates/vrf_yang.j2 @@ -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) }} diff --git a/infrahub/transforms/tests/vrf_yang/input.json b/infrahub/transforms/tests/vrf_yang/input.json new file mode 100644 index 0000000..f35d7bc --- /dev/null +++ b/infrahub/transforms/tests/vrf_yang/input.json @@ -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" + } + } + } + ] + } + } + } + ] + } + } +} diff --git a/infrahub/transforms/tests/vrf_yang/output.json b/infrahub/transforms/tests/vrf_yang/output.json new file mode 100644 index 0000000..ddefca1 --- /dev/null +++ b/infrahub/transforms/tests/vrf_yang/output.json @@ -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" + } +] diff --git a/infrahub/transforms/tests/vrf_yang/test.yml b/infrahub/transforms/tests/vrf_yang/test.yml new file mode 100644 index 0000000..9e41098 --- /dev/null +++ b/infrahub/transforms/tests/vrf_yang/test.yml @@ -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