From 6d9b070c65504f07322fbe21e4f92a5a57f99de6 Mon Sep 17 00:00:00 2001 From: ccoueffe Date: Tue, 12 Aug 2025 17:45:23 +0200 Subject: [PATCH 1/6] update for manual underlay ipv6 --- .../templates/ndfc_underlay_ip_address.j2 | 60 +++++++++- roles/dtc/create/tasks/common/fabric.yml | 12 +- .../208_manual_ipaddress_allocation.py | 108 ++++++++++++++---- 3 files changed, 151 insertions(+), 29 deletions(-) diff --git a/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 b/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 index 3c849600b..50e898ee3 100644 --- a/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 +++ b/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 @@ -17,17 +17,21 @@ {%- endfor %} {% macro generate_loopback_config(loopback_id) %} {%- for switch in vxlan.topology.switches %} -{%- set ns = namespace(ipv4="") %} +{%- set ns = namespace(ip="") %} {%- for interface in switch.interfaces %} {%- if interface.name.lower() == "loopback" ~ loopback_id or interface.name.lower() == "lo" ~ loopback_id %} -{%- set ns.ipv4 = interface.ipv4_address %} +{%- if interface.ipv4_address is defined and interface.ipv4_address is iterable %} +{%- set ns.ip = interface.ipv4_address %} +{%- elif interface.ipv6_address is defined and interface.ipv6_address is iterable %} +{%- set ns.ip = interface.ipv6_address %} +{%- endif %} {%- endif %} {%- endfor %} - entity_name: "{{ switch_list[switch.name].serial_number }}~loopback{{ loopback_id }}" pool_type: IP pool_name: "LOOPBACK{{ loopback_id }}_IP_POOL" scope_type: device_interface - resource: "{{ ns.ipv4 }}" + resource: "{{ ns.ip }}" switch: - "{{ switch_list[switch.name].management_ipv4_address }}" {% endfor %} @@ -48,12 +52,25 @@ - "{{ switch_list[peer.peer1].management_ipv4_address }}" {% endfor %} {% endif %} +{# Configure IPv6 router_id #} +{% for switch in vxlan.topology.switches %} +{% if switch.manual_ipv6_router_id is defined and switch.manual_ipv6_router_id is iterable %} +- entity_name: "{{ switch_list[switch.name].serial_number }}" + pool_type: IP + pool_name: "ROUTER_ID_POOL" + scope_type: device + resource: "{{ switch.manual_ipv6_router_id }}" + switch: + - "{{ switch_list[switch.name].management_ipv4_address }}" +{% endif %} +{% endfor%} + {# build p2p links - Check if ipv4 or ipv6 is present to distinct with fabric_link with template #} {# build subnet #} - {% if vxlan.topology.fabric_links is defined %} {% for switch in vxlan.topology.fabric_links %} -{% if switch.ipv4 is iterable or switch.ipv6 is iterable %} +{# Allocation for IPv4 #} +{% if switch.ipv4 is defined and switch.ipv4 is iterable %} - entity_name: "{{ switch_list[switch.source_device].serial_number }}~{{ switch.source_interface }}~{{ switch_list[switch.dest_device].serial_number }}~{{ switch.dest_interface }}" pool_type: SUBNET pool_name: "SUBNET" @@ -81,7 +98,38 @@ - "{{ switch_list[switch.dest_device].management_ipv4_address }}" {% endif %} {% endif %} -{% endfor %} +{# Allocation for IPv6 #} +{% if switch.ipv6 is defined and switch.ipv6 is iterable %} +- entity_name: "{{ switch_list[switch.source_device].serial_number }}~{{ switch.source_interface }}~{{ + switch_list[switch.dest_device].serial_number }}~{{ switch.dest_interface }}" + pool_type: SUBNET + pool_name: "SUBNET" + scope_type: link + resource: "{{ switch.ipv6.subnet }}" + switch: + - "{{ switch_list[switch.source_device].management_ipv4_address }}" + +{# assign ips #} +{% if switch.ipv6.source_ipv6 is defined and switch.ipv6.dest_ipv6 is defined %} +- entity_name: "{{ switch_list[switch.source_device].serial_number }}~{{ switch.source_interface }}" + pool_type: IP + pool_name: "{{switch.ipv6.subnet}}" + scope_type: device_interface + resource: "{{ switch.ipv6.source_ipv6 }}" + switch: + - "{{ switch_list[switch.source_device].management_ipv4_address }}" + +- entity_name: "{{ switch_list[switch.dest_device].serial_number }}~{{ switch.dest_interface }}" + pool_type: IP + pool_name: "{{switch.ipv6.subnet}}" + scope_type: device_interface + resource: "{{ switch.ipv6.dest_ipv6 }}" + switch: + - "{{ switch_list[switch.dest_device].management_ipv4_address }}" +{% endif %} +{% endif %} + +{% endfor %} {% endif %} {% endif %} diff --git a/roles/dtc/create/tasks/common/fabric.yml b/roles/dtc/create/tasks/common/fabric.yml index 41c4220b7..a4f1560e0 100644 --- a/roles/dtc/create/tasks/common/fabric.yml +++ b/roles/dtc/create/tasks/common/fabric.yml @@ -51,7 +51,8 @@ skip_validation: "{{ True if vxlan.fabric.type == 'ISN' else omit }}" config: "{{ vars_common_local.fabric_config }}" -- name: Create ANYCAST_RP in Nexus Dashboard + +- name: Set and Create ANYCAST_RP in Nexus Dashboard cisco.dcnm.dcnm_resource_manager: state: merged fabric: "{{ vxlan.fabric.name }}" @@ -60,7 +61,14 @@ pool_type: "IP" pool_name: "ANYCAST_RP_IP_POOL" scope_type: "fabric" - resource: "{{ vxlan.underlay.multicast.ipv4.anycast_rp }}" + resource: >- + {{ + vxlan.underlay.multicast.ipv6.anycast_rp + if vxlan.underlay.general.enable_ipv6_underlay | default(false) + else vxlan.underlay.multicast.ipv4.anycast_rp + }} when: - vxlan.underlay.general.manual_underlay_allocation is defined - vxlan.underlay.general.manual_underlay_allocation + - vxlan.underlay.general.replication_mode is defined + - vxlan.underlay.general.replication_mode == "multicast" diff --git a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py index ae7db23f1..d701e9785 100644 --- a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py +++ b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py @@ -15,7 +15,12 @@ def match(cls, inventory): return cls.results # Check if manual_underlay_allocation is set to true general = cls.safeget(inventory, ['vxlan', 'underlay', 'general']) - if "manual_underlay_allocation" in general: + if general.get("enable_ipv6_underlay"): + underlay_af = 6 + else: + underlay_af = 4 + if "manual_underlay_allocation" in general and underlay_af == 4: + # Check if anycast_rp is configured: # Check if anycast_rp is configured check = cls.data_model_key_check(inventory, ['vxlan', 'underlay', 'multicast', 'ipv4']) if 'ipv4' not in check['keys_data']: @@ -51,23 +56,45 @@ def match(cls, inventory): return cls.results interfaces = switch.get("interfaces") - # Check for Loopback{underlay_routing_loopback_id} routing_loopback_name = f"loopback{underlay_routing_loopback_id}" - routing_loopback_found = cls.check_interface_with_ipv4(interfaces, routing_loopback_name) + vtep_loopback_name = f"loopback{underlay_vtep_loopback_id}" - if not routing_loopback_found: - cls.results.append( - f"Switch '{switch_name}' is missing a configured interface '{routing_loopback_name}' with an IPv4 address." - ) + if underlay_af == 4: + routing_loopback_found = cls.check_interface_with_ipv4(interfaces, routing_loopback_name) + vtep_loopback_found = cls.check_interface_with_ipv4(interfaces, vtep_loopback_name) + if not routing_loopback_found: + cls.results.append( + f"Switch '{switch_name}' is missing a configured interface '{routing_loopback_name}' with an IPv{underlay_af} address." + ) + if not vtep_loopback_found: + cls.results.append( + f"Switch '{switch_name}' is missing a configured interface '{vtep_loopback_name}' with an IPv{underlay_af} address." + ) - # Check for Loopback{underlay_vtep_loopback_id} - vtep_loopback_name = f"loopback{underlay_vtep_loopback_id}" - vtep_loopback_found = cls.check_interface_with_ipv4(interfaces, vtep_loopback_name) + if underlay_af == 6: + routing_loopback_found = cls.check_interface_with_ipv6(interfaces, routing_loopback_name) + vtep_loopback_found = cls.check_interface_with_ipv6(interfaces, vtep_loopback_name) + if not routing_loopback_found: + cls.results.append( + f"Switch '{switch_name}' is missing a configured interface '{routing_loopback_name}' with an IPv{underlay_af} address." + ) + if not vtep_loopback_found: + cls.results.append( + f"Switch '{switch_name}' is missing a configured interface '{vtep_loopback_name}' with an IPv{underlay_af} address." + ) + if switch.get('manual_ipv6_router_id', None) is None: + cls.results.append( + f"Switch '{switch_name}' is missing a configured switches.manual_ipv6_router_id' with an IPv{underlay_af} address." + ) - if not vtep_loopback_found: - cls.results.append( - f"Switch '{switch_name}' is missing a configured interface '{vtep_loopback_name}' with an IPv4 address." - ) + check = cls.data_model_key_check(inventory, ["vxlan", "topology", "fabric_links"]) + interface_numbering_v4 = cls.safeget(inventory, ["vxlan", "underlay", "ipv4"]) + interface_numbering_v6 = cls.safeget(inventory, ["vxlan", "underlay", "ipv6"]) + if 'fabric_links' not in check['keys_data'] and ((interface_numbering_v4 and interface_numbering_v4["fabric_interface_numbering"] == "p2p") or ( + interface_numbering_v6 and interface_numbering_v6["enable_ipv6_link_local_address"] is False)): + cls.results.append( + "Fabric Links is not configured, but P2P subnet is expected in this configuration." + ) # Check if vtep_ip exist in vpc_peers cls.validate_vpc_peers_and_vtep_vip(inventory) @@ -111,11 +138,23 @@ def validate_vpc_peers_and_vtep_vip(cls, inventory): if peer.get("fabric_peering") is False or interface_numbering["fabric_interface_numbering"] == "p2p": cls.validate_fabric_links(inventory, vpc_peers_list) + check = cls.data_model_key_check(inventory, ["vxlan", "underlay", "ipv6"]) + if 'ipv6' in check['keys_data']: + interface_numbering = cls.safeget(inventory, ["vxlan", "underlay", "ipv6"]) + # Check IP address under vxlan.topology.fabric_link only if + # fabric numbering is global or fabric peering is false (Use Fabric Peer-Link) + if peer.get("fabric_peering") is False or interface_numbering["enable_ipv6_link_local_address"] is False: + cls.validate_fabric_links(inventory, vpc_peers_list) + + @classmethod def validate_fabric_links(cls, inventory, vpc_peers_list): """ - Validates fabric links to ensure that IPv4 configuration is present for vPC peer connections. + Validates fabric links to ensure that IP configuration is present for vPC peer connections. """ + interface_numbering_v4 = cls.safeget(inventory, ["vxlan", "underlay", "ipv4"]) + interface_numbering_v6 = cls.safeget(inventory, ["vxlan", "underlay", "ipv6"]) + check = cls.data_model_key_check(inventory, ["vxlan", "topology", "fabric_links"]) if 'fabric_links' not in check['keys_data']: @@ -135,13 +174,27 @@ def validate_fabric_links(cls, inventory, vpc_peers_list): fabric_links_name = f"{source_device}-{dest_device}" fabric_links_list.append(fabric_links_name) ipv4_config = link.get("ipv4", {}) - + ipv6_config = link.get("ipv6", {}) # Check IPv4 configuration - if not ipv4_config or not ipv4_config.get("subnet") or not ipv4_config.get("source_ipv4") or not ipv4_config.get("dest_ipv4"): - cls.results.append( - f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv4 configuration." - ) - + if interface_numbering_v4: + if len(ipv4_config.keys()) > 0 and (not ipv4_config.get("subnet") or not ipv4_config.get("source_ipv4") or not ipv4_config.get("dest_ipv4")): + cls.results.append( + f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv4 configuration." + ) + if len(ipv4_config.keys()) == 0 and interface_numbering_v4.get("fabric_interface_numbering", None) == "p2p": + cls.results.append( + f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv4 configuration." + ) + if interface_numbering_v6: + # Check IPv6 configuration + if len(ipv6_config.keys()) > 0 and (not ipv6_config.get("subnet") or not ipv6_config.get("source_ipv6") or not ipv6_config.get("dest_ipv6")): + cls.results.append( + f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv6 configuration." + ) + if len(ipv6_config.keys()) == 0 and interface_numbering_v6["enable_ipv6_link_local_address"] is False: + cls.results.append( + f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv6 configuration." + ) # Check if vpc_peers is on fabric_link vpc_peers = cls.safeget(inventory, ["vxlan", "topology", "vpc_peers"]) for peer in vpc_peers: @@ -165,6 +218,19 @@ def check_interface_with_ipv4(cls, interfaces, loopback_name): return True return False + @classmethod + def check_interface_with_ipv6(cls, interfaces, loopback_name): + """ + Helper method to check if a specific loopback interface exists and has an IPv6 address. + """ + # Create a short_name to catch both values allowed in the schema and change to lower to compare them + short_name = loopback_name.replace("loopback", "lo") + for interface in interfaces: + intf = interface.get("name").lower() + if (intf == loopback_name.lower() or intf == short_name.lower()) and interface.get("ipv6_address"): + return True + return False + @classmethod def data_model_key_check(cls, tested_object, keys): """ From f337588048cc061c84998add306454f247824461 Mon Sep 17 00:00:00 2001 From: ccoueffe Date: Tue, 12 Aug 2025 18:01:43 +0200 Subject: [PATCH 2/6] update script 208 --- .../208_manual_ipaddress_allocation.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py index d701e9785..e6773fbd7 100644 --- a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py +++ b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py @@ -90,11 +90,16 @@ def match(cls, inventory): check = cls.data_model_key_check(inventory, ["vxlan", "topology", "fabric_links"]) interface_numbering_v4 = cls.safeget(inventory, ["vxlan", "underlay", "ipv4"]) interface_numbering_v6 = cls.safeget(inventory, ["vxlan", "underlay", "ipv6"]) - if 'fabric_links' not in check['keys_data'] and ((interface_numbering_v4 and interface_numbering_v4["fabric_interface_numbering"] == "p2p") or ( - interface_numbering_v6 and interface_numbering_v6["enable_ipv6_link_local_address"] is False)): + if ( + 'fabric_links' not in check['keys_data'] + and ( + (interface_numbering_v4 and interface_numbering_v4.get("fabric_interface_numbering") == "p2p") + or (interface_numbering_v6 and interface_numbering_v6.get("enable_ipv6_link_local_address") is False) + ) + ): cls.results.append( - "Fabric Links is not configured, but P2P subnet is expected in this configuration." - ) + "Fabric Links is not configured, but P2P subnet is expected in this configuration." + ) # Check if vtep_ip exist in vpc_peers cls.validate_vpc_peers_and_vtep_vip(inventory) @@ -146,7 +151,6 @@ def validate_vpc_peers_and_vtep_vip(cls, inventory): if peer.get("fabric_peering") is False or interface_numbering["enable_ipv6_link_local_address"] is False: cls.validate_fabric_links(inventory, vpc_peers_list) - @classmethod def validate_fabric_links(cls, inventory, vpc_peers_list): """ @@ -185,8 +189,8 @@ def validate_fabric_links(cls, inventory, vpc_peers_list): cls.results.append( f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv4 configuration." ) - if interface_numbering_v6: # Check IPv6 configuration + if interface_numbering_v6: if len(ipv6_config.keys()) > 0 and (not ipv6_config.get("subnet") or not ipv6_config.get("source_ipv6") or not ipv6_config.get("dest_ipv6")): cls.results.append( f"Fabric link between '{source_device}' and '{dest_device}' is missing a valid IPv6 configuration." From 8c029dfb438bd96aefd68b7a802cc347e29c809e Mon Sep 17 00:00:00 2001 From: ccoueffe Date: Tue, 12 Aug 2025 18:06:53 +0200 Subject: [PATCH 3/6] fix pep8 --- .../rules/ibgp_vxlan/208_manual_ipaddress_allocation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py index e6773fbd7..21b8f33d5 100644 --- a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py +++ b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py @@ -93,12 +93,12 @@ def match(cls, inventory): if ( 'fabric_links' not in check['keys_data'] and ( - (interface_numbering_v4 and interface_numbering_v4.get("fabric_interface_numbering") == "p2p") - or (interface_numbering_v6 and interface_numbering_v6.get("enable_ipv6_link_local_address") is False) + (interface_numbering_v4 and interface_numbering_v4.get("fabric_interface_numbering") == "p2p") + or (interface_numbering_v6 and interface_numbering_v6.get("enable_ipv6_link_local_address") is False) ) ): cls.results.append( - "Fabric Links is not configured, but P2P subnet is expected in this configuration." + "Fabric Links is not configured, but P2P subnet is expected in this configuration." ) # Check if vtep_ip exist in vpc_peers From 0004e1263a1e688b2915a671406ee897b2a803ad Mon Sep 17 00:00:00 2001 From: Charly Coueffe <75327499+ccoueffe@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:08:55 +0200 Subject: [PATCH 4/6] Update fabric.yml --- roles/dtc/create/tasks/common/fabric.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dtc/create/tasks/common/fabric.yml b/roles/dtc/create/tasks/common/fabric.yml index a4f1560e0..a46f6a883 100644 --- a/roles/dtc/create/tasks/common/fabric.yml +++ b/roles/dtc/create/tasks/common/fabric.yml @@ -52,7 +52,7 @@ config: "{{ vars_common_local.fabric_config }}" -- name: Set and Create ANYCAST_RP in Nexus Dashboard +- name: Create ANYCAST_RP in Nexus Dashboard cisco.dcnm.dcnm_resource_manager: state: merged fabric: "{{ vxlan.fabric.name }}" From 80baa3b4ab9df24bdf968fd0dca2a7aaffb22c5f Mon Sep 17 00:00:00 2001 From: Charly Coueffe <75327499+ccoueffe@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:09:42 +0200 Subject: [PATCH 5/6] Update 208_manual_ipaddress_allocation.py --- .../files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py index 21b8f33d5..29d50dfbe 100644 --- a/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py +++ b/roles/validate/files/rules/ibgp_vxlan/208_manual_ipaddress_allocation.py @@ -20,7 +20,6 @@ def match(cls, inventory): else: underlay_af = 4 if "manual_underlay_allocation" in general and underlay_af == 4: - # Check if anycast_rp is configured: # Check if anycast_rp is configured check = cls.data_model_key_check(inventory, ['vxlan', 'underlay', 'multicast', 'ipv4']) if 'ipv4' not in check['keys_data']: From e82599ff859d172682bc3e617b1f1d766ff1dcd7 Mon Sep 17 00:00:00 2001 From: Charly Coueffe <75327499+ccoueffe@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:20:35 +0200 Subject: [PATCH 6/6] Update fabric.yml --- roles/dtc/create/tasks/common/fabric.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dtc/create/tasks/common/fabric.yml b/roles/dtc/create/tasks/common/fabric.yml index a46f6a883..ce1507e8b 100644 --- a/roles/dtc/create/tasks/common/fabric.yml +++ b/roles/dtc/create/tasks/common/fabric.yml @@ -64,7 +64,7 @@ resource: >- {{ vxlan.underlay.multicast.ipv6.anycast_rp - if vxlan.underlay.general.enable_ipv6_underlay | default(false) + if vxlan.underlay.general.enable_ipv6_underlay | default(defaults.vxlan.underlay.general.enable_ipv6_underlay) else vxlan.underlay.multicast.ipv4.anycast_rp }} when: