feat: Add Infrahub Jinja2 transform for MLAG configuration (#22)

- Add GraphQL query mlag_intent.gql: queries InfraMlagPeerConfig by
  device name, returning local IP, peer address, heartbeat peer IP,
  MLAG domain (domain-id, virtual-mac, heartbeat VRF, dual-primary
  detection settings, peer/iBGP VLANs, peer devices), local interface
  SVI name, and peer-link LAG name
- Add Jinja2 template mlag_yang.j2: renders a JSON object targeting
  Arista-native YANG path /arista-mlag-augments:mlag/config; returns
  empty array [] for devices with no MLAG config (spines); all optional
  relationship accesses guarded with 'is defined and is not none' checks
- Update .infrahub.yml: register mlag_intent query and mlag_yang_transform
- Add unit test fixtures for leaf1 (full MLAG config) and spine1 (empty
  response → []) following jinja2-transform-unit-render format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Damien
2026-03-01 11:59:21 +01:00
parent a105e44bbd
commit 21e79e3536
8 changed files with 296 additions and 0 deletions

View File

@@ -26,6 +26,8 @@ queries:
file_path: infrahub/transforms/queries/vxlan_intent.gql
- name: vrf_intent
file_path: infrahub/transforms/queries/vrf_intent.gql
- name: mlag_intent
file_path: infrahub/transforms/queries/mlag_intent.gql
jinja2_transforms:
- name: vlan_yang_transform
@@ -44,3 +46,7 @@ jinja2_transforms:
description: "Generate VRF/L3VNI configuration payload from Infrahub intent"
query: vrf_intent
template_path: infrahub/transforms/templates/vrf_yang.j2
- name: mlag_yang_transform
description: "Generate MLAG configuration payload from Infrahub intent"
query: mlag_intent
template_path: infrahub/transforms/templates/mlag_yang.j2

View File

@@ -0,0 +1,53 @@
query MlagIntent($device_name: String!) {
InfraMlagPeerConfig(device__name__value: $device_name) {
edges {
node {
local_interface_ip { value }
peer_address { value }
heartbeat_peer_ip { value }
device {
node {
name { value }
}
}
mlag_domain {
node {
domain_id { value }
virtual_mac { value }
heartbeat_vrf { value }
dual_primary_detection { value }
dual_primary_delay { value }
dual_primary_action { value }
devices {
edges {
node {
name { value }
}
}
}
peer_vlan {
node {
vlan_id { value }
}
}
ibgp_vlan {
node {
vlan_id { value }
}
}
}
}
local_interface {
node {
name { value }
}
}
peer_link {
node {
name { value }
}
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
{#
mlag_yang.j2 — Produce a JSON object with the MLAG configuration payload
for a single device, targeting Arista-native YANG paths.
Input: GraphQL response from mlag_intent query.
Returns an empty array [] if the device has no MLAG peer config (e.g. spines).
Output structure:
{
"mlag": {
"config": { ... }, # /arista-mlag-augments:mlag/config
"dual_primary_detection": { ... },
"virtual_mac": "...",
"peer_vlan_id": ...,
"ibgp_vlan_id": ...,
"local_interface_ip": "..."
}
}
#}
{%- set peer_configs = data.InfraMlagPeerConfig.edges -%}
{%- if peer_configs | length == 0 -%}
[]
{%- else -%}
{%- set cfg = peer_configs[0].node -%}
{#— Resolve local interface name —#}
{%- set local_iface_name = none -%}
{%- if cfg.local_interface is defined and cfg.local_interface is not none and cfg.local_interface.node is not none -%}
{%- set local_iface_name = cfg.local_interface.node.name.value -%}
{%- endif -%}
{#— Resolve peer-link name —#}
{%- set peer_link_name = none -%}
{%- if cfg.peer_link is defined and cfg.peer_link is not none and cfg.peer_link.node is not none -%}
{%- set peer_link_name = cfg.peer_link.node.name.value -%}
{%- endif -%}
{#— Resolve MLAG domain attributes —#}
{%- set domain_id = none -%}
{%- set virtual_mac = none -%}
{%- set heartbeat_vrf = none -%}
{%- set dual_primary_detection = false -%}
{%- set dual_primary_delay = none -%}
{%- set dual_primary_action = none -%}
{%- set peer_vlan_id = none -%}
{%- set ibgp_vlan_id = none -%}
{%- if cfg.mlag_domain is defined and cfg.mlag_domain is not none and cfg.mlag_domain.node is not none -%}
{%- set domain = cfg.mlag_domain.node -%}
{%- set domain_id = domain.domain_id.value -%}
{%- set virtual_mac = domain.virtual_mac.value | default(none) -%}
{%- set heartbeat_vrf = domain.heartbeat_vrf.value | default("mgmt") -%}
{%- set dual_primary_detection = domain.dual_primary_detection.value | default(false) -%}
{%- set dual_primary_delay = domain.dual_primary_delay.value | default(none) -%}
{%- set dual_primary_action = domain.dual_primary_action.value | default(none) -%}
{%- if domain.peer_vlan is defined and domain.peer_vlan is not none and domain.peer_vlan.node is not none -%}
{%- set peer_vlan_id = domain.peer_vlan.node.vlan_id.value -%}
{%- endif -%}
{%- if domain.ibgp_vlan is defined and domain.ibgp_vlan is not none and domain.ibgp_vlan.node is not none -%}
{%- set ibgp_vlan_id = domain.ibgp_vlan.node.vlan_id.value -%}
{%- endif -%}
{%- endif -%}
{%- set result = {
"mlag": {
"config": {
"domain-id": domain_id,
"peer-link": peer_link_name,
"local-interface": local_iface_name,
"peer-address": cfg.peer_address.value,
"shutdown": false
},
"dual_primary_detection": {
"enabled": dual_primary_detection,
"delay": dual_primary_delay,
"action": dual_primary_action,
"heartbeat_peer_ip": cfg.heartbeat_peer_ip.value,
"heartbeat_vrf": heartbeat_vrf
},
"virtual_mac": virtual_mac,
"peer_vlan_id": peer_vlan_id,
"ibgp_vlan_id": ibgp_vlan_id,
"local_interface_ip": cfg.local_interface_ip.value
}
} -%}
{{ result | tojson(indent=2) }}
{%- endif -%}

View File

@@ -0,0 +1,96 @@
{
"data": {
"InfraMlagPeerConfig": {
"edges": [
{
"node": {
"local_interface_ip": {
"value": "10.0.199.254/31"
},
"peer_address": {
"value": "10.0.199.255"
},
"heartbeat_peer_ip": {
"value": "172.16.0.50"
},
"device": {
"node": {
"name": {
"value": "leaf1"
}
}
},
"mlag_domain": {
"node": {
"domain_id": {
"value": "leafs-1-2"
},
"virtual_mac": {
"value": "c001.cafe.babe"
},
"heartbeat_vrf": {
"value": "mgmt"
},
"dual_primary_detection": {
"value": true
},
"dual_primary_delay": {
"value": 10
},
"dual_primary_action": {
"value": "errdisable"
},
"devices": {
"edges": [
{
"node": {
"name": {
"value": "leaf1"
}
}
},
{
"node": {
"name": {
"value": "leaf2"
}
}
}
]
},
"peer_vlan": {
"node": {
"vlan_id": {
"value": 4090
}
}
},
"ibgp_vlan": {
"node": {
"vlan_id": {
"value": 4091
}
}
}
}
},
"local_interface": {
"node": {
"name": {
"value": "Vlan4090"
}
}
},
"peer_link": {
"node": {
"name": {
"value": "Port-Channel999"
}
}
}
}
}
]
}
}
}

View File

@@ -0,0 +1,22 @@
{
"mlag": {
"config": {
"domain-id": "leafs-1-2",
"peer-link": "Port-Channel999",
"local-interface": "Vlan4090",
"peer-address": "10.0.199.255",
"shutdown": false
},
"dual_primary_detection": {
"enabled": true,
"delay": 10,
"action": "errdisable",
"heartbeat_peer_ip": "172.16.0.50",
"heartbeat_vrf": "mgmt"
},
"virtual_mac": "c001.cafe.babe",
"peer_vlan_id": 4090,
"ibgp_vlan_id": 4091,
"local_interface_ip": "10.0.199.254/31"
}
}

View File

@@ -0,0 +1,7 @@
{
"data": {
"InfraMlagPeerConfig": {
"edges": []
}
}
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,21 @@
---
version: "1.0"
infrahub_tests:
- resource: Jinja2Transform
resource_name: mlag_yang_transform
tests:
- name: smoke_check
spec:
kind: jinja2-transform-smoke
- name: render_leaf1
spec:
kind: jinja2-transform-unit-render
directory: infrahub/transforms/tests/mlag_yang/leaf1
input: input.json
output: output.json
- name: render_spine1
spec:
kind: jinja2-transform-unit-render
directory: infrahub/transforms/tests/mlag_yang/spine1
input: input.json
output: output.json