diff --git a/pyproject.toml b/pyproject.toml index b27f7535abe..ef92ac5cddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,3 +106,13 @@ known-first-party = ["pyavd", "schema_tools"] [tool.ruff.format] docstring-code-format = true + +[tool.pyright] +include = [ + ".github", + "python-avd/pyavd/_eos_designs", +] +exclude = [ + "python-avd/pyavd/_eos_designs/schema/__init__.py", +] +pythonVersion = "3.10" diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/__init__.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/__init__.py index 654e9abb6d7..f7cf98c7766 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/__init__.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/__init__.py @@ -10,13 +10,12 @@ from .mlag import MlagMixin from .overlay import OverlayMixin -from .short_esi import ShortEsiMixin from .uplinks import UplinksMixin from .vlans import VlansMixin from .wan import WanMixin -class EosDesignsFacts(AvdFacts, MlagMixin, ShortEsiMixin, OverlayMixin, WanMixin, UplinksMixin, VlansMixin): +class EosDesignsFacts(MlagMixin, OverlayMixin, WanMixin, UplinksMixin, VlansMixin, AvdFacts): """ `EosDesignsFacts` is based on `AvdFacts`, so make sure to read the description there first. @@ -85,7 +84,7 @@ def evpn_multicast(self) -> bool | None: raise AristaAvdError(msg) if self.shared_utils.mlag is True: - peer_eos_designs_facts: EosDesignsFacts = self.shared_utils.mlag_peer_facts + peer_eos_designs_facts = self.shared_utils.mlag_peer_facts_cls if self.shared_utils.overlay_rd_type_admin_subfield == peer_eos_designs_facts.shared_utils.overlay_rd_type_admin_subfield: msg = "For MLAG devices Route Distinguisher must be unique when 'evpn_multicast: True' since it will create a multi-vtep configuration." raise AristaAvdError(msg) diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/mlag.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/mlag.py index ebe705be8a7..b58e773c90c 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/mlag.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/mlag.py @@ -4,13 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from . import EosDesignsFacts +from .utils import UtilsMixin -class MlagMixin: +class MlagMixin(UtilsMixin): """ Mixin Class used to generate some of the EosDesignsFacts. @@ -19,35 +17,35 @@ class MlagMixin: """ @cached_property - def mlag_peer(self: EosDesignsFacts) -> str | None: + def mlag_peer(self) -> str | None: """Exposed in avd_switch_facts.""" if self.shared_utils.mlag: return self.shared_utils.mlag_peer return None @cached_property - def mlag_port_channel_id(self: EosDesignsFacts) -> int | None: + def mlag_port_channel_id(self) -> int | None: """Exposed in avd_switch_facts.""" if self.shared_utils.mlag: return self.shared_utils.mlag_port_channel_id return None @cached_property - def mlag_interfaces(self: EosDesignsFacts) -> list | None: + def mlag_interfaces(self) -> list | None: """Exposed in avd_switch_facts.""" if self.shared_utils.mlag: return self.shared_utils.mlag_interfaces return None @cached_property - def mlag_ip(self: EosDesignsFacts) -> str | None: + def mlag_ip(self) -> str | None: """Exposed in avd_switch_facts.""" if self.shared_utils.mlag: return self.shared_utils.mlag_ip return None @cached_property - def mlag_l3_ip(self: EosDesignsFacts) -> str | None: + def mlag_l3_ip(self) -> str | None: """ Exposed in avd_switch_facts. @@ -62,7 +60,7 @@ def mlag_l3_ip(self: EosDesignsFacts) -> str | None: return None @cached_property - def mlag_switch_ids(self: EosDesignsFacts) -> dict | None: + def mlag_switch_ids(self) -> dict | None: """ Exposed in avd_switch_facts. diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/overlay.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/overlay.py index 90fd418708c..2020aaee4dd 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/overlay.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/overlay.py @@ -4,13 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from . import EosDesignsFacts +from .utils import UtilsMixin -class OverlayMixin: +class OverlayMixin(UtilsMixin): """ Mixin Class used to generate some of the EosDesignsFacts. @@ -19,17 +17,17 @@ class OverlayMixin: """ @cached_property - def evpn_role(self: EosDesignsFacts) -> str | None: + def evpn_role(self) -> str | None: """Exposed in avd_switch_facts.""" return self.shared_utils.evpn_role @cached_property - def mpls_overlay_role(self: EosDesignsFacts) -> str | None: + def mpls_overlay_role(self) -> str | None: """Exposed in avd_switch_facts.""" return self.shared_utils.mpls_overlay_role @cached_property - def evpn_route_servers(self: EosDesignsFacts) -> list: + def evpn_route_servers(self) -> list: """ Exposed in avd_switch_facts. @@ -43,7 +41,7 @@ def evpn_route_servers(self: EosDesignsFacts) -> list: return [] @cached_property - def mpls_route_reflectors(self: EosDesignsFacts) -> list | None: + def mpls_route_reflectors(self) -> list | None: """Exposed in avd_switch_facts.""" if self.shared_utils.underlay_router is True and ( self.mpls_overlay_role in ["client", "server"] or (self.evpn_role in ["client", "server"] and self.overlay["evpn_mpls"]) @@ -52,7 +50,7 @@ def mpls_route_reflectors(self: EosDesignsFacts) -> list | None: return None @cached_property - def overlay(self: EosDesignsFacts) -> dict | None: + def overlay(self) -> dict | None: """Exposed in avd_switch_facts.""" if self.shared_utils.underlay_router is True: return { @@ -62,7 +60,7 @@ def overlay(self: EosDesignsFacts) -> dict | None: return None @cached_property - def vtep_ip(self: EosDesignsFacts) -> str | None: + def vtep_ip(self) -> str | None: """Exposed in avd_switch_facts.""" if self.shared_utils.vtep: return self.shared_utils.vtep_ip diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/short_esi.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/short_esi.py deleted file mode 100644 index a36c497dff7..00000000000 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/short_esi.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2023-2025 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 - -import re -from functools import cached_property -from hashlib import sha256 -from typing import TYPE_CHECKING - -from pyavd._utils import default - -if TYPE_CHECKING: - from . import EosDesignsFacts - - -class ShortEsiMixin: - """ - Mixin Class used to generate some of the EosDesignsFacts. - - Class should only be used as Mixin to the EosDesignsFacts class. - Using type-hint on self to get proper type-hints on attributes across all Mixins. - """ - - @cached_property - def _short_esi(self: EosDesignsFacts) -> str | None: - """ - If short_esi is set to "auto" we will use sha256 to create a unique short_esi value based on various uplink information. - - Note: Secondary MLAG switch should have the same short-esi value - as primary MLAG switch. - """ - # On the MLAG Secondary use short-esi from MLAG primary - if self.shared_utils.mlag_role == "secondary" and (peer_short_esi := self.shared_utils.mlag_peer_facts._short_esi) is not None: - return peer_short_esi - short_esi = self.shared_utils.node_config.short_esi - if short_esi == "auto": - esi_seed_1 = "".join(self.shared_utils.uplink_switches[:2]) - esi_seed_2 = "".join(self.shared_utils.uplink_switch_interfaces[:2]) - esi_seed_3 = "".join(self.shared_utils.uplink_interfaces[:2]) - esi_seed_4 = default(self.shared_utils.group, "") - esi_hash = sha256(f"{esi_seed_1}{esi_seed_2}{esi_seed_3}{esi_seed_4}".encode()).hexdigest() - short_esi = re.sub(r"([0-9a-f]{4})", r"\1:", esi_hash)[:14] - return short_esi diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/uplinks.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/uplinks.py index 4db673d26bc..42d117adc1b 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/uplinks.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/uplinks.py @@ -5,17 +5,15 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError from pyavd._utils import append_if_not_duplicate from pyavd.j2filters import list_compress, natural_sort, range_expand -if TYPE_CHECKING: - from . import EosDesignsFacts +from .utils import UtilsMixin -class UplinksMixin: +class UplinksMixin(UtilsMixin): """ Mixin Class used to generate some of the EosDesignsFacts. @@ -24,17 +22,17 @@ class UplinksMixin: """ @cached_property - def max_parallel_uplinks(self: EosDesignsFacts) -> int: + def max_parallel_uplinks(self) -> int: """Exposed in avd_switch_facts.""" return self.shared_utils.node_config.max_parallel_uplinks @cached_property - def max_uplink_switches(self: EosDesignsFacts) -> int: + def max_uplink_switches(self) -> int: """Exposed in avd_switch_facts.""" return self.shared_utils.max_uplink_switches @cached_property - def _uplink_port_channel_id(self: EosDesignsFacts) -> int: + def _uplink_port_channel_id(self) -> int: """ For MLAG secondary get the uplink_port_channel_id from the peer's facts. @@ -49,7 +47,7 @@ def _uplink_port_channel_id(self: EosDesignsFacts) -> int: if self.shared_utils.mlag_role == "secondary": # MLAG Secondary - peer_uplink_port_channel_id = self.shared_utils.mlag_peer_facts._uplink_port_channel_id + peer_uplink_port_channel_id = self.shared_utils.mlag_peer_facts_cls._uplink_port_channel_id # check that port-channel IDs are the same as on primary if uplink_port_channel_id is not None and uplink_port_channel_id != peer_uplink_port_channel_id: msg = ( @@ -72,7 +70,7 @@ def _uplink_port_channel_id(self: EosDesignsFacts) -> int: return uplink_port_channel_id @cached_property - def _uplink_switch_port_channel_id(self: EosDesignsFacts) -> int: + def _uplink_switch_port_channel_id(self) -> int: """ For MLAG secondary get the uplink_switch_port_channel_id from the peer's facts. @@ -88,7 +86,7 @@ def _uplink_switch_port_channel_id(self: EosDesignsFacts) -> int: if self.shared_utils.mlag_role == "secondary": # MLAG Secondary - peer_uplink_switch_port_channel_id = self.shared_utils.mlag_peer_facts._uplink_switch_port_channel_id + peer_uplink_switch_port_channel_id = self.shared_utils.mlag_peer_facts_cls._uplink_switch_port_channel_id # check that port-channel IDs are the same as on primary if uplink_switch_port_channel_id is not None and uplink_switch_port_channel_id != peer_uplink_switch_port_channel_id: msg = ( @@ -104,7 +102,7 @@ def _uplink_switch_port_channel_id(self: EosDesignsFacts) -> int: uplink_switch_port_channel_id = int("".join(re.findall(r"\d", self.shared_utils.uplink_switch_interfaces[0]))) # produce an error if the uplink switch is MLAG and port-channel ID is above 2000 - uplink_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(self.shared_utils.uplink_switches[0], required=True) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(self.shared_utils.uplink_switches[0]) if uplink_switch_facts.shared_utils.mlag and not 1 <= uplink_switch_port_channel_id <= 2000: msg = f"'uplink_switch_port_channel_id' must be between 1 and 2000 for MLAG switches. Got '{uplink_switch_port_channel_id}'." @@ -113,7 +111,7 @@ def _uplink_switch_port_channel_id(self: EosDesignsFacts) -> int: return uplink_switch_port_channel_id @cached_property - def uplinks(self: EosDesignsFacts) -> list: + def uplinks(self) -> list: """ Exposed in avd_switch_facts. @@ -165,9 +163,9 @@ def uplinks(self: EosDesignsFacts) -> list: return uplinks - def _get_p2p_uplink(self: EosDesignsFacts, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: + def _get_p2p_uplink(self, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: """Return a single uplink dictionary for uplink_type p2p.""" - uplink_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(uplink_switch) uplink = { "interface": uplink_interface, "peer": uplink_switch, @@ -211,9 +209,9 @@ def _get_p2p_uplink(self: EosDesignsFacts, uplink_index: int, uplink_interface: return uplink - def _get_port_channel_uplink(self: EosDesignsFacts, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: + def _get_port_channel_uplink(self, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: """Return a single uplink dictionary for uplink_type port-channel.""" - uplink_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(uplink_switch) # Reusing get_l2_uplink uplink = self._get_l2_uplink(uplink_index, uplink_interface, uplink_switch, uplink_switch_interface) @@ -229,7 +227,7 @@ def _get_port_channel_uplink(self: EosDesignsFacts, uplink_index: int, uplink_in uplink["node_group"] = self.shared_utils.group # Updating unique_uplink_switches with our mlag peer's uplink switches - unique_uplink_switches.update(self.shared_utils.mlag_peer_facts.shared_utils.uplink_switches) + unique_uplink_switches.update(self.shared_utils.mlag_peer_facts_cls.shared_utils.uplink_switches) # Only enable mlag for this port-channel on the uplink switch if there are multiple unique uplink switches uplink["peer_mlag"] = len(unique_uplink_switches) > 1 @@ -240,14 +238,14 @@ def _get_port_channel_uplink(self: EosDesignsFacts, uplink_index: int, uplink_in return uplink def _get_l2_uplink( - self: EosDesignsFacts, + self, uplink_index: int, # pylint: disable=unused-argument # noqa: ARG002 uplink_interface: str, uplink_switch: str, uplink_switch_interface: str, ) -> dict: """Return a single uplink dictionary for an L2 uplink. Reused for both uplink_type port-channel, lan and TODO: lan-port-channel.""" - uplink_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(uplink_switch) uplink = { "interface": uplink_interface, "peer": uplink_switch, @@ -302,9 +300,9 @@ def _get_l2_uplink( return uplink - def _get_p2p_vrfs_uplink(self: EosDesignsFacts, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: + def _get_p2p_vrfs_uplink(self, uplink_index: int, uplink_interface: str, uplink_switch: str, uplink_switch_interface: str) -> dict: """Return a single uplink dictionary for uplink_type p2p-vrfs.""" - uplink_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(uplink_switch) # Reusing regular p2p logic for main interface. uplink = self._get_p2p_uplink(uplink_index, uplink_interface, uplink_switch, uplink_switch_interface) @@ -348,7 +346,7 @@ def _get_p2p_vrfs_uplink(self: EosDesignsFacts, uplink_index: int, uplink_interf return uplink @cached_property - def uplink_peers(self: EosDesignsFacts) -> list: + def uplink_peers(self) -> list: """ Exposed in avd_switch_facts. @@ -363,7 +361,7 @@ def uplink_peers(self: EosDesignsFacts) -> list: return natural_sort(unique_uplink_switches) @cached_property - def _default_downlink_interfaces(self: EosDesignsFacts) -> list: + def _default_downlink_interfaces(self) -> list: """ Internal _default_downlink_interfaces set based on default_interfaces. @@ -372,7 +370,7 @@ def _default_downlink_interfaces(self: EosDesignsFacts) -> list: return range_expand(self.shared_utils.default_interfaces.downlink_interfaces) @cached_property - def uplink_switch_vrfs(self: EosDesignsFacts) -> list[str] | None: + def uplink_switch_vrfs(self) -> list[str] | None: """ Exposed in avd_switch_facts. @@ -383,7 +381,7 @@ def uplink_switch_vrfs(self: EosDesignsFacts) -> list[str] | None: vrfs = set() for uplink_switch in self.uplink_peers: - uplink_switch_facts = self.shared_utils.get_peer_facts(uplink_switch) + uplink_switch_facts = self.shared_utils.get_peer_facts_cls(uplink_switch) vrfs.update(uplink_switch_facts.shared_utils.vrfs) return natural_sort(vrfs) or None diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/utils.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/utils.py new file mode 100644 index 00000000000..93e22adb07f --- /dev/null +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/utils.py @@ -0,0 +1,283 @@ +# Copyright (c) 2023-2025 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 + +import re +from functools import cached_property +from hashlib import sha256 +from typing import TYPE_CHECKING + +from pyavd._eos_designs.avdfacts import AvdFacts +from pyavd._utils import default +from pyavd.j2filters import list_compress, range_expand + +if TYPE_CHECKING: + from pyavd._eos_designs.schema import EosDesigns + + +class UtilsMixin(AvdFacts): + """ + Mixin Class used to generate some of the EosDesignsFacts. + + Class should only be used as Mixin to the EosDesignsFacts class. + Using type-hint on self to get proper type-hints on attributes across all Mixins. + """ + + @cached_property + def _short_esi(self) -> str | None: + """ + If short_esi is set to "auto" we will use sha256 to create a unique short_esi value based on various uplink information. + + Note: Secondary MLAG switch should have the same short-esi value + as primary MLAG switch. + """ + # On the MLAG Secondary use short-esi from MLAG primary + if self.shared_utils.mlag_role == "secondary" and (peer_short_esi := self.shared_utils.mlag_peer_facts_cls._short_esi) is not None: + return peer_short_esi + short_esi = self.shared_utils.node_config.short_esi + if short_esi == "auto": + esi_seed_1 = "".join(self.shared_utils.uplink_switches[:2]) + esi_seed_2 = "".join(self.shared_utils.uplink_switch_interfaces[:2]) + esi_seed_3 = "".join(self.shared_utils.uplink_interfaces[:2]) + esi_seed_4 = default(self.shared_utils.group, "") + esi_hash = sha256(f"{esi_seed_1}{esi_seed_2}{esi_seed_3}{esi_seed_4}".encode()).hexdigest() + short_esi = re.sub(r"([0-9a-f]{4})", r"\1:", esi_hash)[:14] + return short_esi + + @cached_property + def _vlans(self) -> list[int]: + """ + Return list of vlans after filtering network services. + + The filter is based on filter.tenants, filter.tags and filter.only_vlans_in_use. + + Ex. [1, 2, 3 ,4 ,201, 3021] + """ + if self.shared_utils.any_network_services: + vlans = [] + match_tags = self.shared_utils.filter_tags + + if self.shared_utils.node_config.filter.only_vlans_in_use: + # Only include the vlans that are used by connected endpoints + endpoint_trunk_groups = self._endpoint_trunk_groups + endpoint_vlans = self._endpoint_vlans + + for network_services_key in self.inputs._dynamic_keys.network_services: + tenants = network_services_key.value + for tenant in tenants: + if not set(self.shared_utils.node_config.filter.tenants).intersection([tenant.name, "all"]): + # Not matching tenant filters. Skipping this tenant. + continue + + for vrf in tenant.vrfs: + for svi in vrf.svis: + if "all" in match_tags or set(svi.tags).intersection(match_tags): + if self.shared_utils.node_config.filter.only_vlans_in_use: + # Check if vlan is in use + if svi.id in endpoint_vlans: + vlans.append(svi.id) + continue + # Check if vlan has a trunk group defined which is in use + if self.inputs.enable_trunk_groups and svi.trunk_groups and endpoint_trunk_groups.intersection(svi.trunk_groups): + vlans.append(svi.id) + continue + # Skip since the vlan is not in use + continue + vlans.append(svi.id) + + for l2vlan in tenant.l2vlans: + if "all" in match_tags or set(l2vlan.tags).intersection(match_tags): + if self.shared_utils.node_config.filter.only_vlans_in_use: + # Check if vlan is in use + if l2vlan.id in endpoint_vlans: + vlans.append(l2vlan.id) + continue + # Check if vlan has a trunk group defined which is in use + if self.inputs.enable_trunk_groups and l2vlan.trunk_groups and endpoint_trunk_groups.intersection(l2vlan.trunk_groups): + vlans.append(l2vlan.id) + continue + # Skip since the vlan is not in use + continue + vlans.append(l2vlan.id) + + return vlans + return [] + + @cached_property + def _downstream_switch_endpoint_vlans_and_trunk_groups(self) -> tuple[set, set]: + """ + Return set of vlans and set of trunk groups used by downstream switches. + + Traverse any downstream L2 switches so ensure we can provide connectivity to any vlans / trunk groups used by them. + """ + if not self.shared_utils.any_network_services: + return set(), set() + + vlans = set() + trunk_groups = set() + for fabric_switch in self.shared_utils.all_fabric_devices: + fabric_switch_facts = self.shared_utils.get_peer_facts_cls(fabric_switch) + if fabric_switch_facts.shared_utils.uplink_type == "port-channel" and self.shared_utils.hostname in fabric_switch_facts.uplink_peers: + fabric_switch_endpoint_vlans, fabric_switch_endpoint_trunk_groups = fabric_switch_facts._endpoint_vlans_and_trunk_groups + vlans.update(fabric_switch_endpoint_vlans) + trunk_groups.update(fabric_switch_endpoint_trunk_groups) + + return vlans, trunk_groups + + @cached_property + def _mlag_peer_endpoint_vlans_and_trunk_groups(self) -> tuple[set, set]: + """ + Return set of vlans and set of trunk groups used by connected_endpoints on the MLAG peer. + + This could differ from local vlans and trunk groups if a connected endpoint is only connected to one leaf. + """ + if not self.shared_utils.mlag: + return set(), set() + + return self.shared_utils.mlag_peer_facts_cls._endpoint_vlans_and_trunk_groups + + @cached_property + def _endpoint_vlans_and_trunk_groups(self) -> tuple[set, set]: + """ + Return set of vlans and set of trunk groups. + + The trunk groups are those used by connected_endpoints on this switch, + downstream switches but NOT mlag peer (since we would have circular references then). + """ + local_endpoint_vlans, local_endpoint_trunk_groups = self._local_endpoint_vlans_and_trunk_groups + downstream_switch_endpoint_vlans, downstream_switch_endpoint_trunk_groups = self._downstream_switch_endpoint_vlans_and_trunk_groups + return local_endpoint_vlans.union(downstream_switch_endpoint_vlans), local_endpoint_trunk_groups.union(downstream_switch_endpoint_trunk_groups) + + @cached_property + def _endpoint_vlans(self) -> set[int]: + """ + Return set of vlans in use by endpoints connected to this switch, downstream switches or MLAG peer. + + Ex: {1, 20, 21, 22, 23} or set(). + """ + if not self.shared_utils.node_config.filter.only_vlans_in_use: + return set() + + endpoint_vlans, _ = self._endpoint_vlans_and_trunk_groups + if not self.shared_utils.mlag: + return endpoint_vlans + + mlag_endpoint_vlans, _ = self._mlag_peer_endpoint_vlans_and_trunk_groups + + return endpoint_vlans.union(mlag_endpoint_vlans) + + @cached_property + def endpoint_vlans(self) -> str | None: + """ + Return compressed list of vlans in use by endpoints connected to this switch or MLAG peer. + + Ex: "1,20-30" or "". + """ + if self.shared_utils.node_config.filter.only_vlans_in_use: + return list_compress(list(self._endpoint_vlans)) + + return None + + @cached_property + def _endpoint_trunk_groups(self) -> set[str]: + """Return set of trunk_groups in use by endpoints connected to this switch, downstream switches or MLAG peer.""" + if not self.shared_utils.node_config.filter.only_vlans_in_use: + return set() + + _, endpoint_trunk_groups = self._endpoint_vlans_and_trunk_groups + if not self.shared_utils.mlag: + return endpoint_trunk_groups + + _, mlag_endpoint_trunk_groups = self._mlag_peer_endpoint_vlans_and_trunk_groups + return endpoint_trunk_groups.union(mlag_endpoint_trunk_groups) + + def _parse_adapter_settings( + self, + adapter_settings: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem | EosDesigns.NetworkPortsItem, + ) -> tuple[set, set]: + """Parse the given adapter_settings and return relevant vlans and trunk_groups.""" + vlans = set() + trunk_groups = set(adapter_settings.trunk_groups) + if adapter_settings.vlans not in ["all", "", None]: + vlans.update(map(int, range_expand(adapter_settings.vlans))) + elif adapter_settings.mode == "trunk" and not trunk_groups: + # No vlans or trunk_groups defined, but this is a trunk, so default is all vlans allowed + # No need to check further, since the list is now containing all vlans. + return set(range(1, 4094)), trunk_groups + elif adapter_settings.mode == "trunk phone": + # # EOS default native VLAN is VLAN 1 + if not adapter_settings.native_vlan: + vlans.add(1) + else: + # No vlans or mode defined so this is an access port with only vlan 1 allowed + vlans.add(1) + + if adapter_settings.native_vlan: + vlans.add(adapter_settings.native_vlan) + if adapter_settings.phone_vlan: + vlans.add(adapter_settings.phone_vlan) + + for subinterface in adapter_settings.port_channel.subinterfaces: + if subinterface.vlan_id: + vlans.add(subinterface.vlan_id) + elif subinterface.number: + vlans.add(subinterface.number) + + return vlans, trunk_groups + + @cached_property + def _local_endpoint_vlans_and_trunk_groups(self) -> tuple[set, set]: + """ + Return list of vlans and list of trunk groups used by connected_endpoints on this switch. + + Also includes the inband_mgmt_vlan. + """ + if not (self.shared_utils.any_network_services and self.shared_utils.connected_endpoints): + return set(), set() + + vlans = set() + trunk_groups = set() + + if self.shared_utils.configure_inband_mgmt: + vlans.add(self.shared_utils.node_config.inband_mgmt_vlan) + + for connected_endpoints_key in self.inputs._dynamic_keys.connected_endpoints: + for connected_endpoint in connected_endpoints_key.value: + for index, adapter in enumerate(connected_endpoint.adapters): + adapter._context = f"{connected_endpoints_key.key}[name={connected_endpoint.name}].adapters[{index}]" + adapter_settings = self.shared_utils.get_merged_adapter_settings(adapter) + if self.shared_utils.hostname not in adapter_settings.switches: + # This switch is not connected to this endpoint. Skipping. + continue + + adapter_vlans, adapter_trunk_groups = self._parse_adapter_settings(adapter_settings) + vlans.update(adapter_vlans) + trunk_groups.update(adapter_trunk_groups) + if len(vlans) >= 4094: + # No need to check further, since the set is now containing all vlans. + # The trunk group list may not be complete, but it will not matter, since we will + # configure all vlans anyway. + return vlans, trunk_groups + + for index, network_port_item in enumerate(self.inputs.network_ports): + for switch_regex in network_port_item.switches: + # The match test is built on Python re.match which tests from the beginning of the string #} + # Since the user would not expect "DC1-LEAF1" to also match "DC-LEAF11" we will force ^ and $ around the regex + raw_switch_regex = rf"^{switch_regex}$" + if not re.match(raw_switch_regex, self.shared_utils.hostname): + # Skip entry if no match + continue + + network_port_item._context = f"network_ports[{index}]" + adapter_settings = self.shared_utils.get_merged_adapter_settings(network_port_item) + adapter_vlans, adapter_trunk_groups = self._parse_adapter_settings(adapter_settings) + vlans.update(adapter_vlans) + trunk_groups.update(adapter_trunk_groups) + if len(vlans) >= 4094: + # No need to check further, since the list is now containing all vlans. + # The trunk group list may not be complete, but it will not matter, since we will + # configure all vlans anyway. + return vlans, trunk_groups + + return vlans, trunk_groups diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/vlans.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/vlans.py index 6d4277375e5..c0b5d15e6e8 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/vlans.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/vlans.py @@ -3,19 +3,14 @@ # that can be found in the LICENSE file. from __future__ import annotations -import re from functools import cached_property -from typing import TYPE_CHECKING -from pyavd.j2filters import list_compress, range_expand +from pyavd.j2filters import list_compress -if TYPE_CHECKING: - from pyavd._eos_designs.schema import EosDesigns +from .utils import UtilsMixin - from . import EosDesignsFacts - -class VlansMixin: +class VlansMixin(UtilsMixin): """ Mixin Class used to generate some of the EosDesignsFacts. @@ -24,7 +19,7 @@ class VlansMixin: """ @cached_property - def vlans(self: EosDesignsFacts) -> str: + def vlans(self) -> str: """ Exposed in avd_switch_facts. @@ -37,188 +32,8 @@ def vlans(self: EosDesignsFacts) -> str: """ return list_compress(self._vlans) - def _parse_adapter_settings( - self: EosDesignsFacts, - adapter_settings: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem | EosDesigns.NetworkPortsItem, - ) -> tuple[set, set]: - """Parse the given adapter_settings and return relevant vlans and trunk_groups.""" - vlans = set() - trunk_groups = set(adapter_settings.trunk_groups) - if adapter_settings.vlans not in ["all", "", None]: - vlans.update(map(int, range_expand(adapter_settings.vlans))) - elif adapter_settings.mode == "trunk" and not trunk_groups: - # No vlans or trunk_groups defined, but this is a trunk, so default is all vlans allowed - # No need to check further, since the list is now containing all vlans. - return set(range(1, 4094)), trunk_groups - elif adapter_settings.mode == "trunk phone": - # # EOS default native VLAN is VLAN 1 - if not adapter_settings.native_vlan: - vlans.add(1) - else: - # No vlans or mode defined so this is an access port with only vlan 1 allowed - vlans.add(1) - - if adapter_settings.native_vlan: - vlans.add(adapter_settings.native_vlan) - if adapter_settings.phone_vlan: - vlans.add(adapter_settings.phone_vlan) - - for subinterface in adapter_settings.port_channel.subinterfaces: - if subinterface.vlan_id: - vlans.add(subinterface.vlan_id) - elif subinterface.number: - vlans.add(subinterface.number) - - return vlans, trunk_groups - - @cached_property - def _local_endpoint_vlans_and_trunk_groups(self: EosDesignsFacts) -> tuple[set, set]: - """ - Return list of vlans and list of trunk groups used by connected_endpoints on this switch. - - Also includes the inband_mgmt_vlan. - """ - if not (self.shared_utils.any_network_services and self.shared_utils.connected_endpoints): - return set(), set() - - vlans = set() - trunk_groups = set() - - if self.shared_utils.configure_inband_mgmt: - vlans.add(self.shared_utils.node_config.inband_mgmt_vlan) - - for connected_endpoints_key in self.inputs._dynamic_keys.connected_endpoints: - for connected_endpoint in connected_endpoints_key.value: - for index, adapter in enumerate(connected_endpoint.adapters): - adapter._context = f"{connected_endpoints_key.key}[name={connected_endpoint.name}].adapters[{index}]" - adapter_settings = self.shared_utils.get_merged_adapter_settings(adapter) - if self.shared_utils.hostname not in adapter_settings.switches: - # This switch is not connected to this endpoint. Skipping. - continue - - adapter_vlans, adapter_trunk_groups = self._parse_adapter_settings(adapter_settings) - vlans.update(adapter_vlans) - trunk_groups.update(adapter_trunk_groups) - if len(vlans) >= 4094: - # No need to check further, since the set is now containing all vlans. - # The trunk group list may not be complete, but it will not matter, since we will - # configure all vlans anyway. - return vlans, trunk_groups - - for index, network_port_item in enumerate(self.inputs.network_ports): - for switch_regex in network_port_item.switches: - # The match test is built on Python re.match which tests from the beginning of the string #} - # Since the user would not expect "DC1-LEAF1" to also match "DC-LEAF11" we will force ^ and $ around the regex - raw_switch_regex = rf"^{switch_regex}$" - if not re.match(raw_switch_regex, self.shared_utils.hostname): - # Skip entry if no match - continue - - network_port_item._context = f"network_ports[{index}]" - adapter_settings = self.shared_utils.get_merged_adapter_settings(network_port_item) - adapter_vlans, adapter_trunk_groups = self._parse_adapter_settings(adapter_settings) - vlans.update(adapter_vlans) - trunk_groups.update(adapter_trunk_groups) - if len(vlans) >= 4094: - # No need to check further, since the list is now containing all vlans. - # The trunk group list may not be complete, but it will not matter, since we will - # configure all vlans anyway. - return vlans, trunk_groups - - return vlans, trunk_groups - - @cached_property - def _downstream_switch_endpoint_vlans_and_trunk_groups(self: EosDesignsFacts) -> tuple[set, set]: - """ - Return set of vlans and set of trunk groups used by downstream switches. - - Traverse any downstream L2 switches so ensure we can provide connectivity to any vlans / trunk groups used by them. - """ - if not self.shared_utils.any_network_services: - return set(), set() - - vlans = set() - trunk_groups = set() - for fabric_switch in self.shared_utils.all_fabric_devices: - fabric_switch_facts: EosDesignsFacts = self.shared_utils.get_peer_facts(fabric_switch, required=True) - if fabric_switch_facts.shared_utils.uplink_type == "port-channel" and self.shared_utils.hostname in fabric_switch_facts.uplink_peers: - fabric_switch_endpoint_vlans, fabric_switch_endpoint_trunk_groups = fabric_switch_facts._endpoint_vlans_and_trunk_groups - vlans.update(fabric_switch_endpoint_vlans) - trunk_groups.update(fabric_switch_endpoint_trunk_groups) - - return vlans, trunk_groups - - @cached_property - def _mlag_peer_endpoint_vlans_and_trunk_groups(self: EosDesignsFacts) -> tuple[set, set]: - """ - Return set of vlans and set of trunk groups used by connected_endpoints on the MLAG peer. - - This could differ from local vlans and trunk groups if a connected endpoint is only connected to one leaf. - """ - if not self.shared_utils.mlag: - return set(), set() - - mlag_peer_facts: EosDesignsFacts = self.shared_utils.mlag_peer_facts - - return mlag_peer_facts._endpoint_vlans_and_trunk_groups - - @cached_property - def _endpoint_vlans_and_trunk_groups(self: EosDesignsFacts) -> tuple[set, set]: - """ - Return set of vlans and set of trunk groups. - - The trunk groups are those used by connected_endpoints on this switch, - downstream switches but NOT mlag peer (since we would have circular references then). - """ - local_endpoint_vlans, local_endpoint_trunk_groups = self._local_endpoint_vlans_and_trunk_groups - downstream_switch_endpoint_vlans, downstream_switch_endpoint_trunk_groups = self._downstream_switch_endpoint_vlans_and_trunk_groups - return local_endpoint_vlans.union(downstream_switch_endpoint_vlans), local_endpoint_trunk_groups.union(downstream_switch_endpoint_trunk_groups) - @cached_property - def _endpoint_vlans(self: EosDesignsFacts) -> set[int]: - """ - Return set of vlans in use by endpoints connected to this switch, downstream switches or MLAG peer. - - Ex: {1, 20, 21, 22, 23} or set(). - """ - if not self.shared_utils.node_config.filter.only_vlans_in_use: - return set() - - endpoint_vlans, _ = self._endpoint_vlans_and_trunk_groups - if not self.shared_utils.mlag: - return endpoint_vlans - - mlag_endpoint_vlans, _ = self._mlag_peer_endpoint_vlans_and_trunk_groups - - return endpoint_vlans.union(mlag_endpoint_vlans) - - @cached_property - def endpoint_vlans(self: EosDesignsFacts) -> str | None: - """ - Return compressed list of vlans in use by endpoints connected to this switch or MLAG peer. - - Ex: "1,20-30" or "". - """ - if self.shared_utils.node_config.filter.only_vlans_in_use: - return list_compress(list(self._endpoint_vlans)) - - return None - - @cached_property - def _endpoint_trunk_groups(self: EosDesignsFacts) -> set[str]: - """Return set of trunk_groups in use by endpoints connected to this switch, downstream switches or MLAG peer.""" - if not self.shared_utils.node_config.filter.only_vlans_in_use: - return set() - - _, endpoint_trunk_groups = self._endpoint_vlans_and_trunk_groups - if not self.shared_utils.mlag: - return endpoint_trunk_groups - - _, mlag_endpoint_trunk_groups = self._mlag_peer_endpoint_vlans_and_trunk_groups - return endpoint_trunk_groups.union(mlag_endpoint_trunk_groups) - - @cached_property - def local_endpoint_trunk_groups(self: EosDesignsFacts) -> list[str]: + def local_endpoint_trunk_groups(self) -> list[str]: """ Return list of trunk_groups in use by endpoints connected to this switch only. @@ -232,69 +47,10 @@ def local_endpoint_trunk_groups(self: EosDesignsFacts) -> list[str]: return [] @cached_property - def endpoint_trunk_groups(self: EosDesignsFacts) -> list[str]: + def endpoint_trunk_groups(self) -> list[str]: """ Return list of trunk_groups in use by endpoints connected to this switch, downstream switches or MLAG peer. Used for filtering which vlans we configure on the device. This is a superset of local_endpoint_trunk_groups. """ return list(self._endpoint_trunk_groups) - - @cached_property - def _vlans(self: EosDesignsFacts) -> list[int]: - """ - Return list of vlans after filtering network services. - - The filter is based on filter.tenants, filter.tags and filter.only_vlans_in_use. - - Ex. [1, 2, 3 ,4 ,201, 3021] - """ - if self.shared_utils.any_network_services: - vlans = [] - match_tags = self.shared_utils.filter_tags - - if self.shared_utils.node_config.filter.only_vlans_in_use: - # Only include the vlans that are used by connected endpoints - endpoint_trunk_groups = self._endpoint_trunk_groups - endpoint_vlans = self._endpoint_vlans - - for network_services_key in self.inputs._dynamic_keys.network_services: - tenants = network_services_key.value - for tenant in tenants: - if not set(self.shared_utils.node_config.filter.tenants).intersection([tenant.name, "all"]): - # Not matching tenant filters. Skipping this tenant. - continue - - for vrf in tenant.vrfs: - for svi in vrf.svis: - if "all" in match_tags or set(svi.tags).intersection(match_tags): - if self.shared_utils.node_config.filter.only_vlans_in_use: - # Check if vlan is in use - if svi.id in endpoint_vlans: - vlans.append(svi.id) - continue - # Check if vlan has a trunk group defined which is in use - if self.inputs.enable_trunk_groups and svi.trunk_groups and endpoint_trunk_groups.intersection(svi.trunk_groups): - vlans.append(svi.id) - continue - # Skip since the vlan is not in use - continue - vlans.append(svi.id) - - for l2vlan in tenant.l2vlans: - if "all" in match_tags or set(l2vlan.tags).intersection(match_tags): - if self.shared_utils.node_config.filter.only_vlans_in_use: - # Check if vlan is in use - if l2vlan.id in endpoint_vlans: - vlans.append(l2vlan.id) - continue - # Check if vlan has a trunk group defined which is in use - if self.inputs.enable_trunk_groups and l2vlan.trunk_groups and endpoint_trunk_groups.intersection(l2vlan.trunk_groups): - vlans.append(l2vlan.id) - continue - # Skip since the vlan is not in use - continue - vlans.append(l2vlan.id) - - return vlans - return [] diff --git a/python-avd/pyavd/_eos_designs/eos_designs_facts/wan.py b/python-avd/pyavd/_eos_designs/eos_designs_facts/wan.py index c065b256800..597a62f913d 100644 --- a/python-avd/pyavd/_eos_designs/eos_designs_facts/wan.py +++ b/python-avd/pyavd/_eos_designs/eos_designs_facts/wan.py @@ -4,13 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from . import EosDesignsFacts +from .utils import UtilsMixin -class WanMixin: +class WanMixin(UtilsMixin): """ Mixin Class providing a subset of EosDesignsFacts. @@ -19,7 +17,7 @@ class WanMixin: """ @cached_property - def wan_path_groups(self: EosDesignsFacts) -> list | None: + def wan_path_groups(self) -> list | None: """ Return the list of WAN path_groups directly connected to this router. diff --git a/python-avd/pyavd/_eos_designs/shared_utils/__init__.py b/python-avd/pyavd/_eos_designs/shared_utils/__init__.py index 696e66019ac..8fa991fc851 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/__init__.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/__init__.py @@ -1,8 +1,9 @@ # Copyright (c) 2023-2025 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 pyavd._eos_designs.schema import EosDesigns -from pyavd._schema.avdschema import AvdSchema +from __future__ import annotations + +from typing import TYPE_CHECKING from .cv_topology import CvTopology from .filtered_tenants import FilteredTenantsMixin @@ -26,6 +27,10 @@ from .utils import UtilsMixin from .wan import WanMixin +if TYPE_CHECKING: + from pyavd._eos_designs.schema import EosDesigns + from pyavd._schema.avdschema import AvdSchema + class SharedUtils( FilteredTenantsMixin, @@ -47,8 +52,8 @@ class SharedUtils( WanMixin, RoutingMixin, UnderlayMixin, - UtilsMixin, FlowTrackingMixin, + UtilsMixin, ): """ Class with commonly used methods / cached_properties to be shared between all the python modules loaded in eos_designs. @@ -65,7 +70,6 @@ class SharedUtils( """ def __init__(self, hostvars: dict, inputs: EosDesigns, templar: object, schema: AvdSchema) -> None: - self.hostvars = hostvars - self.inputs = inputs self.templar = templar self.schema = schema + super().__init__(hostvars=hostvars, inputs=inputs, shared_utils=self) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/cv_topology.py b/python-avd/pyavd/_eos_designs/shared_utils/cv_topology.py index c16bec805af..6a63e41f060 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/cv_topology.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/cv_topology.py @@ -9,13 +9,13 @@ from pyavd._errors import AristaAvdInvalidInputsError from pyavd.j2filters import range_expand +from .utils import UtilsMixin + if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import SharedUtils - -class CvTopology: +class CvTopology(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -24,7 +24,7 @@ class CvTopology: """ @cached_property - def cv_topology(self: SharedUtils) -> EosDesigns.CvTopologyItem | None: + def cv_topology(self) -> EosDesigns.CvTopologyItem | None: """ Returns the cv_topology for this device. @@ -47,20 +47,20 @@ def cv_topology(self: SharedUtils) -> EosDesigns.CvTopologyItem | None: msg = "Found 'use_cv_topology:true' so 'cv_topology' is required." raise AristaAvdInvalidInputsError(msg) - if self.hostname not in self.inputs.cv_topology: + if self.shared_utils.hostname not in self.inputs.cv_topology: # Ignoring missing data for this device in cv_topology. Historic behavior and needed for hybrid scenarios. return None - return self.inputs.cv_topology[self.hostname] + return self.inputs.cv_topology[self.shared_utils.hostname] @cached_property - def cv_topology_platform(self: SharedUtils) -> str | None: + def cv_topology_platform(self) -> str | None: if self.cv_topology is not None: return self.cv_topology.platform return None @cached_property - def cv_topology_config(self: SharedUtils) -> dict: + def cv_topology_config(self) -> dict: """ Returns dict with keys derived from cv topology (or empty dict). @@ -78,25 +78,25 @@ def cv_topology_config(self: SharedUtils) -> dict: cv_interfaces = self.cv_topology.interfaces - if not self.default_interfaces.uplink_interfaces: + if not self.shared_utils.default_interfaces.uplink_interfaces: msg = "Found 'use_cv_topology:true' so 'default_interfaces.[].uplink_interfaces' is required." raise AristaAvdInvalidInputsError(msg) config = {} - for uplink_interface in range_expand(self.default_interfaces.uplink_interfaces): + for uplink_interface in range_expand(self.shared_utils.default_interfaces.uplink_interfaces): if cv_interface := cv_interfaces.get(uplink_interface): config.setdefault("uplink_interfaces", []).append(cv_interface.name) config.setdefault("uplink_switches", []).append(cv_interface.neighbor) config.setdefault("uplink_switch_interfaces", []).append(cv_interface.neighbor_interface) - if not self.mlag: + if not self.shared_utils.mlag: return config - if not self.default_interfaces.mlag_interfaces: + if not self.shared_utils.default_interfaces.mlag_interfaces: msg = "Found 'use_cv_topology:true' so 'default_interfaces.[].mlag_interfaces' is required." raise AristaAvdInvalidInputsError(msg) - for mlag_interface in range_expand(self.default_interfaces.mlag_interfaces): + for mlag_interface in range_expand(self.shared_utils.default_interfaces.mlag_interfaces): if cv_interface := cv_interfaces.get(mlag_interface, default=None): config.setdefault("mlag_interfaces", []).append(cv_interface.name) # TODO: Set mlag_peer once we get a user-defined var for that. diff --git a/python-avd/pyavd/_eos_designs/shared_utils/filtered_tenants.py b/python-avd/pyavd/_eos_designs/shared_utils/filtered_tenants.py index b6958109dac..71ee3e63138 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/filtered_tenants.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/filtered_tenants.py @@ -4,18 +4,16 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import default, unique from pyavd.j2filters import natural_sort, range_expand -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class FilteredTenantsMixin: +class FilteredTenantsMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -24,18 +22,18 @@ class FilteredTenantsMixin: """ @cached_property - def filtered_tenants(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServices: + def filtered_tenants(self) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServices: """ Return sorted tenants list from all network_services_keys and filtered based on filter_tenants. Keys of Tenant data model will be converted to lists. All sub data models like vrfs and l2vlans are also converted and filtered. """ - if not self.any_network_services: + if not self.shared_utils.any_network_services: return EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServices() filtered_tenants = EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServices() - filter_tenants = self.node_config.filter.tenants + filter_tenants = self.shared_utils.node_config.filter.tenants for network_services_key in self.inputs._dynamic_keys.network_services: for original_tenant in network_services_key.value: if original_tenant.name not in filter_tenants and "all" not in filter_tenants: @@ -46,7 +44,7 @@ def filtered_tenants(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNetwor filtered_tenants.append(tenant) no_vrf_default = all("default" not in tenant.vrfs for tenant in filtered_tenants) - if self.is_wan_router and no_vrf_default: + if self.shared_utils.is_wan_router and no_vrf_default: filtered_tenants.append( EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem( name="WAN_DEFAULT", @@ -60,7 +58,7 @@ def filtered_tenants(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNetwor ), ) ) - elif self.is_wan_router: + elif self.shared_utils.is_wan_router: # It is enough to check only the first occurrence of default VRF as some other piece of code # checks that if the VRF is in multiple tenants, the configuration is consistent. for tenant in filtered_tenants: @@ -77,18 +75,19 @@ def filtered_tenants(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNetwor return filtered_tenants._natural_sorted() def filtered_l2vlans( - self: SharedUtils, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem + self, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem ) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlans: """ Return sorted and filtered l2vlan list from given tenant. Filtering based on l2vlan tags. """ - if not self.network_services_l2 or not tenant.l2vlans: + if not self.shared_utils.network_services_l2 or not tenant.l2vlans: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlans() filtered_l2vlans = tenant.l2vlans._filtered( - lambda l2vlan: self.is_accepted_vlan(l2vlan) and bool("all" in self.filter_tags or set(l2vlan.tags).intersection(self.filter_tags)) + lambda l2vlan: self.is_accepted_vlan(l2vlan) + and bool("all" in self.shared_utils.filter_tags or set(l2vlan.tags).intersection(self.shared_utils.filter_tags)) ) # Set tenant on all l2vlans TODO: avoid this. for l2vlan in filtered_l2vlans: @@ -101,7 +100,7 @@ def filtered_l2vlans( return filtered_l2vlans._natural_sorted(sort_key="id") def is_accepted_vlan( - self: SharedUtils, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem, ) -> bool: @@ -113,7 +112,7 @@ def is_accepted_vlan( if vlan.id not in self.accepted_vlans: return False - if not self.node_config.filter.only_vlans_in_use: + if not self.shared_utils.node_config.filter.only_vlans_in_use: # No further filtering return True @@ -126,7 +125,7 @@ def is_accepted_vlan( return bool(self.inputs.enable_trunk_groups and vlan.trunk_groups and endpoint_trunk_groups.intersection(vlan.trunk_groups)) @cached_property - def accepted_vlans(self: SharedUtils) -> list[int]: + def accepted_vlans(self) -> list[int]: """ The 'vlans' switch fact is a string representing a vlan range (ex. "1-200"). @@ -138,13 +137,13 @@ def accepted_vlans(self: SharedUtils) -> list[int]: return [] switch_vlans_list = range_expand(switch_vlans) accepted_vlans = [int(vlan) for vlan in switch_vlans_list] - if self.uplink_type != "port-channel": + if self.shared_utils.uplink_type != "port-channel": return accepted_vlans - uplink_switches = unique(self.uplink_switches) - uplink_switches = [uplink_switch for uplink_switch in uplink_switches if uplink_switch in self.all_fabric_devices] + uplink_switches = unique(self.shared_utils.uplink_switches) + uplink_switches = [uplink_switch for uplink_switch in uplink_switches if uplink_switch in self.shared_utils.all_fabric_devices] for uplink_switch in uplink_switches: - uplink_switch_facts = self.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.get_peer_facts(uplink_switch) uplink_switch_vlans = uplink_switch_facts.get("vlans", []) uplink_switch_vlans_list = range_expand(uplink_switch_vlans) uplink_switch_vlans_list = [int(vlan) for vlan in uplink_switch_vlans_list] @@ -152,7 +151,7 @@ def accepted_vlans(self: SharedUtils) -> list[int]: return accepted_vlans - def is_accepted_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: + def is_accepted_vrf(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: """ Returns True if. @@ -162,11 +161,11 @@ def is_accepted_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetwo - filter.not_vrfs == [] OR VRF is NOT in filter.deny_vrfs """ - return ("all" in self.node_config.filter.allow_vrfs or vrf.name in self.node_config.filter.allow_vrfs) and ( - not self.node_config.filter.deny_vrfs or vrf.name not in self.node_config.filter.deny_vrfs + return ("all" in self.shared_utils.node_config.filter.allow_vrfs or vrf.name in self.shared_utils.node_config.filter.allow_vrfs) and ( + not self.shared_utils.node_config.filter.deny_vrfs or vrf.name not in self.shared_utils.node_config.filter.deny_vrfs ) - def is_forced_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant_name: str) -> bool: + def is_forced_vrf(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant_name: str) -> bool: """ Returns True if the given VRF name should be configured even without any loopbacks or SVIs etc. @@ -175,13 +174,16 @@ def is_forced_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetwork - 'always_include_vrfs_in_tenants' is set to ['all'] - This device is using 'p2p-vrfs' as uplink type and the VRF present on the uplink switch. """ - if "all" in self.node_config.filter.always_include_vrfs_in_tenants or tenant_name in self.node_config.filter.always_include_vrfs_in_tenants: + if ( + "all" in self.shared_utils.node_config.filter.always_include_vrfs_in_tenants + or tenant_name in self.shared_utils.node_config.filter.always_include_vrfs_in_tenants + ): return True return vrf.name in (self.get_switch_fact("uplink_switch_vrfs", required=False) or []) def filtered_vrfs( - self: SharedUtils, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem + self, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem ) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.Vrfs: """ Return sorted and filtered vrf list from given tenant. @@ -198,31 +200,31 @@ def filtered_vrfs( # Copying original_vrf vrf._tenant = tenant.name - vrf.bgp_peers = vrf.bgp_peers._filtered(lambda bgp_peer: self.hostname in bgp_peer.nodes)._natural_sorted(sort_key="ip_address") - vrf.static_routes = vrf.static_routes._filtered(lambda route: not route.nodes or self.hostname in route.nodes) - vrf.ipv6_static_routes = vrf.ipv6_static_routes._filtered(lambda route: not route.nodes or self.hostname in route.nodes) + vrf.bgp_peers = vrf.bgp_peers._filtered(lambda bgp_peer: self.shared_utils.hostname in bgp_peer.nodes)._natural_sorted(sort_key="ip_address") + vrf.static_routes = vrf.static_routes._filtered(lambda route: not route.nodes or self.shared_utils.hostname in route.nodes) + vrf.ipv6_static_routes = vrf.ipv6_static_routes._filtered(lambda route: not route.nodes or self.shared_utils.hostname in route.nodes) vrf.svis = self.filtered_svis(vrf) vrf.l3_interfaces = vrf.l3_interfaces._filtered( - lambda l3_interface: bool(self.hostname in l3_interface.nodes and l3_interface.ip_addresses and l3_interface.interfaces) + lambda l3_interface: bool(self.shared_utils.hostname in l3_interface.nodes and l3_interface.ip_addresses and l3_interface.interfaces) ) - vrf.loopbacks = vrf.loopbacks._filtered(lambda loopback: loopback.node == self.hostname) + vrf.loopbacks = vrf.loopbacks._filtered(lambda loopback: loopback.node == self.shared_utils.hostname) - if self.vtep is True: + if self.shared_utils.vtep is True: evpn_l3_multicast_enabled = default(vrf.evpn_l3_multicast.enabled, tenant.evpn_l3_multicast.enabled) # TODO: Consider if all this should be moved out of filtered_vrfs. - if self.evpn_multicast: + if self.shared_utils.evpn_multicast: vrf._evpn_l3_multicast_enabled = evpn_l3_multicast_enabled vrf._evpn_l3_multicast_group_ip = vrf.evpn_l3_multicast.evpn_underlay_l3_multicast_group rps = [] for rp_entry in vrf.pim_rp_addresses or tenant.pim_rp_addresses: - if not rp_entry.nodes or self.hostname in rp_entry.nodes: + if not rp_entry.nodes or self.shared_utils.hostname in rp_entry.nodes: if not rp_entry.rps: # TODO: Evaluate if schema should just have required for this key. msg = f"'pim_rp_addresses.rps' under VRF '{vrf.name}' in Tenant '{tenant.name}' is required." raise AristaAvdInvalidInputsError(msg) for rp_ip in rp_entry.rps: - rp_address = {"address": rp_ip} + rp_address: dict[str, Any] = {"address": rp_ip} if rp_entry.groups: if rp_entry.access_list_name: rp_address["access_lists"] = [rp_entry.access_list_name] @@ -235,12 +237,14 @@ def filtered_vrfs( vrf._pim_rp_addresses = rps for evpn_peg in vrf.evpn_l3_multicast.evpn_peg or tenant.evpn_l3_multicast.evpn_peg: - if not evpn_peg.nodes or self.hostname in evpn_peg.nodes: + if not evpn_peg.nodes or self.shared_utils.hostname in evpn_peg.nodes: vrf._evpn_l3_multicast_evpn_peg_transit = evpn_peg.transit break vrf.additional_route_targets = vrf.additional_route_targets._filtered( - lambda rt: bool((not rt.nodes or self.hostname in rt.nodes) and rt.address_family and rt.route_target and rt.type in ["import", "export"]) + lambda rt: bool( + (not rt.nodes or self.shared_utils.hostname in rt.nodes) and rt.address_family and rt.route_target and rt.type in ["import", "export"] + ) ) if vrf.svis or vrf.l3_interfaces or vrf.loopbacks or self.is_forced_vrf(vrf, tenant.name): @@ -253,7 +257,7 @@ def filtered_vrfs( return filtered_vrfs def get_merged_svi_config( - self: SharedUtils, svi: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem + self, svi: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem ) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem: """ Return structured config for one svi after inheritance. @@ -290,8 +294,8 @@ def get_merged_svi_config( merged_svi = svi # Merge node specific SVI over the general SVI data. - if self.hostname in merged_svi.nodes: - node_specific_svi = merged_svi.nodes[self.hostname]._cast_as( + if self.shared_utils.hostname in merged_svi.nodes: + node_specific_svi = merged_svi.nodes[self.shared_utils.hostname]._cast_as( EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem, ignore_extra_keys=True ) merged_svi._deepmerge(node_specific_svi, list_merge="replace") @@ -299,7 +303,7 @@ def get_merged_svi_config( return merged_svi def filtered_svis( - self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem ) -> EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.Svis: """ Return sorted and filtered svi list from given tenant vrf. @@ -307,7 +311,7 @@ def filtered_svis( Filtering based on accepted vlans since eos_designs_facts already filtered that on tags and trunk_groups. """ - if not (self.network_services_l2 or self.network_services_l2_as_subint): + if not (self.shared_utils.network_services_l2 or self.shared_utils.network_services_l2_as_subint): return EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.Svis() svis = vrf.svis._filtered(self.is_accepted_vlan) @@ -316,7 +320,7 @@ def filtered_svis( svis = EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.Svis([self.get_merged_svi_config(svi) for svi in svis]) # Perform filtering on tags after merge of profiles, to support tags being set inside profiles. - svis = svis._filtered(lambda svi: "all" in self.filter_tags or bool(set(svi.tags).intersection(self.filter_tags))) + svis = svis._filtered(lambda svi: "all" in self.shared_utils.filter_tags or bool(set(svi.tags).intersection(self.shared_utils.filter_tags))) # Set tenant key on all SVIs for svi in svis: @@ -325,7 +329,7 @@ def filtered_svis( return svis._natural_sorted(sort_key="id") @cached_property - def endpoint_vlans(self: SharedUtils) -> list: + def endpoint_vlans(self) -> list: endpoint_vlans = self.get_switch_fact("endpoint_vlans", required=False) if not endpoint_vlans: return [] @@ -348,13 +352,13 @@ def get_vrf_vni(vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkS return vrf_vni @cached_property - def vrfs(self: SharedUtils) -> list[str]: + def vrfs(self) -> list[str]: """ Return the list of vrfs to be defined on this switch. Ex. ["default", "prod"] """ - if not self.network_services_l3: + if not self.shared_utils.network_services_l3: return [] return natural_sort({vrf.name for tenant in self.filtered_tenants for vrf in tenant.vrfs}) @@ -401,18 +405,18 @@ def get_additional_svi_config( svi_config.update({"ospf_authentication": ospf_authentication, "ospf_message_digest_keys": ospf_keys}) @cached_property - def bgp_in_network_services(self: SharedUtils) -> bool: + def bgp_in_network_services(self) -> bool: """ True if BGP is needed or forcefully enabled for any VRF under network services. Used to enable router_bgp even if there is no overlay or underlay routing protocol. """ - if not self.network_services_l3: + if not self.shared_utils.network_services_l3: return False return any(self.bgp_enabled_for_vrf(vrf) for tenant in self.filtered_tenants for vrf in tenant.vrfs) - def bgp_enabled_for_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: + def bgp_enabled_for_vrf(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: """ True if the given VRF should be included under Router BGP. @@ -427,11 +431,11 @@ def bgp_enabled_for_vrf(self: SharedUtils, vrf: EosDesigns._DynamicKeys.DynamicN if vrf.bgp.enabled is not None: return vrf.bgp.enabled - vrf_address_families = [af for af in vrf.address_families if af in self.overlay_address_families] + vrf_address_families = [af for af in vrf.address_families if af in self.shared_utils.overlay_address_families] return any( [ vrf_address_families, vrf.bgp_peers, - (self.uplink_type == "p2p-vrfs" and vrf.name in (self.get_switch_fact("uplink_switch_vrfs", required=False) or [])), + (self.shared_utils.uplink_type == "p2p-vrfs" and vrf.name in (self.get_switch_fact("uplink_switch_vrfs", required=False) or [])), ] ) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/flow_tracking.py b/python-avd/pyavd/_eos_designs/shared_utils/flow_tracking.py index a1fcffd4b37..8f605a42fe7 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/flow_tracking.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/flow_tracking.py @@ -9,9 +9,9 @@ from pyavd._eos_designs.schema import EosDesigns from pyavd._utils import default -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin +if TYPE_CHECKING: FlowTracking = ( EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem.FlowTracking | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.L3InterfacesItem.FlowTracking @@ -26,7 +26,7 @@ ) -class FlowTrackingMixin: +class FlowTrackingMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -35,11 +35,11 @@ class FlowTrackingMixin: """ @cached_property - def flow_tracking_type(self: SharedUtils) -> Literal["sampled", "hardware"]: - default_flow_tracker_type = self.node_type_key_data.default_flow_tracker_type - return self.node_config.flow_tracker_type or default_flow_tracker_type + def flow_tracking_type(self) -> Literal["sampled", "hardware"]: + default_flow_tracker_type = self.shared_utils.node_type_key_data.default_flow_tracker_type + return self.shared_utils.node_config.flow_tracker_type or default_flow_tracker_type - def get_flow_tracker(self: SharedUtils, flow_tracking: FlowTracking) -> dict[str, str] | None: + def get_flow_tracker(self, flow_tracking: FlowTracking) -> dict[str, str] | None: """Return flow_tracking settings for a link, falling back to the fabric flow_tracking_settings if not defined.""" match flow_tracking: case EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem.FlowTracking(): diff --git a/python-avd/pyavd/_eos_designs/shared_utils/inband_management.py b/python-avd/pyavd/_eos_designs/shared_utils/inband_management.py index 1142c40b5a5..2e50f3e2184 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/inband_management.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/inband_management.py @@ -5,17 +5,15 @@ from functools import cached_property from ipaddress import ip_network -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import get from pyavd.j2filters import natural_sort -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class InbandManagementMixin: +class InbandManagementMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -24,37 +22,37 @@ class InbandManagementMixin: """ @cached_property - def configure_inband_mgmt(self: SharedUtils) -> bool: - return bool(self.uplink_type == "port-channel" and self.inband_mgmt_ip) + def configure_inband_mgmt(self) -> bool: + return bool(self.shared_utils.uplink_type == "port-channel" and self.inband_mgmt_ip) @cached_property - def configure_inband_mgmt_ipv6(self: SharedUtils) -> bool: - return bool(self.uplink_type == "port-channel" and self.inband_mgmt_ipv6_address) + def configure_inband_mgmt_ipv6(self) -> bool: + return bool(self.shared_utils.uplink_type == "port-channel" and self.inband_mgmt_ipv6_address) @cached_property - def configure_parent_for_inband_mgmt(self: SharedUtils) -> bool: - return self.configure_inband_mgmt and not self.node_config.inband_mgmt_ip + def configure_parent_for_inband_mgmt(self) -> bool: + return self.configure_inband_mgmt and not self.shared_utils.node_config.inband_mgmt_ip @cached_property - def configure_parent_for_inband_mgmt_ipv6(self: SharedUtils) -> bool: - return self.configure_inband_mgmt_ipv6 and not self.node_config.inband_mgmt_ipv6_address + def configure_parent_for_inband_mgmt_ipv6(self) -> bool: + return self.configure_inband_mgmt_ipv6 and not self.shared_utils.node_config.inband_mgmt_ipv6_address @cached_property - def inband_mgmt_mtu(self: SharedUtils) -> int | None: - if not self.platform_settings.feature_support.per_interface_mtu: + def inband_mgmt_mtu(self) -> int | None: + if not self.shared_utils.platform_settings.feature_support.per_interface_mtu: return None - return self.node_config.inband_mgmt_mtu + return self.shared_utils.node_config.inband_mgmt_mtu @cached_property - def inband_mgmt_vrf(self: SharedUtils) -> str | None: - if (inband_mgmt_vrf := self.node_config.inband_mgmt_vrf) != "default": + def inband_mgmt_vrf(self) -> str | None: + if (inband_mgmt_vrf := self.shared_utils.node_config.inband_mgmt_vrf) != "default": return inband_mgmt_vrf return None @cached_property - def inband_mgmt_gateway(self: SharedUtils) -> str | None: + def inband_mgmt_gateway(self) -> str | None: """ Inband management gateway. @@ -68,16 +66,16 @@ def inband_mgmt_gateway(self: SharedUtils) -> str | None: return None if not self.configure_parent_for_inband_mgmt: - return self.node_config.inband_mgmt_gateway + return self.shared_utils.node_config.inband_mgmt_gateway - if not self.node_config.inband_mgmt_subnet: + if not self.shared_utils.node_config.inband_mgmt_subnet: return None - subnet = ip_network(self.node_config.inband_mgmt_subnet, strict=False) + subnet = ip_network(self.shared_utils.node_config.inband_mgmt_subnet, strict=False) return f"{subnet[1]!s}" @cached_property - def inband_mgmt_ipv6_gateway(self: SharedUtils) -> str | None: + def inband_mgmt_ipv6_gateway(self) -> str | None: """ Inband management ipv6 gateway. @@ -91,16 +89,16 @@ def inband_mgmt_ipv6_gateway(self: SharedUtils) -> str | None: return None if not self.configure_parent_for_inband_mgmt_ipv6: - return self.node_config.inband_mgmt_ipv6_gateway + return self.shared_utils.node_config.inband_mgmt_ipv6_gateway - if not self.node_config.inband_mgmt_ipv6_subnet: + if not self.shared_utils.node_config.inband_mgmt_ipv6_subnet: return None - subnet = ip_network(self.node_config.inband_mgmt_ipv6_subnet, strict=False) + subnet = ip_network(self.shared_utils.node_config.inband_mgmt_ipv6_subnet, strict=False) return f"{subnet[1]!s}" @cached_property - def inband_mgmt_ip(self: SharedUtils) -> str | None: + def inband_mgmt_ip(self) -> str | None: """ Inband management IP. @@ -109,22 +107,22 @@ def inband_mgmt_ip(self: SharedUtils) -> str | None: - deducted IP from inband_mgmt_subnet & id - None. """ - if inband_mgmt_ip := self.node_config.inband_mgmt_ip: + if inband_mgmt_ip := self.shared_utils.node_config.inband_mgmt_ip: return inband_mgmt_ip - if not self.node_config.inband_mgmt_subnet: + if not self.shared_utils.node_config.inband_mgmt_subnet: return None - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and is required to set inband_mgmt_ip from inband_mgmt_subnet" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required to set inband_mgmt_ip from inband_mgmt_subnet" raise AristaAvdInvalidInputsError(msg) - subnet = ip_network(self.node_config.inband_mgmt_subnet, strict=False) - inband_mgmt_ip = str(subnet[3 + self.id]) + subnet = ip_network(self.shared_utils.node_config.inband_mgmt_subnet, strict=False) + inband_mgmt_ip = str(subnet[3 + self.shared_utils.id]) return f"{inband_mgmt_ip}/{subnet.prefixlen}" @cached_property - def inband_mgmt_ipv6_address(self: SharedUtils) -> str | None: + def inband_mgmt_ipv6_address(self) -> str | None: """ Inband management IPv6 Address. @@ -133,47 +131,47 @@ def inband_mgmt_ipv6_address(self: SharedUtils) -> str | None: - deduced IP from inband_mgmt_ipv6_subnet & id - None. """ - if inband_mgmt_ipv6_address := self.node_config.inband_mgmt_ipv6_address: + if inband_mgmt_ipv6_address := self.shared_utils.node_config.inband_mgmt_ipv6_address: return inband_mgmt_ipv6_address - if not self.node_config.inband_mgmt_ipv6_subnet: + if not self.shared_utils.node_config.inband_mgmt_ipv6_subnet: return None - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and is required to set inband_mgmt_ipv6_address from inband_mgmt_ipv6_subnet" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required to set inband_mgmt_ipv6_address from inband_mgmt_ipv6_subnet" raise AristaAvdInvalidInputsError(msg) - subnet = ip_network(self.node_config.inband_mgmt_ipv6_subnet, strict=False) - inband_mgmt_ipv6_address = str(subnet[3 + self.id]) + subnet = ip_network(self.shared_utils.node_config.inband_mgmt_ipv6_subnet, strict=False) + inband_mgmt_ipv6_address = str(subnet[3 + self.shared_utils.id]) return f"{inband_mgmt_ipv6_address}/{subnet.prefixlen}" @cached_property - def inband_mgmt_interface(self: SharedUtils) -> str | None: + def inband_mgmt_interface(self) -> str | None: """ Inband management Interface used only to set as source interface on various management protocols. For L2 switches defaults to Vlan For all other devices set to value of inband_mgmt_interface or None """ - if inband_mgmt_interface := self.node_config.inband_mgmt_interface: + if inband_mgmt_interface := self.shared_utils.node_config.inband_mgmt_interface: return inband_mgmt_interface if self.configure_inband_mgmt or self.configure_inband_mgmt_ipv6: - return f"Vlan{self.node_config.inband_mgmt_vlan}" + return f"Vlan{self.shared_utils.node_config.inband_mgmt_vlan}" return None @cached_property - def inband_management_parent_vlans(self: SharedUtils) -> dict: - if not self.underlay_router: + def inband_management_parent_vlans(self) -> dict: + if not self.shared_utils.underlay_router: return {} svis = {} subnets = [] ipv6_subnets = [] - peers = natural_sort(get(self.hostvars, f"avd_topology_peers..{self.hostname}", separator="..", default=[])) + peers = natural_sort(get(self.shared_utils._hostvars, f"avd_topology_peers..{self.shared_utils.hostname}", separator="..", default=[])) for peer in peers: - peer_facts = self.get_peer_facts(peer, required=True) + peer_facts = self.get_peer_facts(peer) if (vlan := peer_facts.get("inband_mgmt_vlan")) is None: continue diff --git a/python-avd/pyavd/_eos_designs/shared_utils/interface_descriptions.py b/python-avd/pyavd/_eos_designs/shared_utils/interface_descriptions.py index b3a8b0517c9..0456f377cad 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/interface_descriptions.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/interface_descriptions.py @@ -4,16 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import load_python_class from pyavd.api.interface_descriptions import AvdInterfaceDescriptions -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class InterfaceDescriptionsMixin: +class InterfaceDescriptionsMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -22,20 +20,20 @@ class InterfaceDescriptionsMixin: """ @cached_property - def interface_descriptions(self: SharedUtils) -> AvdInterfaceDescriptions: + def interface_descriptions(self) -> AvdInterfaceDescriptions: """ Load the python_module defined in `templates.interface_descriptions.python_module`. Return an instance of the class defined by `templates.interface_descriptions.python_class_name` as cached_property. """ - module_path = self.node_type_key_data.interface_descriptions.python_module + module_path = self.shared_utils.node_type_key_data.interface_descriptions.python_module if module_path is None: - return AvdInterfaceDescriptions(hostvars=self.hostvars, inputs=self.inputs, shared_utils=self) + return AvdInterfaceDescriptions(hostvars=self.shared_utils._hostvars, inputs=self.inputs, shared_utils=self.shared_utils) cls: type[AvdInterfaceDescriptions] = load_python_class( module_path, - self.node_type_key_data.interface_descriptions.python_class_name, + self.shared_utils.node_type_key_data.interface_descriptions.python_class_name, AvdInterfaceDescriptions, ) - return cls(hostvars=self.hostvars, inputs=self.inputs, shared_utils=self) + return cls(hostvars=self.shared_utils._hostvars, inputs=self.inputs, shared_utils=self) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/ip_addressing.py b/python-avd/pyavd/_eos_designs/shared_utils/ip_addressing.py index fa2ed709c58..b25efd3cd74 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/ip_addressing.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/ip_addressing.py @@ -4,17 +4,15 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdMissingVariableError from pyavd._utils import load_python_class from pyavd.api.ip_addressing import AvdIpAddressing -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class IpAddressingMixin: +class IpAddressingMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -23,52 +21,52 @@ class IpAddressingMixin: """ @cached_property - def loopback_ipv6_pool(self: SharedUtils) -> str: - if not self.node_config.loopback_ipv6_pool: + def loopback_ipv6_pool(self) -> str: + if not self.shared_utils.node_config.loopback_ipv6_pool: msg = "loopback_ipv6_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.loopback_ipv6_pool + return self.shared_utils.node_config.loopback_ipv6_pool @cached_property - def loopback_ipv4_pool(self: SharedUtils) -> str: - if not self.node_config.loopback_ipv4_pool: + def loopback_ipv4_pool(self) -> str: + if not self.shared_utils.node_config.loopback_ipv4_pool: msg = "loopback_ipv4_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.loopback_ipv4_pool + return self.shared_utils.node_config.loopback_ipv4_pool @cached_property - def vtep_loopback_ipv4_pool(self: SharedUtils) -> str: - if not self.node_config.vtep_loopback_ipv4_pool: + def vtep_loopback_ipv4_pool(self) -> str: + if not self.shared_utils.node_config.vtep_loopback_ipv4_pool: msg = "vtep_loopback_ipv4_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.vtep_loopback_ipv4_pool + return self.shared_utils.node_config.vtep_loopback_ipv4_pool @cached_property - def vtep_ip(self: SharedUtils) -> str: + def vtep_ip(self) -> str: """Render ipv4 address for vtep_ip using dynamically loaded python module.""" - if self.mlag is True: + if self.shared_utils.mlag is True: return self.ip_addressing.vtep_ip_mlag() return self.ip_addressing.vtep_ip() @cached_property - def ip_addressing(self: SharedUtils) -> AvdIpAddressing: + def ip_addressing(self) -> AvdIpAddressing: """ Load the python_module defined in `templates.ip_addressing.python_module`. Return an instance of the class defined by `templates.ip_addressing.python_class_name` as cached_property. """ - module_path = self.node_type_key_data.ip_addressing.python_module + module_path = self.shared_utils.node_type_key_data.ip_addressing.python_module if module_path is None: - return AvdIpAddressing(hostvars=self.hostvars, inputs=self.inputs, shared_utils=self) + return AvdIpAddressing(hostvars=self.shared_utils._hostvars, inputs=self.inputs, shared_utils=self.shared_utils) cls: type[AvdIpAddressing] = load_python_class( module_path, - self.node_type_key_data.ip_addressing.python_class_name, + self.shared_utils.node_type_key_data.ip_addressing.python_class_name, AvdIpAddressing, ) - return cls(hostvars=self.hostvars, inputs=self.inputs, shared_utils=self) + return cls(hostvars=self.shared_utils._hostvars, inputs=self.inputs, shared_utils=self) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/l3_interfaces.py b/python-avd/pyavd/_eos_designs/shared_utils/l3_interfaces.py index deedc7e088d..efe78d5c682 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/l3_interfaces.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/l3_interfaces.py @@ -4,17 +4,15 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdInvalidInputsError from pyavd.api.interface_descriptions import InterfaceDescriptionData -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class L3InterfacesMixin: +class L3InterfacesMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -22,17 +20,8 @@ class L3InterfacesMixin: Using type-hint on self to get proper type-hints on attributes across all Mixins. """ - def sanitize_interface_name(self: SharedUtils, interface_name: str) -> str: - """ - Interface name is used as value for certain fields, but `/` are not allowed in the value. - - So we transform `/` to `_` - Ethernet1/1.1 is transformed into Ethernet1_1.1 - """ - return interface_name.replace("/", "_") - def apply_l3_interfaces_profile( - self: SharedUtils, l3_interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem + self, l3_interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem ) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem: """Apply a profile to an l3_interface.""" if not l3_interface.profile: @@ -49,14 +38,14 @@ def apply_l3_interfaces_profile( return l3_interface._deepinherited(profile_as_interface) @cached_property - def l3_interfaces(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces: + def l3_interfaces(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces: """Returns the list of l3_interfaces, where any referenced profiles are applied.""" return EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces( - [self.apply_l3_interfaces_profile(l3_interface) for l3_interface in self.node_config.l3_interfaces] + [self.apply_l3_interfaces_profile(l3_interface) for l3_interface in self.shared_utils.node_config.l3_interfaces] ) @cached_property - def l3_interfaces_bgp_neighbors(self: SharedUtils) -> list: + def l3_interfaces_bgp_neighbors(self) -> list: neighbors = [] for interface in self.l3_interfaces: if not (interface.peer_ip and interface.bgp): @@ -75,9 +64,9 @@ def l3_interfaces_bgp_neighbors(self: SharedUtils) -> list: description = interface.description if not description: - description = self.interface_descriptions.underlay_ethernet_interface( + description = self.shared_utils.interface_descriptions.underlay_ethernet_interface( InterfaceDescriptionData( - shared_utils=self, + shared_utils=self.shared_utils, interface=interface.name, peer=interface.peer, peer_interface=interface.peer_interface, diff --git a/python-avd/pyavd/_eos_designs/shared_utils/link_tracking_groups.py b/python-avd/pyavd/_eos_designs/shared_utils/link_tracking_groups.py index 44a9de5fa66..55fad9baf22 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/link_tracking_groups.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/link_tracking_groups.py @@ -4,15 +4,13 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import default, strip_empties_from_list -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class LinkTrackingGroupsMixin: +class LinkTrackingGroupsMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -21,12 +19,12 @@ class LinkTrackingGroupsMixin: """ @cached_property - def link_tracking_groups(self: SharedUtils) -> list | None: - if self.node_config.link_tracking.enabled: + def link_tracking_groups(self) -> list | None: + if self.shared_utils.node_config.link_tracking.enabled: link_tracking_groups = [] - default_recovery_delay = default(self.platform_settings.reload_delay.mlag, 300) - if len(self.node_config.link_tracking.groups) > 0: - for lt_group in self.node_config.link_tracking.groups: + default_recovery_delay = default(self.shared_utils.platform_settings.reload_delay.mlag, 300) + if len(self.shared_utils.node_config.link_tracking.groups) > 0: + for lt_group in self.shared_utils.node_config.link_tracking.groups: lt_group_dict = lt_group._as_dict(include_default_values=True) lt_group_dict["recovery_delay"] = default(lt_group.recovery_delay, default_recovery_delay) link_tracking_groups.append(lt_group_dict) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/mgmt.py b/python-avd/pyavd/_eos_designs/shared_utils/mgmt.py index 93e9ad23f5d..69482da3cdb 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/mgmt.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/mgmt.py @@ -4,16 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import default, get -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class MgmtMixin: +class MgmtMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -22,7 +20,7 @@ class MgmtMixin: """ @cached_property - def mgmt_interface(self: SharedUtils) -> str: + def mgmt_interface(self) -> str: """ mgmt_interface. @@ -32,25 +30,25 @@ def mgmt_interface(self: SharedUtils) -> str: Fabric Topology data model mgmt_interface. """ return default( - self.node_config.mgmt_interface, + self.shared_utils.node_config.mgmt_interface, # Notice that we actually have a default value for the next two, but the precedence order would break if we use it. # TODO: Evaluate if we should remove the default values from either or both. - self.platform_settings._get("management_interface", None), + self.shared_utils.platform_settings._get("management_interface", None), self.inputs._get("mgmt_interface", None), - get(self.cv_topology_config, "mgmt_interface"), + get(self.shared_utils.cv_topology_config, "mgmt_interface"), "Management1", ) @cached_property - def mgmt_gateway(self: SharedUtils) -> str | None: - return default(self.node_config.mgmt_gateway, self.inputs.mgmt_gateway) + def mgmt_gateway(self) -> str | None: + return default(self.shared_utils.node_config.mgmt_gateway, self.inputs.mgmt_gateway) @cached_property - def ipv6_mgmt_gateway(self: SharedUtils) -> str | None: - return default(self.node_config.ipv6_mgmt_gateway, self.inputs.ipv6_mgmt_gateway) + def ipv6_mgmt_gateway(self) -> str | None: + return default(self.shared_utils.node_config.ipv6_mgmt_gateway, self.inputs.ipv6_mgmt_gateway) @cached_property - def default_mgmt_method(self: SharedUtils) -> str | None: + def default_mgmt_method(self) -> str | None: """ This is only executed if some protocol looks for the default value, so we can raise here to ensure a working config. @@ -58,7 +56,7 @@ def default_mgmt_method(self: SharedUtils) -> str | None: """ default_mgmt_method = self.inputs.default_mgmt_method if default_mgmt_method == "oob": - if self.node_config.mgmt_ip is None and self.node_config.ipv6_mgmt_ip is None: + if self.shared_utils.node_config.mgmt_ip is None and self.shared_utils.node_config.ipv6_mgmt_ip is None: msg = "'default_mgmt_method: oob' requires either 'mgmt_ip' or 'ipv6_mgmt_ip' to be set." raise AristaAvdInvalidInputsError(msg) @@ -66,7 +64,7 @@ def default_mgmt_method(self: SharedUtils) -> str | None: if default_mgmt_method == "inband": # Check for missing interface - if self.inband_mgmt_interface is None: + if self.shared_utils.inband_mgmt_interface is None: msg = "'default_mgmt_method: inband' requires 'inband_mgmt_interface' to be set." raise AristaAvdInvalidInputsError(msg) @@ -75,20 +73,20 @@ def default_mgmt_method(self: SharedUtils) -> str | None: return None @cached_property - def default_mgmt_protocol_vrf(self: SharedUtils) -> str | None: + def default_mgmt_protocol_vrf(self) -> str | None: if self.default_mgmt_method == "oob": return self.inputs.mgmt_interface_vrf if self.default_mgmt_method == "inband": # inband_mgmt_vrf returns None for vrf default. - return self.inband_mgmt_vrf or "default" + return self.shared_utils.inband_mgmt_vrf or "default" return None @cached_property - def default_mgmt_protocol_interface(self: SharedUtils) -> str | None: + def default_mgmt_protocol_interface(self) -> str | None: if self.default_mgmt_method == "oob": return self.mgmt_interface if self.default_mgmt_method == "inband": - return self.inband_mgmt_interface + return self.shared_utils.inband_mgmt_interface return None diff --git a/python-avd/pyavd/_eos_designs/shared_utils/misc.py b/python-avd/pyavd/_eos_designs/shared_utils/misc.py index 4fd0711a463..78cc6d90c82 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/misc.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/misc.py @@ -4,20 +4,19 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError, AristaAvdMissingVariableError from pyavd._utils import default, get from pyavd.j2filters import range_expand +from .utils import UtilsMixin + if TYPE_CHECKING: - from pyavd._eos_designs.eos_designs_facts import EosDesignsFacts from pyavd._eos_designs.schema import EosDesigns - from . import SharedUtils - -class MiscMixin: +class MiscMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -26,37 +25,37 @@ class MiscMixin: """ @cached_property - def all_fabric_devices(self: SharedUtils) -> list[str]: - avd_switch_facts: dict = get(self.hostvars, "avd_switch_facts", required=True) + def all_fabric_devices(self) -> list[str]: + avd_switch_facts: dict = get(self.shared_utils._hostvars, "avd_switch_facts", required=True) return list(avd_switch_facts.keys()) @cached_property - def hostname(self: SharedUtils) -> str: + def hostname(self) -> str: """Hostname set based on inventory_hostname variable. TODO: Get a proper attribute on the class instead of gleaning from the regular inputs.""" - return get(self.hostvars, "inventory_hostname", required=True) + return get(self.shared_utils._hostvars, "inventory_hostname", required=True) @cached_property - def id(self: SharedUtils) -> int | None: - return self.node_config.id + def id(self) -> int | None: + return self.shared_utils.node_config.id @cached_property - def filter_tags(self: SharedUtils) -> list: + def filter_tags(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.Filter.Tags: """Return filter.tags + group if defined.""" - filter_tags = self.node_config.filter.tags - if self.group is not None: - filter_tags.append(self.group) + filter_tags = self.shared_utils.node_config.filter.tags + if self.shared_utils.group is not None: + filter_tags.append(self.shared_utils.group) return filter_tags @cached_property - def igmp_snooping_enabled(self: SharedUtils) -> bool: - return default(self.node_config.igmp_snooping_enabled, self.inputs.default_igmp_snooping_enabled) + def igmp_snooping_enabled(self) -> bool: + return default(self.shared_utils.node_config.igmp_snooping_enabled, self.inputs.default_igmp_snooping_enabled) @cached_property - def only_local_vlan_trunk_groups(self: SharedUtils) -> bool: + def only_local_vlan_trunk_groups(self) -> bool: return self.inputs.enable_trunk_groups and self.inputs.only_local_vlan_trunk_groups @cached_property - def system_mac_address(self: SharedUtils) -> str | None: + def system_mac_address(self) -> str | None: """ system_mac_address. @@ -64,21 +63,25 @@ def system_mac_address(self: SharedUtils) -> str | None: Fabric Topology data model system_mac_address -> Host variable var system_mac_address ->. """ - return default(self.node_config.system_mac_address, self.inputs.system_mac_address) + return default(self.shared_utils.node_config.system_mac_address, self.inputs.system_mac_address) @cached_property - def uplink_switches(self: SharedUtils) -> list[str]: - return self.node_config.uplink_switches._as_list() or get(self.cv_topology_config, "uplink_switches") or [] + def uplink_switches(self) -> list[str]: + return self.shared_utils.node_config.uplink_switches._as_list() or get(self.shared_utils.cv_topology_config, "uplink_switches") or [] @cached_property - def uplink_interfaces(self: SharedUtils) -> list[str]: + def uplink_interfaces(self) -> list[str]: return range_expand( - self.node_config.uplink_interfaces or get(self.cv_topology_config, "uplink_interfaces") or self.default_interfaces.uplink_interfaces, + self.shared_utils.node_config.uplink_interfaces + or get(self.shared_utils.cv_topology_config, "uplink_interfaces") + or self.shared_utils.default_interfaces.uplink_interfaces, ) @cached_property - def uplink_switch_interfaces(self: SharedUtils) -> list[str]: - uplink_switch_interfaces = self.node_config.uplink_switch_interfaces or get(self.cv_topology_config, "uplink_switch_interfaces") or [] + def uplink_switch_interfaces(self) -> list[str]: + uplink_switch_interfaces = ( + self.shared_utils.node_config.uplink_switch_interfaces or get(self.shared_utils.cv_topology_config, "uplink_switch_interfaces") or [] + ) if uplink_switch_interfaces: return range_expand(uplink_switch_interfaces) @@ -92,7 +95,7 @@ def uplink_switch_interfaces(self: SharedUtils) -> list[str]: uplink_switch_interfaces = [] uplink_switch_counter = {} for uplink_switch in self.uplink_switches: - uplink_switch_facts: EosDesignsFacts = self.get_peer_facts(uplink_switch, required=True) + uplink_switch_facts = self.get_peer_facts_cls(uplink_switch) # Count the number of instances the current switch was processed uplink_switch_counter[uplink_switch] = uplink_switch_counter.get(uplink_switch, 0) + 1 @@ -101,7 +104,7 @@ def uplink_switch_interfaces(self: SharedUtils) -> list[str]: # Add uplink_switch_interface based on this switch's ID (-1 for 0-based) * max_parallel_uplinks + index_of_parallel_uplinks. # For max_parallel_uplinks: 2 this would assign downlink interfaces like this: # spine1 downlink-interface mapping: [ leaf-id1, leaf-id1, leaf-id2, leaf-id2, leaf-id3, leaf-id3, ... ] - downlink_index = (self.id - 1) * self.node_config.max_parallel_uplinks + index_of_parallel_uplinks + downlink_index = (self.id - 1) * self.shared_utils.node_config.max_parallel_uplinks + index_of_parallel_uplinks if len(uplink_switch_facts._default_downlink_interfaces) > downlink_index: uplink_switch_interfaces.append(uplink_switch_facts._default_downlink_interfaces[downlink_index]) else: @@ -114,7 +117,7 @@ def uplink_switch_interfaces(self: SharedUtils) -> list[str]: return uplink_switch_interfaces @cached_property - def serial_number(self: SharedUtils) -> str | None: + def serial_number(self) -> str | None: """ serial_number. @@ -122,22 +125,22 @@ def serial_number(self: SharedUtils) -> str | None: Fabric Topology data model serial_number -> Host variable var serial_number. """ - return default(self.node_config.serial_number, self.inputs.serial_number) + return default(self.shared_utils.node_config.serial_number, self.inputs.serial_number) @cached_property - def max_uplink_switches(self: SharedUtils) -> int: + def max_uplink_switches(self) -> int: """max_uplink_switches will default to the length of uplink_switches.""" - return default(self.node_config.max_uplink_switches, len(self.uplink_switches)) + return default(self.shared_utils.node_config.max_uplink_switches, len(self.uplink_switches)) @cached_property - def p2p_uplinks_mtu(self: SharedUtils) -> int | None: - if not self.platform_settings.feature_support.per_interface_mtu: + def p2p_uplinks_mtu(self) -> int | None: + if not self.shared_utils.platform_settings.feature_support.per_interface_mtu: return None - p2p_uplinks_mtu = default(self.platform_settings.p2p_uplinks_mtu, self.inputs.p2p_uplinks_mtu) - return default(self.node_config.uplink_mtu, p2p_uplinks_mtu) + p2p_uplinks_mtu = default(self.shared_utils.platform_settings.p2p_uplinks_mtu, self.inputs.p2p_uplinks_mtu) + return default(self.shared_utils.node_config.uplink_mtu, p2p_uplinks_mtu) @cached_property - def fabric_name(self: SharedUtils) -> str: + def fabric_name(self) -> str: if not self.inputs.fabric_name: msg = "fabric_name" raise AristaAvdMissingVariableError(msg) @@ -145,33 +148,23 @@ def fabric_name(self: SharedUtils) -> str: return self.inputs.fabric_name @cached_property - def uplink_interface_speed(self: SharedUtils) -> str | None: - return default(self.node_config.uplink_interface_speed, self.default_interfaces.uplink_interface_speed) + def uplink_interface_speed(self) -> str | None: + return default(self.shared_utils.node_config.uplink_interface_speed, self.shared_utils.default_interfaces.uplink_interface_speed) @cached_property - def uplink_switch_interface_speed(self: SharedUtils) -> str | None: + def uplink_switch_interface_speed(self) -> str | None: # Keeping since we will need it when adding speed support under default interfaces. - return self.node_config.uplink_switch_interface_speed + return self.shared_utils.node_config.uplink_switch_interface_speed @cached_property - def default_interface_mtu(self: SharedUtils) -> int | None: - return default(self.platform_settings.default_interface_mtu, self.inputs.default_interface_mtu) - - def get_switch_fact(self: SharedUtils, key: str, required: bool = True) -> Any: - """ - Return facts from EosDesignsFacts. - - We need to go via avd_switch_facts since PyAVD does not expose "switch.*" in get_avdfacts. - """ - return get(self.hostvars, f"avd_switch_facts..{self.hostname}..switch..{key}", required=required, org_key=f"switch.{key}", separator="..") + def default_interface_mtu(self) -> int | None: + return default(self.shared_utils.platform_settings.default_interface_mtu, self.inputs.default_interface_mtu) @cached_property - def evpn_multicast(self: SharedUtils) -> bool: + def evpn_multicast(self) -> bool: return self.get_switch_fact("evpn_multicast", required=False) is True - def get_ipv4_acl( - self: SharedUtils, name: str, interface_name: str, *, interface_ip: str | None = None, peer_ip: str | None = None - ) -> EosDesigns.Ipv4AclsItem: + def get_ipv4_acl(self, name: str, interface_name: str, *, interface_ip: str | None = None, peer_ip: str | None = None) -> EosDesigns.Ipv4AclsItem: """ Get one IPv4 ACL from "ipv4_acls" where fields have been substituted. diff --git a/python-avd/pyavd/_eos_designs/shared_utils/mlag.py b/python-avd/pyavd/_eos_designs/shared_utils/mlag.py index 382155f4cdf..b10e8692d13 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/mlag.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/mlag.py @@ -11,15 +11,15 @@ from pyavd._utils import default, get, get_ip_from_ip_prefix from pyavd.j2filters import range_expand +from .utils import UtilsMixin + if TYPE_CHECKING: from typing import Literal from pyavd._eos_designs.eos_designs_facts import EosDesignsFacts - from . import SharedUtils - -class MlagMixin: +class MlagMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -28,160 +28,173 @@ class MlagMixin: """ @cached_property - def mlag(self: SharedUtils) -> bool: - return self.node_type_key_data.mlag_support and self.node_config.mlag and self.node_group_is_primary_and_peer_hostname is not None + def mlag(self) -> bool: + return ( + self.shared_utils.node_type_key_data.mlag_support + and self.shared_utils.node_config.mlag + and self.shared_utils.node_group_is_primary_and_peer_hostname is not None + ) @cached_property - def group(self: SharedUtils) -> str | None: + def group(self) -> str | None: """Group set to "node_group" name or None.""" - if self.node_group_config is not None: - return self.node_group_config.group + if self.shared_utils.node_group_config is not None: + return self.shared_utils.node_group_config.group return None @cached_property - def mlag_interfaces(self: SharedUtils) -> list: - return range_expand(self.node_config.mlag_interfaces or get(self.cv_topology_config, "mlag_interfaces") or self.default_interfaces.mlag_interfaces) + def mlag_interfaces(self) -> list: + return range_expand( + self.shared_utils.node_config.mlag_interfaces + or get(self.shared_utils.cv_topology_config, "mlag_interfaces") + or self.shared_utils.default_interfaces.mlag_interfaces + ) @cached_property - def mlag_peer_ipv4_pool(self: SharedUtils) -> str: - if not self.node_config.mlag_peer_ipv4_pool: + def mlag_peer_ipv4_pool(self) -> str: + if not self.shared_utils.node_config.mlag_peer_ipv4_pool: msg = "mlag_peer_ipv4_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.mlag_peer_ipv4_pool + return self.shared_utils.node_config.mlag_peer_ipv4_pool @cached_property - def mlag_peer_ipv6_pool(self: SharedUtils) -> str: - if not self.node_config.mlag_peer_ipv6_pool: + def mlag_peer_ipv6_pool(self) -> str: + if not self.shared_utils.node_config.mlag_peer_ipv6_pool: msg = "mlag_peer_ipv6_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.mlag_peer_ipv6_pool + return self.shared_utils.node_config.mlag_peer_ipv6_pool @cached_property - def mlag_peer_l3_ipv4_pool(self: SharedUtils) -> str: - if not self.node_config.mlag_peer_l3_ipv4_pool: + def mlag_peer_l3_ipv4_pool(self) -> str: + if not self.shared_utils.node_config.mlag_peer_l3_ipv4_pool: msg = "mlag_peer_l3_ipv4_pool" raise AristaAvdMissingVariableError(msg) - return self.node_config.mlag_peer_l3_ipv4_pool + return self.shared_utils.node_config.mlag_peer_l3_ipv4_pool @cached_property - def mlag_role(self: SharedUtils) -> Literal["primary", "secondary"] | None: - if self.mlag and self.node_group_is_primary_and_peer_hostname is not None: - return "primary" if self.node_group_is_primary_and_peer_hostname[0] else "secondary" + def mlag_role(self) -> Literal["primary", "secondary"] | None: + if self.mlag and self.shared_utils.node_group_is_primary_and_peer_hostname is not None: + return "primary" if self.shared_utils.node_group_is_primary_and_peer_hostname[0] else "secondary" return None @cached_property - def mlag_peer(self: SharedUtils) -> str: - if self.node_group_is_primary_and_peer_hostname is not None: - return self.node_group_is_primary_and_peer_hostname[1] + def mlag_peer(self) -> str: + if self.shared_utils.node_group_is_primary_and_peer_hostname is not None: + return self.shared_utils.node_group_is_primary_and_peer_hostname[1] msg = "Unable to find MLAG peer within same node group" raise AristaAvdError(msg) @cached_property - def mlag_l3(self: SharedUtils) -> bool: - return self.mlag is True and self.underlay_router is True + def mlag_l3(self) -> bool: + return self.mlag is True and self.shared_utils.underlay_router is True @cached_property - def mlag_peer_l3_vlan(self: SharedUtils) -> int | None: + def mlag_peer_l3_vlan(self) -> int | None: if self.mlag_l3: - mlag_peer_vlan = self.node_config.mlag_peer_vlan - mlag_peer_l3_vlan = self.node_config.mlag_peer_l3_vlan + mlag_peer_vlan = self.shared_utils.node_config.mlag_peer_vlan + mlag_peer_l3_vlan = self.shared_utils.node_config.mlag_peer_l3_vlan if mlag_peer_l3_vlan not in [None, False, mlag_peer_vlan]: return mlag_peer_l3_vlan return None @cached_property - def mlag_peer_ip(self: SharedUtils) -> str: + def mlag_peer_ip(self) -> str: return self.get_mlag_peer_fact("mlag_ip") @cached_property - def mlag_peer_l3_ip(self: SharedUtils) -> str | None: + def mlag_peer_l3_ip(self) -> str | None: if self.mlag_peer_l3_vlan is not None: return self.get_mlag_peer_fact("mlag_l3_ip") return None @cached_property - def mlag_peer_id(self: SharedUtils) -> int: + def mlag_peer_id(self) -> int: return self.get_mlag_peer_fact("id") - def get_mlag_peer_fact(self: SharedUtils, key: str, required: bool = True) -> Any: + def get_mlag_peer_fact(self, key: str, required: bool = True) -> Any: return get(self.mlag_peer_facts, key, required=required, org_key=f"avd_switch_facts.({self.mlag_peer}).switch.{key}") @cached_property - def mlag_peer_facts(self: SharedUtils) -> EosDesignsFacts | dict: - return self.get_peer_facts(self.mlag_peer, required=True) + def mlag_peer_facts(self) -> EosDesignsFacts | dict: + return self.get_peer_facts(self.mlag_peer) + + @cached_property + def mlag_peer_facts_cls(self) -> EosDesignsFacts: + """Should only be called from eos_designs_facts.""" + return self.get_peer_facts_cls(self.mlag_peer) @cached_property - def mlag_peer_mgmt_ip(self: SharedUtils) -> str | None: + def mlag_peer_mgmt_ip(self) -> str | None: if (mlag_peer_mgmt_ip := self.get_mlag_peer_fact("mgmt_ip", required=False)) is None: return None return get_ip_from_ip_prefix(mlag_peer_mgmt_ip) @cached_property - def mlag_ip(self: SharedUtils) -> str | None: + def mlag_ip(self) -> str | None: """Render ipv4 address for mlag_ip using dynamically loaded python module.""" if self.mlag_role == "primary": - return self.ip_addressing.mlag_ip_primary() + return self.shared_utils.ip_addressing.mlag_ip_primary() if self.mlag_role == "secondary": - return self.ip_addressing.mlag_ip_secondary() + return self.shared_utils.ip_addressing.mlag_ip_secondary() return None @cached_property - def mlag_l3_ip(self: SharedUtils) -> str | None: + def mlag_l3_ip(self) -> str | None: """Render ipv4 address for mlag_l3_ip using dynamically loaded python module.""" if self.mlag_peer_l3_vlan is None: return None if self.mlag_role == "primary": - return self.ip_addressing.mlag_l3_ip_primary() + return self.shared_utils.ip_addressing.mlag_l3_ip_primary() if self.mlag_role == "secondary": - return self.ip_addressing.mlag_l3_ip_secondary() + return self.shared_utils.ip_addressing.mlag_l3_ip_secondary() return None @cached_property - def mlag_switch_ids(self: SharedUtils) -> dict | None: + def mlag_switch_ids(self) -> dict | None: """ Returns the switch id's of both primary and secondary switches for a given node group. {"primary": int, "secondary": int}. """ if self.mlag_role == "primary": - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and is required to compute MLAG ids" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required to compute MLAG ids" raise AristaAvdInvalidInputsError(msg) - return {"primary": self.id, "secondary": self.mlag_peer_id} + return {"primary": self.shared_utils.id, "secondary": self.mlag_peer_id} if self.mlag_role == "secondary": - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and is required to compute MLAG ids" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required to compute MLAG ids" raise AristaAvdInvalidInputsError(msg) - return {"primary": self.mlag_peer_id, "secondary": self.id} + return {"primary": self.mlag_peer_id, "secondary": self.shared_utils.id} return None @cached_property - def mlag_port_channel_id(self: SharedUtils) -> int: + def mlag_port_channel_id(self) -> int: if not self.mlag_interfaces: - msg = f"'mlag_interfaces' not set on '{self.hostname}." + msg = f"'mlag_interfaces' not set on '{self.shared_utils.hostname}." raise AristaAvdInvalidInputsError(msg) default_mlag_port_channel_id = int("".join(findall(r"\d", self.mlag_interfaces[0]))) - return default(self.node_config.mlag_port_channel_id, default_mlag_port_channel_id) + return default(self.shared_utils.node_config.mlag_port_channel_id, default_mlag_port_channel_id) @cached_property - def mlag_peer_port_channel_id(self: SharedUtils) -> int: + def mlag_peer_port_channel_id(self) -> int: return get(self.mlag_peer_facts, "mlag_port_channel_id", default=self.mlag_port_channel_id) @cached_property - def mlag_peer_interfaces(self: SharedUtils) -> list: + def mlag_peer_interfaces(self) -> list: return get(self.mlag_peer_facts, "mlag_interfaces", default=self.mlag_interfaces) @cached_property - def mlag_ibgp_ip(self: SharedUtils) -> str: + def mlag_ibgp_ip(self) -> str: if self.mlag_l3_ip is not None: return self.mlag_l3_ip return self.mlag_ip @cached_property - def mlag_peer_ibgp_ip(self: SharedUtils) -> str: + def mlag_peer_ibgp_ip(self) -> str: if self.mlag_peer_l3_ip is not None: return self.mlag_peer_l3_ip diff --git a/python-avd/pyavd/_eos_designs/shared_utils/node_config.py b/python-avd/pyavd/_eos_designs/shared_utils/node_config.py index 20793e64d42..7bb2af00c74 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/node_config.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/node_config.py @@ -4,16 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdInvalidInputsError -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class NodeConfigMixin: +class NodeConfigMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -22,13 +20,13 @@ class NodeConfigMixin: """ @cached_property - def node_type_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes: + def node_type_config(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes: """ The object representing the `:` containing the `defaults`, `nodes`, `node_groups` etc. The relevant dynamic key is found in self.inputs._dynamic_keys which is populated by the _from_dict() loader on the EosDesigns class. """ - node_type_key = self.node_type_key_data.key + node_type_key = self.shared_utils.node_type_key_data.key if node_type_key in self.inputs._dynamic_keys.custom_node_types: return self.inputs._dynamic_keys.custom_node_types[node_type_key].value._cast_as(EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes) @@ -36,24 +34,24 @@ def node_type_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTy if node_type_key in self.inputs._dynamic_keys.node_types: return self.inputs._dynamic_keys.node_types[node_type_key].value - msg = f"'type' is set to '{self.type}', for which node configs should use the key '{node_type_key}'. '{node_type_key}' was not found." + msg = f"'type' is set to '{self.shared_utils.type}', for which node configs should use the key '{node_type_key}'. '{node_type_key}' was not found." raise AristaAvdInvalidInputsError(msg) @cached_property - def node_group_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodeGroupsItem | None: + def node_group_config(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodeGroupsItem | None: """ The object representing the `.node_groups[]` where this node is found. Used by MLAG and WAN HA logic to find out who our MLAG / WAN HA peer is. """ for node_group in self.node_type_config.node_groups: - if self.hostname in node_group.nodes: + if self.shared_utils.hostname in node_group.nodes: return node_group return None @cached_property - def node_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem: + def node_config(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem: """ NodesItem object containing the fully inherited node config. @@ -64,14 +62,16 @@ def node_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesIt .nodes.[] """ node_config = ( - self.node_type_config.nodes[self.hostname] - if self.hostname in self.node_type_config.nodes + self.node_type_config.nodes[self.shared_utils.hostname] + if self.shared_utils.hostname in self.node_type_config.nodes else EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem() ) if self.node_group_config is not None: node_config._deepinherit( - self.node_group_config.nodes[self.hostname]._cast_as(EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem, ignore_extra_keys=True) + self.node_group_config.nodes[self.shared_utils.hostname]._cast_as( + EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem, ignore_extra_keys=True + ) ) node_config._deepinherit(self.node_group_config._cast_as(EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem, ignore_extra_keys=True)) @@ -82,7 +82,7 @@ def node_config(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesIt return node_config @cached_property - def node_group_is_primary_and_peer_hostname(self: SharedUtils) -> tuple[bool, str] | None: + def node_group_is_primary_and_peer_hostname(self) -> tuple[bool, str] | None: """ Node group position and peer used for MLAG and WAN HA. @@ -94,6 +94,6 @@ def node_group_is_primary_and_peer_hostname(self: SharedUtils) -> tuple[bool, st return None nodes = list(self.node_group_config.nodes.keys()) - index = nodes.index(self.hostname) + index = nodes.index(self.shared_utils.hostname) peer_index = not index # (0->1 and 1>0) return index == 0, nodes[peer_index] diff --git a/python-avd/pyavd/_eos_designs/shared_utils/node_type.py b/python-avd/pyavd/_eos_designs/shared_utils/node_type.py index 3db118131e5..94d0c37c7c2 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/node_type.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/node_type.py @@ -5,16 +5,15 @@ from functools import cached_property from re import search -from typing import TYPE_CHECKING, Literal +from typing import Literal from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import default -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class NodeTypeMixin: +class NodeTypeMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -23,75 +22,75 @@ class NodeTypeMixin: """ @cached_property - def type(self: SharedUtils) -> str: + def type(self) -> str: """Type fact set based on type variable.""" if (node_type := self.inputs.type) is not None: return node_type if self.default_node_type: return self.default_node_type - msg = f"'type' for host {self.hostname}" + msg = f"'type' for host {self.shared_utils.hostname}" raise AristaAvdInvalidInputsError(msg) @cached_property - def default_node_type(self: SharedUtils) -> str | None: + def default_node_type(self) -> str | None: """default_node_type set based on hostname, returning first node type matching a regex in default_node_types.""" for default_node_type in self.inputs.default_node_types: for hostname_regex in default_node_type.match_hostnames: - if search(f"^{hostname_regex}$", self.hostname): + if search(f"^{hostname_regex}$", self.shared_utils.hostname): return default_node_type.node_type return None @cached_property - def connected_endpoints(self: SharedUtils) -> bool: + def connected_endpoints(self) -> bool: """ Should we configure connected endpoints? connected_endpoints set based on node_type_keys..connected_endpoints. """ - return self.node_type_key_data.connected_endpoints + return self.shared_utils.node_type_key_data.connected_endpoints @cached_property - def underlay_router(self: SharedUtils) -> bool: + def underlay_router(self) -> bool: """ Is this an underlay router? underlay_router set based on node_type_keys..underlay_router. """ - return self.node_type_key_data.underlay_router + return self.shared_utils.node_type_key_data.underlay_router @cached_property - def uplink_type(self: SharedUtils) -> Literal["p2p", "port-channel", "p2p-vrfs", "lan"]: + def uplink_type(self) -> Literal["p2p", "port-channel", "p2p-vrfs", "lan"]: """ Uplink type. uplink_type set based on .nodes.[].uplink_type and node_type_keys..uplink_type. """ - return default(self.node_config.uplink_type, self.node_type_key_data.uplink_type) + return default(self.shared_utils.node_config.uplink_type, self.shared_utils.node_type_key_data.uplink_type) @cached_property - def network_services_l1(self: SharedUtils) -> bool: + def network_services_l1(self) -> bool: """ Should we configure L1 network services? network_services_l1 set based on node_type_keys..network_services.l1. """ - return self.node_type_key_data.network_services.l1 + return self.shared_utils.node_type_key_data.network_services.l1 @cached_property - def network_services_l2(self: SharedUtils) -> bool: + def network_services_l2(self) -> bool: """ Should we configure L2 network services? network_services_l2 set based on node_type_keys..network_services.l2. """ - return self.node_type_key_data.network_services.l2 + return self.shared_utils.node_type_key_data.network_services.l2 @cached_property - def network_services_l3(self: SharedUtils) -> bool: + def network_services_l3(self) -> bool: """ Should we configure L3 network services? @@ -99,12 +98,12 @@ def network_services_l3(self: SharedUtils) -> bool: and . | nodes.<> >.evpn_services_l2_only. """ # network_services_l3 override based on evpn_services_l2_only - if self.vtep and self.node_config.evpn_services_l2_only: + if self.vtep and self.shared_utils.node_config.evpn_services_l2_only: return False - return self.node_type_key_data.network_services.l3 + return self.shared_utils.node_type_key_data.network_services.l3 @cached_property - def network_services_l2_as_subint(self: SharedUtils) -> bool: + def network_services_l2_as_subint(self) -> bool: """ Should we deploy SVIs as subinterfaces? @@ -114,22 +113,22 @@ def network_services_l2_as_subint(self: SharedUtils) -> bool: return self.network_services_l3 and self.uplink_type in ["lan", "lan-port-channel"] @cached_property - def any_network_services(self: SharedUtils) -> bool: + def any_network_services(self) -> bool: """Returns True if either L1, L2 or L3 network_services are enabled.""" return self.network_services_l1 or self.network_services_l2 or self.network_services_l3 @cached_property - def mpls_lsr(self: SharedUtils) -> bool: + def mpls_lsr(self) -> bool: """ Is this an MPLS LSR? mpls_lsr set based on node_type_keys..mpls_lsr. """ - return self.node_type_key_data.mpls_lsr + return self.shared_utils.node_type_key_data.mpls_lsr @cached_property - def vtep(self: SharedUtils) -> bool: + def vtep(self) -> bool: """ Is this a VTEP? @@ -137,4 +136,4 @@ def vtep(self: SharedUtils) -> bool: .nodes.[].vtep and node_type_keys..vtep. """ - return default(self.node_config.vtep, self.node_type_key_data.vtep) + return default(self.shared_utils.node_config.vtep, self.shared_utils.node_type_key_data.vtep) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/node_type_keys.py b/python-avd/pyavd/_eos_designs/shared_utils/node_type_keys.py index fd5d74c041e..46cd51ef3e2 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/node_type_keys.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/node_type_keys.py @@ -4,14 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import get, get_item -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin MPLS_DEFAULT_NODE_TYPE_KEYS = [ { @@ -183,7 +181,7 @@ } -class NodeTypeKeysMixin: +class NodeTypeKeysMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -192,19 +190,19 @@ class NodeTypeKeysMixin: """ @cached_property - def node_type_key_data(self: SharedUtils) -> EosDesigns.NodeTypeKeysItem: + def node_type_key_data(self) -> EosDesigns.NodeTypeKeysItem: """node_type_key_data containing settings for this node_type.""" for node_type_key in self.inputs.custom_node_type_keys: - if node_type_key.type == self.type: + if node_type_key.type == self.shared_utils.type: return node_type_key._cast_as(EosDesigns.NodeTypeKeysItem) design_type = self.inputs.design.type default_node_type_keys_for_our_design = EosDesigns.NodeTypeKeys._from_list(get(DEFAULT_NODE_TYPE_KEYS, design_type, default=[])) node_type_keys = self.inputs.node_type_keys or default_node_type_keys_for_our_design for node_type_key in node_type_keys: - if node_type_key.type == self.type: + if node_type_key.type == self.shared_utils.type: return node_type_key # Not found - msg = f"Could not find the given type '{self.type}' in node_type_keys or custom_node_type_keys." + msg = f"Could not find the given type '{self.shared_utils.type}' in node_type_keys or custom_node_type_keys." raise AristaAvdInvalidInputsError(msg) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/overlay.py b/python-avd/pyavd/_eos_designs/shared_utils/overlay.py index 74af539aa46..31cae34bf4e 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/overlay.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/overlay.py @@ -6,16 +6,14 @@ from functools import cached_property from ipaddress import ip_address from re import fullmatch -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import default -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class OverlayMixin: +class OverlayMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -24,52 +22,52 @@ class OverlayMixin: """ @cached_property - def vtep_loopback(self: SharedUtils) -> str: + def vtep_loopback(self) -> str: """The default is Loopback1 except for WAN devices where the default is Dps1.""" - default_vtep_loopback = "Dps1" if self.is_wan_router else "Loopback1" - return default(self.node_config.vtep_loopback, default_vtep_loopback) + default_vtep_loopback = "Dps1" if self.shared_utils.is_wan_router else "Loopback1" + return default(self.shared_utils.node_config.vtep_loopback, default_vtep_loopback) @cached_property - def evpn_role(self: SharedUtils) -> str | None: - if self.underlay_router: - default_evpn_role = self.node_type_key_data.default_evpn_role - return default(self.node_config.evpn_role, default_evpn_role) + def evpn_role(self) -> str | None: + if self.shared_utils.underlay_router: + default_evpn_role = self.shared_utils.node_type_key_data.default_evpn_role + return default(self.shared_utils.node_config.evpn_role, default_evpn_role) return None @cached_property - def mpls_overlay_role(self: SharedUtils) -> str | None: - if self.underlay_router: - default_mpls_overlay_role = self.node_type_key_data.default_mpls_overlay_role - return default(self.node_config.mpls_overlay_role, default_mpls_overlay_role) + def mpls_overlay_role(self) -> str | None: + if self.shared_utils.underlay_router: + default_mpls_overlay_role = self.shared_utils.node_type_key_data.default_mpls_overlay_role + return default(self.shared_utils.node_config.mpls_overlay_role, default_mpls_overlay_role) return None @cached_property - def overlay_rd_type_admin_subfield(self: SharedUtils) -> str: + def overlay_rd_type_admin_subfield(self) -> str: admin_subfield = self.inputs.overlay_rd_type.admin_subfield admin_subfield_offset = self.inputs.overlay_rd_type.admin_subfield_offset return self.get_rd_admin_subfield_value(admin_subfield, admin_subfield_offset) @cached_property - def overlay_rd_type_vrf_admin_subfield(self: SharedUtils) -> str: + def overlay_rd_type_vrf_admin_subfield(self) -> str: vrf_admin_subfield: str = default(self.inputs.overlay_rd_type.vrf_admin_subfield, self.inputs.overlay_rd_type.admin_subfield) vrf_admin_subfield_offset: int = default(self.inputs.overlay_rd_type.vrf_admin_subfield_offset, self.inputs.overlay_rd_type.admin_subfield_offset) return self.get_rd_admin_subfield_value(vrf_admin_subfield, vrf_admin_subfield_offset) - def get_rd_admin_subfield_value(self: SharedUtils, admin_subfield: str, admin_subfield_offset: int) -> str: + def get_rd_admin_subfield_value(self, admin_subfield: str, admin_subfield_offset: int) -> str: if admin_subfield in ["router_id", "overlay_loopback_ip"]: - return self.router_id + return self.shared_utils.router_id if admin_subfield == "vtep_loopback": - return self.vtep_ip + return self.shared_utils.vtep_ip if admin_subfield == "bgp_as": - return self.bgp_as + return self.shared_utils.bgp_as if admin_subfield == "switch_id": - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and 'overlay_rd_type_admin_subfield' is set to 'switch_id'" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and 'overlay_rd_type_admin_subfield' is set to 'switch_id'" raise AristaAvdInvalidInputsError(msg) - return self.id + admin_subfield_offset + return self.shared_utils.id + admin_subfield_offset if fullmatch(r"\d+", str(admin_subfield)): return str(int(admin_subfield) + admin_subfield_offset) @@ -77,25 +75,25 @@ def get_rd_admin_subfield_value(self: SharedUtils, admin_subfield: str, admin_su try: ip_address(admin_subfield) except ValueError: - return self.router_id + return self.shared_utils.router_id return admin_subfield @cached_property - def overlay_routing_protocol_address_family(self: SharedUtils) -> str: + def overlay_routing_protocol_address_family(self) -> str: overlay_routing_protocol_address_family = self.inputs.overlay_routing_protocol_address_family - if overlay_routing_protocol_address_family == "ipv6" and not (self.underlay_ipv6 is True and self.inputs.underlay_rfc5549): + if overlay_routing_protocol_address_family == "ipv6" and not (self.shared_utils.underlay_ipv6 is True and self.inputs.underlay_rfc5549): msg = "'overlay_routing_protocol_address_family: ipv6' is only supported in combination with 'underlay_ipv6: True' and 'underlay_rfc5549: True'" raise AristaAvdError(msg) return overlay_routing_protocol_address_family @cached_property - def evpn_encapsulation(self: SharedUtils) -> str: + def evpn_encapsulation(self) -> str: """EVPN encapsulation based on fabric_evpn_encapsulation and node default_evpn_encapsulation.""" - return default(self.inputs.fabric_evpn_encapsulation, self.node_type_key_data.default_evpn_encapsulation) + return default(self.inputs.fabric_evpn_encapsulation, self.shared_utils.node_type_key_data.default_evpn_encapsulation) @cached_property - def evpn_soo(self: SharedUtils) -> str: + def evpn_soo(self) -> str: """ Site-Of-Origin used as BGP extended community. @@ -105,111 +103,115 @@ def evpn_soo(self: SharedUtils) -> str: TODO: Reconsider if suffix should just be :1 for all WAN routers. """ - if self.is_wan_router: + if self.shared_utils.is_wan_router: # for Pathfinder, no HA, no Site ID - if not self.is_cv_pathfinder_client: - return f"{self.router_id}:0" + if not self.shared_utils.is_cv_pathfinder_client: + return f"{self.shared_utils.router_id}:0" - if self.wan_site is None: + if self.shared_utils.wan_site is None: # Should never happen but just in case. msg = "Could not find 'cv_pathfinder_site' so it is not possible to generate evpn_soo." raise AristaAvdInvalidInputsError(msg) - if not self.wan_ha: - return f"{self.router_id}:{self.wan_site.id}" - if self.is_first_ha_peer: - return f"{self.router_id}:{self.wan_site.id}" + if not self.shared_utils.wan_ha: + return f"{self.shared_utils.router_id}:{self.shared_utils.wan_site.id}" + if self.shared_utils.is_first_ha_peer: + return f"{self.shared_utils.router_id}:{self.shared_utils.wan_site.id}" - peer_fact = self.get_peer_facts(self.wan_ha_peer, required=True) - return f"{peer_fact['router_id']}:{self.wan_site.id}" + peer_fact = self.get_peer_facts_dict(self.shared_utils.wan_ha_peer) + return f"{peer_fact['router_id']}:{self.shared_utils.wan_site.id}" if self.overlay_vtep: - return f"{self.vtep_ip}:1" + return f"{self.shared_utils.vtep_ip}:1" - return f"{self.router_id}:1" + return f"{self.shared_utils.router_id}:1" @cached_property - def overlay_evpn(self: SharedUtils) -> bool: + def overlay_evpn(self) -> bool: # Set overlay_evpn to enable EVPN on the node return ( - self.bgp + self.shared_utils.bgp and (self.evpn_role in ["client", "server"] or self.mpls_overlay_role in ["client", "server"]) - and self.overlay_routing_protocol in ["ebgp", "ibgp"] - and "evpn" in self.overlay_address_families + and self.shared_utils.overlay_routing_protocol in ["ebgp", "ibgp"] + and "evpn" in self.shared_utils.overlay_address_families ) @cached_property - def overlay_mpls(self: SharedUtils) -> bool: + def overlay_mpls(self) -> bool: """Set overlay_mpls to enable MPLS as the primary overlay.""" return any([self.overlay_evpn_mpls, self.overlay_vpn_ipv4, self.overlay_vpn_ipv6]) and not self.overlay_evpn_vxlan @cached_property - def overlay_ipvpn_gateway(self: SharedUtils) -> bool: + def overlay_ipvpn_gateway(self) -> bool: # Set overlay_ipvpn_gateway to trigger ipvpn interworking configuration. - return self.overlay_evpn and self.node_config.ipvpn_gateway.enabled + return self.overlay_evpn and self.shared_utils.node_config.ipvpn_gateway.enabled @cached_property - def overlay_ler(self: SharedUtils) -> bool: - return self.underlay_mpls and (self.mpls_overlay_role in ["client", "server"] or self.evpn_role in ["client", "server"]) and (self.any_network_services) + def overlay_ler(self) -> bool: + return ( + self.shared_utils.underlay_mpls + and (self.mpls_overlay_role in ["client", "server"] or self.evpn_role in ["client", "server"]) + and (self.shared_utils.any_network_services) + ) @cached_property - def overlay_vtep(self: SharedUtils) -> bool: + def overlay_vtep(self) -> bool: # Set overlay_vtep to enable VXLAN VTEP return ( - self.overlay_routing_protocol in ["ebgp", "ibgp", "her", "cvx"] - and (self.network_services_l2 or self.network_services_l3) - and self.underlay_router - and self.uplink_type in ["p2p", "p2p-vrfs", "lan"] - and self.vtep + self.shared_utils.overlay_routing_protocol in ["ebgp", "ibgp", "her", "cvx"] + and (self.shared_utils.network_services_l2 or self.shared_utils.network_services_l3) + and self.shared_utils.underlay_router + and self.shared_utils.uplink_type in ["p2p", "p2p-vrfs", "lan"] + and self.shared_utils.vtep ) @cached_property - def overlay_vpn_ipv4(self: SharedUtils) -> bool: + def overlay_vpn_ipv4(self) -> bool: # Set overlay_vpn_ipv4 enable IP-VPN configuration on the node. - if self.bgp is not True: + if self.shared_utils.bgp is not True: return False - return (self.overlay_routing_protocol == "ibgp" and "vpn-ipv4" in self.overlay_address_families) or ( - "vpn-ipv4" in self.node_config.ipvpn_gateway.address_families and self.overlay_ipvpn_gateway + return (self.shared_utils.overlay_routing_protocol == "ibgp" and "vpn-ipv4" in self.shared_utils.overlay_address_families) or ( + "vpn-ipv4" in self.shared_utils.node_config.ipvpn_gateway.address_families and self.overlay_ipvpn_gateway ) @cached_property - def overlay_vpn_ipv6(self: SharedUtils) -> bool: + def overlay_vpn_ipv6(self) -> bool: # Set overlay_vpn_ipv4 to enable IP-VPN configuration on the node. - if self.bgp is not True: + if self.shared_utils.bgp is not True: return False - return (self.overlay_routing_protocol == "ibgp" and "vpn-ipv6" in self.overlay_address_families) or ( - "vpn-ipv6" in self.node_config.ipvpn_gateway.address_families and self.overlay_ipvpn_gateway + return (self.shared_utils.overlay_routing_protocol == "ibgp" and "vpn-ipv6" in self.shared_utils.overlay_address_families) or ( + "vpn-ipv6" in self.shared_utils.node_config.ipvpn_gateway.address_families and self.overlay_ipvpn_gateway ) @cached_property - def overlay_peering_address(self: SharedUtils) -> str | None: - if not self.underlay_router: + def overlay_peering_address(self) -> str | None: + if not self.shared_utils.underlay_router: return None if self.overlay_routing_protocol_address_family == "ipv6": - return self.ipv6_router_id + return self.shared_utils.ipv6_router_id - return self.router_id + return self.shared_utils.router_id @cached_property - def overlay_cvx(self: SharedUtils) -> bool: - return self.overlay_routing_protocol == "cvx" + def overlay_cvx(self) -> bool: + return self.shared_utils.overlay_routing_protocol == "cvx" @cached_property - def overlay_her(self: SharedUtils) -> bool: - return self.overlay_routing_protocol == "her" + def overlay_her(self) -> bool: + return self.shared_utils.overlay_routing_protocol == "her" @cached_property - def overlay_dpath(self: SharedUtils) -> bool: + def overlay_dpath(self) -> bool: # Set dpath based on ipvpn_gateway parameters - return self.overlay_ipvpn_gateway and self.node_config.ipvpn_gateway.enable_d_path + return self.overlay_ipvpn_gateway and self.shared_utils.node_config.ipvpn_gateway.enable_d_path @cached_property - def overlay_evpn_vxlan(self: SharedUtils) -> bool: + def overlay_evpn_vxlan(self) -> bool: return self.overlay_evpn and self.evpn_encapsulation == "vxlan" @cached_property - def overlay_evpn_mpls(self: SharedUtils) -> bool: + def overlay_evpn_mpls(self) -> bool: return self.overlay_evpn and self.evpn_encapsulation == "mpls" diff --git a/python-avd/pyavd/_eos_designs/shared_utils/platform_mixin.py b/python-avd/pyavd/_eos_designs/shared_utils/platform_mixin.py index 17321e91682..cf00fa99963 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/platform_mixin.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/platform_mixin.py @@ -5,16 +5,14 @@ from functools import cached_property from re import search -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._utils import default -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class PlatformMixin: +class PlatformMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -23,11 +21,11 @@ class PlatformMixin: """ @cached_property - def platform(self: SharedUtils) -> str | None: - return default(self.node_config.platform, self.cv_topology_platform) + def platform(self) -> str | None: + return default(self.shared_utils.node_config.platform, self.shared_utils.cv_topology_platform) @cached_property - def platform_settings(self: SharedUtils) -> EosDesigns.PlatformSettingsItem | EosDesigns.CustomPlatformSettingsItem: + def platform_settings(self) -> EosDesigns.PlatformSettingsItem | EosDesigns.CustomPlatformSettingsItem: # First look for a matching platform setting specifying our platform if self.platform is not None: for platform_setting in self.inputs.custom_platform_settings: @@ -48,20 +46,20 @@ def platform_settings(self: SharedUtils) -> EosDesigns.PlatformSettingsItem | Eo return EosDesigns.PlatformSettingsItem() @cached_property - def default_interfaces(self: SharedUtils) -> EosDesigns.DefaultInterfacesItem: + def default_interfaces(self) -> EosDesigns.DefaultInterfacesItem: """default_interfaces set based on default_interfaces.""" device_platform = self.platform or "default" # First look for a matching default interface set that matches our platform and type for default_interface in self.inputs.default_interfaces: for platform in default_interface.platforms: - if search(f"^{platform}$", device_platform) and self.type in default_interface.types: + if search(f"^{platform}$", device_platform) and self.shared_utils.type in default_interface.types: return default_interface # If not found, then look for a default default_interface that matches our type for default_interface in self.inputs.default_interfaces: for platform in default_interface.platforms: - if search(f"^{platform}$", "default") and self.type in default_interface.types: + if search(f"^{platform}$", "default") and self.shared_utils.type in default_interface.types: return default_interface return EosDesigns.DefaultInterfacesItem() diff --git a/python-avd/pyavd/_eos_designs/shared_utils/ptp.py b/python-avd/pyavd/_eos_designs/shared_utils/ptp.py index 0873b68156c..50a51592253 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/ptp.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/ptp.py @@ -9,13 +9,13 @@ from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import default +from .utils import UtilsMixin + if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import SharedUtils - -class PtpMixin: +class PtpMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -24,17 +24,17 @@ class PtpMixin: """ @cached_property - def ptp_enabled(self: SharedUtils) -> bool: + def ptp_enabled(self) -> bool: default_ptp_enabled = self.inputs.ptp_settings.enabled - return bool(default(self.node_config.ptp.enabled, default_ptp_enabled)) + return bool(default(self.shared_utils.node_config.ptp.enabled, default_ptp_enabled)) @cached_property - def ptp_profile_name(self: SharedUtils) -> str: + def ptp_profile_name(self) -> str: default_ptp_profile = self.inputs.ptp_settings.profile - return self.node_config.ptp.profile or default_ptp_profile + return self.shared_utils.node_config.ptp.profile or default_ptp_profile @cached_property - def ptp_profile(self: SharedUtils) -> EosDesigns.PtpProfilesItem: + def ptp_profile(self) -> EosDesigns.PtpProfilesItem: if self.ptp_profile_name not in self.inputs.ptp_profiles: msg = f"PTP Profile '{self.ptp_profile_name}' referenced under `ptp.profile` node variables does not exist in `ptp_profiles`." raise AristaAvdInvalidInputsError(msg) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/routing.py b/python-avd/pyavd/_eos_designs/shared_utils/routing.py index af2c522ad5d..53cf15d0dc4 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/routing.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/routing.py @@ -4,16 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError, AristaAvdMissingVariableError from pyavd.j2filters import range_expand -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class RoutingMixin: +class RoutingMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -22,63 +20,63 @@ class RoutingMixin: """ @cached_property - def underlay_routing_protocol(self: SharedUtils) -> str: - default_underlay_routing_protocol = self.node_type_key_data.default_underlay_routing_protocol + def underlay_routing_protocol(self) -> str: + default_underlay_routing_protocol = self.shared_utils.node_type_key_data.default_underlay_routing_protocol return (self.inputs.underlay_routing_protocol or default_underlay_routing_protocol).lower() @cached_property - def overlay_routing_protocol(self: SharedUtils) -> str: - default_overlay_routing_protocol = self.node_type_key_data.default_overlay_routing_protocol + def overlay_routing_protocol(self) -> str: + default_overlay_routing_protocol = self.shared_utils.node_type_key_data.default_overlay_routing_protocol return (self.inputs.overlay_routing_protocol or default_overlay_routing_protocol).lower() @cached_property - def overlay_address_families(self: SharedUtils) -> list[str]: + def overlay_address_families(self) -> list[str]: if self.overlay_routing_protocol in ["ebgp", "ibgp"]: - default_overlay_address_families = self.node_type_key_data.default_overlay_address_families - return self.node_config.overlay_address_families._as_list() or default_overlay_address_families._as_list() + default_overlay_address_families = self.shared_utils.node_type_key_data.default_overlay_address_families + return self.shared_utils.node_config.overlay_address_families._as_list() or default_overlay_address_families._as_list() return [] @cached_property - def bgp(self: SharedUtils) -> bool: + def bgp(self) -> bool: """Boolean telling if BGP Routing should be configured.""" - if not self.underlay_router: + if not self.shared_utils.underlay_router: return False return ( - self.uplink_type in ["p2p", "p2p-vrfs", "lan"] + self.shared_utils.uplink_type in ["p2p", "p2p-vrfs", "lan"] and ( self.underlay_routing_protocol == "ebgp" or ( self.overlay_routing_protocol in ["ebgp", "ibgp"] - and (self.evpn_role in ["client", "server"] or self.mpls_overlay_role in ["client", "server"]) + and (self.shared_utils.evpn_role in ["client", "server"] or self.shared_utils.mpls_overlay_role in ["client", "server"]) ) - or self.bgp_in_network_services + or self.shared_utils.bgp_in_network_services ) - ) or bool(self.l3_interfaces_bgp_neighbors) + ) or bool(self.shared_utils.l3_interfaces_bgp_neighbors) @cached_property - def router_id(self: SharedUtils) -> str | None: + def router_id(self) -> str | None: """Render IP address for router_id.""" - if self.underlay_router: - return self.ip_addressing.router_id() + if self.shared_utils.underlay_router: + return self.shared_utils.ip_addressing.router_id() return None @cached_property - def ipv6_router_id(self: SharedUtils) -> str | None: + def ipv6_router_id(self) -> str | None: """Render IPv6 address for router_id.""" - if self.underlay_router and self.underlay_ipv6: - return self.ip_addressing.ipv6_router_id() + if self.shared_utils.underlay_router and self.shared_utils.underlay_ipv6: + return self.shared_utils.ip_addressing.ipv6_router_id() return None @cached_property - def isis_instance_name(self: SharedUtils) -> str | None: - if self.underlay_router and self.underlay_routing_protocol in ["isis", "isis-ldp", "isis-sr", "isis-sr-ldp"]: - default_isis_instance_name = "CORE" if self.mpls_lsr else "EVPN_UNDERLAY" + def isis_instance_name(self) -> str | None: + if self.shared_utils.underlay_router and self.underlay_routing_protocol in ["isis", "isis-ldp", "isis-sr", "isis-sr-ldp"]: + default_isis_instance_name = "CORE" if self.shared_utils.mpls_lsr else "EVPN_UNDERLAY" return self.inputs.underlay_isis_instance_name or default_isis_instance_name return None @cached_property - def bgp_as(self: SharedUtils) -> str | None: + def bgp_as(self) -> str | None: """ Get global bgp_as or fabric_topology bgp_as. @@ -97,21 +95,21 @@ def bgp_as(self: SharedUtils) -> str | None: if self.inputs.bgp_as: return self.inputs.bgp_as - if self.node_config.bgp_as is None: + if self.shared_utils.node_config.bgp_as is None: msg = "bgp_as" raise AristaAvdMissingVariableError(msg) - bgp_as_range_expanded = range_expand(self.node_config.bgp_as) + bgp_as_range_expanded = range_expand(self.shared_utils.node_config.bgp_as) try: if len(bgp_as_range_expanded) == 1: return bgp_as_range_expanded[0] - if self.mlag_switch_ids: - return bgp_as_range_expanded[self.mlag_switch_ids["primary"] - 1] + if self.shared_utils.mlag_switch_ids: + return bgp_as_range_expanded[self.shared_utils.mlag_switch_ids["primary"] - 1] - if self.id is None: - msg = f"'id' is not set on '{self.hostname}' and is required when expanding 'bgp_as'" + if self.shared_utils.id is None: + msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required when expanding 'bgp_as'" raise AristaAvdInvalidInputsError(msg) - return bgp_as_range_expanded[self.id - 1] + return bgp_as_range_expanded[self.shared_utils.id - 1] except IndexError as exc: msg = f"Unable to allocate BGP AS: bgp_as range is too small ({len(bgp_as_range_expanded)}) for the id of the device" raise AristaAvdError(msg) from exc diff --git a/python-avd/pyavd/_eos_designs/shared_utils/underlay.py b/python-avd/pyavd/_eos_designs/shared_utils/underlay.py index 4402becb8df..5cfd0ba24f8 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/underlay.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/underlay.py @@ -4,13 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class UnderlayMixin: +class UnderlayMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -19,60 +17,69 @@ class UnderlayMixin: """ @cached_property - def underlay_bgp(self: SharedUtils) -> bool: - return self.bgp and self.underlay_routing_protocol == "ebgp" and self.underlay_router and self.uplink_type in ["p2p", "p2p-vrfs"] + def underlay_bgp(self) -> bool: + return ( + self.shared_utils.bgp + and self.shared_utils.underlay_routing_protocol == "ebgp" + and self.shared_utils.underlay_router + and self.shared_utils.uplink_type in ["p2p", "p2p-vrfs"] + ) @cached_property - def underlay_mpls(self: SharedUtils) -> bool: + def underlay_mpls(self) -> bool: return ( - self.underlay_routing_protocol in ["isis-sr", "isis-ldp", "isis-sr-ldp", "ospf-ldp"] - and self.mpls_lsr - and self.underlay_router - and self.uplink_type in ["p2p", "p2p-vrfs"] + self.shared_utils.underlay_routing_protocol in ["isis-sr", "isis-ldp", "isis-sr-ldp", "ospf-ldp"] + and self.shared_utils.mpls_lsr + and self.shared_utils.underlay_router + and self.shared_utils.uplink_type in ["p2p", "p2p-vrfs"] ) @cached_property - def underlay_ldp(self: SharedUtils) -> bool: - return self.underlay_routing_protocol in ["isis-ldp", "isis-sr-ldp", "ospf-ldp"] and self.underlay_mpls + def underlay_ldp(self) -> bool: + return self.shared_utils.underlay_routing_protocol in ["isis-ldp", "isis-sr-ldp", "ospf-ldp"] and self.underlay_mpls @cached_property - def underlay_sr(self: SharedUtils) -> bool: - return self.underlay_routing_protocol in ["isis-sr", "isis-sr-ldp"] and self.underlay_mpls + def underlay_sr(self) -> bool: + return self.shared_utils.underlay_routing_protocol in ["isis-sr", "isis-sr-ldp"] and self.underlay_mpls @cached_property - def underlay_ospf(self: SharedUtils) -> bool: - return self.underlay_routing_protocol in ["ospf", "ospf-ldp"] and self.underlay_router and self.uplink_type in ["p2p", "p2p-vrfs"] + def underlay_ospf(self) -> bool: + return ( + self.shared_utils.underlay_routing_protocol in ["ospf", "ospf-ldp"] + and self.shared_utils.underlay_router + and self.shared_utils.uplink_type in ["p2p", "p2p-vrfs"] + ) @cached_property - def underlay_isis(self: SharedUtils) -> bool: + def underlay_isis(self) -> bool: return ( - self.underlay_routing_protocol in ["isis", "isis-sr", "isis-ldp", "isis-sr-ldp"] - and self.underlay_router - and self.uplink_type in ["p2p", "p2p-vrfs"] + self.shared_utils.underlay_routing_protocol in ["isis", "isis-sr", "isis-ldp", "isis-sr-ldp"] + and self.shared_utils.underlay_router + and self.shared_utils.uplink_type in ["p2p", "p2p-vrfs"] ) @cached_property - def underlay_ipv6(self: SharedUtils) -> bool: - return self.inputs.underlay_ipv6 and self.underlay_router + def underlay_ipv6(self) -> bool: + return self.inputs.underlay_ipv6 and self.shared_utils.underlay_router @cached_property - def underlay_multicast(self: SharedUtils) -> bool: - return self.inputs.underlay_multicast and self.underlay_router + def underlay_multicast(self) -> bool: + return self.inputs.underlay_multicast and self.shared_utils.underlay_router @cached_property - def underlay_multicast_rp_interfaces(self: SharedUtils) -> list[dict] | None: + def underlay_multicast_rp_interfaces(self) -> list[dict] | None: if not self.underlay_multicast or not self.inputs.underlay_multicast_rps: return None underlay_multicast_rp_interfaces = [] for rp_entry in self.inputs.underlay_multicast_rps: - if self.hostname not in rp_entry.nodes: + if self.shared_utils.hostname not in rp_entry.nodes: continue underlay_multicast_rp_interfaces.append( { - "name": f"Loopback{rp_entry.nodes[self.hostname].loopback_number}", - "description": rp_entry.nodes[self.hostname].description, + "name": f"Loopback{rp_entry.nodes[self.shared_utils.hostname].loopback_number}", + "description": rp_entry.nodes[self.shared_utils.hostname].description, "ip_address": f"{rp_entry.rp}/32", }, ) diff --git a/python-avd/pyavd/_eos_designs/shared_utils/utils.py b/python-avd/pyavd/_eos_designs/shared_utils/utils.py index ef3572a4ef9..49a26f19146 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/utils.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/utils.py @@ -4,25 +4,25 @@ from __future__ import annotations from functools import lru_cache -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast +from pyavd._eos_designs.avdfacts import AvdFacts +from pyavd._eos_designs.eos_designs_facts import EosDesignsFacts +from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import get, template_var if TYPE_CHECKING: from typing import TypeVar - from pyavd._eos_designs.eos_designs_facts import EosDesignsFacts from pyavd._eos_designs.schema import EosDesigns - from . import SharedUtils - ADAPTER_SETTINGS = TypeVar( "ADAPTER_SETTINGS", EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, EosDesigns.NetworkPortsItem ) -class UtilsMixin: +class UtilsMixin(AvdFacts): """ Mixin Class providing a subset of SharedUtils. @@ -30,7 +30,64 @@ class UtilsMixin: Using type-hint on self to get proper type-hints on attributes across all Mixins. """ - def get_peer_facts(self: SharedUtils, peer_name: str, required: bool = True) -> EosDesignsFacts | dict | None: + def sanitize_interface_name(self, interface_name: str) -> str: + """ + Interface name is used as value for certain fields, but `/` are not allowed in the value. + + So we transform `/` to `_` + Ethernet1/1.1 is transformed into Ethernet1_1.1 + """ + return interface_name.replace("/", "_") + + def get_switch_fact(self, key: str, required: bool = True) -> Any: + """ + Return facts from EosDesignsFacts. + + We need to go via avd_switch_facts since PyAVD does not expose "switch.*" in get_avdfacts. + """ + return get( + self.shared_utils._hostvars, + f"avd_switch_facts..{self.shared_utils.hostname}..switch..{key}", + required=required, + org_key=f"switch.{key}", + separator="..", + ) + + def get_peer_facts_dict_or_none(self, peer_name: str) -> dict | None: + peer_facts = self._get_peer_facts(peer_name, required=False) + if isinstance(peer_facts, EosDesignsFacts): + msg = "Expected peer_facts to be a dict or None." + raise AristaAvdError(msg) + return peer_facts + + def get_peer_facts_dict(self, peer_name: str) -> dict: + peer_facts = self._get_peer_facts(peer_name, required=True) + if not peer_facts or isinstance(peer_facts, EosDesignsFacts): + msg = "Expected peer_facts to be a dict." + raise AristaAvdError(msg) + return peer_facts + + def get_peer_facts_cls_or_none(self, peer_name: str) -> EosDesignsFacts | None: + peer_facts = self._get_peer_facts(peer_name, required=False) + if isinstance(peer_facts, dict): + msg = "Expected peer_facts to be EosDesignsFacts or None." + raise AristaAvdError(msg) + return peer_facts + + def get_peer_facts_cls(self, peer_name: str) -> EosDesignsFacts: + peer_facts = self._get_peer_facts(peer_name, required=True) + if not peer_facts or isinstance(peer_facts, dict): + msg = "Expected peer_facts to be EosDesignsFacts." + raise AristaAvdError(msg) + return peer_facts + + def get_peer_facts_or_none(self, peer_name: str) -> EosDesignsFacts | dict | None: + return self._get_peer_facts(peer_name, required=False) + + def get_peer_facts(self, peer_name: str) -> EosDesignsFacts | dict: + return cast(EosDesignsFacts | dict, self._get_peer_facts(peer_name, required=True)) + + def _get_peer_facts(self, peer_name: str, required: bool = True) -> EosDesignsFacts | dict | None: """ Util function to retrieve peer_facts for peer_name. @@ -40,7 +97,7 @@ def get_peer_facts(self: SharedUtils, peer_name: str, required: bool = True) -> using the separator `..` to be able to handle hostnames with `.` inside """ return get( - self.hostvars, + self.shared_utils._hostvars, f"avd_switch_facts..{peer_name}..switch", separator="..", required=required, @@ -50,16 +107,16 @@ def get_peer_facts(self: SharedUtils, peer_name: str, required: bool = True) -> ), ) - def template_var(self: SharedUtils, template_file: str, template_vars: dict) -> str: + def template_var(self, template_file: str, template_vars: dict) -> str: """Run the simplified templater using the passed Ansible "templar" engine.""" try: - return template_var(template_file, template_vars, self.templar) + return template_var(template_file, template_vars, self.shared_utils.templar) except Exception as e: msg = f"Error during templating of template: {template_file}" raise AristaAvdError(msg) from e @lru_cache # noqa: B019 - def get_merged_port_profile(self: SharedUtils, profile_name: str, context: str) -> EosDesigns.PortProfilesItem: + def get_merged_port_profile(self, profile_name: str, context: str) -> EosDesigns.PortProfilesItem: """Return list of merged "port_profiles" where "parent_profile" has been applied.""" if profile_name not in self.inputs.port_profiles: msg = f"Profile '{profile_name}' applied under '{context}' does not exist in `port_profiles`." @@ -79,7 +136,7 @@ def get_merged_port_profile(self: SharedUtils, profile_name: str, context: str) delattr(port_profile, "parent_profile") return port_profile - def get_merged_adapter_settings(self: SharedUtils, adapter_or_network_port_settings: ADAPTER_SETTINGS) -> ADAPTER_SETTINGS: + def get_merged_adapter_settings(self, adapter_or_network_port_settings: ADAPTER_SETTINGS) -> ADAPTER_SETTINGS: """ Applies port-profiles to the given adapter_or_network_port and returns the combined result. diff --git a/python-avd/pyavd/_eos_designs/shared_utils/wan.py b/python-avd/pyavd/_eos_designs/shared_utils/wan.py index ba94441bb4c..5e3db56e368 100644 --- a/python-avd/pyavd/_eos_designs/shared_utils/wan.py +++ b/python-avd/pyavd/_eos_designs/shared_utils/wan.py @@ -5,18 +5,17 @@ from functools import cached_property from re import findall -from typing import TYPE_CHECKING, Literal +from typing import Literal from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError, AristaAvdMissingVariableError from pyavd._utils import default, get, get_ip_from_ip_prefix, strip_empties_from_dict from pyavd.j2filters import natural_sort -if TYPE_CHECKING: - from . import SharedUtils +from .utils import UtilsMixin -class WanMixin: +class WanMixin(UtilsMixin): """ Mixin Class providing a subset of SharedUtils. @@ -25,52 +24,52 @@ class WanMixin: """ @cached_property - def wan_role(self: SharedUtils) -> str | None: - if self.underlay_router is False: + def wan_role(self) -> str | None: + if self.shared_utils.underlay_router is False: return None - default_wan_role = self.node_type_key_data.default_wan_role - wan_role = self.node_config.wan_role or default_wan_role - if wan_role is not None and self.overlay_routing_protocol != "ibgp": + default_wan_role = self.shared_utils.node_type_key_data.default_wan_role + wan_role = self.shared_utils.node_config.wan_role or default_wan_role + if wan_role is not None and self.shared_utils.overlay_routing_protocol != "ibgp": msg = "Only 'ibgp' is supported as 'overlay_routing_protocol' for WAN nodes." raise AristaAvdError(msg) - if wan_role == "server" and self.evpn_role != "server": + if wan_role == "server" and self.shared_utils.evpn_role != "server": msg = "'wan_role' server requires 'evpn_role' server." raise AristaAvdError(msg) - if wan_role == "client" and self.evpn_role != "client": + if wan_role == "client" and self.shared_utils.evpn_role != "client": msg = "'wan_role' client requires 'evpn_role' client." raise AristaAvdError(msg) return wan_role @cached_property - def is_wan_router(self: SharedUtils) -> bool: + def is_wan_router(self) -> bool: return bool(self.wan_role) @cached_property - def is_wan_server(self: SharedUtils) -> bool: + def is_wan_server(self) -> bool: return self.wan_role == "server" @cached_property - def is_wan_client(self: SharedUtils) -> bool: + def is_wan_client(self) -> bool: return self.wan_role == "client" @cached_property - def wan_listen_ranges(self: SharedUtils) -> EosDesigns.BgpPeerGroups.WanOverlayPeers.ListenRangePrefixes: + def wan_listen_ranges(self) -> EosDesigns.BgpPeerGroups.WanOverlayPeers.ListenRangePrefixes: if not self.inputs.bgp_peer_groups.wan_overlay_peers.listen_range_prefixes: msg = "bgp_peer_groups.wan_overlay_peers.listen_range_prefixes" raise AristaAvdMissingVariableError(msg) return self.inputs.bgp_peer_groups.wan_overlay_peers.listen_range_prefixes @cached_property - def cv_pathfinder_transit_mode(self: SharedUtils) -> Literal["region", "zone"] | None: + def cv_pathfinder_transit_mode(self) -> Literal["region", "zone"] | None: """When wan_mode is CV Pathfinder, return the transit mode "region", "zone" or None.""" if not self.is_cv_pathfinder_client: return None - return self.node_config.cv_pathfinder_transit_mode + return self.shared_utils.node_config.cv_pathfinder_transit_mode @cached_property - def wan_interfaces(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces: + def wan_interfaces(self) -> EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces: """ As a first approach, only interfaces under node config l3_interfaces can be considered as WAN interfaces. @@ -81,7 +80,7 @@ def wan_interfaces(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeType return EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces() wan_interfaces = EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces( - [interface for interface in self.l3_interfaces if interface.wan_carrier] + [interface for interface in self.shared_utils.l3_interfaces if interface.wan_carrier] ) if not wan_interfaces: msg = "At least one WAN interface must be configured on a WAN router. Add WAN interfaces under `l3_interfaces` node setting with `wan_carrier` set." @@ -89,7 +88,7 @@ def wan_interfaces(self: SharedUtils) -> EosDesigns._DynamicKeys.DynamicNodeType return wan_interfaces @cached_property - def wan_local_carriers(self: SharedUtils) -> list: + def wan_local_carriers(self) -> list: """ List of carriers present on this router based on the wan_interfaces with the associated WAN interfaces. @@ -124,7 +123,7 @@ def wan_local_carriers(self: SharedUtils) -> list: return list(local_carriers_dict.values()) @cached_property - def wan_local_path_groups(self: SharedUtils) -> EosDesigns.WanPathGroups: + def wan_local_path_groups(self) -> EosDesigns.WanPathGroups: """ List of path-groups present on this router based on the local carriers. @@ -153,24 +152,24 @@ def wan_local_path_groups(self: SharedUtils) -> EosDesigns.WanPathGroups: return local_path_groups @cached_property - def wan_local_path_group_names(self: SharedUtils) -> list: + def wan_local_path_group_names(self) -> list: """Return a list of wan_local_path_group names.""" return list(self.wan_local_path_groups.keys()) @cached_property - def wan_ha_peer_path_groups(self: SharedUtils) -> list: + def wan_ha_peer_path_groups(self) -> list: """List of WAN HA peer path-groups coming from facts.""" if not self.is_wan_router or not self.wan_ha: return [] - peer_facts = self.get_peer_facts(self.wan_ha_peer, required=True) + peer_facts = self.get_peer_facts_dict(self.wan_ha_peer) return get(peer_facts, "wan_path_groups", required=True) @cached_property - def wan_ha_peer_path_group_names(self: SharedUtils) -> list: + def wan_ha_peer_path_group_names(self) -> list: """Return a list of wan_ha_peer_path_group names.""" return [path_group["name"] for path_group in self.wan_ha_peer_path_groups] - def get_public_ip_for_wan_interface(self: SharedUtils, interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem) -> str: + def get_public_ip_for_wan_interface(self, interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem) -> str: """ Takes a dict which looks like `l3_interface` from node config. @@ -183,8 +182,8 @@ def get_public_ip_for_wan_interface(self: SharedUtils, interface: EosDesigns._Dy if not self.is_wan_server: return default(interface.public_ip, get_ip_from_ip_prefix(interface.ip_address)) - if self.hostname in self.inputs.wan_route_servers: - for path_group in self.inputs.wan_route_servers[self.hostname].path_groups: + if self.shared_utils.hostname in self.inputs.wan_route_servers: + for path_group in self.inputs.wan_route_servers[self.shared_utils.hostname].path_groups: if interface.name not in path_group.interfaces: continue @@ -196,7 +195,7 @@ def get_public_ip_for_wan_interface(self: SharedUtils, interface: EosDesigns._Dy if interface.ip_address == "dhcp": msg = ( - f"The IP address for WAN interface '{interface.name}' on Route Server '{self.hostname}' is set to 'dhcp'. " + f"The IP address for WAN interface '{interface.name}' on Route Server '{self.shared_utils.hostname}' is set to 'dhcp'. " "Clients need to peer with a static IP which must be set under the 'wan_route_servers.path_groups.interfaces' key." ) raise AristaAvdError(msg) @@ -204,13 +203,13 @@ def get_public_ip_for_wan_interface(self: SharedUtils, interface: EosDesigns._Dy return get_ip_from_ip_prefix(interface.ip_address) @cached_property - def wan_site(self: SharedUtils) -> EosDesigns.CvPathfinderRegionsItem.SitesItem | EosDesigns.CvPathfinderGlobalSitesItem | None: + def wan_site(self) -> EosDesigns.CvPathfinderRegionsItem.SitesItem | EosDesigns.CvPathfinderGlobalSitesItem | None: """ WAN site for CV Pathfinder. The site is required for edges, but optional for pathfinders """ - node_defined_site = self.node_config.cv_pathfinder_site + node_defined_site = self.shared_utils.node_config.cv_pathfinder_site if not node_defined_site and self.is_cv_pathfinder_client: msg = "A node variable 'cv_pathfinder_site' must be defined when 'wan_role' is 'client' and 'wan_mode' is 'cv-pathfinder'." raise AristaAvdInvalidInputsError(msg) @@ -235,13 +234,13 @@ def wan_site(self: SharedUtils) -> EosDesigns.CvPathfinderRegionsItem.SitesItem return self.wan_region.sites[node_defined_site] @cached_property - def wan_region(self: SharedUtils) -> EosDesigns.CvPathfinderRegionsItem | None: + def wan_region(self) -> EosDesigns.CvPathfinderRegionsItem | None: """ WAN region for CV Pathfinder. The region is required for edges, but optional for pathfinders """ - node_defined_region = self.node_config.cv_pathfinder_region + node_defined_region = self.shared_utils.node_config.cv_pathfinder_region if not node_defined_region and self.is_cv_pathfinder_client: msg = "A node variable 'cv_pathfinder_region' must be defined when 'wan_role' is 'client' and 'wan_mode' is 'cv-pathfinder'." raise AristaAvdInvalidInputsError(msg) @@ -256,7 +255,7 @@ def wan_region(self: SharedUtils) -> EosDesigns.CvPathfinderRegionsItem | None: return self.inputs.cv_pathfinder_regions[node_defined_region] @property - def wan_zone(self: SharedUtils) -> dict: + def wan_zone(self) -> dict: """ WAN zone for Pathfinder. @@ -271,7 +270,7 @@ def wan_zone(self: SharedUtils) -> dict: return {"name": f"{self.wan_region.name}-ZONE", "id": 1} @cached_property - def filtered_wan_route_servers(self: SharedUtils) -> EosDesigns.WanRouteServers: + def filtered_wan_route_servers(self) -> EosDesigns.WanRouteServers: """ Return a dict keyed by Wan RR based on the the wan_mode type with only the path_groups the router should connect to. @@ -283,20 +282,20 @@ def filtered_wan_route_servers(self: SharedUtils) -> EosDesigns.WanRouteServers: filtered_wan_route_servers = EosDesigns.WanRouteServers() for org_wan_rs in self.inputs.wan_route_servers._natural_sorted(): - if org_wan_rs.hostname == self.hostname: + if org_wan_rs.hostname == self.shared_utils.hostname: # Don't add yourself continue wan_rs = org_wan_rs._deepcopy() # These remote gw can be outside of the inventory - if (peer_facts := self.get_peer_facts(wan_rs.hostname, required=False)) is not None: + if (peer_facts := self.get_peer_facts_or_none(wan_rs.hostname)) 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: - msg = f"Only iBGP is supported for WAN, the BGP AS {bgp_as} on {wan_rs} is different from our own: {self.bgp_as}." + if bgp_as != self.shared_utils.bgp_as: + msg = 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}." raise AristaAvdError(msg) # Prefer values coming from the input variables over peer facts @@ -363,7 +362,7 @@ def filtered_wan_route_servers(self: SharedUtils) -> EosDesigns.WanRouteServers: return filtered_wan_route_servers - def should_connect_to_wan_rs(self: SharedUtils, path_group_names: list[str]) -> bool: + def should_connect_to_wan_rs(self, path_group_names: list[str]) -> 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. @@ -377,22 +376,22 @@ def should_connect_to_wan_rs(self: SharedUtils, path_group_names: list[str]) -> ) @cached_property - def is_cv_pathfinder_router(self: SharedUtils) -> bool: + def is_cv_pathfinder_router(self) -> bool: """Return True is the current wan_mode is cv-pathfinder and the device is a wan router.""" return self.inputs.wan_mode == "cv-pathfinder" and self.is_wan_router @cached_property - def is_cv_pathfinder_client(self: SharedUtils) -> bool: + def is_cv_pathfinder_client(self) -> bool: """Return True is the current wan_mode is cv-pathfinder and the device is either an edge or a transit device.""" return self.is_cv_pathfinder_router and self.is_wan_client @cached_property - def is_cv_pathfinder_server(self: SharedUtils) -> bool: + def is_cv_pathfinder_server(self) -> bool: """Return True is the current wan_mode is cv-pathfinder and the device is a pathfinder device.""" return self.is_cv_pathfinder_router and self.is_wan_server @cached_property - def cv_pathfinder_role(self: SharedUtils) -> str | None: + def cv_pathfinder_role(self) -> str | None: if not self.is_cv_pathfinder_router: return None @@ -407,12 +406,12 @@ def cv_pathfinder_role(self: SharedUtils) -> str | None: return "edge" @cached_property - def wan_ha(self: SharedUtils) -> bool: + def wan_ha(self) -> bool: """Only trigger HA if 2 cv_pathfinder clients are in the same group and wan_ha.enabled is true.""" - if not self.is_cv_pathfinder_client or self.node_group_is_primary_and_peer_hostname is None: + if not self.is_cv_pathfinder_client or self.shared_utils.node_group_is_primary_and_peer_hostname is None: return False - if self.node_config.wan_ha.enabled is None: + if self.shared_utils.node_config.wan_ha.enabled is None: msg = ( "Placing two WAN routers in a common node group will trigger WAN HA in a future AVD release. " "Currently WAN HA is in preview, so it will not be automatically enabled. " @@ -420,45 +419,42 @@ def wan_ha(self: SharedUtils) -> bool: "it is currently required to set 'wan_ha.enabled' to 'true' or 'false'." ) raise AristaAvdError(msg) - return self.node_config.wan_ha.enabled + return self.shared_utils.node_config.wan_ha.enabled @cached_property - def wan_ha_ipsec(self: SharedUtils) -> bool: - return self.wan_ha and self.node_config.wan_ha.ipsec + def wan_ha_ipsec(self) -> bool: + return self.wan_ha and self.shared_utils.node_config.wan_ha.ipsec @cached_property - def is_first_ha_peer(self: SharedUtils) -> bool: + def is_first_ha_peer(self) -> bool: """ Returns True if the device is the first device in the node_group, false otherwise. This should be called only from functions which have checked that HA is enabled. """ - return self.node_group_is_primary_and_peer_hostname is not None and self.node_group_is_primary_and_peer_hostname[0] + return self.shared_utils.node_group_is_primary_and_peer_hostname is not None and self.shared_utils.node_group_is_primary_and_peer_hostname[0] @cached_property - def wan_ha_peer(self: SharedUtils) -> str | None: + def wan_ha_peer(self) -> str: """Return the name of the WAN HA peer.""" - if not self.wan_ha: - return None - - if self.node_group_is_primary_and_peer_hostname is not None: - return self.node_group_is_primary_and_peer_hostname[1] + if self.shared_utils.node_group_is_primary_and_peer_hostname is not None: + return self.shared_utils.node_group_is_primary_and_peer_hostname[1] msg = "Unable to find WAN HA peer within same node group" raise AristaAvdError(msg) @cached_property - def vrf_default_uplinks(self: SharedUtils) -> list: + def vrf_default_uplinks(self) -> list: """Return the uplinkss in VRF default.""" return [uplink for uplink in self.get_switch_fact("uplinks") if get(uplink, "vrf") is None] @cached_property - def vrf_default_uplink_interfaces(self: SharedUtils) -> list: + def vrf_default_uplink_interfaces(self) -> list: """Return the uplink interfaces in VRF default.""" return [uplink["interface"] for uplink in self.vrf_default_uplinks] @cached_property - def use_uplinks_for_wan_ha(self: SharedUtils) -> bool: + def use_uplinks_for_wan_ha(self) -> bool: """ Indicates whether the device is using its uplinks for WAN HA or direct HA. @@ -468,7 +464,7 @@ def use_uplinks_for_wan_ha(self: SharedUtils) -> bool: Raises: AristaAvdError: when the list of configured interfaces is a mix of uplinks and none uplinks. """ - interfaces = set(self.node_config.wan_ha.ha_interfaces) + interfaces = set(self.shared_utils.node_config.wan_ha.ha_interfaces) uplink_interfaces = set(self.vrf_default_uplink_interfaces) if interfaces.issubset(uplink_interfaces): @@ -479,7 +475,7 @@ def use_uplinks_for_wan_ha(self: SharedUtils) -> bool: raise AristaAvdError(msg) @cached_property - def wan_ha_interfaces(self: SharedUtils) -> list: + def wan_ha_interfaces(self) -> list: """ Return the list of interfaces for WAN HA. @@ -487,21 +483,21 @@ def wan_ha_interfaces(self: SharedUtils) -> list: else returns all of them. """ if self.use_uplinks_for_wan_ha: - return natural_sort(set(self.node_config.wan_ha.ha_interfaces)) or self.vrf_default_uplink_interfaces + return natural_sort(set(self.shared_utils.node_config.wan_ha.ha_interfaces)) or self.vrf_default_uplink_interfaces # Using node values - return natural_sort(set(self.node_config.wan_ha.ha_interfaces)) + return natural_sort(set(self.shared_utils.node_config.wan_ha.ha_interfaces)) @cached_property - def wan_ha_port_channel_id(self: SharedUtils) -> int: + def wan_ha_port_channel_id(self) -> int: """ Port-channel ID to use for direct WAN HA port-channel. If not provided, computed from the list of configured members. """ - return default(self.node_config.wan_ha.port_channel_id, int("".join(findall(r"\d", self.wan_ha_interfaces[0])))) + return default(self.shared_utils.node_config.wan_ha.port_channel_id, int("".join(findall(r"\d", self.wan_ha_interfaces[0])))) @cached_property - def use_port_channel_for_direct_ha(self: SharedUtils) -> bool: + def use_port_channel_for_direct_ha(self) -> bool: """ Indicate if port-channel should be used for direct HA. @@ -513,12 +509,12 @@ def use_port_channel_for_direct_ha(self: SharedUtils) -> bool: if self.use_uplinks_for_wan_ha: return False - interfaces = set(self.node_config.wan_ha.ha_interfaces) + interfaces = set(self.shared_utils.node_config.wan_ha.ha_interfaces) - return len(interfaces) > 1 or self.node_config.wan_ha.use_port_channel_for_direct_ha + return len(interfaces) > 1 or self.shared_utils.node_config.wan_ha.use_port_channel_for_direct_ha @cached_property - def wan_ha_peer_ip_addresses(self: SharedUtils) -> list: + def wan_ha_peer_ip_addresses(self) -> list: """ Read the IP addresses/prefix length from HA peer uplinks. @@ -526,9 +522,9 @@ def wan_ha_peer_ip_addresses(self: SharedUtils) -> list: """ ip_addresses = [] if self.use_uplinks_for_wan_ha: - peer_facts = self.get_peer_facts(self.wan_ha_peer, required=True) + peer_facts = self.get_peer_facts(self.wan_ha_peer) vrf_default_peer_uplinks = [uplink for uplink in get(peer_facts, "uplinks", required=True) if get(uplink, "vrf") is None] - interfaces = set(self.node_config.wan_ha.ha_interfaces) + interfaces = set(self.shared_utils.node_config.wan_ha.ha_interfaces) for uplink in vrf_default_peer_uplinks: if not interfaces or uplink["interface"] in interfaces: ip_address = get( @@ -543,11 +539,11 @@ def wan_ha_peer_ip_addresses(self: SharedUtils) -> list: ip_addresses.append(f"{ip_address}/{prefix_length}") else: # Only one supported HA interface today when not using uplinks - ip_addresses.append(self.ip_addressing.wan_ha_peer_ip()) + ip_addresses.append(self.shared_utils.ip_addressing.wan_ha_peer_ip()) return ip_addresses @cached_property - def wan_ha_ip_addresses(self: SharedUtils) -> list: + def wan_ha_ip_addresses(self) -> list: """ Read the IP addresses/prefix length from this device uplinks used for HA. @@ -556,7 +552,7 @@ def wan_ha_ip_addresses(self: SharedUtils) -> list: ip_addresses = [] if self.use_uplinks_for_wan_ha: - interfaces = set(self.node_config.wan_ha.ha_interfaces) + interfaces = set(self.shared_utils.node_config.wan_ha.ha_interfaces) for uplink in self.vrf_default_uplinks: if not interfaces or uplink["interface"] in interfaces: ip_address = get( @@ -570,23 +566,23 @@ def wan_ha_ip_addresses(self: SharedUtils) -> list: ip_addresses.append(f"{ip_address}/{prefix_length}") else: # Only one supported HA interface today when not using uplinks - ip_addresses.append(self.ip_addressing.wan_ha_ip()) + ip_addresses.append(self.shared_utils.ip_addressing.wan_ha_ip()) return ip_addresses @cached_property - def wan_ha_ipv4_pool(self: SharedUtils) -> str: + def wan_ha_ipv4_pool(self) -> str: """Return the configured wan_ha.ha_ipv4_pool.""" - if not self.node_config.wan_ha.ha_ipv4_pool: + if not self.shared_utils.node_config.wan_ha.ha_ipv4_pool: msg = "Missing `wan_ha.ha_ipv4_pool` node settings to allocate an IP address to defined HA interface." raise AristaAvdInvalidInputsError(msg) - return self.node_config.wan_ha.ha_ipv4_pool + return self.shared_utils.node_config.wan_ha.ha_ipv4_pool - def generate_lb_policy_name(self: SharedUtils, name: str) -> str: + def generate_lb_policy_name(self, name: str) -> str: """Returns LB-{name}.""" return f"LB-{name}" @cached_property - def wan_stun_dtls_profile_name(self: SharedUtils) -> str | None: + def wan_stun_dtls_profile_name(self) -> str | None: """Return the DTLS profile name to use for STUN for WAN.""" if not self.is_wan_router or self.inputs.wan_stun_dtls_disable: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/base/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/base/__init__.py index 517a578097c..95cb11612b9 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/base/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/base/__init__.py @@ -19,7 +19,7 @@ from pyavd._eos_designs.schema import EosDesigns -class AvdStructuredConfigBase(StructuredConfigGenerator, NtpMixin, SnmpServerMixin, RouterGeneralMixin): +class AvdStructuredConfigBase(NtpMixin, SnmpServerMixin, RouterGeneralMixin, StructuredConfigGenerator): """ The AvdStructuredConfig Class is imported by "get_structured_config" to render parts of the structured config. diff --git a/python-avd/pyavd/_eos_designs/structured_config/base/ntp.py b/python-avd/pyavd/_eos_designs/structured_config/base/ntp.py index 092662cd6a5..c76e15ddc08 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/base/ntp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/base/ntp.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError from pyavd._utils import strip_null_from_data from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigBase - class NtpMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class NtpMixin(UtilsMixin): """ @cached_property - def ntp(self: AvdStructuredConfigBase) -> dict | None: + def ntp(self) -> dict | None: """Ntp set based on "ntp_settings" data-model.""" if not (ntp_settings := self.inputs.ntp_settings): return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/base/router_general.py b/python-avd/pyavd/_eos_designs/structured_config/base/router_general.py index 94ccd957aec..fb06a282df2 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/base/router_general.py +++ b/python-avd/pyavd/_eos_designs/structured_config/base/router_general.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigBase - class RouterGeneralMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterGeneralMixin(UtilsMixin): """ @cached_property - def router_general(self: AvdStructuredConfigBase) -> dict | None: + def router_general(self) -> dict | None: if self.inputs.use_router_general_for_router_id: return strip_empties_from_dict( { diff --git a/python-avd/pyavd/_eos_designs/structured_config/base/snmp_server.py b/python-avd/pyavd/_eos_designs/structured_config/base/snmp_server.py index 3fde26aa30f..f3c5b72015a 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/base/snmp_server.py +++ b/python-avd/pyavd/_eos_designs/structured_config/base/snmp_server.py @@ -17,8 +17,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigBase - class SnmpServerMixin(UtilsMixin): """ @@ -28,7 +26,7 @@ class SnmpServerMixin(UtilsMixin): """ @cached_property - def snmp_server(self: AvdStructuredConfigBase) -> dict | None: + def snmp_server(self) -> dict | None: """ snmp_server set based on snmp_settings data-model, using various snmp_settings information. @@ -69,7 +67,7 @@ def snmp_server(self: AvdStructuredConfigBase) -> dict | None: }, ) - def _snmp_engine_ids(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSettings) -> dict | None: + def _snmp_engine_ids(self, snmp_settings: EosDesigns.SnmpSettings) -> dict | None: """ Return dict of engine ids if "snmp_settings.compute_local_engineid" is True. @@ -95,7 +93,7 @@ def _snmp_engine_ids(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.Sn return {"local": local_engine_id} - def _snmp_location(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSettings) -> str | None: + def _snmp_location(self, snmp_settings: EosDesigns.SnmpSettings) -> str | None: """ Return location if "snmp_settings.location" is True. @@ -114,7 +112,7 @@ def _snmp_location(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.Snmp location_elements = [location for location in location_elements if location not in [None, ""]] return " ".join(location_elements) - def _snmp_users(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSettings, engine_ids: dict | None) -> list | None: + def _snmp_users(self, snmp_settings: EosDesigns.SnmpSettings, engine_ids: dict | None) -> list | None: """ Return users if "snmp_settings.users" is set. @@ -159,7 +157,7 @@ def _snmp_users(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSet return snmp_users or None - def _snmp_hosts(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSettings) -> EosCliConfigGen.SnmpServer.Hosts: + def _snmp_hosts(self, snmp_settings: EosDesigns.SnmpSettings) -> EosCliConfigGen.SnmpServer.Hosts: """ Return hosts if "snmp_settings.hosts" is set. @@ -206,7 +204,7 @@ def _snmp_hosts(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSet return snmp_hosts - def _snmp_local_interfaces(self: AvdStructuredConfigBase, source_interfaces_inputs: EosDesigns.SourceInterfaces.Snmp) -> list | None: + def _snmp_local_interfaces(self, source_interfaces_inputs: EosDesigns.SourceInterfaces.Snmp) -> list | None: """ Return local_interfaces if "source_interfaces.snmp" is set. @@ -219,7 +217,7 @@ def _snmp_local_interfaces(self: AvdStructuredConfigBase, source_interfaces_inpu local_interfaces = self._build_source_interfaces(source_interfaces_inputs.mgmt_interface, source_interfaces_inputs.inband_mgmt_interface, "SNMP") return local_interfaces or None - def _snmp_vrfs(self: AvdStructuredConfigBase, snmp_settings: EosDesigns.SnmpSettings) -> EosDesigns.SnmpSettings.Vrfs: + def _snmp_vrfs(self, snmp_settings: EosDesigns.SnmpSettings) -> EosDesigns.SnmpSettings.Vrfs: """ Return list of dicts for enabling/disabling SNMP for VRFs. diff --git a/python-avd/pyavd/_eos_designs/structured_config/base/utils.py b/python-avd/pyavd/_eos_designs/structured_config/base/utils.py index 721d679c8e6..55615819b8a 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/base/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/base/utils.py @@ -4,22 +4,19 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError -if TYPE_CHECKING: - from . import AvdStructuredConfigBase - -class UtilsMixin: +class UtilsMixin(StructuredConfigGenerator): """ Mixin Class with internal functions. Class should only be used as Mixin to a AvdStructuredConfig class or other Mixins. """ - def _build_source_interfaces(self: AvdStructuredConfigBase, include_mgmt_interface: bool, include_inband_mgmt_interface: bool, error_context: str) -> list: + def _build_source_interfaces(self, include_mgmt_interface: bool, include_inband_mgmt_interface: bool, error_context: str) -> list: """ Return list of source interfaces with VRFs. @@ -61,7 +58,7 @@ def _build_source_interfaces(self: AvdStructuredConfigBase, include_mgmt_interfa return source_interfaces @cached_property - def _router_bgp_redistribute_routes(self: AvdStructuredConfigBase) -> dict | None: + def _router_bgp_redistribute_routes(self) -> dict | None: """Return structured config for router_bgp.redistribute.""" if not (self.shared_utils.underlay_bgp or self.shared_utils.is_wan_router or self.shared_utils.l3_interfaces_bgp_neighbors): return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py index eb334d9c973..8bf9ce2ab05 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py @@ -8,12 +8,7 @@ from .port_channel_interfaces import PortChannelInterfacesMixin -class AvdStructuredConfigConnectedEndpoints( - StructuredConfigGenerator, - EthernetInterfacesMixin, - PortChannelInterfacesMixin, - MonitorSessionsMixin, -): +class AvdStructuredConfigConnectedEndpoints(EthernetInterfacesMixin, PortChannelInterfacesMixin, MonitorSessionsMixin, StructuredConfigGenerator): """ The AvdStructuredConfig Class is imported by "get_structured_config" to render parts of the structured config. diff --git a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py index 0ff520eefe1..5099478be78 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py @@ -5,7 +5,6 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError @@ -15,9 +14,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigConnectedEndpoints - class EthernetInterfacesMixin(UtilsMixin): """ @@ -27,7 +23,7 @@ class EthernetInterfacesMixin(UtilsMixin): """ @cached_property - def ethernet_interfaces(self: AvdStructuredConfigConnectedEndpoints) -> list | None: + def ethernet_interfaces(self) -> list | None: """ Return structured config for ethernet_interfaces. @@ -83,7 +79,7 @@ def ethernet_interfaces(self: AvdStructuredConfigConnectedEndpoints) -> list | N return None def _update_ethernet_interface_cfg( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, ethernet_interface: dict, connected_endpoint: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem, @@ -130,7 +126,7 @@ def _update_ethernet_interface_cfg( return strip_null_from_data(ethernet_interface, strip_values_tuple=(None, "", {})) def _get_ethernet_interface_cfg( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, node_index: int, connected_endpoint: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem, diff --git a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/monitor_sessions.py b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/monitor_sessions.py index 5a1103ec89a..d5215b7bcda 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/monitor_sessions.py +++ b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/monitor_sessions.py @@ -16,8 +16,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigConnectedEndpoints - class MonitorSessionsMixin(UtilsMixin): """ @@ -27,7 +25,7 @@ class MonitorSessionsMixin(UtilsMixin): """ @cached_property - def monitor_sessions(self: AvdStructuredConfigConnectedEndpoints) -> list | None: + def monitor_sessions(self) -> list | None: """Return structured_config for monitor_sessions.""" if not self._monitor_session_configs: return None @@ -87,7 +85,7 @@ def monitor_sessions(self: AvdStructuredConfigConnectedEndpoints) -> list | None @cached_property def _monitor_session_configs( - self: AvdStructuredConfigConnectedEndpoints, + self, ) -> list[EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem.MonitorSessionsItem]: """Return list of monitor session configs extracted from every interface.""" monitor_session_configs = [] diff --git a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py index cf998d5582d..56d98f4e6be 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py @@ -5,7 +5,6 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdInvalidInputsError @@ -15,9 +14,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigConnectedEndpoints - class PortChannelInterfacesMixin(UtilsMixin): """ @@ -27,7 +23,7 @@ class PortChannelInterfacesMixin(UtilsMixin): """ @cached_property - def port_channel_interfaces(self: AvdStructuredConfigConnectedEndpoints) -> list | None: + def port_channel_interfaces(self) -> list | None: """ Return structured config for port_channel_interfaces. @@ -116,7 +112,7 @@ def port_channel_interfaces(self: AvdStructuredConfigConnectedEndpoints) -> list return None def _get_port_channel_interface_cfg( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, port_channel_interface_name: str, channel_group_id: int, @@ -226,7 +222,7 @@ def _get_port_channel_interface_cfg( return strip_null_from_data(port_channel_interface, strip_values_tuple=(None, "", {})) def _get_port_channel_subinterface_cfg( - self: AvdStructuredConfigConnectedEndpoints, + self, subinterface: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem.PortChannel.SubinterfacesItem, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, port_channel_subinterface_name: str, diff --git a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/utils.py b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/utils.py index b6f56672a3d..aac95b2152c 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/connected_endpoints/utils.py @@ -8,16 +8,17 @@ from hashlib import sha256 from typing import TYPE_CHECKING +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import default, get_v2, short_esi_to_route_target if TYPE_CHECKING: - from pyavd._eos_designs.schema import EosDesigns + from collections.abc import Iterable - from . import AvdStructuredConfigConnectedEndpoints + from pyavd._eos_designs.schema import EosDesigns -class UtilsMixin: +class UtilsMixin(StructuredConfigGenerator): """ Mixin Class with internal functions. @@ -25,9 +26,7 @@ class UtilsMixin: """ @cached_property - def _filtered_connected_endpoints( - self: AvdStructuredConfigConnectedEndpoints, - ) -> list[EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem]: + def _filtered_connected_endpoints(self) -> list[EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem]: """ Return list of endpoints defined under one of the keys in "connected_endpoints_keys" which are connected to this switch. @@ -66,7 +65,7 @@ def _filtered_connected_endpoints( return filtered_connected_endpoints @cached_property - def _filtered_network_ports(self: AvdStructuredConfigConnectedEndpoints) -> list[EosDesigns.NetworkPortsItem]: + def _filtered_network_ports(self) -> list[EosDesigns.NetworkPortsItem]: """Return list of endpoints defined under "network_ports" which are connected to this switch.""" filtered_network_ports = [] for index, network_port in enumerate(self.inputs.network_ports): @@ -77,14 +76,16 @@ def _filtered_network_ports(self: AvdStructuredConfigConnectedEndpoints) -> list continue if network_port_settings.switches and not self._match_regexes(network_port_settings.switches, self.shared_utils.hostname): continue - if network_port_settings.platforms and not self._match_regexes(network_port_settings.platforms, self.shared_utils.platform): + if network_port_settings.platforms and not ( + self.shared_utils.platform and self._match_regexes(network_port_settings.platforms, self.shared_utils.platform) + ): continue filtered_network_ports.append(network_port_settings) return filtered_network_ports - def _match_regexes(self: AvdStructuredConfigConnectedEndpoints, regexes: list, value: str) -> bool: + def _match_regexes(self, regexes: Iterable[str], value: str) -> bool: """ Match a list of regexes with the supplied value. @@ -93,7 +94,7 @@ def _match_regexes(self: AvdStructuredConfigConnectedEndpoints, regexes: list, v return any(re.fullmatch(regex, value) for regex in regexes) def _get_short_esi( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, channel_group_id: int, short_esi: str | None = None, @@ -125,7 +126,7 @@ def _get_short_esi( return short_esi def _get_adapter_trunk_groups( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, connected_endpoint: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem, ) -> list | None: @@ -140,7 +141,7 @@ def _get_adapter_trunk_groups( return adapter.trunk_groups._as_list() def _get_adapter_storm_control( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, ) -> dict | None: """Return storm_control for one adapter.""" @@ -150,7 +151,7 @@ def _get_adapter_storm_control( return None def _get_adapter_evpn_ethernet_segment_cfg( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, short_esi: str | None, node_index: int, @@ -198,7 +199,7 @@ def _get_adapter_evpn_ethernet_segment_cfg( return evpn_ethernet_segment def _get_adapter_link_tracking_groups( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, ) -> list | None: """Return link_tracking_groups for one adapter.""" @@ -212,9 +213,7 @@ def _get_adapter_link_tracking_groups( }, ] - def _get_adapter_ptp( - self: AvdStructuredConfigConnectedEndpoints, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem - ) -> dict | None: + def _get_adapter_ptp(self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem) -> dict | None: """Return ptp for one adapter.""" if not adapter.ptp.enabled: return None @@ -239,7 +238,7 @@ def _get_adapter_ptp( return ptp_config def _get_adapter_poe( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, ) -> dict | None: """Return poe settings for one adapter.""" @@ -249,7 +248,7 @@ def _get_adapter_poe( return None def _get_adapter_phone( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, connected_endpoint: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem, ) -> dict | None: @@ -276,7 +275,7 @@ def _get_adapter_phone( } def _get_adapter_sflow( - self: AvdStructuredConfigConnectedEndpoints, + self, adapter: EosDesigns._DynamicKeys.DynamicConnectedEndpointsItem.ConnectedEndpointsItem.AdaptersItem, ) -> dict | None: if (adapter_sflow := default(adapter.sflow, self.inputs.fabric_sflow.endpoints)) is not None: diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/__init__.py index 80925effe71..a9593d56fba 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/__init__.py @@ -3,8 +3,6 @@ # that can be found in the LICENSE file. from __future__ import annotations -from typing import TYPE_CHECKING - from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from .ethernet_interfaces import EthernetInterfacesMixin @@ -12,18 +10,15 @@ from .router_bgp import RouterBgpMixin from .router_ospf import RouterOspfMixin -if TYPE_CHECKING: - from pyavd._eos_designs.schema import EosDesigns - DATA_MODELS = ["core_interfaces", "l3_edge"] class AvdStructuredConfigCoreInterfacesAndL3Edge( - StructuredConfigGenerator, EthernetInterfacesMixin, PortChannelInterfacesMixin, RouterBgpMixin, RouterOspfMixin, + StructuredConfigGenerator, ): """ The AvdStructuredConfig Class is imported by "get_structured_config" to render parts of the structured config. @@ -38,9 +33,7 @@ class AvdStructuredConfigCoreInterfacesAndL3Edge( The order of the @cached_properties methods imported from Mixins will also control the order in the output. """ - inputs_data: EosDesigns.CoreInterfaces | EosDesigns.L3Edge - - def render(self) -> dict: + def render(self) -> list: """Render structured configs for core_interfaces and l3_Edge.""" result_list = [] diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/ethernet_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/ethernet_interfaces.py index a807dff43e6..2690064666f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/ethernet_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/ethernet_interfaces.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from pyavd.api.interface_descriptions import InterfaceDescriptionData from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigCoreInterfacesAndL3Edge - class EthernetInterfacesMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class EthernetInterfacesMixin(UtilsMixin): """ @cached_property - def ethernet_interfaces(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> list | None: + def ethernet_interfaces(self) -> list | None: """Return structured config for ethernet_interfaces.""" ethernet_interfaces = [] @@ -67,7 +63,7 @@ def ethernet_interfaces(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> lis return None - def _p2p_link_ethernet_description(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link_data: dict) -> str: + def _p2p_link_ethernet_description(self, p2p_link_data: dict) -> str: return self.shared_utils.interface_descriptions.underlay_ethernet_interface( InterfaceDescriptionData( shared_utils=self.shared_utils, @@ -79,7 +75,7 @@ def _p2p_link_ethernet_description(self: AvdStructuredConfigCoreInterfacesAndL3E ), ) - def _port_channel_member_description(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link_data: dict, member: dict) -> str: + def _port_channel_member_description(self, p2p_link_data: dict, member: dict) -> str: return self.shared_utils.interface_descriptions.underlay_ethernet_interface( InterfaceDescriptionData( shared_utils=self.shared_utils, diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/port_channel_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/port_channel_interfaces.py index e4472a5231a..5fb10956fce 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/port_channel_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/port_channel_interfaces.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import default, get from pyavd.api.interface_descriptions import InterfaceDescriptionData from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigCoreInterfacesAndL3Edge - class PortChannelInterfacesMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class PortChannelInterfacesMixin(UtilsMixin): """ @cached_property - def port_channel_interfaces(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> list | None: + def port_channel_interfaces(self) -> list | None: """Return structured config for port_channel_interfaces.""" port_channel_interfaces = [] for p2p_link, p2p_link_data in self._filtered_p2p_links: @@ -44,7 +40,7 @@ def port_channel_interfaces(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> return None - def _p2p_link_port_channel_description(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link_data: dict) -> str: + def _p2p_link_port_channel_description(self, p2p_link_data: dict) -> str: return self.shared_utils.interface_descriptions.underlay_port_channel_interface( InterfaceDescriptionData( shared_utils=self.shared_utils, diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_bgp.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_bgp.py index 89e7ab5629c..fbd432854e7 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_bgp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_bgp.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import get_ip_from_ip_prefix from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigCoreInterfacesAndL3Edge - class RouterBgpMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class RouterBgpMixin(UtilsMixin): """ @cached_property - def router_bgp(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> dict | None: + def router_bgp(self) -> dict | None: """Return structured config for router_bgp.""" if not self.shared_utils.underlay_bgp: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_ospf.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_ospf.py index 4c0fc3ec5d4..7670ff0a2d9 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_ospf.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/router_ospf.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigCoreInterfacesAndL3Edge - class RouterOspfMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class RouterOspfMixin(UtilsMixin): """ @cached_property - def router_ospf(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> dict | None: + def router_ospf(self) -> dict | None: """Return structured config for router_ospf.""" if not self.shared_utils.underlay_ospf: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py index 07072286516..ace2f9aab8f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py @@ -7,44 +7,45 @@ from functools import cached_property from ipaddress import ip_network from itertools import islice -from typing import TYPE_CHECKING, TypeVar +from typing import Any, TypeVar from pyavd._eos_cli_config_gen.schema import EosCliConfigGen from pyavd._eos_designs.schema import EosDesigns +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdInvalidInputsError, AristaAvdMissingVariableError from pyavd._utils import default, get_ip_from_pool -if TYPE_CHECKING: - from . import AvdStructuredConfigCoreInterfacesAndL3Edge - T_P2pLinksItem = TypeVar("T_P2pLinksItem", EosDesigns.CoreInterfaces.P2pLinksItem, EosDesigns.L3Edge.P2pLinksItem) T_P2pLinksProfiles = TypeVar("T_P2pLinksProfiles", EosDesigns.CoreInterfaces.P2pLinksProfiles, EosDesigns.L3Edge.P2pLinksProfiles) -class UtilsMixin: +class UtilsMixin(StructuredConfigGenerator): """ Mixin Class with internal functions. Class should only be used as Mixin to a AvdStructuredConfig class. """ + data_model: str + inputs_data: EosDesigns.CoreInterfaces | EosDesigns.L3Edge + @cached_property - def _p2p_links_sflow(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> bool | None: + def _p2p_links_sflow(self) -> bool | None: return self.inputs.fabric_sflow.core_interfaces if self.data_model == "core_interfaces" else self.inputs.fabric_sflow.l3_edge @cached_property - def _filtered_p2p_links(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> list[tuple[T_P2pLinksItem, dict]]: + def _filtered_p2p_links(self) -> list[tuple[T_P2pLinksItem, dict]]: """ Returns a filtered list of p2p_links, which only contains links with our hostname. For each links any referenced profiles are applied and IP addresses are resolved from pools or subnets. """ - if not (p2p_links := self.inputs_data.p2p_links): + if not self.inputs_data.p2p_links: return [] # Apply p2p_profiles if set. Silently ignoring missing profile. - p2p_links: list[T_P2pLinksItem] = [self._apply_p2p_links_profile(p2p_link) for p2p_link in p2p_links] + p2p_links: list[T_P2pLinksItem] = [self._apply_p2p_links_profile(p2p_link) for p2p_link in self.inputs_data.p2p_links] # Filter to only include p2p_links with our hostname under "nodes" p2p_links = [p2p_link for p2p_link in p2p_links if self.shared_utils.hostname in p2p_link.nodes] @@ -57,7 +58,7 @@ def _filtered_p2p_links(self: AvdStructuredConfigCoreInterfacesAndL3Edge) -> lis # Parse P2P data model and create simplified data return [(p2p_link, self._get_p2p_data(p2p_link)) for p2p_link in p2p_links] - def _apply_p2p_links_profile(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem) -> T_P2pLinksItem: + def _apply_p2p_links_profile(self, p2p_link: T_P2pLinksItem) -> T_P2pLinksItem: """Apply a profile to a p2p_link. Always returns a new instance. TODO: Raise if profile is missing.""" if not p2p_link.profile or p2p_link.profile not in self.inputs_data.p2p_links_profiles: # Nothing to do @@ -66,7 +67,7 @@ def _apply_p2p_links_profile(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p profile_as_p2p_link_item = self.inputs_data.p2p_links_profiles[p2p_link.profile]._cast_as(type(p2p_link), ignore_extra_keys=True) return p2p_link._deepinherited(profile_as_p2p_link_item) - def _resolve_p2p_ips(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem) -> T_P2pLinksItem: + def _resolve_p2p_ips(self, p2p_link: T_P2pLinksItem) -> T_P2pLinksItem: if p2p_link.ip: # ip already set, so nothing to do return p2p_link @@ -89,7 +90,7 @@ def _resolve_p2p_ips(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: return p2p_link - def _get_p2p_data(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem) -> dict: + def _get_p2p_data(self, p2p_link: T_P2pLinksItem) -> dict: """ Parses p2p_link data model and extracts information which is easier to parse. @@ -112,7 +113,7 @@ def _get_p2p_data(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_ index = p2p_link.nodes.index(self.shared_utils.hostname) peer_index = (index + 1) % 2 peer = p2p_link.nodes[peer_index] - peer_facts = self.shared_utils.get_peer_facts(peer, required=False) + peer_facts = self.shared_utils.get_peer_facts_dict_or_none(peer) peer_type = "other" if peer_facts is None else peer_facts.get("type", "other") # Set ip or fallback to list with None values @@ -184,7 +185,7 @@ def _get_p2p_data(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_ msg = f"{self.data_model}.p2p_links must have either 'interfaces' or 'port_channel' with correct members set." raise AristaAvdInvalidInputsError(msg) - def _get_common_interface_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem, p2p_link_data: dict) -> dict: + def _get_common_interface_cfg(self, p2p_link: T_P2pLinksItem, p2p_link_data: dict) -> dict: """ Return partial structured_config for one p2p_link. @@ -192,7 +193,7 @@ def _get_common_interface_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, This config will only be used on the main interface - so not port-channel members. """ index = p2p_link.nodes.index(self.shared_utils.hostname) - interface_cfg = { + interface_cfg: dict[str, Any] = { "name": p2p_link_data["interface"], "peer": p2p_link_data["peer"], "peer_interface": p2p_link_data["peer_interface"], @@ -299,7 +300,7 @@ def _get_common_interface_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, return interface_cfg - def _get_ethernet_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem) -> dict: + def _get_ethernet_cfg(self, p2p_link: T_P2pLinksItem) -> dict: """ Return partial structured_config for one p2p_link. @@ -308,7 +309,7 @@ def _get_ethernet_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link """ return {"speed": p2p_link.speed} - def _get_port_channel_member_cfg(self: AvdStructuredConfigCoreInterfacesAndL3Edge, p2p_link: T_P2pLinksItem, p2p_link_data: dict, member: dict) -> dict: + def _get_port_channel_member_cfg(self, p2p_link: T_P2pLinksItem, p2p_link_data: dict, member: dict) -> dict: """ Return partial structured_config for one p2p_link. diff --git a/python-avd/pyavd/_eos_designs/structured_config/metadata/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/metadata/__init__.py index 409941b45f9..fd53c71b2c4 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/metadata/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/metadata/__init__.py @@ -12,7 +12,7 @@ from .cv_tags import CvTagsMixin -class AvdStructuredConfigMetadata(StructuredConfigGenerator, CvTagsMixin, CvPathfinderMixin): +class AvdStructuredConfigMetadata(CvTagsMixin, CvPathfinderMixin, StructuredConfigGenerator): """ This returns the metadata data structure as per the below example. diff --git a/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_pathfinder.py b/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_pathfinder.py index 15ffa97eda7..06e5093bc41 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_pathfinder.py +++ b/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_pathfinder.py @@ -3,23 +3,19 @@ # that can be found in the LICENSE file. from __future__ import annotations -from typing import TYPE_CHECKING - +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import default, strip_empties_from_list -if TYPE_CHECKING: - from . import AvdStructuredConfigMetadata - -class CvPathfinderMixin: +class CvPathfinderMixin(StructuredConfigGenerator): """ 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: + def _cv_pathfinder(self) -> dict | None: """ Generate metadata for CV Pathfinder feature. @@ -61,7 +57,7 @@ def _cv_pathfinder(self: AvdStructuredConfigMetadata) -> dict | None: "pathfinders": self._metadata_pathfinder_vtep_ips(), } - def _metadata_interfaces(self: AvdStructuredConfigMetadata) -> list: + def _metadata_interfaces(self) -> list: return [ { "name": interface["name"], @@ -74,7 +70,7 @@ def _metadata_interfaces(self: AvdStructuredConfigMetadata) -> list: for interface in carrier["interfaces"] ] - def _metadata_pathgroups(self: AvdStructuredConfigMetadata) -> list: + def _metadata_pathgroups(self) -> list: return [ { "name": pathgroup.name, @@ -96,7 +92,7 @@ def _metadata_pathgroups(self: AvdStructuredConfigMetadata) -> list: for pathgroup in self.inputs.wan_path_groups ] - def _metadata_regions(self: AvdStructuredConfigMetadata) -> list: + def _metadata_regions(self) -> list: if not self.inputs.cv_pathfinder_regions: msg = "'cv_pathfinder_regions' key must be set when 'wan_mode' is 'cv-pathfinder'." raise AristaAvdInvalidInputsError(msg) @@ -125,7 +121,7 @@ def _metadata_regions(self: AvdStructuredConfigMetadata) -> list: for region in regions ] - def _metadata_pathfinder_vtep_ips(self: AvdStructuredConfigMetadata) -> list: + def _metadata_pathfinder_vtep_ips(self) -> list: return [ { "vtep_ip": wan_route_server.vtep_ip, @@ -133,7 +129,7 @@ def _metadata_pathfinder_vtep_ips(self: AvdStructuredConfigMetadata) -> list: for wan_route_server in self.shared_utils.filtered_wan_route_servers ] - def _metadata_vrfs(self: AvdStructuredConfigMetadata) -> list: + def _metadata_vrfs(self) -> list: """Extracting metadata for VRFs by parsing the generated structured config and flatten it a bit (like hiding load-balance policies).""" if not (avt_vrfs := self.structured_config.router_adaptive_virtual_topology.vrfs): return [] @@ -203,7 +199,7 @@ def _metadata_vrfs(self: AvdStructuredConfigMetadata) -> list: return strip_empties_from_list(metadata_vrfs) - def _get_vni_for_vrf_name(self: AvdStructuredConfigMetadata, vrf_name: str) -> int: + def _get_vni_for_vrf_name(self, vrf_name: str) -> int: if vrf_name not in self.inputs.wan_virtual_topologies.vrfs or (wan_vni := self.inputs.wan_virtual_topologies.vrfs[vrf_name].wan_vni) is None: if vrf_name == "default": return 1 diff --git a/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_tags.py b/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_tags.py index cdbb5c9a157..5cc60be274f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_tags.py +++ b/python-avd/pyavd/_eos_designs/structured_config/metadata/cv_tags.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError from pyavd._schema.models.avd_base import AvdBase from pyavd._utils import default, get_v2, strip_empties_from_dict, strip_empties_from_list @@ -12,8 +13,6 @@ if TYPE_CHECKING: from pyavd._eos_cli_config_gen.schema import EosCliConfigGen - from . import AvdStructuredConfigMetadata - INVALID_CUSTOM_DEVICE_TAGS = [ "topology_hint_type", "topology_type", @@ -40,14 +39,14 @@ """These tag names overlap with CV system tags or topology_hints""" -class CvTagsMixin: +class CvTagsMixin(StructuredConfigGenerator): """ Mixin Class used to generate structured config for one key. Class should only be used as Mixin to a AvdStructuredConfig class. """ - def _cv_tags(self: AvdStructuredConfigMetadata) -> dict | None: + def _cv_tags(self) -> dict | None: """Generate the data structure `metadata.cv_tags`.""" if not self.inputs.generate_cv_tags and not self.shared_utils.is_cv_pathfinder_router: return None @@ -66,7 +65,7 @@ def _tag_dict(name: str, value: Any) -> dict | None: return None return {"name": name, "value": str(value)} - def _get_topology_hints(self: AvdStructuredConfigMetadata) -> list: + def _get_topology_hints(self) -> list: """Return list of topology_hint tags.""" if not self.inputs.generate_cv_tags.topology_hints: return [] @@ -82,7 +81,7 @@ def _get_topology_hints(self: AvdStructuredConfigMetadata) -> list: ], ) - def _get_cv_pathfinder_device_tags(self: AvdStructuredConfigMetadata) -> list: + def _get_cv_pathfinder_device_tags(self) -> list: """ Return list of device_tags for cv_pathfinder solution. @@ -116,7 +115,7 @@ def _get_cv_pathfinder_device_tags(self: AvdStructuredConfigMetadata) -> list: return strip_empties_from_list(device_tags) - def _get_device_tags(self: AvdStructuredConfigMetadata) -> list: + def _get_device_tags(self) -> list: """Return list of device_tags.""" if not (tags_to_generate := self.inputs.generate_cv_tags.device_tags): return [] @@ -151,7 +150,7 @@ def _get_device_tags(self: AvdStructuredConfigMetadata) -> list: return device_tags - def _get_interface_tags(self: AvdStructuredConfigMetadata) -> list: + def _get_interface_tags(self) -> list: """Return list of interface_tags.""" if not (tags_to_generate := self.inputs.generate_cv_tags.interface_tags) and not self.shared_utils.is_cv_pathfinder_router: return [] @@ -187,7 +186,7 @@ def _get_interface_tags(self: AvdStructuredConfigMetadata) -> list: return interface_tags - def _get_cv_pathfinder_interface_tags(self: AvdStructuredConfigMetadata, ethernet_interface: EosCliConfigGen.EthernetInterfacesItem) -> list: + def _get_cv_pathfinder_interface_tags(self, ethernet_interface: EosCliConfigGen.EthernetInterfacesItem) -> list: """ Return list of device_tags for cv_pathfinder solution. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/__init__.py index 31729b4e730..6af9821104c 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/__init__.py @@ -42,7 +42,6 @@ class AvdStructuredConfigNetworkServices( - StructuredConfigGenerator, ApplicationTrafficRecognitionMixin, SpanningTreeMixin, PatchPanelMixin, @@ -79,6 +78,7 @@ class AvdStructuredConfigNetworkServices( TunnelInterfacesMixin, MonitorConnectivityMixin, MetadataMixin, + StructuredConfigGenerator, ): """ The AvdStructuredConfig Class is imported by "get_structured_config" to render parts of the structured config. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/application_traffic_recognition.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/application_traffic_recognition.py index d5fcaad54ac..38396cefe6d 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/application_traffic_recognition.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/application_traffic_recognition.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property, partial -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import append_if_not_duplicate, get, get_item, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class ApplicationTrafficRecognitionMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class ApplicationTrafficRecognitionMixin(UtilsMixin): """ @cached_property - def application_traffic_recognition(self: AvdStructuredConfigNetworkServices) -> dict | None: + def application_traffic_recognition(self) -> dict | None: """Return structured config for application_traffic_recognition if wan router.""" if not self.shared_utils.is_wan_router: return None @@ -36,18 +32,18 @@ def application_traffic_recognition(self: AvdStructuredConfigNetworkServices) -> # self._wan_control_plane_application_profile is defined in utils.py @cached_property - def _wan_control_plane_application(self: AvdStructuredConfigNetworkServices) -> str: + def _wan_control_plane_application(self) -> str: return "APP-CONTROL-PLANE" @cached_property - def _wan_cp_app_dst_prefix(self: AvdStructuredConfigNetworkServices) -> str: + def _wan_cp_app_dst_prefix(self) -> str: return "PFX-PATHFINDERS" @cached_property - def _wan_cp_app_src_prefix(self: AvdStructuredConfigNetworkServices) -> str: + def _wan_cp_app_src_prefix(self) -> str: return "PFX-LOCAL-VTEP-IP" - def _generate_control_plane_application_profile(self: AvdStructuredConfigNetworkServices, app_dict: dict) -> None: + def _generate_control_plane_application_profile(self, app_dict: dict) -> None: """ Generate an application profile using a single application matching. @@ -135,7 +131,7 @@ def _generate_control_plane_application_profile(self: AvdStructuredConfigNetwork {"name": self._wan_cp_app_src_prefix, "prefix_values": [f"{self.shared_utils.vtep_ip}/32"]}, ) - def _filtered_application_classification(self: AvdStructuredConfigNetworkServices) -> dict: + def _filtered_application_classification(self) -> dict: """ Based on the filtered policies local to the device, filter which application profiles should be configured on the device. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/dps_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/dps_interfaces.py index 65645faaaca..0e51feaab76 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/dps_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/dps_interfaces.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class DpsInterfacesMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class DpsInterfacesMixin(UtilsMixin): """ @cached_property - def dps_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def dps_interfaces(self) -> list | None: """ Returns structured config for dps_interfaces. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/eos_cli.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/eos_cli.py index 8d382c9425c..05497f7e1de 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/eos_cli.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/eos_cli.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class EosCliMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class EosCliMixin(UtilsMixin): """ @cached_property - def eos_cli(self: AvdStructuredConfigNetworkServices) -> str | None: + def eos_cli(self) -> str | None: """Return existing eos_cli plus any eos_cli from VRFs.""" if not self.shared_utils.network_services_l3: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ethernet_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ethernet_interfaces.py index 9c4c0e89a49..6282067d305 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ethernet_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ethernet_interfaces.py @@ -5,7 +5,6 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError from pyavd._utils import append_if_not_duplicate, get @@ -13,9 +12,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class EthernetInterfacesMixin(UtilsMixin): """ @@ -25,7 +21,7 @@ class EthernetInterfacesMixin(UtilsMixin): """ @cached_property - def ethernet_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def ethernet_interfaces(self) -> list | None: """ Return structured config for ethernet_interfaces. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py index 4abea10e868..cab71179ad9 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Literal +from typing import Literal from pyavd._errors import AristaAvdError from pyavd._utils import append_if_not_duplicate, get_ip_from_ip_prefix @@ -12,9 +12,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class IpAccesslistsMixin(UtilsMixin): """ @@ -24,7 +21,7 @@ class IpAccesslistsMixin(UtilsMixin): """ @cached_property - def _acl_internet_exit_zscaler(self: AvdStructuredConfigNetworkServices) -> dict: + def _acl_internet_exit_zscaler(self) -> dict: return { "name": self.get_internet_exit_nat_acl_name("zscaler"), "entries": [ @@ -39,7 +36,7 @@ def _acl_internet_exit_zscaler(self: AvdStructuredConfigNetworkServices) -> dict } @cached_property - def _acl_internet_exit_direct(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _acl_internet_exit_direct(self) -> dict | None: interface_ips = set() for ie_policy, connections in self._filtered_internet_exit_policies_and_connections: if ie_policy.type == "direct": @@ -76,7 +73,7 @@ def _acl_internet_exit_direct(self: AvdStructuredConfigNetworkServices) -> dict } return None - def _acl_internet_exit_user_defined(self: AvdStructuredConfigNetworkServices, internet_exit_policy_type: Literal["zscaler", "direct"]) -> list[dict] | None: + def _acl_internet_exit_user_defined(self, internet_exit_policy_type: Literal["zscaler", "direct"]) -> list[dict] | None: acl_name = self.get_internet_exit_nat_acl_name(internet_exit_policy_type) if acl_name not in self.inputs.ipv4_acls: # TODO: Evaluate if we should continue so we raise when there is no ACL. @@ -93,7 +90,7 @@ def _acl_internet_exit_user_defined(self: AvdStructuredConfigNetworkServices, in msg = f"ipv4_acls[name={acl_name}] field substitution is not supported for internet exit access lists" raise AristaAvdError(msg) - def _acl_internet_exit(self: AvdStructuredConfigNetworkServices, internet_exit_policy_type: Literal["zscaler", "direct"]) -> list[dict] | None: + def _acl_internet_exit(self, internet_exit_policy_type: Literal["zscaler", "direct"]) -> list[dict] | None: acls = self._acl_internet_exit_user_defined(internet_exit_policy_type) if acls: return acls @@ -105,7 +102,7 @@ def _acl_internet_exit(self: AvdStructuredConfigNetworkServices, internet_exit_p return None @cached_property - def ip_access_lists(self: AvdStructuredConfigNetworkServices) -> list | None: + def ip_access_lists(self) -> list | None: """Return structured config for ip_access_lists.""" ip_access_lists = [] if self._svi_acls: diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_igmp_snooping.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_igmp_snooping.py index 3e6562fbfaa..695a6a7a802 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_igmp_snooping.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_igmp_snooping.py @@ -13,8 +13,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - class IpIgmpSnoopingMixin(UtilsMixin): """ @@ -24,7 +22,7 @@ class IpIgmpSnoopingMixin(UtilsMixin): """ @cached_property - def ip_igmp_snooping(self: AvdStructuredConfigNetworkServices) -> dict | None: + def ip_igmp_snooping(self) -> dict | None: """Return structured config for ip_igmp_snooping.""" if not self.shared_utils.network_services_l2: return None @@ -63,7 +61,7 @@ def ip_igmp_snooping(self: AvdStructuredConfigNetworkServices) -> dict | None: return ip_igmp_snooping def _ip_igmp_snooping_vlan( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_nat.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_nat.py index 0be42010012..1de4f9fd91b 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_nat.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_nat.py @@ -5,13 +5,9 @@ from collections import defaultdict from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class IpNatMixin(UtilsMixin): """ @@ -21,7 +17,7 @@ class IpNatMixin(UtilsMixin): """ @cached_property - def ip_nat(self: AvdStructuredConfigNetworkServices) -> dict | None: + def ip_nat(self) -> dict | None: """Returns structured config for ip_nat.""" if not self.shared_utils.is_cv_pathfinder_client: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_security.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_security.py index db754321c5f..1fb0716cd41 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_security.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_security.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import strip_null_from_data from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class IpSecurityMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class IpSecurityMixin(UtilsMixin): """ @cached_property - def ip_security(self: AvdStructuredConfigNetworkServices) -> dict | None: + def ip_security(self) -> dict | None: """ip_security set based on cv_pathfinder_internet_exit_policies.""" if not self._filtered_internet_exit_policies_and_connections: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_virtual_router_mac_address.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_virtual_router_mac_address.py index 3999d25866f..4e7a438c186 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_virtual_router_mac_address.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ip_virtual_router_mac_address.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class IpVirtualRouterMacAddressMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class IpVirtualRouterMacAddressMixin(UtilsMixin): """ @cached_property - def ip_virtual_router_mac_address(self: AvdStructuredConfigNetworkServices) -> str | None: + def ip_virtual_router_mac_address(self) -> str | None: """Return structured config for ip_virtual_router_mac_address.""" if ( self.shared_utils.network_services_l2 diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/ipv6_static_routes.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/ipv6_static_routes.py index 25719717141..2cde1446bdf 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/ipv6_static_routes.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/ipv6_static_routes.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class Ipv6StaticRoutesMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class Ipv6StaticRoutesMixin(UtilsMixin): """ @cached_property - def ipv6_static_routes(self: AvdStructuredConfigNetworkServices) -> list[dict] | None: + def ipv6_static_routes(self) -> list[dict] | None: """ Returns structured config for ipv6_static_routes. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/loopback_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/loopback_interfaces.py index 76750ea8cdd..74e97a39117 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/loopback_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/loopback_interfaces.py @@ -4,17 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING -from pyavd._utils import AvdStringFormatter, append_if_not_duplicate, default, strip_empties_from_dict +from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from pyavd._eos_designs.schema import EosDesigns - - from . import AvdStructuredConfigNetworkServices - class LoopbackInterfacesMixin(UtilsMixin): """ @@ -24,7 +18,7 @@ class LoopbackInterfacesMixin(UtilsMixin): """ @cached_property - def loopback_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def loopback_interfaces(self) -> list | None: """ Return structured config for loopback_interfaces. @@ -78,33 +72,3 @@ def loopback_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None return loopback_interfaces return None - - def _get_vtep_diagnostic_loopback_for_vrf( - self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem - ) -> dict | None: - if (loopback := vrf.vtep_diagnostic.loopback) is None: - return None - - pod_name = self.inputs.pod_name - loopback_ip_pools = vrf.vtep_diagnostic.loopback_ip_pools - if not (loopback_ipv4_pool := vrf.vtep_diagnostic.loopback_ip_range) and pod_name and loopback_ip_pools and pod_name in loopback_ip_pools: - loopback_ipv4_pool = loopback_ip_pools[pod_name].ipv4_pool - - if not (loopback_ipv6_pool := vrf.vtep_diagnostic.loopback_ipv6_range) and pod_name and loopback_ip_pools and pod_name in loopback_ip_pools: - loopback_ipv6_pool = loopback_ip_pools[pod_name].ipv6_pool - - if not loopback_ipv4_pool and not loopback_ipv6_pool: - return None - - interface_name = f"Loopback{loopback}" - description_template = default(vrf.vtep_diagnostic.loopback_description, self.inputs.default_vrf_diag_loopback_description) - return strip_empties_from_dict( - { - "name": interface_name, - "description": AvdStringFormatter().format(description_template, interface=interface_name, vrf=vrf.name, tenant=vrf._tenant), - "shutdown": False, - "vrf": vrf.name, - "ip_address": f"{self.shared_utils.ip_addressing.vrf_loopback_ip(loopback_ipv4_pool)}/32" if loopback_ipv4_pool else None, - "ipv6_address": f"{self.shared_utils.ip_addressing.vrf_loopback_ipv6(loopback_ipv6_pool)}/128" if loopback_ipv6_pool else None, - } - ) diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/metadata.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/metadata.py index ef65291321b..c7da2ecf77e 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/metadata.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/metadata.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import get, get_all, strip_empties_from_list, strip_null_from_data from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class MetadataMixin(UtilsMixin): """ @@ -21,8 +17,10 @@ class MetadataMixin(UtilsMixin): Class should only be used as Mixin to a AvdStructuredConfig class. """ + application_traffic_recognition: dict | None + @cached_property - def metadata(self: AvdStructuredConfigNetworkServices) -> dict | None: + def metadata(self) -> dict | None: """ Generate metadata.cv_pathfinder for CV Pathfinder routers. @@ -43,7 +41,7 @@ def metadata(self: AvdStructuredConfigNetworkServices) -> dict | None: return {"cv_pathfinder": cv_pathfinder_metadata} - def get_cv_pathfinder_metadata_internet_exit_policies(self: AvdStructuredConfigNetworkServices) -> list[dict] | None: + def get_cv_pathfinder_metadata_internet_exit_policies(self) -> list[dict] | None: """Generate metadata.cv_pathfinder.internet_exit_policies if available.""" if not self._filtered_internet_exit_policies_and_connections: return None @@ -86,7 +84,7 @@ def get_cv_pathfinder_metadata_internet_exit_policies(self: AvdStructuredConfigN return strip_empties_from_list(internet_exit_polices, (None, [], {})) - def get_cv_pathfinder_metadata_applications(self: AvdStructuredConfigNetworkServices) -> dict | None: + def get_cv_pathfinder_metadata_applications(self) -> dict | None: """Generate metadata.cv_pathfinder.applications if available.""" if not self.shared_utils.is_cv_pathfinder_server or self.application_traffic_recognition is None: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/monitor_connectivity.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/monitor_connectivity.py index 6029ff61d1d..4dc7bb5dbf3 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/monitor_connectivity.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/monitor_connectivity.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class MonitorConnectivityMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class MonitorConnectivityMixin(UtilsMixin): """ @cached_property - def monitor_connectivity(self: AvdStructuredConfigNetworkServices) -> dict | None: + def monitor_connectivity(self) -> dict | None: """ Return structured config for monitor_connectivity. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/patch_panel.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/patch_panel.py index fd03123688c..5d57a370305 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/patch_panel.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/patch_panel.py @@ -5,15 +5,11 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class PatchPanelMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class PatchPanelMixin(UtilsMixin): """ @cached_property - def patch_panel(self: AvdStructuredConfigNetworkServices) -> dict | None: + def patch_panel(self) -> dict | None: """Return structured config for patch_panel.""" if not self.shared_utils.network_services_l1: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/port_channel_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/port_channel_interfaces.py index 2b978e34184..9b6852d76eb 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/port_channel_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/port_channel_interfaces.py @@ -5,15 +5,11 @@ import re from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, short_esi_to_route_target from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class PortChannelInterfacesMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class PortChannelInterfacesMixin(UtilsMixin): """ @cached_property - def port_channel_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def port_channel_interfaces(self) -> list | None: """ Return structured config for port_channel_interfaces. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/prefix_lists.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/prefix_lists.py index 120672b54e9..e105954fd27 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/prefix_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/prefix_lists.py @@ -4,16 +4,9 @@ from __future__ import annotations from functools import cached_property -from ipaddress import IPv4Network -from typing import TYPE_CHECKING - -from pyavd.j2filters import natural_sort from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class PrefixListsMixin(UtilsMixin): """ @@ -23,7 +16,7 @@ class PrefixListsMixin(UtilsMixin): """ @cached_property - def prefix_lists(self: AvdStructuredConfigNetworkServices) -> list | None: + def prefix_lists(self) -> list | None: """ Return structured config for prefix_lists. @@ -46,7 +39,7 @@ def prefix_lists(self: AvdStructuredConfigNetworkServices) -> list | None: return None - def _prefix_lists_vrf_default(self: AvdStructuredConfigNetworkServices) -> list: + def _prefix_lists_vrf_default(self) -> list: """prefix_lists for EVPN services in VRF "default".""" if not self._vrf_default_evpn: return [] @@ -71,25 +64,3 @@ def _prefix_lists_vrf_default(self: AvdStructuredConfigNetworkServices) -> list: prefix_list["sequence_numbers"].append({"sequence": sequence, "action": f"permit {static_route}"}) prefix_lists.append(prefix_list) return prefix_lists - - @cached_property - def _mlag_ibgp_peering_subnets_without_redistribution(self: AvdStructuredConfigNetworkServices) -> list: - """Return sorted list of MLAG peerings for VRFs where MLAG iBGP peering should not be redistributed.""" - mlag_prefixes = set() - for tenant in self.shared_utils.filtered_tenants: - for vrf in tenant.vrfs: - if self._mlag_ibgp_peering_vlan_vrf(vrf, tenant) is None: - continue - - if not self._exclude_mlag_ibgp_peering_from_redistribute(vrf, tenant): - # By default the BGP peering is redistributed, so we only need the prefix-list for the false case. - continue - - if (mlag_ip_address := self._get_vlan_ip_config_for_mlag_peering(vrf).get("ip_address")) is None: - # No MLAG prefix for this VRF (could be RFC5549) - continue - - # Convert mlag_ip_address to network prefix string and add to set. - mlag_prefixes.add(str(IPv4Network(mlag_ip_address, strict=False))) - - return natural_sort(mlag_prefixes) diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/route_maps.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/route_maps.py index c1c3b0324cd..c2466126af0 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/route_maps.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/route_maps.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, strip_empties_from_list from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouteMapsMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouteMapsMixin(UtilsMixin): """ @cached_property - def route_maps(self: AvdStructuredConfigNetworkServices) -> list | None: + def route_maps(self) -> list | None: """ Return structured config for route_maps. @@ -82,7 +78,7 @@ def route_maps(self: AvdStructuredConfigNetworkServices) -> list | None: return None @cached_property - def _route_maps_vrf_default(self: AvdStructuredConfigNetworkServices) -> list | None: + def _route_maps_vrf_default(self) -> list | None: """ Route-maps for EVPN services in VRF "default". @@ -107,7 +103,7 @@ def _route_maps_vrf_default(self: AvdStructuredConfigNetworkServices) -> list | return route_maps or None - def _bgp_mlag_peer_group_route_map(self: AvdStructuredConfigNetworkServices) -> dict: + def _bgp_mlag_peer_group_route_map(self) -> dict: """ Return dict with one route-map. @@ -127,7 +123,7 @@ def _bgp_mlag_peer_group_route_map(self: AvdStructuredConfigNetworkServices) -> ], } - def _connected_to_bgp_vrfs_route_map(self: AvdStructuredConfigNetworkServices) -> dict: + def _connected_to_bgp_vrfs_route_map(self) -> dict: """ Return dict with one route-map. @@ -148,7 +144,7 @@ def _connected_to_bgp_vrfs_route_map(self: AvdStructuredConfigNetworkServices) - ], } - def _evpn_export_vrf_default_route_map(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _evpn_export_vrf_default_route_map(self) -> dict | None: """ Match the following prefixes to be exported in EVPN for VRF default. @@ -191,7 +187,7 @@ def _evpn_export_vrf_default_route_map(self: AvdStructuredConfigNetworkServices) return {"name": "RM-EVPN-EXPORT-VRF-DEFAULT", "sequence_numbers": sequence_numbers} - def _bgp_underlay_peers_route_map(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _bgp_underlay_peers_route_map(self) -> dict | None: """ For non WAN routers filter EVPN routes away from underlay. @@ -233,7 +229,7 @@ def _bgp_underlay_peers_route_map(self: AvdStructuredConfigNetworkServices) -> d return {"name": "RM-BGP-UNDERLAY-PEERS-OUT", "sequence_numbers": sequence_numbers} - def _redistribute_connected_to_bgp_route_map(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _redistribute_connected_to_bgp_route_map(self) -> dict | None: """ Append network services relevant entries to the route-map used to redistribute connected subnets in BGP. @@ -261,7 +257,7 @@ def _redistribute_connected_to_bgp_route_map(self: AvdStructuredConfigNetworkSer return {"name": "RM-CONN-2-BGP", "sequence_numbers": sequence_numbers} - def _redistribute_static_to_bgp_route_map(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _redistribute_static_to_bgp_route_map(self) -> dict | None: """Append network services relevant entries to the route-map used to redistribute static routes to BGP.""" if not (self.shared_utils.wan_role and self._vrf_default_ipv4_static_routes["redistribute_in_overlay"]): return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_adaptive_virtual_topology.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_adaptive_virtual_topology.py index cefb44732a9..13df45b05c6 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_adaptive_virtual_topology.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_adaptive_virtual_topology.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, get, get_item, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterAdaptiveVirtualTopologyMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterAdaptiveVirtualTopologyMixin(UtilsMixin): """ @cached_property - def router_adaptive_virtual_topology(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_adaptive_virtual_topology(self) -> dict | None: """Return structured config for profiles, policies and VRFs for router adaptive-virtual-topology (AVT).""" if not self.shared_utils.is_cv_pathfinder_router: return None @@ -35,7 +31,7 @@ def router_adaptive_virtual_topology(self: AvdStructuredConfigNetworkServices) - return strip_empties_from_dict(router_adaptive_virtual_topology) - def _cv_pathfinder_wan_vrfs(self: AvdStructuredConfigNetworkServices) -> list: + def _cv_pathfinder_wan_vrfs(self) -> list: """Return a list of WAN VRFs based on filtered tenants and the AVT.""" # For CV Pathfinder, it is required to go through all the AVT profiles in the policy to assign an ID. wan_vrfs = [] @@ -71,7 +67,7 @@ def _cv_pathfinder_wan_vrfs(self: AvdStructuredConfigNetworkServices) -> list: return wan_vrfs - def _cv_pathfinder_policies(self: AvdStructuredConfigNetworkServices) -> list: + def _cv_pathfinder_policies(self) -> list: """ Build and return the CV Pathfinder policies based on the computed _filtered_wan_policies. @@ -101,7 +97,7 @@ def _cv_pathfinder_policies(self: AvdStructuredConfigNetworkServices) -> list: return policies - def _cv_pathfinder_profiles(self: AvdStructuredConfigNetworkServices) -> list: + def _cv_pathfinder_profiles(self) -> list: """Return a list of router adaptive-virtual-topology profiles for this router.""" profiles = [] for policy in self._filtered_wan_policies: diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_bgp.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_bgp.py index f96731f7b80..b4eefa767d0 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_bgp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_bgp.py @@ -3,21 +3,16 @@ # that can be found in the LICENSE file. from __future__ import annotations -import ipaddress from functools import cached_property from itertools import groupby as itertools_groupby -from typing import TYPE_CHECKING from pyavd._eos_designs.schema import EosDesigns from pyavd._errors import AristaAvdInvalidInputsError -from pyavd._utils import AvdStringFormatter, append_if_not_duplicate, default, get_item, merge, strip_empties_from_dict +from pyavd._utils import AvdStringFormatter, append_if_not_duplicate, default, merge, strip_empties_from_dict from pyavd.j2filters import list_compress from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterBgpMixin(UtilsMixin): """ @@ -27,7 +22,7 @@ class RouterBgpMixin(UtilsMixin): """ @cached_property - def router_bgp(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_bgp(self) -> dict | None: """ Return the structured config for router_bgp. @@ -62,7 +57,7 @@ def router_bgp(self: AvdStructuredConfigNetworkServices) -> dict | None: # Strip None values from vlan before returning return {key: value for key, value in router_bgp.items() if value is not None} - def _router_bgp_peer_groups(self: AvdStructuredConfigNetworkServices) -> dict: + def _router_bgp_peer_groups(self) -> dict: """ Return the structured config for router_bgp.peer_groups. @@ -130,258 +125,7 @@ def _router_bgp_peer_groups(self: AvdStructuredConfigNetworkServices) -> dict: return strip_empties_from_dict(router_bgp) - @cached_property - def _router_bgp_vrfs(self: AvdStructuredConfigNetworkServices) -> dict: - """ - Return partial structured config for router_bgp. - - Covers these areas: - - vrfs for all VRFs. - - neighbors and address_family_ipv4/6 for VRF default. - """ - if not self.shared_utils.network_services_l3: - return {} - - router_bgp = {"vrfs": []} - - for tenant in self.shared_utils.filtered_tenants: - for vrf in tenant.vrfs: - if not self.shared_utils.bgp_enabled_for_vrf(vrf): - continue - - vrf_name = vrf.name - bgp_vrf = strip_empties_from_dict( - { - "eos_cli": vrf.bgp.raw_eos_cli, - } - ) - - if vrf.bgp.structured_config: - self.custom_structured_configs.nested.router_bgp.vrfs.obtain(vrf_name)._deepmerge( - vrf.bgp.structured_config, list_merge=self.custom_structured_configs.list_merge_strategy - ) - - if vrf_address_families := [af for af in vrf.address_families if af in self.shared_utils.overlay_address_families]: - # The called function in-place updates the bgp_vrf dict. - self._update_router_bgp_vrf_evpn_or_mpls_cfg(bgp_vrf, vrf, vrf_address_families) - - if vrf_name != "default": - bgp_vrf["router_id"] = self.get_vrf_router_id(vrf, vrf.bgp.router_id, tenant.name) - - if vrf.redistribute_connected: - bgp_vrf["redistribute"] = {"connected": {"enabled": True}} - # Redistribution of static routes for VRF default are handled elsewhere - # since there is a choice between redistributing to underlay or overlay. - if vrf.redistribute_static or (vrf.static_routes and vrf.redistribute_static is None): - bgp_vrf["redistribute"].update({"static": {"enabled": True}}) - - if self.shared_utils.inband_mgmt_vrf == vrf_name and self.shared_utils.inband_management_parent_vlans: - bgp_vrf["redistribute"].update({"attached_host": {"enabled": True}}) - - else: - # VRF default - if bgp_vrf: - # RD/RT and/or eos_cli/struct_cfg which should go under the vrf default context. - # Any peers added later will be put directly under router_bgp - append_if_not_duplicate( - list_of_dicts=router_bgp["vrfs"], - primary_key="name", - new_dict={"name": vrf_name, **bgp_vrf}, - context="BGP VRFs defined under network services", - context_keys=["name"], - ) - # Resetting bgp_vrf so we only add global keys if there are any neighbors for VRF default - bgp_vrf = {} - - if self.shared_utils.underlay_routing_protocol == "none": - # We need to add redistribute connected for the default VRF when underlay_routing_protocol is "none" - bgp_vrf["redistribute"] = {"connected": {"enabled": True}} - - # MLAG IBGP Peering VLANs per VRF - # Will only be configured for VRF default if underlay_routing_protocol == "none". - if (vlan_id := self._mlag_ibgp_peering_vlan_vrf(vrf, tenant)) is not None: - self._update_router_bgp_vrf_mlag_neighbor_cfg(bgp_vrf, vrf, tenant, vlan_id) - - for bgp_peer in vrf.bgp_peers: - # Below we pop various keys that are not supported by the eos_cli_config_gen schema. - # The rest of the keys are relayed directly to eos_cli_config_gen. - # 'ip_address' is popped even though it is supported. It will be added again later - # to ensure it comes first in the generated dict. - bgp_peer_dict = bgp_peer._as_dict() - peer_ip = bgp_peer_dict.pop("ip_address") - address_family = f"address_family_ipv{ipaddress.ip_address(peer_ip).version}" - neighbor = strip_empties_from_dict( - { - "ip_address": peer_ip, - "activate": True, - "prefix_list_in": bgp_peer_dict.pop("prefix_list_in", None), - "prefix_list_out": bgp_peer_dict.pop("prefix_list_out", None), - }, - ) - - append_if_not_duplicate( - list_of_dicts=bgp_vrf.setdefault(address_family, {}).setdefault("neighbors", []), - primary_key="ip_address", - new_dict=neighbor, - context="BGP peer defined under VRFs", - context_keys=["ip_address"], - ) - - if bgp_peer.set_ipv4_next_hop or bgp_peer.set_ipv6_next_hop: - route_map = f"RM-{vrf_name}-{peer_ip}-SET-NEXT-HOP-OUT" - bgp_peer_dict["route_map_out"] = route_map - if bgp_peer_dict.get("default_originate") is not None: - bgp_peer_dict["default_originate"].setdefault("route_map", route_map) - - bgp_peer_dict.pop("set_ipv4_next_hop", None) - bgp_peer_dict.pop("set_ipv6_next_hop", None) - - bgp_peer_dict.pop("nodes", None) - - append_if_not_duplicate( - list_of_dicts=bgp_vrf.setdefault("neighbors", []), - primary_key="ip_address", - new_dict={"ip_address": peer_ip, **bgp_peer_dict}, - context="BGP peer defined under VRFs", - context_keys=["ip_address"], - ) - - if vrf.ospf.enabled and vrf.redistribute_ospf and (not vrf.ospf.nodes or self.shared_utils.hostname in vrf.ospf.nodes): - bgp_vrf.setdefault("redistribute", {}).update({"ospf": {"enabled": True}}) - - if ( - bgp_vrf.get("neighbors") - and self.inputs.bgp_update_wait_install - and self.shared_utils.platform_settings.feature_support.bgp_update_wait_install - ): - bgp_vrf.setdefault("updates", {})["wait_install"] = True - - bgp_vrf = strip_empties_from_dict(bgp_vrf) - - # Skip adding the VRF if we have no config. - if not bgp_vrf: - continue - - if vrf_name == "default": - # VRF default is added directly under router_bgp - router_bgp.update(bgp_vrf) - else: - append_if_not_duplicate( - list_of_dicts=router_bgp["vrfs"], - primary_key="name", - new_dict={"name": vrf_name, **bgp_vrf}, - context="BGP VRFs defined under network services", - context_keys=["name"], - ) - return strip_empties_from_dict(router_bgp) - - def _update_router_bgp_vrf_evpn_or_mpls_cfg( - self: AvdStructuredConfigNetworkServices, - bgp_vrf: dict, - vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, - vrf_address_families: list[str], - ) -> None: - """In-place update EVPN/MPLS part of structured config for *one* VRF under router_bgp.vrfs.""" - vrf_name = vrf.name - bgp_vrf["rd"] = self.get_vrf_rd(vrf) - vrf_rt = self.get_vrf_rt(vrf) - route_targets = {"import": [], "export": []} - - for af in vrf_address_families: - if (target := get_item(route_targets["import"], "address_family", af)) is None: - route_targets["import"].append({"address_family": af, "route_targets": [vrf_rt]}) - else: - target["route_targets"].append(vrf_rt) - - if (target := get_item(route_targets["export"], "address_family", af)) is None: - route_targets["export"].append({"address_family": af, "route_targets": [vrf_rt]}) - else: - target["route_targets"].append(vrf_rt) - - for rt in vrf.additional_route_targets: - if rt.type is None: - continue - if (target := get_item(route_targets[rt.type], "address_family", rt.address_family)) is None: - route_targets[rt.type].append({"address_family": rt.address_family, "route_targets": [rt.route_target]}) - else: - target["route_targets"].append(rt.route_target) - - if vrf_name == "default" and self._vrf_default_evpn and self._route_maps_vrf_default: - # Special handling of vrf default with evpn. - - if (target := get_item(route_targets["export"], "address_family", "evpn")) is None: - route_targets["export"].append({"address_family": "evpn", "route_targets": ["route-map RM-EVPN-EXPORT-VRF-DEFAULT"]}) - else: - target.setdefault("route_targets", []).append("route-map RM-EVPN-EXPORT-VRF-DEFAULT") - - bgp_vrf["route_targets"] = route_targets - - # VRF default - if vrf_name == "default": - return - - # Not VRF default - bgp_vrf["evpn_multicast"] = getattr(vrf, "_evpn_l3_multicast_enabled", None) - if evpn_multicast_transit_mode := getattr(vrf, "_evpn_l3_multicast_evpn_peg_transit", False): - bgp_vrf["evpn_multicast_address_family"] = {"ipv4": {"transit": evpn_multicast_transit_mode}} - - def _update_router_bgp_vrf_mlag_neighbor_cfg( - self: AvdStructuredConfigNetworkServices, - bgp_vrf: dict, - vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, - tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, - vlan_id: int, - ) -> None: - """In-place update MLAG neighbor part of structured config for *one* VRF under router_bgp.vrfs.""" - if self._exclude_mlag_ibgp_peering_from_redistribute(vrf, tenant): - bgp_vrf["redistribute"]["connected"] = {"enabled": True, "route_map": "RM-CONN-2-BGP-VRFS"} - - interface_name = f"Vlan{vlan_id}" - if self.inputs.underlay_rfc5549 and self.inputs.overlay_mlag_rfc5549: - bgp_vrf.setdefault("neighbor_interfaces", []).append( - { - "name": interface_name, - "peer_group": self.inputs.bgp_peer_groups.mlag_ipv4_underlay_peer.name, - "remote_as": self.shared_utils.bgp_as, - "description": AvdStringFormatter().format( - self.inputs.mlag_bgp_peer_description, - mlag_peer=self.shared_utils.mlag_peer, - interface=interface_name, - peer_interface=interface_name, - ), - }, - ) - else: - if not vrf.mlag_ibgp_peering_ipv4_pool: - ip_address = self.shared_utils.mlag_peer_ibgp_ip - elif self.shared_utils.mlag_role == "primary": - ip_address = self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_secondary(vrf.mlag_ibgp_peering_ipv4_pool) - else: - ip_address = self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_primary(vrf.mlag_ibgp_peering_ipv4_pool) - - bgp_vrf.setdefault("neighbors", []).append( - { - "ip_address": ip_address, - "peer_group": self.inputs.bgp_peer_groups.mlag_ipv4_underlay_peer.name, - "description": AvdStringFormatter().format( - self.inputs.mlag_bgp_peer_description, - **strip_empties_from_dict( - {"mlag_peer": self.shared_utils.mlag_peer, "interface": interface_name, "peer_interface": interface_name, "vrf": vrf.name} - ), - ), - }, - ) - if self.inputs.underlay_rfc5549: - bgp_vrf.setdefault("address_family_ipv4", {}).setdefault("neighbors", []).append( - { - "ip_address": ip_address, - "next_hop": { - "address_family_ipv6": {"enabled": False}, - }, - }, - ) - - def _router_bgp_sorted_vlans_and_svis_lists(self: AvdStructuredConfigNetworkServices) -> dict: + def _router_bgp_sorted_vlans_and_svis_lists(self) -> dict: tenant_svis_l2vlans_dict = {} for tenant in self.shared_utils.filtered_tenants: tenant_svis_l2vlans_dict[tenant.name] = {} @@ -428,7 +172,7 @@ def _router_bgp_sorted_vlans_and_svis_lists(self: AvdStructuredConfigNetworkServ return tenant_svis_l2vlans_dict - def _router_bgp_vlans(self: AvdStructuredConfigNetworkServices, tenant_svis_l2vlans_dict: dict) -> list | None: + def _router_bgp_vlans(self, tenant_svis_l2vlans_dict: dict) -> list | None: """Return structured config for router_bgp.vlans.""" if not ( self.shared_utils.network_services_l2 @@ -472,7 +216,7 @@ def _router_bgp_vlans(self: AvdStructuredConfigNetworkServices, tenant_svis_l2vl return vlans or None def _router_bgp_vlans_vlan( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -519,7 +263,7 @@ def _router_bgp_vlans_vlan( return {key: value for key, value in bgp_vlan.items() if value is not None} def _get_vlan_aware_bundle_name_tuple_for_l2vlans( - self: AvdStructuredConfigNetworkServices, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem ) -> tuple[str, bool] | None: """Return a tuple with string with the vlan-aware-bundle name for one VLAN and a boolean saying if this is a evpn_vlan_bundle.""" if vlan.evpn_vlan_bundle: @@ -527,7 +271,7 @@ def _get_vlan_aware_bundle_name_tuple_for_l2vlans( return (vlan.name, False) def _get_vlan_aware_bundle_name_tuple_for_svis( - self: AvdStructuredConfigNetworkServices, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem ) -> tuple[str, bool] | None: """ Return a tuple with string with the vlan-aware-bundle name for one VLAN and a boolean saying if this is a evpn_vlan_bundle. @@ -540,7 +284,7 @@ def _get_vlan_aware_bundle_name_tuple_for_svis( return ("", False) def _get_evpn_vlan_bundle( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, bundle_name: str, @@ -555,7 +299,7 @@ def _get_evpn_vlan_bundle( return self.inputs.evpn_vlan_bundles[bundle_name] def _get_svi_l2vlan_bundle( - self: AvdStructuredConfigNetworkServices, + self, evpn_vlan_bundle: EosDesigns.EvpnVlanBundlesItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, vlans: list, @@ -583,7 +327,7 @@ def _get_svi_l2vlan_bundle( return None - def _router_bgp_vlan_aware_bundles(self: AvdStructuredConfigNetworkServices, tenant_svis_l2vlans_dict: dict) -> list | None: + def _router_bgp_vlan_aware_bundles(self, tenant_svis_l2vlans_dict: dict) -> list | None: """Return structured config for router_bgp.vlan_aware_bundles.""" if not self.shared_utils.network_services_l2 or not self.shared_utils.overlay_evpn: return None @@ -672,7 +416,7 @@ def _router_bgp_vlan_aware_bundles(self: AvdStructuredConfigNetworkServices, ten return bundles or None def _router_bgp_vlan_aware_bundles_vrf( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, svis: list[EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem], @@ -688,7 +432,7 @@ def _router_bgp_vlan_aware_bundles_vrf( ) def _router_bgp_vlan_aware_bundle( - self: AvdStructuredConfigNetworkServices, + self, name: str, vlans: list[EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem] | list[EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem], @@ -725,7 +469,7 @@ def _router_bgp_vlan_aware_bundle( return bundle @cached_property - def _router_bgp_redistribute_routes(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _router_bgp_redistribute_routes(self) -> dict | None: """ Return structured config for router_bgp.redistribute. @@ -747,7 +491,7 @@ def _router_bgp_redistribute_routes(self: AvdStructuredConfigNetworkServices) -> return {"static": {"enabled": True}} @cached_property - def _router_bgp_vpws(self: AvdStructuredConfigNetworkServices) -> list[dict] | None: + def _router_bgp_vpws(self) -> list[dict] | None: """Return structured config for router_bgp.vpws.""" if not (self.shared_utils.network_services_l1 and self.shared_utils.overlay_ler and self.shared_utils.overlay_evpn_mpls): return None @@ -804,7 +548,7 @@ def _router_bgp_vpws(self: AvdStructuredConfigNetworkServices) -> list[dict] | N return None - def _router_bgp_mlag_peer_group(self: AvdStructuredConfigNetworkServices) -> dict: + def _router_bgp_mlag_peer_group(self) -> dict: """ Return a partial router_bgp structured_config covering the MLAG peer_group and associated address_family activations. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_internet_exit.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_internet_exit.py index d98d7d98ef0..b5f57a60f09 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_internet_exit.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_internet_exit.py @@ -5,13 +5,9 @@ from collections import defaultdict from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterInternetExitMixin(UtilsMixin): """ @@ -21,7 +17,7 @@ class RouterInternetExitMixin(UtilsMixin): """ @cached_property - def router_internet_exit(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_internet_exit(self) -> dict | None: """ Return structured config for router_internet_exit. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_isis.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_isis.py index d52b2fd8091..b21d415dc96 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_isis.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_isis.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterIsisMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class RouterIsisMixin(UtilsMixin): """ @cached_property - def router_isis(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_isis(self) -> dict | None: """ Return structured config for router_isis. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_multicast.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_multicast.py index 9aa861871df..afd1eea5858 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_multicast.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_multicast.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterMulticastMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterMulticastMixin(UtilsMixin): """ @cached_property - def router_multicast(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_multicast(self) -> dict | None: """ Return structured config for router_multicast. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_ospf.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_ospf.py index f7c045a0100..c192bbfe4f1 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_ospf.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_ospf.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import append_if_not_duplicate, default from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterOspfMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class RouterOspfMixin(UtilsMixin): """ @cached_property - def router_ospf(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_ospf(self) -> dict | None: """ Return structured config for router_ospf. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_path_selection.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_path_selection.py index 7c109bbcff7..01979fee392 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_path_selection.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_path_selection.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, get, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterPathSelectionMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterPathSelectionMixin(UtilsMixin): """ @cached_property - def router_path_selection(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_path_selection(self) -> dict | None: """Return structured config for router path-selection (DPS).""" if not self.shared_utils.is_wan_router: return None @@ -47,7 +43,7 @@ def router_path_selection(self: AvdStructuredConfigNetworkServices) -> dict | No return strip_empties_from_dict(router_path_selection) - def _wan_load_balance_policies(self: AvdStructuredConfigNetworkServices) -> list: + def _wan_load_balance_policies(self) -> list: """Return a list of load balance policies.""" load_balance_policies = [] for policy in self._filtered_wan_policies: @@ -70,7 +66,7 @@ def _wan_load_balance_policies(self: AvdStructuredConfigNetworkServices) -> list return load_balance_policies - def _autovpn_policies(self: AvdStructuredConfigNetworkServices) -> list: + def _autovpn_policies(self) -> list: """Return a list of policies for AutoVPN.""" policies = [] for policy in self._filtered_wan_policies: diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_pim_sparse_mode.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_pim_sparse_mode.py index 5814eac5b22..7318e4d23aa 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_pim_sparse_mode.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_pim_sparse_mode.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterPimSparseModeMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterPimSparseModeMixin(UtilsMixin): """ @cached_property - def router_pim_sparse_mode(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_pim_sparse_mode(self) -> dict | None: """ Return structured config for router_pim. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_service_insertion.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_service_insertion.py index 48490cf07e3..bd82eee619f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/router_service_insertion.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/router_service_insertion.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class RouterServiceInsertionMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class RouterServiceInsertionMixin(UtilsMixin): """ @cached_property - def router_service_insertion(self: AvdStructuredConfigNetworkServices) -> dict | None: + def router_service_insertion(self) -> dict | None: """ Return structured config for router_service_insertion. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/spanning_tree.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/spanning_tree.py index 53d14a296d7..69d6bedb12f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/spanning_tree.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/spanning_tree.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd.j2filters import list_compress from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class SpanningTreeMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class SpanningTreeMixin(UtilsMixin): """ @cached_property - def spanning_tree(self: AvdStructuredConfigNetworkServices) -> dict | None: + def spanning_tree(self) -> dict | None: """spanning_tree priorities set per VLAN if spanning_tree mode is "rapid-pvst".""" if not self.shared_utils.network_services_l2: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/standard_access_lists.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/standard_access_lists.py index 664c85a8967..db6615fe995 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/standard_access_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/standard_access_lists.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class StandardAccessListsMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class StandardAccessListsMixin(UtilsMixin): """ @cached_property - def standard_access_lists(self: AvdStructuredConfigNetworkServices) -> list | None: + def standard_access_lists(self) -> list | None: """ Return structured config for standard_access_lists. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/static_routes.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/static_routes.py index 7af61c83d48..1fec5083ef7 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/static_routes.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/static_routes.py @@ -5,13 +5,9 @@ import ipaddress from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class StaticRoutesMixin(UtilsMixin): """ @@ -21,7 +17,7 @@ class StaticRoutesMixin(UtilsMixin): """ @cached_property - def static_routes(self: AvdStructuredConfigNetworkServices) -> list[dict] | None: + def static_routes(self) -> list[dict] | None: """ Returns structured config for static_routes. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/struct_cfgs.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/struct_cfgs.py index 98aaf16935d..816ee32a096 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/struct_cfgs.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/struct_cfgs.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class StructCfgsMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class StructCfgsMixin(UtilsMixin): """ @cached_property - def struct_cfgs(self: AvdStructuredConfigNetworkServices) -> None: + def struct_cfgs(self) -> None: """Return the combined structured config from VRFs.""" if not self.shared_utils.network_services_l3: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/tunnel_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/tunnel_interfaces.py index cfbd8a652b9..6624af975c1 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/tunnel_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/tunnel_interfaces.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class TunnelInterfacesMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class TunnelInterfacesMixin(UtilsMixin): """ @cached_property - def tunnel_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def tunnel_interfaces(self) -> list | None: """ Return structured config for tunnel_interfaces. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils.py index e6113514373..444f6017cdb 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils.py @@ -8,20 +8,20 @@ from re import fullmatch as re_fullmatch from typing import TYPE_CHECKING +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError -from pyavd._utils import default, get, get_ip_from_ip_prefix +from pyavd._utils import append_if_not_duplicate, default, get, get_ip_from_ip_prefix, get_item +from pyavd._utils.format_string import AvdStringFormatter +from pyavd._utils.strip_empties import strip_empties_from_dict, strip_empties_from_list from pyavd.j2filters import natural_sort from .utils_wan import UtilsWanMixin -from .utils_zscaler import UtilsZscalerMixin if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - -class UtilsMixin(UtilsWanMixin, UtilsZscalerMixin): +class UtilsMixin(UtilsWanMixin, StructuredConfigGenerator): """ Mixin Class with internal functions. @@ -29,11 +29,11 @@ class UtilsMixin(UtilsWanMixin, UtilsZscalerMixin): """ @cached_property - def _local_endpoint_trunk_groups(self: AvdStructuredConfigNetworkServices) -> set: + def _local_endpoint_trunk_groups(self) -> set: return set(get(self._hostvars, "switch.local_endpoint_trunk_groups", default=[])) @cached_property - def _vrf_default_evpn(self: AvdStructuredConfigNetworkServices) -> bool: + def _vrf_default_evpn(self) -> bool: """Return boolean telling if VRF "default" is running EVPN or not.""" if not (self.shared_utils.network_services_l3 and self.shared_utils.overlay_vtep and self.shared_utils.overlay_evpn): return False @@ -51,7 +51,7 @@ def _vrf_default_evpn(self: AvdStructuredConfigNetworkServices) -> bool: return False @cached_property - def _vrf_default_ipv4_subnets(self: AvdStructuredConfigNetworkServices) -> list[str]: + def _vrf_default_ipv4_subnets(self) -> list[str]: """Return list of ipv4 subnets in VRF "default".""" subnets = [] for tenant in self.shared_utils.filtered_tenants: @@ -70,7 +70,7 @@ def _vrf_default_ipv4_subnets(self: AvdStructuredConfigNetworkServices) -> list[ return subnets @cached_property - def _vrf_default_ipv4_static_routes(self: AvdStructuredConfigNetworkServices) -> dict: + def _vrf_default_ipv4_static_routes(self) -> dict: """ Finds static routes defined under VRF "default" and find out if they should be redistributed in underlay and/or overlay. @@ -118,7 +118,7 @@ def _vrf_default_ipv4_static_routes(self: AvdStructuredConfigNetworkServices) -> } def _mlag_ibgp_peering_enabled( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, ) -> bool: @@ -136,7 +136,7 @@ def _mlag_ibgp_peering_enabled( return bool((vrf.name != "default" or self.shared_utils.underlay_routing_protocol == "none") and mlag_ibgp_peering) def _mlag_ibgp_peering_vlan_vrf( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, ) -> int | None: @@ -162,7 +162,7 @@ def _mlag_ibgp_peering_vlan_vrf( return vlan_id def _exclude_mlag_ibgp_peering_from_redistribute( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, ) -> bool: @@ -177,7 +177,7 @@ def _exclude_mlag_ibgp_peering_from_redistribute( return False @cached_property - def _configure_bgp_mlag_peer_group(self: AvdStructuredConfigNetworkServices) -> bool: + def _configure_bgp_mlag_peer_group(self) -> bool: """ Flag set during creating of BGP VRFs if an MLAG peering is needed. @@ -202,7 +202,7 @@ def _configure_bgp_mlag_peer_group(self: AvdStructuredConfigNetworkServices) -> return False @cached_property - def _rt_admin_subfield(self: AvdStructuredConfigNetworkServices) -> str | None: + def _rt_admin_subfield(self) -> str | None: """ Return a string with the route-target admin subfield unless set to "vrf_id" or "vrf_vni" or "id". @@ -222,7 +222,7 @@ def _rt_admin_subfield(self: AvdStructuredConfigNetworkServices) -> str | None: return None def get_vlan_mac_vrf_id( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -237,7 +237,7 @@ def get_vlan_mac_vrf_id( return mac_vrf_id_base + vlan.id def get_vlan_mac_vrf_vni( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -252,7 +252,7 @@ def get_vlan_mac_vrf_vni( return mac_vrf_vni_base + vlan.id def get_vlan_rd( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -275,7 +275,7 @@ def get_vlan_rd( return f"{self.shared_utils.overlay_rd_type_admin_subfield}:{assigned_number_subfield}" def get_vlan_rt( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -309,7 +309,7 @@ def get_vlan_rt( return f"{admin_subfield}:{assigned_number_subfield}" @cached_property - def _vrf_rt_admin_subfield(self: AvdStructuredConfigNetworkServices) -> str | None: + def _vrf_rt_admin_subfield(self) -> str | None: """ Return a string with the VRF route-target admin subfield unless set to "vrf_id" or "vrf_vni" or "id". @@ -325,7 +325,7 @@ def _vrf_rt_admin_subfield(self: AvdStructuredConfigNetworkServices) -> str | No return None - def get_vrf_rd(self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> str: + def get_vrf_rd(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> str: """Return a string with the route-destinguisher for one VRF.""" rd_override = vrf.rd_override @@ -337,7 +337,7 @@ def get_vrf_rd(self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._Dynami return f"{self.shared_utils.overlay_rd_type_vrf_admin_subfield}:{self.shared_utils.get_vrf_id(vrf)}" - def get_vrf_rt(self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> str: + def get_vrf_rt(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> str: """Return a string with the route-target for one VRF.""" rt_override = vrf.rt_override @@ -358,7 +358,7 @@ def get_vrf_rt(self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._Dynami return f"{admin_subfield}:{self.shared_utils.get_vrf_id(vrf)}" def get_vlan_aware_bundle_rd( - self: AvdStructuredConfigNetworkServices, + self, id: int, # noqa: A002 tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, is_vrf: bool, @@ -377,7 +377,7 @@ def get_vlan_aware_bundle_rd( return f"{admin_subfield}:{bundle_number}" def get_vlan_aware_bundle_rt( - self: AvdStructuredConfigNetworkServices, + self, id: int, # noqa: A002 vni: int, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -404,7 +404,7 @@ def get_vlan_aware_bundle_rt( return f"{admin_subfield}:{bundle_number}" def get_vrf_router_id( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, router_id: str, tenant_name: str, @@ -443,3 +443,528 @@ def get_vrf_router_id( # Default to the specified router ID return router_id + + def _get_vtep_diagnostic_loopback_for_vrf(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> dict | None: + if (loopback := vrf.vtep_diagnostic.loopback) is None: + return None + + pod_name = self.inputs.pod_name + loopback_ip_pools = vrf.vtep_diagnostic.loopback_ip_pools + if not (loopback_ipv4_pool := vrf.vtep_diagnostic.loopback_ip_range) and pod_name and loopback_ip_pools and pod_name in loopback_ip_pools: + loopback_ipv4_pool = loopback_ip_pools[pod_name].ipv4_pool + + if not (loopback_ipv6_pool := vrf.vtep_diagnostic.loopback_ipv6_range) and pod_name and loopback_ip_pools and pod_name in loopback_ip_pools: + loopback_ipv6_pool = loopback_ip_pools[pod_name].ipv6_pool + + if not loopback_ipv4_pool and not loopback_ipv6_pool: + return None + + interface_name = f"Loopback{loopback}" + description_template = default(vrf.vtep_diagnostic.loopback_description, self.inputs.default_vrf_diag_loopback_description) + return strip_empties_from_dict( + { + "name": interface_name, + "description": AvdStringFormatter().format(description_template, interface=interface_name, vrf=vrf.name, tenant=vrf._tenant), + "shutdown": False, + "vrf": vrf.name, + "ip_address": f"{self.shared_utils.ip_addressing.vrf_loopback_ip(loopback_ipv4_pool)}/32" if loopback_ipv4_pool else None, + "ipv6_address": f"{self.shared_utils.ip_addressing.vrf_loopback_ipv6(loopback_ipv6_pool)}/128" if loopback_ipv6_pool else None, + } + ) + + @cached_property + def _route_maps_vrf_default(self) -> list | None: + """ + Route-maps for EVPN services in VRF "default". + + Called from main route_maps function + + Also checked under router_bgp_vrfs to figure out if a route-map should be set on EVPN export. + """ + if not self._vrf_default_evpn: + return None + + if not any([self._vrf_default_ipv4_subnets, self._vrf_default_ipv4_static_routes["static_routes"], self.shared_utils.is_wan_router]): + return None + + route_maps = strip_empties_from_list( + [ + self._evpn_export_vrf_default_route_map(), + self._bgp_underlay_peers_route_map(), + self._redistribute_connected_to_bgp_route_map(), + self._redistribute_static_to_bgp_route_map(), + ], + ) + + return route_maps or None + + def _bgp_mlag_peer_group_route_map(self) -> dict: + """ + Return dict with one route-map. + + Origin Incomplete for MLAG iBGP learned routes. + + TODO: Partially duplicated from mlag. Should be moved to a common class + """ + return { + "name": "RM-MLAG-PEER-IN", + "sequence_numbers": [ + { + "sequence": 10, + "type": "permit", + "set": ["origin incomplete"], + "description": "Make routes learned over MLAG Peer-link less preferred on spines to ensure optimal routing", + }, + ], + } + + def _connected_to_bgp_vrfs_route_map(self) -> dict: + """ + Return dict with one route-map. + + Filter MLAG peer subnets for redistribute connected for overlay VRFs. + """ + return { + "name": "RM-CONN-2-BGP-VRFS", + "sequence_numbers": [ + { + "sequence": 10, + "type": "deny", + "match": ["ip address prefix-list PL-MLAG-PEER-VRFS"], + }, + { + "sequence": 20, + "type": "permit", + }, + ], + } + + def _evpn_export_vrf_default_route_map(self) -> dict | None: + """ + Match the following prefixes to be exported in EVPN for VRF default. + + * SVI subnets in VRF default + * Static routes subnets in VRF default. + + * for WAN routers, all the routes matching the SOO (which includes the two above) + """ + sequence_numbers = [] + if self.shared_utils.is_wan_router: + sequence_numbers.append( + { + "sequence": 10, + "type": "permit", + "match": ["extcommunity ECL-EVPN-SOO"], + }, + ) + else: + # TODO: refactor existing behavior to SoO? + if self._vrf_default_ipv4_subnets: + sequence_numbers.append( + { + "sequence": 10, + "type": "permit", + "match": ["ip address prefix-list PL-SVI-VRF-DEFAULT"], + }, + ) + + if self._vrf_default_ipv4_static_routes["static_routes"]: + sequence_numbers.append( + { + "sequence": 20, + "type": "permit", + "match": ["ip address prefix-list PL-STATIC-VRF-DEFAULT"], + }, + ) + + if not sequence_numbers: + return None + + return {"name": "RM-EVPN-EXPORT-VRF-DEFAULT", "sequence_numbers": sequence_numbers} + + def _bgp_underlay_peers_route_map(self) -> dict | None: + """ + For non WAN routers filter EVPN routes away from underlay. + + For WAN routers the underlay towards LAN side also permits the tenant routes for VRF default, + so routes should not be filtered. + """ + sequence_numbers = [] + + if self.shared_utils.is_wan_router: + return None + + if self._vrf_default_ipv4_subnets: + sequence_numbers.append( + { + "sequence": 10, + "type": "deny", + "match": ["ip address prefix-list PL-SVI-VRF-DEFAULT"], + }, + ) + + if self._vrf_default_ipv4_static_routes["static_routes"]: + sequence_numbers.append( + { + "sequence": 15, + "type": "deny", + "match": ["ip address prefix-list PL-STATIC-VRF-DEFAULT"], + }, + ) + + if not sequence_numbers: + return None + + sequence_numbers.append( + { + "sequence": 20, + "type": "permit", + }, + ) + + return {"name": "RM-BGP-UNDERLAY-PEERS-OUT", "sequence_numbers": sequence_numbers} + + def _redistribute_connected_to_bgp_route_map(self) -> dict | None: + """ + Append network services relevant entries to the route-map used to redistribute connected subnets in BGP. + + sequence 10 is set in underlay and sequence 20 in inband management, so avoid setting those here + """ + if not self.inputs.underlay_filter_redistribute_connected: + return None + + sequence_numbers = [] + + if self._vrf_default_ipv4_subnets: + # Add subnets to redistribution in default VRF + sequence_30 = { + "sequence": 30, + "type": "permit", + "match": ["ip address prefix-list PL-SVI-VRF-DEFAULT"], + } + if self.shared_utils.wan_role: + sequence_30["set"] = [f"extcommunity soo {self.shared_utils.evpn_soo} additive"] + + sequence_numbers.append(sequence_30) + + if not sequence_numbers: + return None + + return {"name": "RM-CONN-2-BGP", "sequence_numbers": sequence_numbers} + + def _redistribute_static_to_bgp_route_map(self) -> dict | None: + """Append network services relevant entries to the route-map used to redistribute static routes to BGP.""" + if not (self.shared_utils.wan_role and self._vrf_default_ipv4_static_routes["redistribute_in_overlay"]): + return None + + return { + "name": "RM-STATIC-2-BGP", + "sequence_numbers": [ + { + "sequence": 10, + "type": "permit", + "match": ["ip address prefix-list PL-STATIC-VRF-DEFAULT"], + "set": [f"extcommunity soo {self.shared_utils.evpn_soo} additive"], + }, + ], + } + + @cached_property + def _router_bgp_vrfs(self) -> dict: + """ + Return partial structured config for router_bgp. + + Covers these areas: + - vrfs for all VRFs. + - neighbors and address_family_ipv4/6 for VRF default. + """ + if not self.shared_utils.network_services_l3: + return {} + + router_bgp = {"vrfs": []} + + for tenant in self.shared_utils.filtered_tenants: + for vrf in tenant.vrfs: + if not self.shared_utils.bgp_enabled_for_vrf(vrf): + continue + + vrf_name = vrf.name + bgp_vrf = strip_empties_from_dict( + { + "eos_cli": vrf.bgp.raw_eos_cli, + } + ) + + if vrf.bgp.structured_config: + self.custom_structured_configs.nested.router_bgp.vrfs.obtain(vrf_name)._deepmerge( + vrf.bgp.structured_config, list_merge=self.custom_structured_configs.list_merge_strategy + ) + + if vrf_address_families := [af for af in vrf.address_families if af in self.shared_utils.overlay_address_families]: + # The called function in-place updates the bgp_vrf dict. + self._update_router_bgp_vrf_evpn_or_mpls_cfg(bgp_vrf, vrf, vrf_address_families) + + if vrf_name != "default": + bgp_vrf["router_id"] = self.get_vrf_router_id(vrf, vrf.bgp.router_id, tenant.name) + + if vrf.redistribute_connected: + bgp_vrf["redistribute"] = {"connected": {"enabled": True}} + # Redistribution of static routes for VRF default are handled elsewhere + # since there is a choice between redistributing to underlay or overlay. + if vrf.redistribute_static or (vrf.static_routes and vrf.redistribute_static is None): + bgp_vrf["redistribute"].update({"static": {"enabled": True}}) + + if self.shared_utils.inband_mgmt_vrf == vrf_name and self.shared_utils.inband_management_parent_vlans: + bgp_vrf["redistribute"].update({"attached_host": {"enabled": True}}) + + else: + # VRF default + if bgp_vrf: + # RD/RT and/or eos_cli/struct_cfg which should go under the vrf default context. + # Any peers added later will be put directly under router_bgp + append_if_not_duplicate( + list_of_dicts=router_bgp["vrfs"], + primary_key="name", + new_dict={"name": vrf_name, **bgp_vrf}, + context="BGP VRFs defined under network services", + context_keys=["name"], + ) + # Resetting bgp_vrf so we only add global keys if there are any neighbors for VRF default + bgp_vrf = {} + + if self.shared_utils.underlay_routing_protocol == "none": + # We need to add redistribute connected for the default VRF when underlay_routing_protocol is "none" + bgp_vrf["redistribute"] = {"connected": {"enabled": True}} + + # MLAG IBGP Peering VLANs per VRF + # Will only be configured for VRF default if underlay_routing_protocol == "none". + if (vlan_id := self._mlag_ibgp_peering_vlan_vrf(vrf, tenant)) is not None: + self._update_router_bgp_vrf_mlag_neighbor_cfg(bgp_vrf, vrf, tenant, vlan_id) + + for bgp_peer in vrf.bgp_peers: + # Below we pop various keys that are not supported by the eos_cli_config_gen schema. + # The rest of the keys are relayed directly to eos_cli_config_gen. + # 'ip_address' is popped even though it is supported. It will be added again later + # to ensure it comes first in the generated dict. + bgp_peer_dict = bgp_peer._as_dict() + peer_ip = bgp_peer_dict.pop("ip_address") + address_family = f"address_family_ipv{ipaddress.ip_address(peer_ip).version}" + neighbor = strip_empties_from_dict( + { + "ip_address": peer_ip, + "activate": True, + "prefix_list_in": bgp_peer_dict.pop("prefix_list_in", None), + "prefix_list_out": bgp_peer_dict.pop("prefix_list_out", None), + }, + ) + + append_if_not_duplicate( + list_of_dicts=bgp_vrf.setdefault(address_family, {}).setdefault("neighbors", []), + primary_key="ip_address", + new_dict=neighbor, + context="BGP peer defined under VRFs", + context_keys=["ip_address"], + ) + + if bgp_peer.set_ipv4_next_hop or bgp_peer.set_ipv6_next_hop: + route_map = f"RM-{vrf_name}-{peer_ip}-SET-NEXT-HOP-OUT" + bgp_peer_dict["route_map_out"] = route_map + if bgp_peer_dict.get("default_originate") is not None: + bgp_peer_dict["default_originate"].setdefault("route_map", route_map) + + bgp_peer_dict.pop("set_ipv4_next_hop", None) + bgp_peer_dict.pop("set_ipv6_next_hop", None) + + bgp_peer_dict.pop("nodes", None) + + append_if_not_duplicate( + list_of_dicts=bgp_vrf.setdefault("neighbors", []), + primary_key="ip_address", + new_dict={"ip_address": peer_ip, **bgp_peer_dict}, + context="BGP peer defined under VRFs", + context_keys=["ip_address"], + ) + + if vrf.ospf.enabled and vrf.redistribute_ospf and (not vrf.ospf.nodes or self.shared_utils.hostname in vrf.ospf.nodes): + bgp_vrf.setdefault("redistribute", {}).update({"ospf": {"enabled": True}}) + + if ( + bgp_vrf.get("neighbors") + and self.inputs.bgp_update_wait_install + and self.shared_utils.platform_settings.feature_support.bgp_update_wait_install + ): + bgp_vrf.setdefault("updates", {})["wait_install"] = True + + bgp_vrf = strip_empties_from_dict(bgp_vrf) + + # Skip adding the VRF if we have no config. + if not bgp_vrf: + continue + + if vrf_name == "default": + # VRF default is added directly under router_bgp + router_bgp.update(bgp_vrf) + else: + append_if_not_duplicate( + list_of_dicts=router_bgp["vrfs"], + primary_key="name", + new_dict={"name": vrf_name, **bgp_vrf}, + context="BGP VRFs defined under network services", + context_keys=["name"], + ) + return strip_empties_from_dict(router_bgp) + + def _update_router_bgp_vrf_evpn_or_mpls_cfg( + self, + bgp_vrf: dict, + vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, + vrf_address_families: list[str], + ) -> None: + """In-place update EVPN/MPLS part of structured config for *one* VRF under router_bgp.vrfs.""" + vrf_name = vrf.name + bgp_vrf["rd"] = self.get_vrf_rd(vrf) + vrf_rt = self.get_vrf_rt(vrf) + route_targets = {"import": [], "export": []} + + for af in vrf_address_families: + if (target := get_item(route_targets["import"], "address_family", af)) is None: + route_targets["import"].append({"address_family": af, "route_targets": [vrf_rt]}) + else: + target["route_targets"].append(vrf_rt) + + if (target := get_item(route_targets["export"], "address_family", af)) is None: + route_targets["export"].append({"address_family": af, "route_targets": [vrf_rt]}) + else: + target["route_targets"].append(vrf_rt) + + for rt in vrf.additional_route_targets: + if rt.type is None: + continue + if (target := get_item(route_targets[rt.type], "address_family", rt.address_family)) is None: + route_targets[rt.type].append({"address_family": rt.address_family, "route_targets": [rt.route_target]}) + else: + target["route_targets"].append(rt.route_target) + + if vrf_name == "default" and self._vrf_default_evpn and self._route_maps_vrf_default: + # Special handling of vrf default with evpn. + + if (target := get_item(route_targets["export"], "address_family", "evpn")) is None: + route_targets["export"].append({"address_family": "evpn", "route_targets": ["route-map RM-EVPN-EXPORT-VRF-DEFAULT"]}) + else: + target.setdefault("route_targets", []).append("route-map RM-EVPN-EXPORT-VRF-DEFAULT") + + bgp_vrf["route_targets"] = route_targets + + # VRF default + if vrf_name == "default": + return + + # Not VRF default + bgp_vrf["evpn_multicast"] = getattr(vrf, "_evpn_l3_multicast_enabled", None) + if evpn_multicast_transit_mode := getattr(vrf, "_evpn_l3_multicast_evpn_peg_transit", False): + bgp_vrf["evpn_multicast_address_family"] = {"ipv4": {"transit": evpn_multicast_transit_mode}} + + def _update_router_bgp_vrf_mlag_neighbor_cfg( + self, + bgp_vrf: dict, + vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, + tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, + vlan_id: int, + ) -> None: + """In-place update MLAG neighbor part of structured config for *one* VRF under router_bgp.vrfs.""" + if self._exclude_mlag_ibgp_peering_from_redistribute(vrf, tenant): + bgp_vrf["redistribute"]["connected"] = {"enabled": True, "route_map": "RM-CONN-2-BGP-VRFS"} + + interface_name = f"Vlan{vlan_id}" + if self.inputs.underlay_rfc5549 and self.inputs.overlay_mlag_rfc5549: + bgp_vrf.setdefault("neighbor_interfaces", []).append( + { + "name": interface_name, + "peer_group": self.inputs.bgp_peer_groups.mlag_ipv4_underlay_peer.name, + "remote_as": self.shared_utils.bgp_as, + "description": AvdStringFormatter().format( + self.inputs.mlag_bgp_peer_description, + mlag_peer=self.shared_utils.mlag_peer, + interface=interface_name, + peer_interface=interface_name, + ), + }, + ) + else: + if not vrf.mlag_ibgp_peering_ipv4_pool: + ip_address = self.shared_utils.mlag_peer_ibgp_ip + elif self.shared_utils.mlag_role == "primary": + ip_address = self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_secondary(vrf.mlag_ibgp_peering_ipv4_pool) + else: + ip_address = self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_primary(vrf.mlag_ibgp_peering_ipv4_pool) + + bgp_vrf.setdefault("neighbors", []).append( + { + "ip_address": ip_address, + "peer_group": self.inputs.bgp_peer_groups.mlag_ipv4_underlay_peer.name, + "description": AvdStringFormatter().format( + self.inputs.mlag_bgp_peer_description, + **strip_empties_from_dict( + {"mlag_peer": self.shared_utils.mlag_peer, "interface": interface_name, "peer_interface": interface_name, "vrf": vrf.name} + ), + ), + }, + ) + if self.inputs.underlay_rfc5549: + bgp_vrf.setdefault("address_family_ipv4", {}).setdefault("neighbors", []).append( + { + "ip_address": ip_address, + "next_hop": { + "address_family_ipv6": {"enabled": False}, + }, + }, + ) + + def _get_vlan_ip_config_for_mlag_peering(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> dict: + """ + Build IP config for MLAG peering SVI for the given VRF. + + Called from _get_vlan_interface_config_for_mlag_peering and prefix_lists. + """ + if self.inputs.underlay_rfc5549 and self.inputs.overlay_mlag_rfc5549: + return {"ipv6_enable": True} + + if vrf.mlag_ibgp_peering_ipv4_pool: + if self.shared_utils.mlag_role == "primary": + return { + "ip_address": ( + f"{self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_primary(vrf.mlag_ibgp_peering_ipv4_pool)}/" + f"{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}" + ) + } + + return { + "ip_address": ( + f"{self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_secondary(vrf.mlag_ibgp_peering_ipv4_pool)}/" + f"{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}" + ) + } + + return {"ip_address": f"{self.shared_utils.mlag_ibgp_ip}/{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}"} + + @cached_property + def _mlag_ibgp_peering_subnets_without_redistribution(self) -> list: + """Return sorted list of MLAG peerings for VRFs where MLAG iBGP peering should not be redistributed.""" + mlag_prefixes = set() + for tenant in self.shared_utils.filtered_tenants: + for vrf in tenant.vrfs: + if self._mlag_ibgp_peering_vlan_vrf(vrf, tenant) is None: + continue + + if not self._exclude_mlag_ibgp_peering_from_redistribute(vrf, tenant): + # By default the BGP peering is redistributed, so we only need the prefix-list for the false case. + continue + + if (mlag_ip_address := self._get_vlan_ip_config_for_mlag_peering(vrf).get("ip_address")) is None: + # No MLAG prefix for this VRF (could be RFC5549) + continue + + # Convert mlag_ip_address to network prefix string and add to set. + mlag_prefixes.add(str(ipaddress.IPv4Network(mlag_ip_address, strict=False))) + + return natural_sort(mlag_prefixes) diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_wan.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_wan.py index 5a5d359b0a6..edb432c8091 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_wan.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_wan.py @@ -4,19 +4,19 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Literal +from typing import Literal from pyavd._eos_designs.schema import EosDesigns +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError, AristaAvdMissingVariableError from pyavd._utils import get, get_ip_from_ip_prefix from pyavd._utils.password_utils.password import simple_7_encrypt from pyavd.j2filters import natural_sort, range_expand -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices +from .utils_zscaler import UtilsZscalerMixin -class UtilsWanMixin: +class UtilsWanMixin(UtilsZscalerMixin, StructuredConfigGenerator): """ Mixin Class with internal functions for WAN. @@ -24,7 +24,7 @@ class UtilsWanMixin: """ @cached_property - def _filtered_wan_vrfs(self: AvdStructuredConfigNetworkServices) -> EosDesigns.WanVirtualTopologies.Vrfs: + def _filtered_wan_vrfs(self) -> EosDesigns.WanVirtualTopologies.Vrfs: """Loop through all the VRFs defined under `wan_virtual_topologies.vrfs` and returns a list of mode.""" wan_vrfs = EosDesigns.WanVirtualTopologies.Vrfs( vrf for vrf in self.inputs.wan_virtual_topologies.vrfs if vrf.name in self.shared_utils.vrfs or self.shared_utils.is_wan_server @@ -37,7 +37,7 @@ def _filtered_wan_vrfs(self: AvdStructuredConfigNetworkServices) -> EosDesigns.W return wan_vrfs @cached_property - def _wan_virtual_topologies_policies(self: AvdStructuredConfigNetworkServices) -> EosDesigns.WanVirtualTopologies.Policies: + def _wan_virtual_topologies_policies(self) -> EosDesigns.WanVirtualTopologies.Policies: """This function parses the input data and append the default-policy if not already present.""" # If not overwritten, inject the default policy in case it is required for one of the VRFs if self._default_wan_policy_name in self.inputs.wan_virtual_topologies.policies: @@ -48,7 +48,7 @@ def _wan_virtual_topologies_policies(self: AvdStructuredConfigNetworkServices) - return policies @cached_property - def _filtered_wan_policies(self: AvdStructuredConfigNetworkServices) -> list: + def _filtered_wan_policies(self) -> list: """ Loop through all the VRFs defined under `wan_virtual_topologies.vrfs` and returns a list of policies to configure on this device. @@ -88,7 +88,7 @@ def _filtered_wan_policies(self: AvdStructuredConfigNetworkServices) -> list: return filtered_policies - def _update_policy_match_statements(self: AvdStructuredConfigNetworkServices, policy: dict) -> None: + def _update_policy_match_statements(self, policy: dict) -> None: """ Update the policy dict with two keys: `matches` and `default_match`. @@ -219,7 +219,7 @@ def _update_policy_match_statements(self: AvdStructuredConfigNetworkServices, po policy["matches"] = matches policy["default_match"] = default_match - def _generate_wan_load_balance_policy(self: AvdStructuredConfigNetworkServices, name: str, input_dict: dict, context_path: str) -> dict | None: + def _generate_wan_load_balance_policy(self, name: str, input_dict: dict, context_path: str) -> dict | None: """ Generate and return a router path-selection load-balance policy. @@ -290,7 +290,7 @@ def _generate_wan_load_balance_policy(self: AvdStructuredConfigNetworkServices, return wan_load_balance_policy - def _path_group_preference_to_eos_priority(self: AvdStructuredConfigNetworkServices, path_group_preference: int | str, context_path: str) -> int: + def _path_group_preference_to_eos_priority(self, path_group_preference: int | str, context_path: str) -> int: """ Convert "preferred" to 1 and "alternate" to 2. Everything else is returned as is. @@ -304,13 +304,12 @@ def _path_group_preference_to_eos_priority(self: AvdStructuredConfigNetworkServi if path_group_preference == "alternate": return 2 - failed_conversion = False try: priority = int(path_group_preference) except ValueError: - failed_conversion = True + priority = 0 - if failed_conversion or not 1 <= priority <= 65535: + if not 1 <= priority <= 65535: msg = ( f"Invalid value '{path_group_preference}' for Path-Group preference - should be either 'preferred', " f"'alternate' or an integer[1-65535] for {context_path}." @@ -320,12 +319,12 @@ def _path_group_preference_to_eos_priority(self: AvdStructuredConfigNetworkServi return priority @cached_property - def _default_wan_policy_name(self: AvdStructuredConfigNetworkServices) -> str: + def _default_wan_policy_name(self) -> str: """TODO: make this configurable.""" return "DEFAULT-POLICY" @cached_property - def _default_policy_path_group_names(self: AvdStructuredConfigNetworkServices) -> list[str]: + def _default_policy_path_group_names(self) -> list[str]: """ Return a list of path group names for the default policy. @@ -344,7 +343,7 @@ def _default_policy_path_group_names(self: AvdStructuredConfigNetworkServices) - return natural_sort(path_group_names) @cached_property - def _default_wan_policy(self: AvdStructuredConfigNetworkServices) -> EosDesigns.WanVirtualTopologies.PoliciesItem: + def _default_wan_policy(self) -> EosDesigns.WanVirtualTopologies.PoliciesItem: """ Returning policy containing all path groups not excluded from default policy. @@ -366,7 +365,7 @@ def _default_wan_policy(self: AvdStructuredConfigNetworkServices) -> EosDesigns. ), ) - def _default_profile_name(self: AvdStructuredConfigNetworkServices, profile_name: str, application_profile: str) -> str: + def _default_profile_name(self, profile_name: str, application_profile: str) -> str: """ Helper function to consistently return the default name of a profile. @@ -375,7 +374,7 @@ def _default_profile_name(self: AvdStructuredConfigNetworkServices, profile_name return f"{profile_name}-{application_profile}" @cached_property - def _wan_control_plane_virtual_topology(self: AvdStructuredConfigNetworkServices) -> EosDesigns.WanVirtualTopologies.ControlPlaneVirtualTopology: + def _wan_control_plane_virtual_topology(self) -> EosDesigns.WanVirtualTopologies.ControlPlaneVirtualTopology: """ Return the Control plane virtual topology or the default one. @@ -399,18 +398,18 @@ def _wan_control_plane_virtual_topology(self: AvdStructuredConfigNetworkServices ) @cached_property - def _wan_control_plane_profile_name(self: AvdStructuredConfigNetworkServices) -> str: + def _wan_control_plane_profile_name(self) -> str: """Control plane profile name.""" vrf_default_policy_name = self._filtered_wan_vrfs["default"].policy return self._wan_control_plane_virtual_topology.name or f"{vrf_default_policy_name}-CONTROL-PLANE" @cached_property - def _wan_control_plane_application_profile_name(self: AvdStructuredConfigNetworkServices) -> str: + def _wan_control_plane_application_profile_name(self) -> str: """Control plane application profile name.""" return self.inputs.wan_virtual_topologies.control_plane_virtual_topology.application_profile @cached_property - def _local_path_groups_connected_to_pathfinder(self: AvdStructuredConfigNetworkServices) -> list: + def _local_path_groups_connected_to_pathfinder(self) -> list: """Return list of names of local path_groups connected to pathfinder.""" return [ path_group.name @@ -419,7 +418,7 @@ def _local_path_groups_connected_to_pathfinder(self: AvdStructuredConfigNetworkS ] @cached_property - def _svi_acls(self: AvdStructuredConfigNetworkServices) -> dict[str, dict[str, dict]] | None: + def _svi_acls(self) -> dict[str, dict[str, dict]]: """ Returns a dict of SVI ACLs. @@ -432,7 +431,7 @@ def _svi_acls(self: AvdStructuredConfigNetworkServices) -> dict[str, dict[str, d so use `get(self._svi_acls, f"{interface_name}.ipv4_acl_in")` to get the value. """ if not self.shared_utils.network_services_l3: - return None + return {} svi_acls = {} for tenant in self.shared_utils.filtered_tenants: @@ -463,16 +462,16 @@ def _svi_acls(self: AvdStructuredConfigNetworkServices) -> dict[str, dict[str, d return svi_acls - def get_internet_exit_nat_profile_name(self: AvdStructuredConfigNetworkServices, internet_exit_policy_type: Literal["zscaler", "direct"]) -> str: + def get_internet_exit_nat_profile_name(self, internet_exit_policy_type: Literal["zscaler", "direct"]) -> str: if internet_exit_policy_type == "zscaler": return "NAT-IE-ZSCALER" return "NAT-IE-DIRECT" - def get_internet_exit_nat_acl_name(self: AvdStructuredConfigNetworkServices, internet_exit_policy_type: Literal["zscaler", "direct"]) -> str: + def get_internet_exit_nat_acl_name(self, internet_exit_policy_type: Literal["zscaler", "direct"]) -> str: return f"ACL-{self.get_internet_exit_nat_profile_name(internet_exit_policy_type)}" def get_internet_exit_nat_pool_and_profile( - self: AvdStructuredConfigNetworkServices, + self, internet_exit_policy_type: Literal["zscaler", "direct"], ) -> tuple[dict | None, dict | None]: if internet_exit_policy_type == "zscaler": @@ -517,11 +516,11 @@ def get_internet_exit_nat_pool_and_profile( return None @cached_property - def _filtered_internet_exit_policy_types(self: AvdStructuredConfigNetworkServices) -> list: + def _filtered_internet_exit_policy_types(self) -> list: return sorted({internet_exit_policy.type for internet_exit_policy, _connections in self._filtered_internet_exit_policies_and_connections}) @cached_property - def _l3_interface_acls(self: AvdStructuredConfigNetworkServices) -> dict | None: + def _l3_interface_acls(self) -> dict | None: """ Returns a dict of interfaces and ACLs set on the interfaces. @@ -569,7 +568,7 @@ def _l3_interface_acls(self: AvdStructuredConfigNetworkServices) -> dict | None: @cached_property def _filtered_internet_exit_policies_and_connections( - self: AvdStructuredConfigNetworkServices, + self, ) -> list[tuple[EosDesigns.CvPathfinderInternetExitPoliciesItem, list[dict]]]: """ Only supported for CV Pathfinder Edge routers. Returns an empty list for pathfinders. @@ -642,7 +641,7 @@ def _filtered_internet_exit_policies_and_connections( return internet_exit_policies def get_internet_exit_connections( - self: AvdStructuredConfigNetworkServices, + self, internet_exit_policy: EosDesigns.CvPathfinderInternetExitPoliciesItem, local_interfaces: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces, ) -> list: @@ -661,7 +660,7 @@ def get_internet_exit_connections( raise AristaAvdError(msg) def get_direct_internet_exit_connections( - self: AvdStructuredConfigNetworkServices, + self, internet_exit_policy: EosDesigns.CvPathfinderInternetExitPoliciesItem, local_interfaces: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces, ) -> list[dict]: @@ -708,7 +707,7 @@ def get_direct_internet_exit_connections( return connections def get_zscaler_internet_exit_connections( - self: AvdStructuredConfigNetworkServices, + self, internet_exit_policy: EosDesigns.CvPathfinderInternetExitPoliciesItem, local_interfaces: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3Interfaces, ) -> list: @@ -782,9 +781,7 @@ def get_zscaler_internet_exit_connections( ) return connections - def _get_ipsec_credentials( - self: AvdStructuredConfigNetworkServices, internet_exit_policy: EosDesigns.CvPathfinderInternetExitPoliciesItem - ) -> tuple[str, str]: + def _get_ipsec_credentials(self, internet_exit_policy: EosDesigns.CvPathfinderInternetExitPoliciesItem) -> tuple[str, str]: """Returns ufqdn, shared_key based on various details from the given internet_exit_policy.""" if not internet_exit_policy.zscaler.domain_name: msg = "zscaler.domain_name" @@ -798,7 +795,7 @@ def _get_ipsec_credentials( ufqdn = f"{self.shared_utils.hostname}_{internet_exit_policy.name}@{internet_exit_policy.zscaler.domain_name}" return ufqdn, ipsec_key - def _generate_ipsec_key(self: AvdStructuredConfigNetworkServices, name: str, salt: str) -> str: + def _generate_ipsec_key(self, name: str, salt: str) -> str: """ Build a secret containing various components for this policy and device. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py index cf598b061d6..193823413db 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py @@ -12,17 +12,16 @@ from pyavd._cv.workflows.models import CVDevice from pyavd._cv.workflows.verify_devices_on_cv import verify_devices_in_cloudvision_inventory from pyavd._eos_designs.schema import EosDesigns +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError if TYPE_CHECKING: from pyavd._cv.api.arista.swg.v1 import Location, VpnEndpoint - from . import AvdStructuredConfigNetworkServices - LOGGER = getLogger(__name__) -class UtilsZscalerMixin: +class UtilsZscalerMixin(StructuredConfigGenerator): """ Mixin Class with internal functions for Zscaler. @@ -30,7 +29,7 @@ class UtilsZscalerMixin: """ @cached_property - def _zscaler_endpoints(self: AvdStructuredConfigNetworkServices) -> EosDesigns.ZscalerEndpoints: + def _zscaler_endpoints(self) -> EosDesigns.ZscalerEndpoints: """ Returns zscaler_endpoints data model built via CloudVision API calls, unless they are provided in the input variables. @@ -38,7 +37,7 @@ def _zscaler_endpoints(self: AvdStructuredConfigNetworkServices) -> EosDesigns.Z """ return self.inputs.zscaler_endpoints or asyncio.run(self._generate_zscaler_endpoints()) - async def _generate_zscaler_endpoints(self: AvdStructuredConfigNetworkServices) -> EosDesigns.ZscalerEndpoints: + async def _generate_zscaler_endpoints(self) -> EosDesigns.ZscalerEndpoints: """ Call CloudVision SWG APIs to generate the zscaler_endpoints model. @@ -79,15 +78,15 @@ async def _generate_zscaler_endpoints(self: AvdStructuredConfigNetworkServices) "Set 'serial_number' for the device in AVD vars, to ensure a unique match." ) raise AristaAvdError(msg) - device_id: str = cv_inventory_devices[0].serial_number + device_id: str = cv_inventory_devices[0].serial_number or "" request_time, _ = await cv_client.set_swg_device(device_id=device_id, service="zscaler", location=wan_site_location) cv_endpoint_status = await cv_client.wait_for_swg_endpoint_status(device_id=device_id, service="zscaler", start_time=request_time) device_location: Location = cv_endpoint_status.device_location zscaler_endpoints = EosDesigns.ZscalerEndpoints( - cloud_name=cv_endpoint_status.cloud_name, - device_location=EosDesigns.ZscalerEndpoints.DeviceLocation(city=device_location.city, country=device_location.country), + cloud_name=cv_endpoint_status.cloud_name or "", + device_location=EosDesigns.ZscalerEndpoints.DeviceLocation(city=device_location.city or "", country=device_location.country or ""), ) if not getattr(cv_endpoint_status, "vpn_endpoints", None) or not getattr(cv_endpoint_status.vpn_endpoints, "values", None): msg = f"{context} but did not get any IPsec Tunnel endpoints back from the Zscaler API." @@ -106,12 +105,12 @@ async def _generate_zscaler_endpoints(self: AvdStructuredConfigNetworkServices) key, cls( ip_address=vpn_endpoint.ip_address.value, - datacenter=vpn_endpoint.datacenter, - city=location.city, - country=location.country, - region=location.region, - latitude=location.latitude, - longitude=location.longitude, + datacenter=vpn_endpoint.datacenter or "", + city=location.city or "", + country=location.country or "", + region=location.region or "", + latitude=str(location.latitude or ""), + longitude=str(location.longitude or ""), ), ) diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/virtual_source_nat_vrfs.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/virtual_source_nat_vrfs.py index 43ff7308a2d..5a41a1b86b6 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/virtual_source_nat_vrfs.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/virtual_source_nat_vrfs.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate, get_ip_from_ip_prefix, strip_null_from_data from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigNetworkServices - class VirtualSourceNatVrfsMixin(UtilsMixin): """ @@ -21,8 +17,10 @@ class VirtualSourceNatVrfsMixin(UtilsMixin): Class should only be used as Mixin to a AvdStructuredConfig class. """ + loopback_interfaces: list[dict] | None + @cached_property - def virtual_source_nat_vrfs(self: AvdStructuredConfigNetworkServices) -> list | None: + def virtual_source_nat_vrfs(self) -> list | None: """ Return structured config for virtual_source_nat_vrfs. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py index 98fb63b7745..ccc91eaf4f1 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py @@ -15,8 +15,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - class VlanInterfacesMixin(UtilsMixin): """ @@ -26,7 +24,7 @@ class VlanInterfacesMixin(UtilsMixin): """ @cached_property - def vlan_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: + def vlan_interfaces(self) -> list | None: """ Return structured config for vlan_interfaces. @@ -70,7 +68,7 @@ def vlan_interfaces(self: AvdStructuredConfigNetworkServices) -> list | None: return None def _get_vlan_interface_config_for_svi( - self: AvdStructuredConfigNetworkServices, + self, svi: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, ) -> dict: @@ -165,7 +163,7 @@ def _check_virtual_router_mac_address(vlan_interface_config: dict, variables: li return strip_empties_from_dict(vlan_interface_config) def _get_vlan_interface_config_for_mlag_peering( - self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, vlan_id: int + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, vlan_id: int ) -> dict: """Build full config for MLAG peering SVI for the given VRF.""" vlan_interface_config = { @@ -180,32 +178,3 @@ def _get_vlan_interface_config_for_mlag_peering( } vlan_interface_config.update(self._get_vlan_ip_config_for_mlag_peering(vrf)) return vlan_interface_config - - def _get_vlan_ip_config_for_mlag_peering( - self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem - ) -> dict: - """ - Build IP config for MLAG peering SVI for the given VRF. - - Called from _get_vlan_interface_config_for_mlag_peering and prefix_lists. - """ - if self.inputs.underlay_rfc5549 and self.inputs.overlay_mlag_rfc5549: - return {"ipv6_enable": True} - - if vrf.mlag_ibgp_peering_ipv4_pool: - if self.shared_utils.mlag_role == "primary": - return { - "ip_address": ( - f"{self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_primary(vrf.mlag_ibgp_peering_ipv4_pool)}/" - f"{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}" - ) - } - - return { - "ip_address": ( - f"{self.shared_utils.ip_addressing.mlag_ibgp_peering_ip_secondary(vrf.mlag_ibgp_peering_ipv4_pool)}/" - f"{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}" - ) - } - - return {"ip_address": f"{self.shared_utils.mlag_ibgp_ip}/{self.inputs.fabric_ip_addressing.mlag.ipv4_prefix_length}"} diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/vlans.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/vlans.py index 4ef84b67763..03d8ea3f1df 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/vlans.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/vlans.py @@ -14,8 +14,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - class VlansMixin(UtilsMixin): """ @@ -25,7 +23,7 @@ class VlansMixin(UtilsMixin): """ @cached_property - def vlans(self: AvdStructuredConfigNetworkServices) -> list | None: + def vlans(self) -> list | None: """ Return structured config for vlans. @@ -91,7 +89,7 @@ def vlans(self: AvdStructuredConfigNetworkServices) -> list | None: return None def _get_vlan_config( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, ) -> dict: diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/vrfs.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/vrfs.py index 9e4fc952838..bf4d3bb9982 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/vrfs.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/vrfs.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pyavd._utils import append_if_not_duplicate @@ -13,8 +13,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - class VrfsMixin(UtilsMixin): """ @@ -24,7 +22,7 @@ class VrfsMixin(UtilsMixin): """ @cached_property - def vrfs(self: AvdStructuredConfigNetworkServices) -> list | None: + def vrfs(self) -> list | None: """ Return structured config for vrfs. @@ -43,7 +41,7 @@ def vrfs(self: AvdStructuredConfigNetworkServices) -> list | None: if vrf_name == "default": continue - new_vrf = { + new_vrf: dict[str, Any] = { "name": vrf_name, "tenant": tenant.name, } @@ -75,7 +73,7 @@ def vrfs(self: AvdStructuredConfigNetworkServices) -> list | None: return None - def _has_ipv6(self: AvdStructuredConfigNetworkServices, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: + def _has_ipv6(self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem) -> bool: """ Return bool if IPv6 is configured in the given VRF. diff --git a/python-avd/pyavd/_eos_designs/structured_config/network_services/vxlan_interface.py b/python-avd/pyavd/_eos_designs/structured_config/network_services/vxlan_interface.py index abab779eebd..a2f6802edde 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/network_services/vxlan_interface.py +++ b/python-avd/pyavd/_eos_designs/structured_config/network_services/vxlan_interface.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import append_if_not_duplicate, default, unique @@ -15,8 +15,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigNetworkServices - class VxlanInterfaceMixin(UtilsMixin): """ @@ -26,7 +24,7 @@ class VxlanInterfaceMixin(UtilsMixin): """ @cached_property - def vxlan_interface(self: AvdStructuredConfigNetworkServices) -> dict | None: + def vxlan_interface(self) -> dict | None: """ Returns structured config for vxlan_interface. @@ -38,7 +36,7 @@ def vxlan_interface(self: AvdStructuredConfigNetworkServices) -> dict | None: if not (self.shared_utils.overlay_vtep or self.shared_utils.is_wan_router): return None - vxlan = { + vxlan: dict[str, Any] = { "udp_port": 4789, } @@ -125,7 +123,7 @@ def vxlan_interface(self: AvdStructuredConfigNetworkServices) -> dict | None: } def _get_vxlan_interface_config_for_vrf( - self: AvdStructuredConfigNetworkServices, + self, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, vrfs: list[dict], @@ -181,7 +179,7 @@ def _get_vxlan_interface_config_for_vrf( return # NOTE: this can never be None here, it would be caught previously in the code - vrf_id: int = default(vrf.vrf_id, vrf.vrf_vni) + vrf_id = cast(int, default(vrf.vrf_id, vrf.vrf_vni)) vrf_data = {"name": vrf_name, "vni": vni} @@ -219,7 +217,7 @@ def _get_vxlan_interface_config_for_vrf( ) def _get_vxlan_interface_config_for_vlan( - self: AvdStructuredConfigNetworkServices, + self, vlan: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem | EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.L2vlansItem, tenant: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem, @@ -232,7 +230,7 @@ def _get_vxlan_interface_config_for_vlan( if not vlan.vxlan: return {} - vxlan_interface_vlan = {"id": vlan.id} + vxlan_interface_vlan: dict[str, Any] = {"id": vlan.id} if vlan.vni_override: vxlan_interface_vlan["vni"] = vlan.vni_override else: @@ -259,7 +257,7 @@ def _get_vxlan_interface_config_for_vlan( return vxlan_interface_vlan @cached_property - def _overlay_her_flood_lists(self: AvdStructuredConfigNetworkServices) -> dict[str | int, list]: + def _overlay_her_flood_lists(self) -> dict[str | int, list]: """ Returns a dict with HER Flood Lists. @@ -286,7 +284,7 @@ def _overlay_her_flood_lists(self: AvdStructuredConfigNetworkServices) -> dict[s if peer == self.shared_utils.hostname: continue - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) if overlay_her_flood_list_scope == "dc" and peer_facts.get("dc_name") != self.inputs.dc_name: continue @@ -308,5 +306,5 @@ def _overlay_her_flood_lists(self: AvdStructuredConfigNetworkServices) -> dict[s return overlay_her_flood_lists @cached_property - def _multi_vtep(self: AvdStructuredConfigNetworkServices) -> bool: + def _multi_vtep(self) -> bool: return self.shared_utils.mlag is True and self.shared_utils.evpn_multicast is True diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/__init__.py index 42d954c948f..c44334fc260 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/__init__.py @@ -18,7 +18,6 @@ class AvdStructuredConfigOverlay( - StructuredConfigGenerator, CvxMixin, IpExtCommunityListsMixin, IpSecurityMixin, @@ -31,6 +30,7 @@ class AvdStructuredConfigOverlay( RouterPathSelectionMixin, RouterTrafficEngineering, StunMixin, + StructuredConfigGenerator, ): """ The AvdStructuredConfig Class is imported used "get_structured_config" to render parts of the structured config. diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/cvx.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/cvx.py index 9f26745c5f1..1ec90597e8c 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/cvx.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/cvx.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import get, get_ip_from_ip_prefix from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class CvxMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class CvxMixin(UtilsMixin): """ @cached_property - def cvx(self: AvdStructuredConfigOverlay) -> dict | None: + def cvx(self) -> dict | None: """Detect if this is a CVX server for overlay and configure service & peer hosts accordingly.""" if not self.shared_utils.overlay_cvx: return None @@ -35,7 +31,7 @@ def cvx(self: AvdStructuredConfigOverlay) -> dict | None: if overlay_cvx_server == self.shared_utils.hostname: continue - peer_switch_facts = self.shared_utils.get_peer_facts(overlay_cvx_server, required=True) + peer_switch_facts = self.shared_utils.get_peer_facts_dict(overlay_cvx_server) cvx_server_ip = get(peer_switch_facts, "mgmt_ip", required=True, custom_error_msg=f"'mgmt_ip' for CVX Server {overlay_cvx_server} is required.") peer_hosts.append(get_ip_from_ip_prefix(cvx_server_ip)) diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_extcommunity_lists.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_extcommunity_lists.py index 76847d0499a..c518be08017 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_extcommunity_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_extcommunity_lists.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class IpExtCommunityListsMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class IpExtCommunityListsMixin(UtilsMixin): """ @cached_property - def ip_extcommunity_lists(self: AvdStructuredConfigOverlay) -> list | None: + def ip_extcommunity_lists(self) -> list | None: """Return structured config for ip_extcommunity_lists.""" if self.shared_utils.overlay_routing_protocol != "ibgp": return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_security.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_security.py index 5fc4a501c2c..7eefa0f35ca 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_security.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/ip_security.py @@ -4,16 +4,13 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import Any from pyavd._errors import AristaAvdMissingVariableError from pyavd._utils import get, strip_null_from_data from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class IpSecurityMixin(UtilsMixin): """ @@ -23,7 +20,7 @@ class IpSecurityMixin(UtilsMixin): """ @cached_property - def ip_security(self: AvdStructuredConfigOverlay) -> dict | None: + def ip_security(self) -> dict | None: """ ip_security set based on wan_ipsec_profiles data_model. @@ -51,7 +48,7 @@ def ip_security(self: AvdStructuredConfigOverlay) -> dict | None: return strip_null_from_data(ip_security) - def _append_data_plane(self: AvdStructuredConfigOverlay, ip_security: dict, data_plane_config: dict) -> None: + def _append_data_plane(self, ip_security: dict, data_plane_config: dict) -> None: """In place update of ip_security for DataPlane.""" ike_policy_name = get(data_plane_config, "ike_policy_name", default="DP-IKE-POLICY") if self.shared_utils.wan_ha_ipsec else None sa_policy_name = get(data_plane_config, "sa_policy_name", default="DP-SA-POLICY") @@ -59,7 +56,7 @@ def _append_data_plane(self: AvdStructuredConfigOverlay, ip_security: dict, data key = get(data_plane_config, "shared_key", required=True) # IKE policy for data-plane is not required for dynamic tunnels except for HA cases - if self.shared_utils.wan_ha_ipsec: + if ike_policy_name: ip_security["ike_policies"].append(self._ike_policy(ike_policy_name)) ip_security["sa_policies"].append(self._sa_policy(sa_policy_name)) ip_security["profiles"].append(self._profile(profile_name, ike_policy_name, sa_policy_name, key)) @@ -67,7 +64,7 @@ def _append_data_plane(self: AvdStructuredConfigOverlay, ip_security: dict, data # For data plane, adding key_controller by default ip_security["key_controller"] = self._key_controller(profile_name) - def _append_control_plane(self: AvdStructuredConfigOverlay, ip_security: dict, control_plane_config: dict) -> None: + def _append_control_plane(self, ip_security: dict, control_plane_config: dict) -> None: """ In place update of ip_security for control plane data. @@ -86,27 +83,27 @@ def _append_control_plane(self: AvdStructuredConfigOverlay, ip_security: dict, c # If there is no data plane IPSec profile, use the control plane one for key controller ip_security["key_controller"] = self._key_controller(profile_name) - def _ike_policy(self: AvdStructuredConfigOverlay, name: str) -> dict | None: + def _ike_policy(self, name: str) -> dict | None: """Return an IKE policy.""" return { "name": name, "local_id": self.shared_utils.vtep_ip, } - def _sa_policy(self: AvdStructuredConfigOverlay, name: str) -> dict | None: + def _sa_policy(self, name: str) -> dict | None: """ Return an SA policy. By default using aes256gcm128 as GCM variants give higher performance. """ - sa_policy = {"name": name} + sa_policy: dict[str, Any] = {"name": name} if self.shared_utils.is_cv_pathfinder_router: # TODO: provide options to change this cv_pathfinder_wide sa_policy["esp"] = {"encryption": "aes256gcm128"} sa_policy["pfs_dh_group"] = 14 return sa_policy - def _profile(self: AvdStructuredConfigOverlay, profile_name: str, ike_policy_name: str | None, sa_policy_name: str, key: str) -> dict | None: + def _profile(self, profile_name: str, ike_policy_name: str | None, sa_policy_name: str, key: str) -> dict | None: """ Return one IPsec Profile. @@ -129,6 +126,6 @@ def _profile(self: AvdStructuredConfigOverlay, profile_name: str, ike_policy_nam "mode": "transport", } - def _key_controller(self: AvdStructuredConfigOverlay, profile_name: str) -> dict | None: + def _key_controller(self, profile_name: str) -> dict | None: """Return a key_controller structure if the device is not a RR or pathfinder.""" return None if self.shared_utils.is_wan_server else {"profile": profile_name} diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/management_cvx.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/management_cvx.py index e977aa0eef5..ff33f3d5084 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/management_cvx.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/management_cvx.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdMissingVariableError from pyavd._utils import get, get_ip_from_ip_prefix from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class ManagementCvxMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class ManagementCvxMixin(UtilsMixin): """ @cached_property - def management_cvx(self: AvdStructuredConfigOverlay) -> dict | None: + def management_cvx(self) -> dict | None: if not (self.shared_utils.overlay_cvx and self.shared_utils.overlay_vtep): return None @@ -33,7 +29,7 @@ def management_cvx(self: AvdStructuredConfigOverlay) -> dict | None: server_hosts = [] for overlay_cvx_server in self.inputs.overlay_cvx_servers: - peer_switch_facts = self.shared_utils.get_peer_facts(overlay_cvx_server, required=True) + peer_switch_facts = self.shared_utils.get_peer_facts_dict(overlay_cvx_server) cvx_server_ip = get(peer_switch_facts, "mgmt_ip", required=True, custom_error_msg=f"'mgmt_ip' for CVX Server {overlay_cvx_server} is required.") server_hosts.append(get_ip_from_ip_prefix(cvx_server_ip)) diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/management_security.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/management_security.py index 77521d9dd6e..388cedf1f8f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/management_security.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/management_security.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class ManagementSecurityMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class ManagementSecurityMixin(UtilsMixin): """ @cached_property - def management_security(self: AvdStructuredConfigOverlay) -> dict | None: + def management_security(self) -> dict | None: """ Return structured config for management_security. diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/route_maps.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/route_maps.py index df7e11c796c..b3be59db51f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/route_maps.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/route_maps.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd.j2filters import natural_sort from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class RouteMapsMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouteMapsMixin(UtilsMixin): """ @cached_property - def route_maps(self: AvdStructuredConfigOverlay) -> list | None: + def route_maps(self) -> list | None: """Return structured config for route_maps.""" if self.shared_utils.overlay_cvx: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_adaptive_virtual_topology.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_adaptive_virtual_topology.py index b5327dc49ed..746efc65b81 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_adaptive_virtual_topology.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_adaptive_virtual_topology.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class RouterAdaptiveVirtualTopologyMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterAdaptiveVirtualTopologyMixin(UtilsMixin): """ @cached_property - def router_adaptive_virtual_topology(self: AvdStructuredConfigOverlay) -> dict | None: + def router_adaptive_virtual_topology(self) -> dict | None: """Return structured config for router adaptive-virtual-topology (AVT).""" if not self.shared_utils.is_cv_pathfinder_router: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bfd.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bfd.py index 25252753467..43199da7603 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bfd.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bfd.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class RouterBfdMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class RouterBfdMixin(UtilsMixin): """ @cached_property - def router_bfd(self: AvdStructuredConfigOverlay) -> dict | None: + def router_bfd(self) -> dict | None: """Return structured config for router_bfd.""" if self.shared_utils.overlay_cvx: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bgp.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bgp.py index c34abe424ce..d30a45b18dc 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bgp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_bgp.py @@ -5,7 +5,7 @@ import ipaddress from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pyavd._errors import AristaAvdError from pyavd._utils import AvdStringFormatter, default, strip_empties_from_dict @@ -16,8 +16,6 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigOverlay - class RouterBgpMixin(UtilsMixin): """ @@ -27,7 +25,7 @@ class RouterBgpMixin(UtilsMixin): """ @cached_property - def router_bgp(self: AvdStructuredConfigOverlay) -> dict | None: + def router_bgp(self) -> dict | None: """Return the structured config for router_bgp.""" if self.shared_utils.overlay_cvx: return None @@ -51,14 +49,14 @@ def router_bgp(self: AvdStructuredConfigOverlay) -> dict | None: # Need to keep potentially empty dict for redistribute_routes return strip_empties_from_dict(router_bgp, strip_values_tuple=(None, "")) - def _bgp_cluster_id(self: AvdStructuredConfigOverlay) -> str | None: + def _bgp_cluster_id(self) -> str | None: if self.shared_utils.overlay_routing_protocol == "ibgp" and ( self.shared_utils.evpn_role == "server" or self.shared_utils.mpls_overlay_role == "server" ): return default(self.shared_utils.node_config.bgp_cluster_id, self.shared_utils.router_id) return None - def _bgp_listen_ranges(self: AvdStructuredConfigOverlay) -> list | None: + def _bgp_listen_ranges(self) -> list | None: """Generate listen-ranges. Currently only supported for WAN RR.""" if not self.shared_utils.is_wan_server: return None @@ -73,7 +71,7 @@ def _bgp_listen_ranges(self: AvdStructuredConfigOverlay) -> list | None: ] or None def _generate_base_peer_group( - self: AvdStructuredConfigOverlay, + self, pg_type: str, pg_name: str, maximum_routes: int = 0, @@ -96,7 +94,7 @@ def _generate_base_peer_group( "maximum_routes": maximum_routes, } - def _peer_groups(self: AvdStructuredConfigOverlay) -> list | None: + def _peer_groups(self) -> list | None: peer_groups = [] if self.shared_utils.overlay_routing_protocol == "ebgp": @@ -133,7 +131,7 @@ def _peer_groups(self: AvdStructuredConfigOverlay) -> list | None: peer_groups.append(mpls_peer_group) if self.shared_utils.overlay_evpn_vxlan is True: - peer_group_config = {"remote_as": self.shared_utils.bgp_as} + peer_group_config: dict[str, Any] = {"remote_as": self.shared_utils.bgp_as} if self.shared_utils.is_wan_router: # WAN OVERLAY peer group peer_group_config["ttl_maximum_hops"] = self.inputs.bgp_peer_groups.wan_overlay_peers.ttl_maximum_hops @@ -185,7 +183,7 @@ def _peer_groups(self: AvdStructuredConfigOverlay) -> list | None: return peer_groups - def _address_family_ipv4(self: AvdStructuredConfigOverlay) -> dict: + def _address_family_ipv4(self) -> dict: """Deactivate the relevant peer_groups in address_family_ipv4.""" peer_groups = [] @@ -216,7 +214,7 @@ def _address_family_ipv4(self: AvdStructuredConfigOverlay) -> dict: return {"peer_groups": peer_groups} - def _address_family_evpn(self: AvdStructuredConfigOverlay) -> dict | None: + def _address_family_evpn(self) -> dict | None: address_family_evpn = {} peer_groups = [] @@ -321,7 +319,7 @@ def _address_family_evpn(self: AvdStructuredConfigOverlay) -> dict | None: return address_family_evpn or None - def _address_family_ipv4_sr_te(self: AvdStructuredConfigOverlay) -> dict | None: + def _address_family_ipv4_sr_te(self) -> dict | None: """Generate structured config for IPv4 SR-TE address family.""" if not self.shared_utils.is_cv_pathfinder_router: return None @@ -340,12 +338,12 @@ def _address_family_ipv4_sr_te(self: AvdStructuredConfigOverlay) -> dict | None: return address_family_ipv4_sr_te - def _address_family_link_state(self: AvdStructuredConfigOverlay) -> dict | None: + def _address_family_link_state(self) -> dict | None: """Generate structured config for link-state address family.""" if not self.shared_utils.is_cv_pathfinder_router: return None - address_family_link_state = { + address_family_link_state: dict[str, Any] = { "peer_groups": [ { "name": self.inputs.bgp_peer_groups.wan_overlay_peers.name, @@ -371,7 +369,7 @@ def _address_family_link_state(self: AvdStructuredConfigOverlay) -> dict | None: return address_family_link_state - def _address_family_path_selection(self: AvdStructuredConfigOverlay) -> dict | None: + def _address_family_path_selection(self) -> dict | None: if not self.shared_utils.is_wan_router: return None @@ -390,7 +388,7 @@ def _address_family_path_selection(self: AvdStructuredConfigOverlay) -> dict | N return address_family_path_selection - def _address_family_rtc(self: AvdStructuredConfigOverlay) -> dict | None: + def _address_family_rtc(self) -> dict | None: """ Activate EVPN OVERLAY peer group and EVPN OVERLAY CORE peer group (if present) in address_family_rtc. @@ -402,7 +400,7 @@ def _address_family_rtc(self: AvdStructuredConfigOverlay) -> dict | None: address_family_rtc = {} peer_groups = [] - evpn_overlay_peers = {"name": self.inputs.bgp_peer_groups.evpn_overlay_peers.name} + evpn_overlay_peers: dict[str, Any] = {"name": self.inputs.bgp_peer_groups.evpn_overlay_peers.name} if self.shared_utils.overlay_evpn_vxlan is True: evpn_overlay_peers["activate"] = True @@ -434,7 +432,7 @@ def _address_family_rtc(self: AvdStructuredConfigOverlay) -> dict | None: return address_family_rtc - def _address_family_vpn_ipvx(self: AvdStructuredConfigOverlay, version: int) -> dict | None: + def _address_family_vpn_ipvx(self, version: int) -> dict | None: if version not in [4, 6]: msg = "_address_family_vpn_ipvx should be called with version 4 or 6 only" raise AristaAvdError(msg) @@ -468,14 +466,14 @@ def _address_family_vpn_ipvx(self: AvdStructuredConfigOverlay, version: int) -> return address_family_vpn_ipvx def _create_neighbor( - self: AvdStructuredConfigOverlay, + self, ip_address: str, name: str, peer_group: str, remote_as: str | None = None, overlay_peering_interface: str | None = None, ) -> dict: - neighbor = { + neighbor: dict[str, Any] = { "ip_address": ip_address, "peer_group": peer_group, "peer": name, @@ -491,13 +489,13 @@ def _create_neighbor( neighbor["remote_as"] = remote_as if self.inputs.shutdown_bgp_towards_undeployed_peers and name in self._avd_overlay_peers: - peer_facts = self.shared_utils.get_peer_facts(name) + peer_facts = self.shared_utils.get_peer_facts_dict(name) if peer_facts["is_deployed"] is False: neighbor["shutdown"] = True return neighbor - def _neighbors(self: AvdStructuredConfigOverlay) -> list | None: + def _neighbors(self) -> list | None: neighbors = [] if self.shared_utils.overlay_routing_protocol == "ebgp": @@ -649,14 +647,12 @@ def _neighbors(self: AvdStructuredConfigOverlay) -> list | None: return None - def _ip_in_listen_ranges( - self: AvdStructuredConfigOverlay, source_ip: str, listen_range_prefixes: EosDesigns.BgpPeerGroups.WanOverlayPeers.ListenRangePrefixes - ) -> bool: + def _ip_in_listen_ranges(self, source_ip: str, listen_range_prefixes: EosDesigns.BgpPeerGroups.WanOverlayPeers.ListenRangePrefixes) -> bool: """Check if our source IP is in any of the listen range prefixes.""" ip = ipaddress.ip_address(source_ip) return any(ip in ipaddress.ip_network(prefix) for prefix in listen_range_prefixes) - def _bgp_overlay_dpath(self: AvdStructuredConfigOverlay) -> dict | None: + def _bgp_overlay_dpath(self) -> dict | None: if self.shared_utils.overlay_dpath is True: return { "bestpath": { diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_path_selection.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_path_selection.py index 589c780a626..716607fd524 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_path_selection.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_path_selection.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdError from pyavd._utils import get, get_ip_from_ip_prefix, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class RouterPathSelectionMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class RouterPathSelectionMixin(UtilsMixin): """ @cached_property - def router_path_selection(self: AvdStructuredConfigOverlay) -> dict | None: + def router_path_selection(self) -> dict | None: """Return structured config for router path-selection (DPS).""" if not self.shared_utils.is_wan_router: return None @@ -39,7 +35,7 @@ def router_path_selection(self: AvdStructuredConfigOverlay) -> dict | None: return strip_empties_from_dict(router_path_selection) @cached_property - def _dp_ipsec_profile_name(self: AvdStructuredConfigOverlay) -> str: + def _dp_ipsec_profile_name(self) -> str: """Returns the IPsec profile name to use for Data-Plane. If no data-plane config is present for IPsec, default to the control-plane profile-name. @@ -48,7 +44,7 @@ def _dp_ipsec_profile_name(self: AvdStructuredConfigOverlay) -> str: return self.inputs.wan_ipsec_profiles.data_plane.profile_name return self.inputs.wan_ipsec_profiles.control_plane.profile_name - def _get_path_groups(self: AvdStructuredConfigOverlay) -> list: + def _get_path_groups(self) -> list: """Generate the required path-groups locally.""" path_groups = [] @@ -96,7 +92,7 @@ def _get_path_groups(self: AvdStructuredConfigOverlay) -> list: return path_groups - def _generate_ha_path_group(self: AvdStructuredConfigOverlay) -> dict: + def _generate_ha_path_group(self) -> dict: """Called only when self.shared_utils.wan_ha is True or on Pathfinders.""" ha_path_group = { "name": self.inputs.wan_ha.lan_ha_path_group_name, @@ -130,15 +126,15 @@ def _generate_ha_path_group(self: AvdStructuredConfigOverlay) -> dict: return ha_path_group - def _wan_ha_interfaces(self: AvdStructuredConfigOverlay) -> list: + def _wan_ha_interfaces(self) -> list: """Return list of interfaces for HA.""" return [uplink for uplink in self.shared_utils.get_switch_fact("uplinks") if get(uplink, "vrf") is None] - def _wan_ha_peer_vtep_ip(self: AvdStructuredConfigOverlay) -> str: - peer_facts = self.shared_utils.get_peer_facts(self.shared_utils.wan_ha_peer, required=True) + def _wan_ha_peer_vtep_ip(self) -> str: + peer_facts = self.shared_utils.get_peer_facts_dict(self.shared_utils.wan_ha_peer) return get(peer_facts, "vtep_ip", required=True) - def _get_path_group_id(self: AvdStructuredConfigOverlay, path_group_name: str, config_id: int | None = None) -> int: + def _get_path_group_id(self, path_group_name: str, config_id: int | None = None) -> int: """ Get path group id. @@ -151,7 +147,7 @@ def _get_path_group_id(self: AvdStructuredConfigOverlay, path_group_name: str, c return config_id return 500 - def _get_local_interfaces_for_path_group(self: AvdStructuredConfigOverlay, path_group_name: str) -> list: + def _get_local_interfaces_for_path_group(self, path_group_name: str) -> list: """ Generate the router_path_selection.local_interfaces list. @@ -173,7 +169,7 @@ def _get_local_interfaces_for_path_group(self: AvdStructuredConfigOverlay, path_ return local_interfaces - def _get_dynamic_peers(self: AvdStructuredConfigOverlay, disable_ipsec: bool) -> dict | None: + def _get_dynamic_peers(self, disable_ipsec: bool) -> dict | None: """TODO: support ip_local ?""" if not self.shared_utils.is_wan_client: return None @@ -183,7 +179,7 @@ def _get_dynamic_peers(self: AvdStructuredConfigOverlay, disable_ipsec: bool) -> dynamic_peers["ipsec"] = False return dynamic_peers - def _get_static_peers_for_path_group(self: AvdStructuredConfigOverlay, path_group_name: str) -> list | None: + def _get_static_peers_for_path_group(self, path_group_name: str) -> list | None: """Retrieves the static peers to configure for a given path-group based on the connected nodes.""" if not self.shared_utils.is_wan_router: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_traffic_engineering.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_traffic_engineering.py index 93860e58ded..4e9314d20ea 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/router_traffic_engineering.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/router_traffic_engineering.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class RouterTrafficEngineering(UtilsMixin): """ @@ -20,7 +16,7 @@ class RouterTrafficEngineering(UtilsMixin): """ @cached_property - def router_traffic_engineering(self: AvdStructuredConfigOverlay) -> dict | None: + def router_traffic_engineering(self) -> dict | None: """Return structured config for router traffic-engineering.""" if not self.shared_utils.is_cv_pathfinder_router: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/stun.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/stun.py index ea1442a61f0..49d61f6ac83 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/stun.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/stun.py @@ -5,15 +5,11 @@ import itertools from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - class StunMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class StunMixin(UtilsMixin): """ @cached_property - def stun(self: AvdStructuredConfigOverlay) -> dict | None: + def stun(self) -> dict | None: """Return structured config for stun.""" if not self.shared_utils.is_wan_router: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/overlay/utils.py b/python-avd/pyavd/_eos_designs/structured_config/overlay/utils.py index 94573d76371..6d4959a5144 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/overlay/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/overlay/utils.py @@ -4,17 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError from pyavd._utils import get, strip_empties_from_dict from pyavd.j2filters import natural_sort -if TYPE_CHECKING: - from . import AvdStructuredConfigOverlay - -class UtilsMixin: +class UtilsMixin(StructuredConfigGenerator): """ Mixin Class with internal functions. @@ -22,7 +19,7 @@ class UtilsMixin: """ @cached_property - def _avd_overlay_peers(self: AvdStructuredConfigOverlay) -> list: + def _avd_overlay_peers(self) -> list: """ Returns a list of overlay peers for the device. @@ -32,7 +29,7 @@ def _avd_overlay_peers(self: AvdStructuredConfigOverlay) -> list: return get(self._hostvars, f"avd_overlay_peers..{self.shared_utils.hostname}", separator="..", default=[]) @cached_property - def _evpn_gateway_remote_peers(self: AvdStructuredConfigOverlay) -> dict: + def _evpn_gateway_remote_peers(self) -> dict: if not self.shared_utils.overlay_evpn: return {} @@ -50,7 +47,7 @@ def _evpn_gateway_remote_peers(self: AvdStructuredConfigOverlay) -> dict: } ) - peer_facts = self.shared_utils.get_peer_facts(remote_peer_name, required=False) + peer_facts = self.shared_utils.get_peer_facts_dict_or_none(remote_peer_name) if peer_facts is None: # No matching host found in the inventory for this remote gateway evpn_gateway_remote_peers[remote_peer_name] = gw_info @@ -67,7 +64,7 @@ def _evpn_gateway_remote_peers(self: AvdStructuredConfigOverlay) -> dict: return evpn_gateway_remote_peers @cached_property - def _evpn_route_clients(self: AvdStructuredConfigOverlay) -> dict: + def _evpn_route_clients(self) -> dict: if not self.shared_utils.overlay_evpn: return {} @@ -77,7 +74,7 @@ def _evpn_route_clients(self: AvdStructuredConfigOverlay) -> dict: evpn_route_clients = {} for avd_peer in self._avd_overlay_peers: - peer_facts = self.shared_utils.get_peer_facts(avd_peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(avd_peer) if ( self.shared_utils.hostname in peer_facts.get("evpn_route_servers", []) and peer_facts.get("evpn_role") in ["server", "client"] @@ -88,14 +85,14 @@ def _evpn_route_clients(self: AvdStructuredConfigOverlay) -> dict: return evpn_route_clients @cached_property - def _evpn_route_servers(self: AvdStructuredConfigOverlay) -> dict: + def _evpn_route_servers(self) -> dict: if not self.shared_utils.overlay_evpn: return {} evpn_route_servers = {} for route_server in natural_sort(get(self._hostvars, "switch.evpn_route_servers", default=[])): - peer_facts = self.shared_utils.get_peer_facts(route_server, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(route_server) if peer_facts.get("evpn_role") != "server": continue @@ -105,21 +102,21 @@ def _evpn_route_servers(self: AvdStructuredConfigOverlay) -> dict: # The next four should probably be moved to facts @cached_property - def _is_mpls_client(self: AvdStructuredConfigOverlay) -> bool: + def _is_mpls_client(self) -> bool: return self.shared_utils.mpls_overlay_role == "client" or (self.shared_utils.evpn_role == "client" and self.shared_utils.overlay_evpn_mpls) @cached_property - def _is_mpls_server(self: AvdStructuredConfigOverlay) -> bool: + def _is_mpls_server(self) -> bool: return self.shared_utils.mpls_overlay_role == "server" or (self.shared_utils.evpn_role == "server" and self.shared_utils.overlay_evpn_mpls) - def _is_peer_mpls_client(self: AvdStructuredConfigOverlay, peer_facts: dict) -> bool: + def _is_peer_mpls_client(self, peer_facts: dict) -> bool: return peer_facts.get("mpls_overlay_role") == "client" or (peer_facts.get("evpn_role") == "client" and get(peer_facts, "overlay.evpn_mpls") is True) - def _is_peer_mpls_server(self: AvdStructuredConfigOverlay, peer_facts: dict) -> bool: + def _is_peer_mpls_server(self, peer_facts: dict) -> bool: return peer_facts.get("mpls_overlay_role") == "server" or (peer_facts.get("evpn_role") == "server" and get(peer_facts, "overlay.evpn_mpls") is True) @cached_property - def _ipvpn_gateway_remote_peers(self: AvdStructuredConfigOverlay) -> dict: + def _ipvpn_gateway_remote_peers(self) -> dict: if self.shared_utils.overlay_ipvpn_gateway is not True: return {} @@ -139,14 +136,14 @@ def _ipvpn_gateway_remote_peers(self: AvdStructuredConfigOverlay) -> dict: return ipvpn_gateway_remote_peers @cached_property - def _mpls_route_clients(self: AvdStructuredConfigOverlay) -> dict: + def _mpls_route_clients(self) -> dict: if self._is_mpls_server is not True: return {} mpls_route_clients = {} for avd_peer in self._avd_overlay_peers: - peer_facts = self.shared_utils.get_peer_facts(avd_peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(avd_peer) if self._is_peer_mpls_client(peer_facts) is not True: continue @@ -156,7 +153,7 @@ def _mpls_route_clients(self: AvdStructuredConfigOverlay) -> dict: return mpls_route_clients @cached_property - def _mpls_mesh_pe(self: AvdStructuredConfigOverlay) -> dict: + def _mpls_mesh_pe(self) -> dict: if not self.shared_utils.overlay_mpls or not self.inputs.bgp_mesh_pes: return {} @@ -167,7 +164,7 @@ def _mpls_mesh_pe(self: AvdStructuredConfigOverlay) -> dict: if fabric_switch == self.shared_utils.hostname: continue - peer_facts = self.shared_utils.get_peer_facts(fabric_switch, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(fabric_switch) if self._is_peer_mpls_client(peer_facts) is not True: continue @@ -176,7 +173,7 @@ def _mpls_mesh_pe(self: AvdStructuredConfigOverlay) -> dict: return mpls_mesh_pe @cached_property - def _mpls_route_reflectors(self: AvdStructuredConfigOverlay) -> dict: + def _mpls_route_reflectors(self) -> dict: if self._is_mpls_client is not True: return {} @@ -186,7 +183,7 @@ def _mpls_route_reflectors(self: AvdStructuredConfigOverlay) -> dict: if route_reflector == self.shared_utils.hostname: continue - peer_facts = self.shared_utils.get_peer_facts(route_reflector, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(route_reflector) if self._is_peer_mpls_server(peer_facts) is not True: continue @@ -195,7 +192,7 @@ def _mpls_route_reflectors(self: AvdStructuredConfigOverlay) -> dict: return mpls_route_reflectors @cached_property - def _mpls_rr_peers(self: AvdStructuredConfigOverlay) -> dict: + def _mpls_rr_peers(self) -> dict: if self._is_mpls_server is not True: return {} @@ -205,14 +202,14 @@ def _mpls_rr_peers(self: AvdStructuredConfigOverlay) -> dict: if route_reflector == self.shared_utils.hostname: continue - peer_facts = self.shared_utils.get_peer_facts(route_reflector, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(route_reflector) if self._is_peer_mpls_server(peer_facts) is not True: continue self._append_peer(mpls_rr_peers, route_reflector, peer_facts) for avd_peer in self._avd_overlay_peers: - peer_facts = self.shared_utils.get_peer_facts(avd_peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(avd_peer) if self._is_peer_mpls_server(peer_facts) is not True: continue @@ -225,7 +222,7 @@ def _mpls_rr_peers(self: AvdStructuredConfigOverlay) -> dict: return mpls_rr_peers - def _append_peer(self: AvdStructuredConfigOverlay, peers_dict: dict, peer_name: str, peer_facts: dict) -> None: + def _append_peer(self, peers_dict: dict, peer_name: str, peer_facts: dict) -> None: """ Retrieve bgp_as and "overlay.peering_address" from peer_facts and append a new peer to peers_dict. @@ -250,10 +247,10 @@ def _append_peer(self: AvdStructuredConfigOverlay, peers_dict: dict, peer_name: } @cached_property - def _is_wan_server_with_peers(self: AvdStructuredConfigOverlay) -> bool: + def _is_wan_server_with_peers(self) -> bool: return self.shared_utils.is_wan_server and len(self.shared_utils.filtered_wan_route_servers) > 0 - def _stun_server_profile_name(self: AvdStructuredConfigOverlay, wan_route_server_name: str, path_group_name: str, interface_name: str) -> str: + 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. @@ -264,7 +261,7 @@ def _stun_server_profile_name(self: AvdStructuredConfigOverlay, wan_route_server return f"{path_group_name}-{wan_route_server_name}-{sanitized_interface_name}" @cached_property - def _stun_server_profiles(self: AvdStructuredConfigOverlay) -> dict: + def _stun_server_profiles(self) -> dict: """Return a dictionary of _stun_server_profiles with ip_address per local path_group.""" stun_server_profiles = {} for wan_route_server in self.shared_utils.filtered_wan_route_servers: @@ -279,6 +276,6 @@ def _stun_server_profiles(self: AvdStructuredConfigOverlay) -> dict: ) return stun_server_profiles - def _wan_ha_peer_vtep_ip(self: AvdStructuredConfigOverlay) -> str: - peer_facts = self.shared_utils.get_peer_facts(self.shared_utils.wan_ha_peer, required=True) + def _wan_ha_peer_vtep_ip(self) -> str: + peer_facts = self.shared_utils.get_peer_facts_dict(self.shared_utils.wan_ha_peer) return get(peer_facts, "vtep_ip", required=True) diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/__init__.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/__init__.py index a117ffbcef7..3cb1cbbaf9f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/__init__.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/__init__.py @@ -24,7 +24,6 @@ class AvdStructuredConfigUnderlay( - StructuredConfigGenerator, VlansMixin, EthernetInterfacesMixin, PortChannelInterfacesMixin, @@ -43,6 +42,7 @@ class AvdStructuredConfigUnderlay( AgentsMixin, IpAccesslistsMixin, DhcpServersMixin, + StructuredConfigGenerator, ): """ The AvdStructuredConfig Class is imported used "get_structured_config" to render parts of the structured config. @@ -53,6 +53,4 @@ class AvdStructuredConfigUnderlay( The Class uses StructuredConfigGenerator, as the base class, to get the render, keys and other attributes. All other methods are included as "Mixins" to make the files more manageable. - - The order of the @cached_properties methods imported from Mixins will also control the order in the output. """ diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/agents.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/agents.py index 0ca58e6829d..b57e0faad30 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/agents.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/agents.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class AgentsMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class AgentsMixin(UtilsMixin): """ @cached_property - def agents(self: AvdStructuredConfigUnderlay) -> list | None: + def agents(self) -> list | None: """Return structured config for agents.""" if not self.shared_utils.is_wan_router: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/as_path.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/as_path.py index f695e5ee92d..ba6188810df 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/as_path.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/as_path.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class AsPathMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class AsPathMixin(UtilsMixin): """ @cached_property - def as_path(self: AvdStructuredConfigUnderlay) -> dict | None: + def as_path(self) -> dict | None: """Return structured config for as_path.""" if self.shared_utils.underlay_routing_protocol != "ebgp": return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py index 235c7c8c53b..e432aa16c63 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py @@ -6,16 +6,12 @@ import re from functools import cached_property from ipaddress import AddressValueError, IPv4Address, ip_network -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import get from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class DhcpServersMixin(UtilsMixin): """ @@ -25,7 +21,7 @@ class DhcpServersMixin(UtilsMixin): """ @cached_property - def _subnets(self: AvdStructuredConfigUnderlay) -> list: + def _subnets(self) -> list: """ Returns a list of dhcp subnets for downstream p2p interfaces. @@ -33,7 +29,7 @@ def _subnets(self: AvdStructuredConfigUnderlay) -> list: """ subnets = [] for peer in self._avd_peers: - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) for uplink in peer_facts["uplinks"]: if ( uplink["peer"] == self.shared_utils.hostname @@ -53,7 +49,7 @@ def _subnets(self: AvdStructuredConfigUnderlay) -> list: return subnets @cached_property - def _ipv4_ztp_boot_file(self: AvdStructuredConfigUnderlay) -> str | None: + def _ipv4_ztp_boot_file(self) -> str | None: """Returns the file name to allow for ZTP to CV. TODO: Add inband_ztp_bootstrap_file to schema.""" if custom_bootfile := get(self._hostvars, "inband_ztp_bootstrap_file"): return custom_bootfile @@ -67,7 +63,7 @@ def _ipv4_ztp_boot_file(self: AvdStructuredConfigUnderlay) -> str | None: return f"https://{cvp_instance_ips[0]}/ztp/bootstrap" @cached_property - def _ntp_servers(self: AvdStructuredConfigUnderlay) -> dict | None: + def _ntp_servers(self) -> dict | None: """Returns the list of NTP servers.""" ntp_servers_settings = self.inputs.ntp_settings.servers if not ntp_servers_settings: @@ -88,7 +84,7 @@ def _ntp_servers(self: AvdStructuredConfigUnderlay) -> dict | None: raise AristaAvdInvalidInputsError(msg) @cached_property - def dhcp_servers(self: AvdStructuredConfigUnderlay) -> list | None: + def dhcp_servers(self) -> list | None: """Return structured config for dhcp_server.""" dhcp_servers = [] # Set subnets for DHCP server diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py index 8c02ce5a51c..ec90c635ca4 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import Any from pyavd._eos_cli_config_gen.schema import EosCliConfigGen from pyavd._errors import AristaAvdError, AristaAvdMissingVariableError @@ -14,9 +14,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class EthernetInterfacesMixin(UtilsMixin): """ @@ -26,10 +23,13 @@ class EthernetInterfacesMixin(UtilsMixin): """ @cached_property - def ethernet_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: + def ethernet_interfaces(self) -> list | None: """Return structured config for ethernet_interfaces.""" ethernet_interfaces = [] + # Added to help type checker, but overwritten later. + ethernet_subinterfaces = [] + for link in self._underlay_links: # common values description = self.shared_utils.interface_descriptions.underlay_ethernet_interface( @@ -81,7 +81,7 @@ def ethernet_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: # MPLS if self.shared_utils.underlay_mpls is True: - mpls_dict = {"ip": True} + mpls_dict: dict[str, Any] = {"ip": True} if self.shared_utils.underlay_ldp is True: mpls_dict["ldp"] = { "interface": True, @@ -312,7 +312,7 @@ def ethernet_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: return None - def _get_direct_ha_ethernet_interfaces(self: AvdStructuredConfigUnderlay) -> list: + def _get_direct_ha_ethernet_interfaces(self) -> list: """ Return a list of ethernet interfaces to be configured for WAN direct HA. diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/ip_access_lists.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/ip_access_lists.py index 2866c6093df..8b32469f834 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/ip_access_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/ip_access_lists.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import append_if_not_duplicate from pyavd.j2filters import natural_sort from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class IpAccesslistsMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class IpAccesslistsMixin(UtilsMixin): """ @cached_property - def ip_access_lists(self: AvdStructuredConfigUnderlay) -> list | None: + def ip_access_lists(self) -> list | None: """ Return structured config for ip_access_lists. diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py index d85e9f3b0d9..c19f0e0cc1c 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py @@ -4,7 +4,6 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import default @@ -12,9 +11,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class LoopbackInterfacesMixin(UtilsMixin): """ @@ -24,7 +20,7 @@ class LoopbackInterfacesMixin(UtilsMixin): """ @cached_property - def loopback_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: + def loopback_interfaces(self) -> list | None: """Return structured config for loopback_interfaces.""" if not self.shared_utils.underlay_router: return None @@ -103,7 +99,7 @@ def loopback_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: return loopback_interfaces @cached_property - def _node_sid(self: AvdStructuredConfigUnderlay) -> int: + def _node_sid(self) -> int: if self.shared_utils.id is None: msg = f"'id' is not set on '{self.shared_utils.hostname}' and is required to set node SID" raise AristaAvdInvalidInputsError(msg) diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/mpls.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/mpls.py index a712bbc1d55..cbb55586035 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/mpls.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/mpls.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class MplsMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class MplsMixin(UtilsMixin): """ @cached_property - def mpls(self: AvdStructuredConfigUnderlay) -> dict | None: + def mpls(self) -> dict | None: """Return structured config for mpls.""" if self.shared_utils.underlay_mpls is not True: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py index 883db8071f8..abe828a6c6d 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py @@ -4,7 +4,6 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._eos_cli_config_gen.schema import EosCliConfigGen from pyavd._utils import append_if_not_duplicate, get, short_esi_to_route_target, strip_null_from_data @@ -12,9 +11,6 @@ from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class PortChannelInterfacesMixin(UtilsMixin): """ @@ -24,7 +20,7 @@ class PortChannelInterfacesMixin(UtilsMixin): """ @cached_property - def port_channel_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: + def port_channel_interfaces(self) -> list | None: """Return structured config for port_channel_interfaces.""" port_channel_interfaces = [] port_channel_list = [] @@ -128,7 +124,7 @@ def port_channel_interfaces(self: AvdStructuredConfigUnderlay) -> list | None: return None - def _get_direct_ha_port_channel_interface(self: AvdStructuredConfigUnderlay) -> dict | None: + def _get_direct_ha_port_channel_interface(self) -> dict | None: """Return a dict containing the port-channel interface for direct HA.""" if not self.shared_utils.use_port_channel_for_direct_ha: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/prefix_lists.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/prefix_lists.py index 6c9930d25ca..3771c3fb701 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/prefix_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/prefix_lists.py @@ -6,15 +6,11 @@ import ipaddress from functools import cached_property from ipaddress import collapse_addresses, ip_network -from typing import TYPE_CHECKING from pyavd._utils import get, get_ipv4_networks_from_pool, get_ipv6_networks_from_pool from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class PrefixListsMixin(UtilsMixin): """ @@ -24,7 +20,7 @@ class PrefixListsMixin(UtilsMixin): """ @cached_property - def prefix_lists(self: AvdStructuredConfigUnderlay) -> list | None: + def prefix_lists(self) -> list | None: """Return structured config for prefix_lists.""" if self.shared_utils.underlay_bgp is not True and not self.shared_utils.is_wan_router: return None @@ -84,7 +80,7 @@ def prefix_lists(self: AvdStructuredConfigUnderlay) -> list | None: p2p_links_sequence_numbers = [] sequence_number = 0 for peer in self._avd_peers: - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) for uplink in peer_facts["uplinks"]: if ( uplink["peer"] == self.shared_utils.hostname @@ -102,7 +98,7 @@ def prefix_lists(self: AvdStructuredConfigUnderlay) -> list | None: return prefix_lists @cached_property - def ipv6_prefix_lists(self: AvdStructuredConfigUnderlay) -> list | None: + def ipv6_prefix_lists(self) -> list | None: """Return structured config for IPv6 prefix_lists.""" if self.shared_utils.underlay_bgp is not True: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/route_maps.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/route_maps.py index 71b9da1e8c2..64d5b3a52e4 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/route_maps.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/route_maps.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import get from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouteMapsMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouteMapsMixin(UtilsMixin): """ @cached_property - def route_maps(self: AvdStructuredConfigUnderlay) -> list | None: + def route_maps(self) -> list | None: """ Return structured config for route_maps. @@ -77,7 +73,7 @@ def route_maps(self: AvdStructuredConfigUnderlay) -> list | None: add_p2p_links = False for peer in self._avd_peers: - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) for uplink in peer_facts["uplinks"]: if ( uplink["peer"] == self.shared_utils.hostname diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_bgp.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_bgp.py index 3289ed049e9..7bc0a7703bb 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_bgp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_bgp.py @@ -4,15 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import Any from pyavd._utils import append_if_not_duplicate, get, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouterBgpMixin(UtilsMixin): """ @@ -22,7 +19,7 @@ class RouterBgpMixin(UtilsMixin): """ @cached_property - def router_bgp(self: AvdStructuredConfigUnderlay) -> dict | None: + def router_bgp(self) -> dict | None: """Return the structured config for router_bgp.""" if not self.shared_utils.underlay_bgp: return None @@ -55,7 +52,7 @@ def router_bgp(self: AvdStructuredConfigUnderlay) -> dict | None: # Address Families # TODO: - see if it makes sense to extract logic in method - address_family_ipv4_peer_group = {"activate": True} + address_family_ipv4_peer_group: dict[str, Any] = {"activate": True} if self.inputs.underlay_rfc5549 is True: address_family_ipv4_peer_group["next_hop"] = {"address_family_ipv6": {"enabled": True, "originate": True}} diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_isis.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_isis.py index 3e7978c66a0..2c53ef1875e 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_isis.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_isis.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from pyavd._utils import default, strip_empties_from_dict from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouterIsisMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class RouterIsisMixin(UtilsMixin): """ @cached_property - def router_isis(self: AvdStructuredConfigUnderlay) -> dict | None: + def router_isis(self) -> dict | None: """Return structured config for router_isis.""" if self.shared_utils.underlay_isis is not True: return None @@ -68,7 +64,7 @@ def router_isis(self: AvdStructuredConfigUnderlay) -> dict | None: return strip_empties_from_dict(router_isis) @cached_property - def _isis_net(self: AvdStructuredConfigUnderlay) -> str | None: + def _isis_net(self) -> str | None: if self.inputs.isis_system_id_format == "node_id": isis_system_id_prefix = self.shared_utils.node_config.isis_system_id_prefix if self.shared_utils.underlay_isis is True and isis_system_id_prefix is None: @@ -89,7 +85,7 @@ def _isis_net(self: AvdStructuredConfigUnderlay) -> str | None: return f"{isis_area_id}.{system_id}.00" @cached_property - def _is_type(self: AvdStructuredConfigUnderlay) -> str: + def _is_type(self) -> str: return default(self.shared_utils.node_config.is_type, self.inputs.isis_default_is_type) @staticmethod diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_msdp.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_msdp.py index 416dfbd969e..752615c85e8 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_msdp.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_msdp.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import get from pyavd.j2filters import natural_sort from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouterMsdpMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class RouterMsdpMixin(UtilsMixin): """ @cached_property - def router_msdp(self: AvdStructuredConfigUnderlay) -> dict | None: + def router_msdp(self) -> dict | None: """ Return structured config for router_msdp. @@ -50,7 +46,7 @@ def router_msdp(self: AvdStructuredConfigUnderlay) -> dict | None: "originator_id_local_interface": "Loopback0", "peers": [ { - "ipv4_address": get(self.shared_utils.get_peer_facts(peer), "router_id", required=True), + "ipv4_address": get(self.shared_utils.get_peer_facts_dict(peer), "router_id"), "local_interface": "Loopback0", "description": peer, "mesh_groups": [{"name": "ANYCAST-RP"}], diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_ospf.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_ospf.py index f859701d4b8..62b718c26ee 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_ospf.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_ospf.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import default from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouterOspfMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class RouterOspfMixin(UtilsMixin): """ @cached_property - def router_ospf(self: AvdStructuredConfigUnderlay) -> dict | None: + def router_ospf(self) -> dict | None: """Return structured config for router_ospf.""" if self.shared_utils.underlay_ospf is not True: return None diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_pim_sparse_mode.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_pim_sparse_mode.py index 45f008760c7..970143bdc6f 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/router_pim_sparse_mode.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/router_pim_sparse_mode.py @@ -4,15 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import Any from pyavd._utils import get from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class RouterPimSparseModeMixin(UtilsMixin): """ @@ -22,7 +19,7 @@ class RouterPimSparseModeMixin(UtilsMixin): """ @cached_property - def router_pim_sparse_mode(self: AvdStructuredConfigUnderlay) -> dict | None: + def router_pim_sparse_mode(self) -> dict | None: """ Return structured config for router_pim_sparse_mode. @@ -34,7 +31,7 @@ def router_pim_sparse_mode(self: AvdStructuredConfigUnderlay) -> dict | None: rp_addresses = [] anycast_rps = [] for rp_entry in self.inputs.underlay_multicast_rps: - rp_address = {"address": rp_entry.rp} + rp_address: dict[str, Any] = {"address": rp_entry.rp} if rp_entry.groups: if rp_entry.access_list_name: rp_address["access_lists"] = [rp_entry.access_list_name] @@ -52,7 +49,7 @@ def router_pim_sparse_mode(self: AvdStructuredConfigUnderlay) -> dict | None: "address": rp_entry.rp, "other_anycast_rp_addresses": [ { - "address": get(self.shared_utils.get_peer_facts(node.name), "router_id", required=True), + "address": get(self.shared_utils.get_peer_facts_dict(node.name), "router_id"), } for node in rp_entry.nodes ], diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/standard_access_lists.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/standard_access_lists.py index 962bffdf227..b1957f33d90 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/standard_access_lists.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/standard_access_lists.py @@ -4,13 +4,9 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class StandardAccessListsMixin(UtilsMixin): """ @@ -20,7 +16,7 @@ class StandardAccessListsMixin(UtilsMixin): """ @cached_property - def standard_access_lists(self: AvdStructuredConfigUnderlay) -> list | None: + def standard_access_lists(self) -> list | None: """ Return structured config for standard_access_lists. diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/static_routes.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/static_routes.py index 3e486443d81..27a774b49c0 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/static_routes.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/static_routes.py @@ -4,15 +4,11 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._errors import AristaAvdInvalidInputsError from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class StaticRoutesMixin(UtilsMixin): """ @@ -22,7 +18,7 @@ class StaticRoutesMixin(UtilsMixin): """ @cached_property - def static_routes(self: AvdStructuredConfigUnderlay) -> list[dict] | None: + def static_routes(self) -> list[dict] | None: """ Returns structured config for static_routes. diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/utils.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/utils.py index e4cc87359c6..948703c9e73 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/utils.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/utils.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from pyavd._eos_cli_config_gen.schema import EosCliConfigGen +from pyavd._eos_designs.structured_config.structured_config_generator import StructuredConfigGenerator from pyavd._errors import AristaAvdError, AristaAvdMissingVariableError from pyavd._utils import default, get, get_ip_from_ip_prefix, get_item, strip_empties_from_dict from pyavd.api.interface_descriptions import InterfaceDescriptionData @@ -15,10 +16,8 @@ if TYPE_CHECKING: from pyavd._eos_designs.schema import EosDesigns - from . import AvdStructuredConfigUnderlay - -class UtilsMixin: +class UtilsMixin(StructuredConfigGenerator): """ Mixin Class with internal functions. @@ -26,7 +25,7 @@ class UtilsMixin: """ @cached_property - def _avd_peers(self: AvdStructuredConfigUnderlay) -> list: + def _avd_peers(self) -> list: """ Returns a list of peers. @@ -36,7 +35,7 @@ def _avd_peers(self: AvdStructuredConfigUnderlay) -> list: return natural_sort(get(self._hostvars, f"avd_topology_peers..{self.shared_utils.hostname}", separator="..", default=[])) @cached_property - def _underlay_filter_peer_as_route_maps_asns(self: AvdStructuredConfigUnderlay) -> list: + def _underlay_filter_peer_as_route_maps_asns(self) -> list: """Filtered ASNs.""" if not self.inputs.underlay_filter_peer_as: return [] @@ -45,7 +44,7 @@ def _underlay_filter_peer_as_route_maps_asns(self: AvdStructuredConfigUnderlay) return natural_sort({link["peer_bgp_as"] for link in self._underlay_links if link["type"] == "underlay_p2p"}) @cached_property - def _underlay_links(self: AvdStructuredConfigUnderlay) -> list: + def _underlay_links(self) -> list: """Returns the list of underlay links for this device.""" underlay_links = [] underlay_links.extend(self._uplinks) @@ -61,7 +60,7 @@ def _underlay_links(self: AvdStructuredConfigUnderlay) -> list: downlinks_flow_tracker = self.shared_utils.get_flow_tracker(self.inputs.fabric_flow_tracking.downlinks) for peer in self._avd_peers: - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) for uplink in peer_facts["uplinks"]: if uplink["peer"] == self.shared_utils.hostname: link = { @@ -117,7 +116,7 @@ def _underlay_links(self: AvdStructuredConfigUnderlay) -> list: return natural_sort(underlay_links, "interface") @cached_property - def _underlay_vlan_trunk_groups(self: AvdStructuredConfigUnderlay) -> list: + def _underlay_vlan_trunk_groups(self) -> list: """Returns a list of trunk groups to configure on the underlay link.""" if self.inputs.enable_trunk_groups is not True: return [] @@ -125,7 +124,7 @@ def _underlay_vlan_trunk_groups(self: AvdStructuredConfigUnderlay) -> list: trunk_groups = [] for peer in self._avd_peers: - peer_facts = self.shared_utils.get_peer_facts(peer, required=True) + peer_facts = self.shared_utils.get_peer_facts_dict(peer) for uplink in peer_facts["uplinks"]: if uplink["peer"] == self.shared_utils.hostname: if (peer_trunk_groups := get(uplink, "peer_trunk_groups")) is None: @@ -144,12 +143,10 @@ def _underlay_vlan_trunk_groups(self: AvdStructuredConfigUnderlay) -> list: return [] @cached_property - def _uplinks(self: AvdStructuredConfigUnderlay) -> list: + def _uplinks(self) -> list: return get(self._hostvars, "switch.uplinks") - def _get_l3_interface_cfg( - self: AvdStructuredConfigUnderlay, l3_interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem - ) -> dict | None: + def _get_l3_interface_cfg(self, l3_interface: EosDesigns._DynamicKeys.DynamicNodeTypesItem.NodeTypes.NodesItem.L3InterfacesItem) -> dict: """Returns structured_configuration for one L3 interface.""" interface_description = l3_interface.description if not interface_description: @@ -214,7 +211,7 @@ def _get_l3_interface_cfg( return strip_empties_from_dict(interface) - def _get_l3_uplink_with_l2_as_subint(self: AvdStructuredConfigUnderlay, link: dict) -> tuple[dict, list[dict]]: + def _get_l3_uplink_with_l2_as_subint(self, link: dict) -> tuple[dict, list[dict]]: """Return a tuple with main uplink interface, list of subinterfaces representing each SVI.""" vlans = [int(vlan) for vlan in range_expand(link["vlans"])] @@ -246,7 +243,7 @@ def _get_l3_uplink_with_l2_as_subint(self: AvdStructuredConfigUnderlay, link: di return main_interface, [interface for interface in interfaces if interface["name"] != link["interface"]] def _get_l2_as_subint( - self: AvdStructuredConfigUnderlay, + self, link: dict, svi: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem.SvisItem, vrf: EosDesigns._DynamicKeys.DynamicNetworkServicesItem.NetworkServicesItem.VrfsItem, @@ -306,7 +303,7 @@ def _get_l2_as_subint( return strip_empties_from_dict(subinterface) @cached_property - def _l3_interface_acls(self: AvdStructuredConfigUnderlay) -> dict[str, dict[str, dict]]: + def _l3_interface_acls(self) -> dict[str, dict[str, dict]]: """ Return dict of l3 interface ACLs. diff --git a/python-avd/pyavd/_eos_designs/structured_config/underlay/vlans.py b/python-avd/pyavd/_eos_designs/structured_config/underlay/vlans.py index 738ea40229e..72f7afe7e7d 100644 --- a/python-avd/pyavd/_eos_designs/structured_config/underlay/vlans.py +++ b/python-avd/pyavd/_eos_designs/structured_config/underlay/vlans.py @@ -4,16 +4,12 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING from pyavd._utils import get, get_item from pyavd.j2filters import natural_sort, range_expand from .utils import UtilsMixin -if TYPE_CHECKING: - from . import AvdStructuredConfigUnderlay - class VlansMixin(UtilsMixin): """ @@ -23,7 +19,7 @@ class VlansMixin(UtilsMixin): """ @cached_property - def vlans(self: AvdStructuredConfigUnderlay) -> list | None: + def vlans(self) -> list | None: """ Return structured config for vlans. diff --git a/python-avd/pyavd/api/ip_addressing/__init__.py b/python-avd/pyavd/api/ip_addressing/__init__.py index d94136d112a..e4385263db9 100644 --- a/python-avd/pyavd/api/ip_addressing/__init__.py +++ b/python-avd/pyavd/api/ip_addressing/__init__.py @@ -12,7 +12,7 @@ from .utils import UtilsMixin -class AvdIpAddressing(AvdFacts, UtilsMixin): +class AvdIpAddressing(UtilsMixin, AvdFacts): """ Class used to render IP addresses either from custom Jinja2 templates or using default Python Logic. diff --git a/python-avd/pyavd/api/ip_addressing/utils.py b/python-avd/pyavd/api/ip_addressing/utils.py index a18c2b6d22d..be657b6d4cc 100644 --- a/python-avd/pyavd/api/ip_addressing/utils.py +++ b/python-avd/pyavd/api/ip_addressing/utils.py @@ -4,17 +4,14 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from pyavd._eos_designs.avdfacts import AvdFacts from pyavd._errors import AristaAvdError, AristaAvdInvalidInputsError from pyavd._utils import get from pyavd.j2filters import range_expand -if TYPE_CHECKING: - from . import AvdIpAddressing - -class UtilsMixin: +class UtilsMixin(AvdFacts): """ Mixin Class with internal functions. @@ -22,83 +19,83 @@ class UtilsMixin: """ @cached_property - def _mlag_primary_id(self: AvdIpAddressing) -> int: + def _mlag_primary_id(self) -> int: if self.shared_utils.mlag_switch_ids is None or self.shared_utils.mlag_switch_ids.get("primary") is None: msg = "'mlag_switch_ids' is required to calculate MLAG IP addresses." raise AristaAvdInvalidInputsError(msg) return self.shared_utils.mlag_switch_ids["primary"] @cached_property - def _mlag_secondary_id(self: AvdIpAddressing) -> int: + def _mlag_secondary_id(self) -> int: if self.shared_utils.mlag_switch_ids is None or self.shared_utils.mlag_switch_ids.get("secondary") is None: msg = "'mlag_switch_ids' is required to calculate MLAG IP addresses." raise AristaAvdInvalidInputsError(msg) return self.shared_utils.mlag_switch_ids["secondary"] @cached_property - def _mlag_peer_ipv4_pool(self: AvdIpAddressing) -> str: + def _mlag_peer_ipv4_pool(self) -> str: return self.shared_utils.mlag_peer_ipv4_pool @cached_property - def _mlag_peer_ipv6_pool(self: AvdIpAddressing) -> str: + def _mlag_peer_ipv6_pool(self) -> str: return self.shared_utils.mlag_peer_ipv6_pool @cached_property - def _mlag_peer_l3_ipv4_pool(self: AvdIpAddressing) -> str: + def _mlag_peer_l3_ipv4_pool(self) -> str: return self.shared_utils.mlag_peer_l3_ipv4_pool @cached_property - def _uplink_ipv4_pool(self: AvdIpAddressing) -> str: + def _uplink_ipv4_pool(self) -> str: if self.shared_utils.node_config.uplink_ipv4_pool is None: msg = "'uplink_ipv4_pool' is required to calculate uplink IP addresses." raise AristaAvdInvalidInputsError(msg) return self.shared_utils.node_config.uplink_ipv4_pool @cached_property - def _id(self: AvdIpAddressing) -> int: + def _id(self) -> int: if self.shared_utils.id is None: msg = "'id' is required to calculate IP addresses." raise AristaAvdInvalidInputsError(msg) return self.shared_utils.id @cached_property - def _max_uplink_switches(self: AvdIpAddressing) -> int: + def _max_uplink_switches(self) -> int: return self.shared_utils.max_uplink_switches @cached_property - def _max_parallel_uplinks(self: AvdIpAddressing) -> int: + def _max_parallel_uplinks(self) -> int: return self.shared_utils.node_config.max_parallel_uplinks @cached_property - def _loopback_ipv4_address(self: AvdIpAddressing) -> str | None: + def _loopback_ipv4_address(self) -> str | None: return self.shared_utils.node_config.loopback_ipv4_address @cached_property - def _loopback_ipv4_pool(self: AvdIpAddressing) -> str: + def _loopback_ipv4_pool(self) -> str: return self.shared_utils.loopback_ipv4_pool @cached_property - def _loopback_ipv4_offset(self: AvdIpAddressing) -> int: + def _loopback_ipv4_offset(self) -> int: return self.shared_utils.node_config.loopback_ipv4_offset @cached_property - def _loopback_ipv6_pool(self: AvdIpAddressing) -> str: + def _loopback_ipv6_pool(self) -> str: return self.shared_utils.loopback_ipv6_pool @cached_property - def _loopback_ipv6_offset(self: AvdIpAddressing) -> int: + def _loopback_ipv6_offset(self) -> int: return self.shared_utils.node_config.loopback_ipv6_offset @cached_property - def _vtep_loopback_ipv4_address(self: AvdIpAddressing) -> str | None: + def _vtep_loopback_ipv4_address(self) -> str | None: return self.shared_utils.node_config.vtep_loopback_ipv4_address @cached_property - def _vtep_loopback_ipv4_pool(self: AvdIpAddressing) -> str: + def _vtep_loopback_ipv4_pool(self) -> str: return self.shared_utils.vtep_loopback_ipv4_pool @cached_property - def _mlag_odd_id_based_offset(self: AvdIpAddressing) -> int: + def _mlag_odd_id_based_offset(self) -> int: """ Return the subnet offset for an MLAG pair based on odd id. @@ -115,7 +112,7 @@ def _mlag_odd_id_based_offset(self: AvdIpAddressing) -> int: return int((odd_id - 1) / 2) - def _get_downlink_ipv4_pool_and_offset(self: AvdIpAddressing, uplink_switch_index: int) -> tuple[str, int]: + def _get_downlink_ipv4_pool_and_offset(self, uplink_switch_index: int) -> tuple[str, int]: """ Returns the downlink IP pool and offset as a tuple according to the uplink_switch_index. @@ -124,8 +121,8 @@ def _get_downlink_ipv4_pool_and_offset(self: AvdIpAddressing, uplink_switch_inde """ uplink_switch_interface = self.shared_utils.uplink_switch_interfaces[uplink_switch_index] uplink_switch = self.shared_utils.uplink_switches[uplink_switch_index] - peer_facts = self.shared_utils.get_peer_facts(uplink_switch, required=True) - downlink_pools = get(peer_facts, "downlink_pools") + peer_facts = self.shared_utils.get_peer_facts(uplink_switch) + downlink_pools = peer_facts.get("downlink_pools") if not downlink_pools: return (None, None) @@ -144,7 +141,7 @@ def _get_downlink_ipv4_pool_and_offset(self: AvdIpAddressing, uplink_switch_inde ) raise AristaAvdError(msg) - def _get_p2p_ipv4_pool_and_offset(self: AvdIpAddressing, uplink_switch_index: int) -> tuple[str, int]: + def _get_p2p_ipv4_pool_and_offset(self, uplink_switch_index: int) -> tuple[str, int]: """ Returns IP pool and offset as a tuple according to the uplink_switch_index. diff --git a/python-avd/tests/pyavd/schema/data_merging_schema_class.py b/python-avd/tests/pyavd/schema/data_merging_schema_class.py index 78674071cd9..741aff332af 100644 --- a/python-avd/tests/pyavd/schema/data_merging_schema_class.py +++ b/python-avd/tests/pyavd/schema/data_merging_schema_class.py @@ -3,8 +3,8 @@ # that can be found in the LICENSE file. from __future__ import annotations -from pyavd._schema.models.eos_cli_config_gen_root_model import EosCliConfigGenRootModel from typing import ClassVar +from pyavd._schema.models.eos_cli_config_gen_root_model import EosCliConfigGenRootModel from typing import Any from typing import TYPE_CHECKING