diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/DC1-BL1A.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/DC1-BL1A.yml
index c3ea942b090..694e69cd41f 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/DC1-BL1A.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/DC1-BL1A.yml
@@ -629,6 +629,8 @@ metadata:
value: EOS_DESIGNS_UNIT_TESTS
- name: topology_hint_type
value: leaf
+ - name: topology_hint_rack
+ value: DC1_BL1
sflow:
vrfs:
- name: OOB
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-edge.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-edge.yml
index 87d15969a98..4d18040257c 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-edge.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-edge.yml
@@ -210,3 +210,46 @@ vxlan_interface:
vrfs:
- name: default
vni: 1
+metadata:
+ cv_tags:
+ device_tags:
+ - name: Role
+ value: edge
+ - name: Region
+ value: AVD_Land_East
+ - name: Zone
+ value: DEFAULT-ZONE
+ - name: Site
+ value: Site511
+ interface_tags:
+ - interface: Ethernet1
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: ATT
+ - name: Circuit
+ value: '666'
+ - interface: Ethernet2
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Colt
+ - name: Circuit
+ value: '10555'
+ cv_pathfinder:
+ role: edge
+ vtep_ip: 192.168.42.1
+ region: AVD_Land_East
+ zone: DEFAULT-ZONE
+ site: Site511
+ interfaces:
+ - name: Ethernet1
+ carrier: ATT
+ pathgroup: INET
+ - name: Ethernet2
+ carrier: Colt
+ pathgroup: MPLS
+ pathfinders:
+ - vtep_ip: 192.168.44.1
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder.yml
index 760db54b905..3967e3e4ce1 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder.yml
@@ -204,3 +204,84 @@ vxlan_interface:
vrfs:
- name: default
vni: 1
+metadata:
+ cv_tags:
+ device_tags:
+ - name: Role
+ value: pathfinder
+ - name: PathfinderSet
+ value: PATHFINDERS
+ interface_tags:
+ - interface: Ethernet1
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Bouygues_Telecom
+ - name: Circuit
+ value: '777'
+ - interface: Ethernet2
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Colt
+ - name: Circuit
+ value: '10000'
+ - interface: Ethernet3
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Another-ISP
+ - name: Circuit
+ value: '999'
+ cv_pathfinder:
+ role: pathfinder
+ vtep_ip: 192.168.44.1
+ interfaces:
+ - name: Ethernet1
+ carrier: Bouygues_Telecom
+ pathgroup: INET
+ public_ip: 10.7.7.7
+ - name: Ethernet2
+ carrier: Colt
+ pathgroup: MPLS
+ public_ip: 172.16.0.1
+ - name: Ethernet3
+ carrier: Another-ISP
+ pathgroup: INET
+ public_ip: 10.9.9.9
+ pathgroups:
+ - name: MPLS
+ carriers:
+ - name: Colt
+ - name: ATT-MPLS
+ - name: INET
+ carriers:
+ - name: Comcast
+ - name: ATT
+ - name: Bouygues_Telecom
+ - name: SFR
+ - name: Orange
+ - name: Another-ISP
+ - name: LTE
+ regions:
+ - name: AVD_Land_West
+ id: 42
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site422
+ id: 422
+ location:
+ address: Somewhere
+ - name: AVD_Land_East
+ id: 43
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site511
+ id: 511
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder1.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder1.yml
index 06b6b0b9de7..91363cf08e0 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder1.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder1.yml
@@ -214,3 +214,60 @@ vxlan_interface:
vrfs:
- name: default
vni: 1
+metadata:
+ cv_tags:
+ device_tags:
+ - name: Role
+ value: pathfinder
+ - name: PathfinderSet
+ value: PATHFINDERS
+ interface_tags:
+ - interface: Ethernet1
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Orange
+ - name: Circuit
+ value: '888'
+ cv_pathfinder:
+ role: pathfinder
+ vtep_ip: 192.168.44.2
+ interfaces:
+ - name: Ethernet1
+ carrier: Orange
+ pathgroup: INET
+ public_ip: 10.8.8.8
+ pathgroups:
+ - name: MPLS
+ carriers:
+ - name: Colt
+ - name: ATT-MPLS
+ - name: INET
+ carriers:
+ - name: Comcast
+ - name: ATT
+ - name: Bouygues_Telecom
+ - name: SFR
+ - name: Orange
+ - name: Another-ISP
+ - name: LTE
+ regions:
+ - name: AVD_Land_West
+ id: 42
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site422
+ id: 422
+ location:
+ address: Somewhere
+ - name: AVD_Land_East
+ id: 43
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site511
+ id: 511
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder2.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder2.yml
index 1eee5cd3489..af94d773154 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder2.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-pathfinder2.yml
@@ -232,3 +232,72 @@ vxlan_interface:
vrfs:
- name: default
vni: 1
+metadata:
+ cv_tags:
+ device_tags:
+ - name: Role
+ value: pathfinder
+ - name: PathfinderSet
+ value: PATHFINDERS
+ interface_tags:
+ - interface: Ethernet1
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: SFR
+ - name: Circuit
+ value: '999'
+ - interface: Ethernet2
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: ATT-MPLS
+ - name: Circuit
+ value: '10999'
+ cv_pathfinder:
+ role: pathfinder
+ vtep_ip: 192.168.44.3
+ interfaces:
+ - name: Ethernet1
+ carrier: SFR
+ pathgroup: INET
+ public_ip: 10.9.9.9
+ - name: Ethernet2
+ carrier: ATT-MPLS
+ pathgroup: MPLS
+ public_ip: 172.19.9.9
+ pathgroups:
+ - name: MPLS
+ carriers:
+ - name: Colt
+ - name: ATT-MPLS
+ - name: INET
+ carriers:
+ - name: Comcast
+ - name: ATT
+ - name: Bouygues_Telecom
+ - name: SFR
+ - name: Orange
+ - name: Another-ISP
+ - name: LTE
+ regions:
+ - name: AVD_Land_West
+ id: 42
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site422
+ id: 422
+ location:
+ address: Somewhere
+ - name: AVD_Land_East
+ id: 43
+ zones:
+ - name: DEFAULT-ZONE
+ id: 1
+ sites:
+ - name: Site511
+ id: 511
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-transit.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-transit.yml
index 28215c556ce..de58006329e 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-transit.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/intended/structured_configs/cv-pathfinder-transit.yml
@@ -220,3 +220,46 @@ vxlan_interface:
vrfs:
- name: default
vni: 1
+metadata:
+ cv_tags:
+ device_tags:
+ - name: Role
+ value: transit region
+ - name: Region
+ value: AVD_Land_West
+ - name: Zone
+ value: DEFAULT-ZONE
+ - name: Site
+ value: Site422
+ interface_tags:
+ - interface: Ethernet1
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Comcast
+ - name: Circuit
+ value: '667'
+ - interface: Ethernet2
+ tags:
+ - name: Type
+ value: wan
+ - name: Carrier
+ value: Colt
+ - name: Circuit
+ value: '10666'
+ cv_pathfinder:
+ role: transit region
+ vtep_ip: 192.168.43.1
+ region: AVD_Land_West
+ zone: DEFAULT-ZONE
+ site: Site422
+ interfaces:
+ - name: Ethernet1
+ carrier: Comcast
+ pathgroup: INET
+ - name: Ethernet2
+ carrier: Colt
+ pathgroup: MPLS
+ pathfinders:
+ - vtep_ip: 192.168.44.1
diff --git a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/inventory/group_vars/CV_PATHFINDER_TESTS.yml b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/inventory/group_vars/CV_PATHFINDER_TESTS.yml
index b5e131305ce..f6e410668e3 100644
--- a/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/inventory/group_vars/CV_PATHFINDER_TESTS.yml
+++ b/ansible_collections/arista/avd/molecule/eos_designs_unit_tests/inventory/group_vars/CV_PATHFINDER_TESTS.yml
@@ -11,6 +11,7 @@ cv_pathfinder_regions:
sites:
- name: Site422
id: 422
+ location: Somewhere
- name: AVD_Land_East
id: 43
description: AVD Region
diff --git a/ansible_collections/arista/avd/plugins/plugin_utils/eos_designs_shared_utils/wan.py b/ansible_collections/arista/avd/plugins/plugin_utils/eos_designs_shared_utils/wan.py
index 5b11eed8c29..2c677885d3f 100644
--- a/ansible_collections/arista/avd/plugins/plugin_utils/eos_designs_shared_utils/wan.py
+++ b/ansible_collections/arista/avd/plugins/plugin_utils/eos_designs_shared_utils/wan.py
@@ -6,7 +6,9 @@
from functools import cached_property
from typing import TYPE_CHECKING
-from ansible_collections.arista.avd.plugins.plugin_utils.errors import AristaAvdError
+from ansible_collections.arista.avd.plugins.filter.natural_sort import natural_sort
+from ansible_collections.arista.avd.plugins.plugin_utils.errors import AristaAvdError, AristaAvdMissingVariableError
+from ansible_collections.arista.avd.plugins.plugin_utils.strip_empties import strip_empties_from_dict
from ansible_collections.arista.avd.plugins.plugin_utils.utils import get, get_item
if TYPE_CHECKING:
@@ -70,24 +72,27 @@ def wan_interfaces(self: SharedUtils) -> list:
return wan_interfaces
+ @cached_property
+ def wan_carriers(self: SharedUtils) -> list:
+ return get(self.hostvars, "wan_carriers", required=True)
+
@cached_property
def wan_local_carriers(self: SharedUtils) -> list:
"""
List of carriers present on this router based on the wan_interfaces with the associated WAN interfaces
interfaces:
- name: ...
- ip: ...
+ ip: ... (for route-servers the IP may come from wan_route_servers)
"""
if not self.wan_role:
return []
local_carriers_dict = {}
- global_carriers = get(self.hostvars, "wan_carriers", required=True)
for interface in self.wan_interfaces:
interface_carrier = interface["wan_carrier"]
if interface_carrier not in local_carriers_dict:
local_carriers_dict[interface_carrier] = get_item(
- global_carriers,
+ self.wan_carriers,
"name",
interface["wan_carrier"],
required=True,
@@ -98,13 +103,17 @@ def wan_local_carriers(self: SharedUtils) -> list:
local_carriers_dict[interface_carrier]["interfaces"].append(
{
"name": get(interface, "name", required=True),
- "ip_address": get(interface, "ip", required=True),
+ "ip_address": self.get_public_ip_for_wan_interface(interface),
"connected_to_pathfinder": get(interface, "connected_to_pathfinder", default=True),
}
)
return list(local_carriers_dict.values())
+ @cached_property
+ def wan_path_groups(self: SharedUtils) -> list:
+ return get(self.hostvars, "wan_path_groups", required=True)
+
@cached_property
def wan_local_path_groups(self: SharedUtils) -> list:
"""
@@ -118,12 +127,12 @@ def wan_local_path_groups(self: SharedUtils) -> list:
return []
local_path_groups_dict = {}
- global_path_groups = get(self.hostvars, "wan_path_groups", required=True)
+
for carrier in self.wan_local_carriers:
path_group_name = get(carrier, "path_group", required=True)
if path_group_name not in local_path_groups_dict:
local_path_groups_dict[path_group_name] = get_item(
- global_path_groups,
+ self.wan_path_groups,
"name",
path_group_name,
required=True,
@@ -136,3 +145,188 @@ def wan_local_path_groups(self: SharedUtils) -> list:
local_path_groups_dict[path_group_name]["interfaces"].extend(carrier["interfaces"])
return list(local_path_groups_dict.values())
+
+ @cached_property
+ def this_wan_route_server(self: SharedUtils) -> dict:
+ """
+ Returns the instance for this wan_rs found under wan_route_servers.
+ Should only be called when the device is actually a wan_rs.
+ """
+ wan_route_servers = get(self.hostvars, "wan_route_servers", default=[])
+ return get_item(wan_route_servers, "hostname", self.hostname, default={})
+
+ def get_public_ip_for_wan_interface(self: SharedUtils, interface: dict) -> str:
+ """
+ Takes a dict which looks like `l3_interface` from node config
+
+ If not a WAN route-server this just returns the interface IP.
+
+ For WAN route-servers we try to find the IP under wan_route_servers.path_groups.interfaces.
+ If not found we use the IP under the interface, unless it is "dhcp" where we raise.
+ """
+ if self.wan_role != "server":
+ return interface["ip"]
+
+ for path_group in self.this_wan_route_server.get("path_groups", []):
+ if (found_interface := get_item(path_group["interfaces"], "name", interface["name"])) is None:
+ continue
+
+ if found_interface.get("ip_address") is not None:
+ return found_interface["ip_address"]
+
+ if interface["ip"] == "dhcp":
+ raise AristaAvdError(
+ f"The IP address for WAN interface '{interface['name']}' on Route Server '{self.hostname}' is set 'dhcp'."
+ "Clients need to peer with a static IP which must be set under the 'wan_route_servers.path_groups.interfaces' key."
+ )
+
+ return interface["ip"]
+
+ @cached_property
+ def wan_site(self: SharedUtils) -> dict:
+ """
+ WAN site for CV Pathfinder
+ """
+ node_defined_site = get(
+ self.switch_data_combined,
+ "cv_pathfinder_site",
+ required=True,
+ org_key="A node variable 'cv_pathfinder_site' must be defined when 'cv_pathfinder_role' is 'edge' or 'transit'.",
+ )
+ sites = get(self.wan_region, "sites", required=True, org_key=f"The CV Pathfinder region '{self.wan_region['name']}' is missing a list of sites")
+ return get_item(
+ sites,
+ "name",
+ node_defined_site,
+ required=True,
+ custom_error_msg=(
+ f"The 'cv_pathfinder_site '{node_defined_site}' defined at the node level could not be found under the 'sites' list for the region"
+ f" '{self.wan_region['name']}'."
+ ),
+ )
+
+ @cached_property
+ def wan_region(self: SharedUtils) -> dict:
+ """
+ WAN region for CV Pathfinder
+
+ Also checking if site names are unique across all regions.
+ """
+ node_defined_region = get(
+ self.switch_data_combined,
+ "cv_pathfinder_region",
+ required=True,
+ org_key="A node variable 'cv_pathfinder_region' must be defined when 'cv_pathfinder_role' is 'edge' or 'transit'.",
+ )
+ regions = get(
+ self.hostvars, "cv_pathfinder_regions", required=True, org_key="'cv_pathfinder_regions' key must be set when 'wan_mode' is 'cv-pathfinder'."
+ )
+
+ # Verify that site names are unique across all regions.
+ site_names = [site["name"] for region in regions for site in region["sites"]]
+ if len(site_names) != len(set(site_names)):
+ # We have some site names that are not unique
+ # Now find them (slow so no need to do if we don't have duplicates)
+ duplicate_site_names = [site_name for site_name in site_names if site_names.count(site_name) > 1]
+ raise AristaAvdError(
+ "WAN Site names must be unique across all regions. "
+ f"Found the following duplicate site name(s) under 'cv_pathfinder_regions.[].sites. {duplicate_site_names}"
+ )
+
+ return get_item(
+ regions,
+ "name",
+ node_defined_region,
+ required=True,
+ custom_error_msg="The 'cv_pathfinder_region' defined at the node level could not be found under the 'cv_pathfinder_regions' key.",
+ )
+
+ @property
+ def wan_zone(self: SharedUtils) -> dict:
+ """
+ WAN zone for Pathfinder
+
+ Currently, only default zone DEFAULT-ZONE with ID 1 is supported.
+ """
+ # Injecting zone DEFAULT-ZONE with id 1.
+ return {"name": "DEFAULT-ZONE", "id": 1}
+
+ @cached_property
+ def filtered_wan_route_servers(self: SharedUtils) -> dict:
+ """
+ Return a dict keyed by Wan RR based on the the wan_mode type with only the path_groups the router should connect to.
+
+ It the RR is part of the inventory, the peer_facts are read..
+ If any key is specified in the variables, it overwrites whatever is in the peer_facts.
+
+ If no peer_fact is found the variables are required in the inventory.
+ """
+ wan_route_servers = {}
+
+ wan_route_servers_list = get(self.hostvars, "wan_route_servers", default=[])
+
+ for wan_rs_dict in natural_sort(wan_route_servers_list, sort_key="hostname"):
+ # These remote gw can be outside of the inventory
+ wan_rs = wan_rs_dict["hostname"]
+
+ if wan_rs == self.hostname:
+ # Don't add yourself
+ continue
+
+ if (peer_facts := self.get_peer_facts(wan_rs, required=False)) is not None:
+ # Found a matching server in inventory
+ bgp_as = peer_facts.get("bgp_as")
+ # Only ibgp is supported for WAN so raise if peer from peer_facts BGP AS is different from ours.
+ if bgp_as != self.bgp_as:
+ raise AristaAvdError(f"Only iBGP is supported for WAN, the BGP AS {bgp_as} on {wan_rs} is different from our own: {self.bgp_as}.")
+
+ # Prefer values coming from the input variables over peer facts
+ router_id = get(wan_rs_dict, "router_id", default=peer_facts.get("router_id"))
+ wan_path_groups = get(wan_rs_dict, "path_groups", default=peer_facts.get("wan_path_groups"))
+
+ if router_id is None:
+ raise AristaAvdMissingVariableError(
+ f"'router_id' is missing for peering with {wan_rs}, either set it in under 'wan_route_servers' or something is wrong with the peer"
+ " facts."
+ )
+ if wan_path_groups is None:
+ raise AristaAvdMissingVariableError(
+ f"'wan_path_groups' is missing for peering with {wan_rs}, either set it in under 'wan_route_servers'"
+ " or something is wrong with the peer facts."
+ )
+
+ else:
+ # Retrieve the values from the dictionary, making them required if the peer_facts were not found
+ router_id = get(wan_rs_dict, "router_id", required=True)
+ wan_path_groups = get(
+ wan_rs_dict,
+ "path_groups",
+ required=True,
+ org_key=(
+ f"'path_groups' is missing for peering with {wan_rs} which was not found in the inventory, either set it in under 'wan_route_servers'"
+ " or check your inventory."
+ ),
+ )
+
+ # Filtering wan_path_groups to only take the ones this device uses to connect to pathfinders.
+ wan_rs_result_dict = {
+ "router_id": router_id,
+ "wan_path_groups": [path_group for path_group in wan_path_groups if self.should_connect_to_wan_rs([path_group["name"]])],
+ }
+
+ wan_route_servers[wan_rs] = strip_empties_from_dict(wan_rs_result_dict)
+
+ return wan_route_servers
+
+ def should_connect_to_wan_rs(self: SharedUtils, path_groups: list) -> bool:
+ """
+ This helper implements whether or not a connection to the wan_router_server should be made or not based on a list of path-groups.
+
+ To do this the logic is the following:
+ * Look at the wan_interfaces on the router and check if there is any path-group in common with the RR where
+ `connected_to_pathfinder` is not False.
+ """
+ return any(
+ local_path_group["name"] in path_groups and any(wan_interface["connected_to_pathfinder"] for wan_interface in local_path_group["interfaces"])
+ for local_path_group in self.wan_local_path_groups
+ )
diff --git a/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md b/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
index e8ce449ef24..b9130ead69a 100644
--- a/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
+++ b/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
@@ -18,6 +18,52 @@
| [ tags](## "metadata.cv_tags.interface_tags.[].tags") | List, items: Dictionary | | | | |
| [ - name](## "metadata.cv_tags.interface_tags.[].tags.[].name") | String | Required | | | |
| [ value](## "metadata.cv_tags.interface_tags.[].tags.[].value") | String | Required | | | |
+ | [ cv_pathfinder](## "metadata.cv_pathfinder") | Dictionary | | | | Metadata used for CV Pathfinder visualization on CloudVision |
+ | [ role](## "metadata.cv_pathfinder.role") | String | | | | |
+ | [ region](## "metadata.cv_pathfinder.region") | String | | | | |
+ | [ zone](## "metadata.cv_pathfinder.zone") | String | | | | |
+ | [ site](## "metadata.cv_pathfinder.site") | String | | | | |
+ | [ vtep_ip](## "metadata.cv_pathfinder.vtep_ip") | String | | | | |
+ | [ ssl_profile](## "metadata.cv_pathfinder.ssl_profile") | String | | | | |
+ | [ pathfinders](## "metadata.cv_pathfinder.pathfinders") | List, items: Dictionary | | | | |
+ | [ - vtep_ip](## "metadata.cv_pathfinder.pathfinders.[].vtep_ip") | String | | | | |
+ | [ interfaces](## "metadata.cv_pathfinder.interfaces") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.interfaces.[].name") | String | | | | |
+ | [ carrier](## "metadata.cv_pathfinder.interfaces.[].carrier") | String | | | | |
+ | [ circuit_id](## "metadata.cv_pathfinder.interfaces.[].circuit_id") | String | | | | |
+ | [ pathgroup](## "metadata.cv_pathfinder.interfaces.[].pathgroup") | String | | | | |
+ | [ public_ip](## "metadata.cv_pathfinder.interfaces.[].public_ip") | String | | | | |
+ | [ pathgroups](## "metadata.cv_pathfinder.pathgroups") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.pathgroups.[].name") | String | | | | |
+ | [ carriers](## "metadata.cv_pathfinder.pathgroups.[].carriers") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.pathgroups.[].carriers.[].name") | String | | | | |
+ | [ imported_carriers](## "metadata.cv_pathfinder.pathgroups.[].imported_carriers") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.pathgroups.[].imported_carriers.[].name") | String | | | | |
+ | [ regions](## "metadata.cv_pathfinder.regions") | List, items: Dictionary | | | | |
+ | [ - id](## "metadata.cv_pathfinder.regions.[].id") | Integer | | | | |
+ | [ name](## "metadata.cv_pathfinder.regions.[].name") | String | | | | |
+ | [ zones](## "metadata.cv_pathfinder.regions.[].zones") | List, items: Dictionary | | | | |
+ | [ - id](## "metadata.cv_pathfinder.regions.[].zones.[].id") | Integer | | | | |
+ | [ name](## "metadata.cv_pathfinder.regions.[].zones.[].name") | String | | | | |
+ | [ sites](## "metadata.cv_pathfinder.regions.[].zones.[].sites") | List, items: Dictionary | | | | |
+ | [ - id](## "metadata.cv_pathfinder.regions.[].zones.[].sites.[].id") | Integer | | | | |
+ | [ name](## "metadata.cv_pathfinder.regions.[].zones.[].sites.[].name") | String | | | | |
+ | [ location](## "metadata.cv_pathfinder.regions.[].zones.[].sites.[].location") | Dictionary | | | | |
+ | [ address](## "metadata.cv_pathfinder.regions.[].zones.[].sites.[].location.address") | String | | | | |
+ | [ vrfs](## "metadata.cv_pathfinder.vrfs") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.vrfs.[].name") | String | | | | |
+ | [ vni](## "metadata.cv_pathfinder.vrfs.[].vni") | Integer | | | | |
+ | [ avts](## "metadata.cv_pathfinder.vrfs.[].avts") | List, items: Dictionary | | | | |
+ | [ - constraints](## "metadata.cv_pathfinder.vrfs.[].avts.[].constraints") | Dictionary | | | | |
+ | [ jitter](## "metadata.cv_pathfinder.vrfs.[].avts.[].constraints.jitter") | Integer | | | | |
+ | [ latency](## "metadata.cv_pathfinder.vrfs.[].avts.[].constraints.latency") | Integer | | | | |
+ | [ lossrate](## "metadata.cv_pathfinder.vrfs.[].avts.[].constraints.lossrate") | String | | | | |
+ | [ description](## "metadata.cv_pathfinder.vrfs.[].avts.[].description") | String | | | | |
+ | [ id](## "metadata.cv_pathfinder.vrfs.[].avts.[].id") | Integer | | | | |
+ | [ name](## "metadata.cv_pathfinder.vrfs.[].avts.[].name") | String | | | | |
+ | [ pathgroups](## "metadata.cv_pathfinder.vrfs.[].avts.[].pathgroups") | List, items: Dictionary | | | | |
+ | [ - name](## "metadata.cv_pathfinder.vrfs.[].avts.[].pathgroups.[].name") | String | | | | |
+ | [ preference](## "metadata.cv_pathfinder.vrfs.[].avts.[].pathgroups.[].preference") | String | | | | |
=== "YAML"
@@ -35,4 +81,52 @@
tags:
- name:
value:
+
+ # Metadata used for CV Pathfinder visualization on CloudVision
+ cv_pathfinder:
+ role:
+ region:
+ zone:
+ site:
+ vtep_ip:
+ ssl_profile:
+ pathfinders:
+ - vtep_ip:
+ interfaces:
+ - name:
+ carrier:
+ circuit_id:
+ pathgroup:
+ public_ip:
+ pathgroups:
+ - name:
+ carriers:
+ - name:
+ imported_carriers:
+ - name:
+ regions:
+ - id:
+ name:
+ zones:
+ - id:
+ name:
+ sites:
+ - id:
+ name:
+ location:
+ address:
+ vrfs:
+ - name:
+ vni:
+ avts:
+ - constraints:
+ jitter:
+ latency:
+ lossrate:
+ description:
+ id:
+ name:
+ pathgroups:
+ - name:
+ preference:
```
diff --git a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.jsonschema.json b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.jsonschema.json
index 69bc6478094..9674457a480 100644
--- a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.jsonschema.json
+++ b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.jsonschema.json
@@ -10284,6 +10284,308 @@
"^_.+$": {}
},
"title": "Cv Tags"
+ },
+ "cv_pathfinder": {
+ "type": "object",
+ "description": "Metadata used for CV Pathfinder visualization on CloudVision",
+ "properties": {
+ "role": {
+ "type": "string",
+ "title": "Role"
+ },
+ "region": {
+ "type": "string",
+ "title": "Region"
+ },
+ "zone": {
+ "type": "string",
+ "title": "Zone"
+ },
+ "site": {
+ "type": "string",
+ "title": "Site"
+ },
+ "vtep_ip": {
+ "type": "string",
+ "title": "Vtep IP"
+ },
+ "ssl_profile": {
+ "type": "string",
+ "title": "SSL Profile"
+ },
+ "pathfinders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vtep_ip": {
+ "type": "string",
+ "title": "Vtep IP"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Pathfinders"
+ },
+ "interfaces": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "carrier": {
+ "type": "string",
+ "title": "Carrier"
+ },
+ "circuit_id": {
+ "type": "string",
+ "title": "Circuit ID"
+ },
+ "pathgroup": {
+ "type": "string",
+ "title": "Pathgroup"
+ },
+ "public_ip": {
+ "type": "string",
+ "title": "Public IP"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Interfaces"
+ },
+ "pathgroups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "carriers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Carriers"
+ },
+ "imported_carriers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Imported Carriers"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Pathgroups"
+ },
+ "regions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "title": "ID"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "zones": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "title": "ID"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "sites": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "title": "ID"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "location": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string",
+ "title": "Address"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ },
+ "title": "Location"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Sites"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Zones"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Regions"
+ },
+ "vrfs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "vni": {
+ "type": "integer",
+ "title": "Vni"
+ },
+ "avts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "constraints": {
+ "type": "object",
+ "properties": {
+ "jitter": {
+ "type": "integer",
+ "title": "Jitter"
+ },
+ "latency": {
+ "type": "integer",
+ "title": "Latency"
+ },
+ "lossrate": {
+ "type": "string",
+ "title": "Lossrate"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ },
+ "title": "Constraints"
+ },
+ "description": {
+ "type": "string",
+ "title": "Description"
+ },
+ "id": {
+ "type": "integer",
+ "title": "ID"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "pathgroups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "preference": {
+ "type": "string",
+ "title": "Preference"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Pathgroups"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "Avts"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ }
+ },
+ "title": "VRFs"
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^_.+$": {}
+ },
+ "title": "Cv Pathfinder"
}
},
"additionalProperties": false,
diff --git a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.schema.yml b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.schema.yml
index 4d2b1ce1ded..f34dac555ce 100644
--- a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.schema.yml
+++ b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/eos_cli_config_gen.schema.yml
@@ -6070,6 +6070,135 @@ keys:
value:
type: str
required: true
+ cv_pathfinder:
+ type: dict
+ description: Metadata used for CV Pathfinder visualization on CloudVision
+ keys:
+ role:
+ type: str
+ region:
+ type: str
+ zone:
+ type: str
+ site:
+ type: str
+ vtep_ip:
+ type: str
+ ssl_profile:
+ type: str
+ pathfinders:
+ type: list
+ items:
+ type: dict
+ keys:
+ vtep_ip:
+ type: str
+ interfaces:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ carrier:
+ type: str
+ circuit_id:
+ type: str
+ pathgroup:
+ type: str
+ public_ip:
+ type: str
+ pathgroups:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ carriers:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ imported_carriers:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ regions:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ zones:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ sites:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ location:
+ type: dict
+ keys:
+ address:
+ type: str
+ vrfs:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ vni:
+ type: int
+ avts:
+ type: list
+ items:
+ type: dict
+ keys:
+ constraints:
+ type: dict
+ keys:
+ jitter:
+ type: int
+ latency:
+ type: int
+ lossrate:
+ type: str
+ description:
+ type: str
+ id:
+ type: int
+ name:
+ type: str
+ pathgroups:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ preference:
+ type: str
mlag_configuration:
type: dict
display_name: Multi-Chassis Link Aggregation (MLAG) Configuration
diff --git a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/schema_fragments/metadata.schema.yml b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/schema_fragments/metadata.schema.yml
index ff6c2c014aa..38fa8f8c49e 100644
--- a/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/schema_fragments/metadata.schema.yml
+++ b/ansible_collections/arista/avd/roles/eos_cli_config_gen/schemas/schema_fragments/metadata.schema.yml
@@ -47,3 +47,132 @@ keys:
value:
type: str
required: true
+ cv_pathfinder:
+ type: dict
+ description: Metadata used for CV Pathfinder visualization on CloudVision
+ keys:
+ role:
+ type: str
+ region:
+ type: str
+ zone:
+ type: str
+ site:
+ type: str
+ vtep_ip:
+ type: str
+ ssl_profile:
+ type: str
+ pathfinders:
+ type: list
+ items:
+ type: dict
+ keys:
+ vtep_ip:
+ type: str
+ interfaces:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ carrier:
+ type: str
+ circuit_id:
+ type: str
+ pathgroup:
+ type: str
+ public_ip:
+ type: str
+ pathgroups:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ carriers:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ imported_carriers:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ regions:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ zones:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ sites:
+ type: list
+ items:
+ type: dict
+ keys:
+ id:
+ type: int
+ name:
+ type: str
+ location:
+ type: dict
+ keys:
+ address:
+ type: str
+ vrfs:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ vni:
+ type: int
+ avts:
+ type: list
+ items:
+ type: dict
+ keys:
+ constraints:
+ type: dict
+ keys:
+ jitter:
+ type: int
+ latency:
+ type: int
+ lossrate:
+ type: str
+ description:
+ type: str
+ id:
+ type: int
+ name:
+ type: str
+ pathgroups:
+ type: list
+ items:
+ type: dict
+ keys:
+ name:
+ type: str
+ preference:
+ type: str
diff --git a/ansible_collections/arista/avd/roles/eos_designs/docs/wan-preview.md b/ansible_collections/arista/avd/roles/eos_designs/docs/wan-preview.md
index 977b10e3daa..aeadd457d69 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/docs/wan-preview.md
+++ b/ansible_collections/arista/avd/roles/eos_designs/docs/wan-preview.md
@@ -143,3 +143,25 @@ roles/eos_designs/docs/tables/node-type-wan-configuration.md
--8<--
roles/eos_designs/docs/tables/node-type-key-wan-configuration.md
--8<--
+
+### CloudVision Tags
+
+`arista.avd.eos_designs` will generate CloudVision Tags that assist CloudVision with visualizing the WAN.
+
+#### Device Tags
+
+| Tag Name | Source of information |
+| -------- | --------------------- |
+| `Region` | `cv_pathfinder_region` if `cv_pathfinder_role` is set but not `pathfinder` |
+| `Zone` | `DEFAULT-ZONE` if `cv_pathfinder_role` is set but not `pathfinder` |
+| `Site` | `cv_pathfinder_site` if `cv_pathfinder_role` is set but not `pathfinder` |
+| `PathfinderSet` | name of `node_group` or default `PATHFINDERS` if `cv_pathfinder_role` is `pathfinder` |
+| `Role` | `cv_pathfinder_role` if set |
+
+#### Interface Tags
+
+| Hint Tag Name | Source of information |
+| ------------- | --------------------- |
+| `Type` | `lan` or `wan` if `cv_pathfinder_role` is set |
+| `Carrier` | `wan_carrier` if `cv_pathfinder_role` is set and this is a WAN interface |
+| `Circuit` | `wan_circiot_id` if `cv_pathfinder_role` is set and this is a LAN interface |
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/avdstructuredconfig.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/avdstructuredconfig.py
index 6d41d0358b4..2b69d42a263 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/avdstructuredconfig.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/avdstructuredconfig.py
@@ -6,12 +6,13 @@
from functools import cached_property
from ansible_collections.arista.avd.plugins.plugin_utils.avdfacts import AvdFacts
-from ansible_collections.arista.avd.plugins.plugin_utils.strip_empties import strip_null_from_data
+from ansible_collections.arista.avd.plugins.plugin_utils.strip_empties import strip_empties_from_dict
+from .cv_pathfinder import CvPathfinderMixin
from .cv_tags import CvTagsMixin
-class AvdStructuredConfigMetadata(AvdFacts, CvTagsMixin):
+class AvdStructuredConfigMetadata(AvdFacts, CvTagsMixin, CvPathfinderMixin):
"""
This returns the metadata data strucutre as per the below example
{
@@ -25,20 +26,26 @@ class AvdStructuredConfigMetadata(AvdFacts, CvTagsMixin):
{"name": "topology_hint_pod", "value": },
{"name": "topolgoy_hint_rack", "value": },
{"name": "", "value": "custom tag value"},
- {"name": "", "value": ""}
+ {"name": "", "value": ""},
+ {"name": "Region", "value": },
+ {"name": "Zone", "value": },
+ {"name": "Site", "value": },
+ {"name": "PathfinderSet", "value": },
+ {"name": "Role", "value": }
},
"interface_tags": [
{
"interface": "Ethernet1",
"tags":[
- {
- "name": "peer"
- "value": "leaf1a"
- }
+ {"name": "peer", "value": "leaf1a"}
+ {"name": "Type", <"lan" or "wan" if cv_pathfinder_role is set>},
+ {"name": "Carrier", },
+ {"name": "Circuit", }
]
}
]
- }
+ },
+ "cv_pathfinder": {}
}
}
"""
@@ -48,5 +55,6 @@ def metadata(self) -> dict | None:
metadata = {
"platform": self.shared_utils.platform,
"cv_tags": self._cv_tags(),
+ "cv_pathfinder": self._cv_pathfinder(),
}
- return strip_null_from_data(metadata) or None
+ return strip_empties_from_dict(metadata) or None
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_pathfinder.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_pathfinder.py
new file mode 100644
index 00000000000..6fb900eb5cc
--- /dev/null
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_pathfinder.py
@@ -0,0 +1,126 @@
+# Copyright (c) 2023-2024 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from ansible_collections.arista.avd.plugins.plugin_utils.utils import get
+
+if TYPE_CHECKING:
+ from .avdstructuredconfig import AvdStructuredConfigMetadata
+
+
+class CvPathfinderMixin:
+ """
+ Mixin Class used to generate structured config for one key.
+ Class should only be used as Mixin to a AvdStructuredConfig class
+ """
+
+ def _cv_pathfinder(self: AvdStructuredConfigMetadata) -> dict | None:
+ """
+ Generate metadata for CV Pathfinder feature.
+ Only relevant if cv_pathfinder_role is not None
+ """
+ if self.shared_utils.cv_pathfinder_role is None:
+ return None
+
+ # Pathfinder
+ if self.shared_utils.cv_pathfinder_role == "pathfinder":
+ return {
+ "role": self.shared_utils.cv_pathfinder_role,
+ "ssl_profile": None, # TODO: Pick up ssl profile from self.shared_utils.this_wan_route_server.ssl_profile_name
+ "vtep_ip": self.shared_utils.router_id,
+ "interfaces": self._metadata_interfaces(),
+ "pathgroups": self._metadata_pathgroups(),
+ "regions": self._metadata_regions(),
+ "vrfs": self._metadata_vrfs(),
+ }
+
+ # Edge or transit
+ return {
+ "role": self.shared_utils.cv_pathfinder_role,
+ "vtep_ip": self.shared_utils.router_id,
+ "region": self.shared_utils.wan_region["name"],
+ "zone": self.shared_utils.wan_zone["name"],
+ "site": self.shared_utils.wan_site["name"],
+ "interfaces": self._metadata_interfaces(),
+ "pathfinders": self._metadata_pathfinder_vtep_ips(),
+ }
+
+ def _metadata_interfaces(self: AvdStructuredConfigMetadata) -> list:
+ return [
+ {
+ "name": interface["name"],
+ "carrier": carrier["name"],
+ "circuit_id": interface.get("wan_circuit_id"),
+ "pathgroup": carrier["path_group"],
+ "public_ip": str(interface["ip_address"]).split("/", maxsplit=1)[0] if self.shared_utils.cv_pathfinder_role == "pathfinder" else None,
+ }
+ for carrier in self.shared_utils.wan_local_carriers
+ for interface in carrier["interfaces"]
+ ]
+
+ def _metadata_pathgroups(self: AvdStructuredConfigMetadata) -> list:
+ return [
+ {
+ "name": pathgroup["name"],
+ "carriers": [
+ {
+ "name": carrier["name"],
+ }
+ for carrier in self.shared_utils.wan_carriers
+ if carrier["path_group"] == pathgroup["name"]
+ ],
+ "imported_carriers": [
+ {
+ "name": carrier["name"],
+ }
+ for carrier in self.shared_utils.wan_carriers
+ if carrier["path_group"] in [imported_pathgroup["remote"] for imported_pathgroup in pathgroup.get("import_path_groups", [])]
+ ],
+ }
+ for pathgroup in self.shared_utils.wan_path_groups
+ ]
+
+ def _metadata_regions(self: AvdStructuredConfigMetadata) -> list:
+ regions = get(
+ self._hostvars, "cv_pathfinder_regions", required=True, org_key="'cv_pathfinder_regions' key must be set when 'wan_mode' is 'cv-pathfinder'."
+ )
+ return [
+ {
+ "name": region["name"],
+ "id": region["id"],
+ "zones": [
+ {
+ # TODO: Once we give configurable zones this should be updated
+ "name": self.shared_utils.wan_zone["name"],
+ "id": self.shared_utils.wan_zone["id"],
+ "sites": [
+ {
+ "name": site["name"],
+ "id": site["id"],
+ "location": {
+ "address": site.get("location"),
+ }
+ if site.get("location")
+ else None,
+ }
+ for site in region["sites"]
+ ],
+ }
+ ],
+ }
+ for region in regions
+ ]
+
+ def _metadata_vrfs(self: AvdStructuredConfigMetadata) -> list:
+ return [] # TODO
+
+ def _metadata_pathfinder_vtep_ips(self: AvdStructuredConfigMetadata) -> list:
+ return [
+ {
+ "vtep_ip": wan_route_server["router_id"],
+ }
+ for wan_route_server in self.shared_utils.filtered_wan_route_servers.values()
+ ]
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_tags.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_tags.py
index e45abbbc1ae..a2983a9bfd8 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_tags.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/metadata/cv_tags.py
@@ -8,7 +8,7 @@
from ansible_collections.arista.avd.plugins.plugin_utils.errors import AristaAvdError
from ansible_collections.arista.avd.plugins.plugin_utils.strip_empties import strip_empties_from_dict, strip_empties_from_list
-from ansible_collections.arista.avd.plugins.plugin_utils.utils import get
+from ansible_collections.arista.avd.plugins.plugin_utils.utils import default, get, get_item
if TYPE_CHECKING:
from .avdstructuredconfig import AvdStructuredConfigMetadata
@@ -36,7 +36,7 @@
"hostname",
"terminattr",
]
-"""These tag names overlap with CV system tags"""
+"""These tag names overlap with CV system tags or topology_hints"""
class CvTagsMixin:
@@ -53,10 +53,11 @@ def _cv_tags(self: AvdStructuredConfigMetadata) -> dict | None:
"""
Generate the data structure `metadata.cv_tags`.
"""
- if not self._generate_cv_tags:
+ if not self._generate_cv_tags and not self.shared_utils.cv_pathfinder_role:
return None
device_tags = self._get_topology_hints()
+ device_tags.extend(self._get_cv_pathfinder_device_tags())
device_tags.extend(self._get_device_tags())
cv_tags = {"device_tags": device_tags, "interface_tags": self._get_interface_tags()}
@@ -83,10 +84,40 @@ def _get_topology_hints(self: AvdStructuredConfigMetadata) -> list:
self._tag_dict("topology_hint_fabric", self.shared_utils.fabric_name),
self._tag_dict("topology_hint_pod", self.shared_utils.pod_name),
self._tag_dict("topology_hint_type", get(self._hostvars, "cv_tags_topology_type", default=default_type_hint)),
- self._tag_dict("topology_hint_rack", self.shared_utils.rack),
+ self._tag_dict("topology_hint_rack", default(self.shared_utils.rack, self.shared_utils.group)),
]
)
+ def _get_cv_pathfinder_device_tags(self: AvdStructuredConfigMetadata) -> list:
+ """
+ Return list of device_tags for cv_pathfinder solution
+ Example: [
+ {"name": "Region", "value": },
+ {"name": "Zone", "value": },
+ {"name": "Site", "value": },
+ {"name": "PathfinderSet", "value": },
+ {"name": "Role", "value": }
+ ]
+ """
+ if self.shared_utils.cv_pathfinder_role is None:
+ return []
+
+ device_tags = [
+ self._tag_dict("Role", self.shared_utils.cv_pathfinder_role),
+ ]
+ if self.shared_utils.cv_pathfinder_role == "pathfinder":
+ device_tags.append(self._tag_dict("PathfinderSet", self.shared_utils.group or "PATHFINDERS"))
+ else:
+ device_tags.extend(
+ [
+ self._tag_dict("Region", self.shared_utils.wan_region["name"]),
+ self._tag_dict("Zone", "DEFAULT-ZONE"),
+ self._tag_dict("Site", self.shared_utils.wan_site["name"]),
+ ]
+ )
+
+ return strip_empties_from_list(device_tags)
+
def _get_device_tags(self: AvdStructuredConfigMetadata) -> list:
"""
Return list of device_tags
@@ -125,7 +156,7 @@ def _get_interface_tags(self: AvdStructuredConfigMetadata) -> list:
"""
Return list of interface_tags
"""
- if not (tags_to_generate := get(self._generate_cv_tags, "interface_tags")):
+ if not (tags_to_generate := get(self._generate_cv_tags, "interface_tags", default=[])) and not self.shared_utils.cv_pathfinder_role:
return []
interface_tags = []
@@ -151,7 +182,38 @@ def _get_interface_tags(self: AvdStructuredConfigMetadata) -> list:
if value:
tags.append(self._tag_dict(generate_tag["name"], value))
+ tags.extend(self._get_cv_pathfinder_interface_tags(ethernet_interface))
+
if tags:
interface_tags.append({"interface": ethernet_interface["name"], "tags": tags})
return interface_tags
+
+ def _get_cv_pathfinder_interface_tags(self: AvdStructuredConfigMetadata, ethernet_interface: dict) -> list:
+ """
+ Return list of device_tags for cv_pathfinder solution
+ Example: [
+ {"name": "Type", <"lan" or "wan" if cv_pathfinder_role is set>},
+ {"name": "Carrier", },
+ {"name": "Circuit", }
+ ]
+ """
+ # Skip if not cv_pathfinder_role or if this is a subinterface.
+ if self.shared_utils.cv_pathfinder_role is None or "." in ethernet_interface["name"]:
+ return []
+
+ if ethernet_interface["name"] in self._wan_interface_names:
+ wan_interface = get_item(self.shared_utils.wan_interfaces, "name", ethernet_interface["name"], required=True)
+ return strip_empties_from_list(
+ [
+ self._tag_dict("Type", "wan"),
+ self._tag_dict("Carrier", get(wan_interface, "wan_carrier")),
+ self._tag_dict("Circuit", get(wan_interface, "wan_circuit_id")),
+ ]
+ )
+
+ return [self._tag_dict("Type", "lan")]
+
+ @cached_property
+ def _wan_interface_names(self: AvdStructuredConfigMetadata):
+ return [wan_interface["name"] for wan_interface in self.shared_utils.wan_interfaces]
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_adaptive_virtual_topology.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_adaptive_virtual_topology.py
index 58f4f42d94f..17ed069c487 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_adaptive_virtual_topology.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_adaptive_virtual_topology.py
@@ -28,13 +28,13 @@ def router_adaptive_virtual_topology(self) -> dict | None:
router_adaptive_virtual_topology.update(
{
"region": {
- "name": self._wan_region["name"],
- "id": self._wan_region["id"],
+ "name": self.shared_utils.wan_region["name"],
+ "id": self.shared_utils.wan_region["id"],
},
- "zone": self._wan_zone,
+ "zone": self.shared_utils.wan_zone,
"site": {
- "name": self._wan_site["name"],
- "id": self._wan_site["id"],
+ "name": self.shared_utils.wan_site["name"],
+ "id": self.shared_utils.wan_site["id"],
},
}
)
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_bgp.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_bgp.py
index aa72e9a60df..027779163ae 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_bgp.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_bgp.py
@@ -484,12 +484,12 @@ def _neighbors(self) -> list | None:
f"Loopback0 IP {self.shared_utils.router_id} is not in the Route Reflector listen range prefixes"
" 'bgp_peer_groups.wan_overlay_peers.listen_range_prefixes'."
)
- for wan_route_server, data in self._filtered_wan_route_servers.items():
+ for wan_route_server, data in self.shared_utils.filtered_wan_route_servers.items():
neighbor = self._create_neighbor(data["router_id"], wan_route_server, self.shared_utils.bgp_peer_groups["wan_overlay_peers"]["name"])
neighbors.append(neighbor)
if self.shared_utils.wan_role == "server":
# No neighbor configured on the `wan_overlay_peers` peer group as it is covered by listen ranges
- for wan_route_server, data in self._filtered_wan_route_servers.items():
+ for wan_route_server, data in self.shared_utils.filtered_wan_route_servers.items():
neighbor = self._create_neighbor(data["router_id"], wan_route_server, self.shared_utils.bgp_peer_groups["rr_overlay_peers"]["name"])
neighbors.append(neighbor)
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_path_selection.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_path_selection.py
index 2e673d812f8..02a30a97462 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_path_selection.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/router_path_selection.py
@@ -128,7 +128,7 @@ def _get_local_interfaces_for_path_group(self, path_group_name: str) -> list | N
for interface in path_group.get("interfaces", []):
local_interface = {"name": get(interface, "name", required=True)}
- if self.shared_utils.wan_role == "client" and self._should_connect_to_wan_rs([path_group_name]):
+ if self.shared_utils.wan_role == "client" and self.shared_utils.should_connect_to_wan_rs([path_group_name]):
stun_server_profiles = self._stun_server_profiles.get(path_group_name, [])
if stun_server_profiles:
local_interface["stun"] = {"server_profiles": [profile["name"] for profile in stun_server_profiles]}
@@ -153,7 +153,7 @@ def _get_static_peers_for_path_group(self, path_group_name: str) -> list | None:
return None
static_peers = []
- for wan_route_server, data in self._filtered_wan_route_servers.items():
+ for wan_route_server, data in self.shared_utils.filtered_wan_route_servers.items():
if (path_group := get_item(data["wan_path_groups"], "name", path_group_name)) is not None:
ipv4_addresses = []
diff --git a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/utils.py b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/utils.py
index 0fd72889f81..c5df100de65 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/utils.py
+++ b/ansible_collections/arista/avd/roles/eos_designs/python_modules/overlay/utils.py
@@ -7,9 +7,7 @@
from ansible_collections.arista.avd.plugins.filter.natural_sort import natural_sort
from ansible_collections.arista.avd.plugins.plugin_utils.eos_designs_shared_utils.shared_utils import SharedUtils
-from ansible_collections.arista.avd.plugins.plugin_utils.errors import AristaAvdError, AristaAvdMissingVariableError
-from ansible_collections.arista.avd.plugins.plugin_utils.strip_empties import strip_empties_from_dict
-from ansible_collections.arista.avd.plugins.plugin_utils.utils import get, get_item
+from ansible_collections.arista.avd.plugins.plugin_utils.utils import get
class UtilsMixin:
@@ -262,200 +260,25 @@ def _append_peer(self, peers_dict: dict, peer_name: str, peer_facts: dict) -> No
@cached_property
def _is_wan_server_with_peers(self) -> bool:
- return self.shared_utils.wan_role == "server" and len(self._filtered_wan_route_servers) > 0
+ return self.shared_utils.wan_role == "server" and len(self.shared_utils.filtered_wan_route_servers) > 0
@cached_property
def _wan_listen_ranges(self):
return get(self.shared_utils.bgp_peer_groups["wan_overlay_peers"], "listen_range_prefixes", required=True)
- @cached_property
- def _wan_site(self) -> dict | None:
- """
- Here assuming that cv_pathfinder_name is unique across zones and regions
- """
- if not self.shared_utils.cv_pathfinder_role:
- return None
-
- node_defined_site = get(
- self.shared_utils.switch_data_combined,
- "cv_pathfinder_site",
- required=True,
- org_key="A node variable 'cv_pathfinder_site' must be defined when 'cv_pathfinder_role' is 'edge' or 'transit'.",
- )
- sites = get(self._wan_region, "sites", required=True, org_key=f"The CV Pathfinder region '{self._wan_region['name']}' is missing a list of sites")
- return get_item(
- sites,
- "name",
- node_defined_site,
- required=True,
- custom_error_msg=(
- f"The 'cv_pathfinder_site '{node_defined_site}' defined at the node level could not be found under the 'sites' list for the region"
- f" '{self._wan_region['name']}'."
- ),
- )
-
- @cached_property
- def _wan_region(self) -> dict | None:
- """
- WAN region for Pathfinder
- """
- if not self.shared_utils.cv_pathfinder_role:
- return None
-
- node_defined_region = get(
- self.shared_utils.switch_data_combined,
- "cv_pathfinder_region",
- required=True,
- org_key="A node variable 'cv_pathfinder_region' must be defined when 'cv_pathfinder_role' is 'edge' or 'transit'.",
- )
- regions = get(
- self._hostvars, "cv_pathfinder_regions", required=True, org_key="'cv_pathfinder_regions' key must be set when 'wan_mode' is 'cv-pathfinder'."
- )
-
- return get_item(
- regions,
- "name",
- node_defined_region,
- required=True,
- custom_error_msg="The 'cv_pathfinder_region' defined at the node level could not be found under the 'cv_pathfinder_regions' key.",
- )
-
- @cached_property
- def _wan_zone(self) -> dict | None:
- """
- WAN zone for Pathfinder
-
- Currently, only default zone DEFAULT-ZONE with ID 1 is supported.
- """
- if not self.shared_utils.cv_pathfinder_role:
- return None
-
- # Injecting zone DEFAULT-ZONE with id 1.
- return {"name": "DEFAULT-ZONE", "id": 1}
-
- @cached_property
- def _wan_route_servers(self) -> dict:
- """
- Return a dict keyed by Wan RR based on the the wan_mode type.
-
- It the RR is part of the inventory, the peer_facts are read..
- If any key is specified in the variables, it overwrites whatever is in the peer_facts.
-
- If no peer_fact is found the variables are required in the inventory.
- """
- # TODO - need to factor this with other function once we fix
- # https://github.com/aristanetworks/ansible-avd/issues/3392
- if not self.shared_utils.wan_mode:
- return {}
-
- wan_route_servers = {}
-
- wan_route_servers_list = get(self._hostvars, "wan_route_servers", default=[])
-
- for wan_rs_dict in natural_sort(wan_route_servers_list, sort_key="hostname"):
- # These remote gw can be outside of the inventory
- wan_rs = wan_rs_dict["hostname"]
-
- if wan_rs == self.shared_utils.hostname:
- # Don't add yourself
- continue
-
- if (peer_facts := self.shared_utils.get_peer_facts(wan_rs, required=False)) is not None:
- # Found a matching server in inventory
- bgp_as = peer_facts.get("bgp_as")
- # Only ibgp is supported for WAN so raise if peer from peer_facts BGP AS is different from ours.
- if bgp_as != self.shared_utils.bgp_as:
- raise AristaAvdError(
- f"Only iBGP is supported for WAN, the BGP AS {bgp_as} on {wan_rs} is different from our own: {self.shared_utils.bgp_as}."
- )
-
- # Prefer values coming from the input variables over peer facts
- router_id = get(wan_rs_dict, "router_id", default=peer_facts.get("router_id"))
- wan_path_groups = get(wan_rs_dict, "path_groups", default=peer_facts.get("wan_path_groups"))
-
- if router_id is None:
- raise AristaAvdMissingVariableError(
- f"'router_id' is missing for peering with {wan_rs}, either set it in under 'wan_route_servers' or something is wrong with the peer"
- " facts."
- )
- if wan_path_groups is None:
- raise AristaAvdMissingVariableError(
- f"'wan_path_groups' is missing for peering with {wan_rs}, either set it in under 'wan_route_servers'"
- " or something is wrong with the peer facts."
- )
-
- else:
- # Retrieve the values from the dictionary, making them required if the peer_facts were not found
- router_id = get(wan_rs_dict, "router_id", required=True)
- wan_path_groups = get(
- wan_rs_dict,
- "path_groups",
- required=True,
- org_key=(
- f"'path_groups' is missing for peering with {wan_rs} which was not found in the inventory, either set it in under 'wan_route_servers'"
- " or check your inventory."
- ),
- )
-
- wan_rs_result_dict = {
- "router_id": router_id,
- "wan_path_groups": wan_path_groups,
- }
-
- if any(interface["ip_address"] == "dhcp" for path_group in wan_rs_result_dict["wan_path_groups"] for interface in path_group.get("interfaces", [])):
- raise AristaAvdError(
- f"The IP address for a WAN interface on a Route Server '{wan_rs}' is set 'dhcp'. Clients need to peer with a static IP which can be set"
- " under the 'wan_route_servers.path_groups.interfaces' key."
- )
-
- wan_route_servers[wan_rs] = strip_empties_from_dict(wan_rs_result_dict)
-
- return wan_route_servers
-
def _stun_server_profile_name(self, wan_route_server_name: str, path_group_name: str, interface_name: str) -> str:
"""
Return a string to use as the name of the stun server_profile
"""
return f"{path_group_name}-{wan_route_server_name}-{interface_name}"
- def _should_connect_to_wan_rs(self, path_groups: list) -> bool:
- """
- This helper implements wherther or not a connection to the wan_rs should be made or not based on a list of path-groups.
-
- To do this the logic is the following:
- * Look at the wan_interfaces on the router and check if there is any path-group in common with the RR where
- `connected_to_pathfinder` is not False.
- """
- return any(
- local_path_group["name"] in path_groups and any(wan_interface["connected_to_pathfinder"] for wan_interface in local_path_group["interfaces"])
- for local_path_group in self.shared_utils.wan_local_path_groups
- )
-
- @cached_property
- def _filtered_wan_route_servers(self) -> dict:
- """
- Return a dictionary of wan_route_servers with only the path_groups the router should connect to
- """
- filtered_wan_route_servers = {}
- for wan_route_server, data in self._wan_route_servers.items():
- if wan_route_server == self.shared_utils.hostname:
- # Do not include yourself
- continue
- for path_group in data.get("wan_path_groups", []):
- if self._should_connect_to_wan_rs([path_group["name"]]):
- filtered_wan_route_servers.setdefault(wan_route_server, {"router_id": data["router_id"], "wan_path_groups": []})["wan_path_groups"].append(
- path_group
- )
-
- return filtered_wan_route_servers
-
@cached_property
def _stun_server_profiles(self) -> list:
"""
Return a dictionary of _stun_server_profiles with ip_address per local path_group
"""
stun_server_profiles = {}
- for wan_route_server, data in self._filtered_wan_route_servers.items():
+ for wan_route_server, data in self.shared_utils.filtered_wan_route_servers.items():
for path_group in data.get("wan_path_groups", []):
stun_server_profiles.setdefault(path_group["name"], []).extend(
{