diff --git a/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 b/roles/dtc/common/templates/ndfc_underlay_ip_address.j2 index 3c849600..50e898ee 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 41c4220b..ce1507e8 100644 --- a/roles/dtc/create/tasks/common/fabric.yml +++ b/roles/dtc/create/tasks/common/fabric.yml @@ -51,6 +51,7 @@ skip_validation: "{{ True if vxlan.fabric.type == 'ISN' else omit }}" config: "{{ vars_common_local.fabric_config }}" + - name: Create ANYCAST_RP in Nexus Dashboard cisco.dcnm.dcnm_resource_manager: state: merged @@ -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(defaults.vxlan.underlay.general.enable_ipv6_underlay) + 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 ae7db23f..29d50dfb 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,11 @@ 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 = cls.data_model_key_check(inventory, ['vxlan', 'underlay', 'multicast', 'ipv4']) if 'ipv4' not in check['keys_data']: @@ -51,23 +55,50 @@ 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.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." + ) # Check if vtep_ip exist in vpc_peers cls.validate_vpc_peers_and_vtep_vip(inventory) @@ -111,11 +142,22 @@ 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 +177,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." + ) + # 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." + ) + 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 +221,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): """