From 21e79e353653025636ba52be144f89192cb7e3c2 Mon Sep 17 00:00:00 2001 From: Damien Date: Sun, 1 Mar 2026 11:59:21 +0100 Subject: [PATCH] feat: Add Infrahub Jinja2 transform for MLAG configuration (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .infrahub.yml | 6 ++ infrahub/transforms/queries/mlag_intent.gql | 53 ++++++++++ infrahub/transforms/templates/mlag_yang.j2 | 90 +++++++++++++++++ .../tests/mlag_yang/leaf1/input.json | 96 +++++++++++++++++++ .../tests/mlag_yang/leaf1/output.json | 22 +++++ .../tests/mlag_yang/spine1/input.json | 7 ++ .../tests/mlag_yang/spine1/output.json | 1 + infrahub/transforms/tests/mlag_yang/test.yml | 21 ++++ 8 files changed, 296 insertions(+) create mode 100644 infrahub/transforms/queries/mlag_intent.gql create mode 100644 infrahub/transforms/templates/mlag_yang.j2 create mode 100644 infrahub/transforms/tests/mlag_yang/leaf1/input.json create mode 100644 infrahub/transforms/tests/mlag_yang/leaf1/output.json create mode 100644 infrahub/transforms/tests/mlag_yang/spine1/input.json create mode 100644 infrahub/transforms/tests/mlag_yang/spine1/output.json create mode 100644 infrahub/transforms/tests/mlag_yang/test.yml diff --git a/.infrahub.yml b/.infrahub.yml index fc0b43d..a842200 100644 --- a/.infrahub.yml +++ b/.infrahub.yml @@ -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 diff --git a/infrahub/transforms/queries/mlag_intent.gql b/infrahub/transforms/queries/mlag_intent.gql new file mode 100644 index 0000000..2d07d8c --- /dev/null +++ b/infrahub/transforms/queries/mlag_intent.gql @@ -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 } + } + } + } + } + } +} diff --git a/infrahub/transforms/templates/mlag_yang.j2 b/infrahub/transforms/templates/mlag_yang.j2 new file mode 100644 index 0000000..915413f --- /dev/null +++ b/infrahub/transforms/templates/mlag_yang.j2 @@ -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 -%} diff --git a/infrahub/transforms/tests/mlag_yang/leaf1/input.json b/infrahub/transforms/tests/mlag_yang/leaf1/input.json new file mode 100644 index 0000000..35e1033 --- /dev/null +++ b/infrahub/transforms/tests/mlag_yang/leaf1/input.json @@ -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" + } + } + } + } + } + ] + } + } +} diff --git a/infrahub/transforms/tests/mlag_yang/leaf1/output.json b/infrahub/transforms/tests/mlag_yang/leaf1/output.json new file mode 100644 index 0000000..3c30528 --- /dev/null +++ b/infrahub/transforms/tests/mlag_yang/leaf1/output.json @@ -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" + } +} diff --git a/infrahub/transforms/tests/mlag_yang/spine1/input.json b/infrahub/transforms/tests/mlag_yang/spine1/input.json new file mode 100644 index 0000000..ec3d226 --- /dev/null +++ b/infrahub/transforms/tests/mlag_yang/spine1/input.json @@ -0,0 +1,7 @@ +{ + "data": { + "InfraMlagPeerConfig": { + "edges": [] + } + } +} diff --git a/infrahub/transforms/tests/mlag_yang/spine1/output.json b/infrahub/transforms/tests/mlag_yang/spine1/output.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/infrahub/transforms/tests/mlag_yang/spine1/output.json @@ -0,0 +1 @@ +[] diff --git a/infrahub/transforms/tests/mlag_yang/test.yml b/infrahub/transforms/tests/mlag_yang/test.yml new file mode 100644 index 0000000..359b43f --- /dev/null +++ b/infrahub/transforms/tests/mlag_yang/test.yml @@ -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