From 62928811179e66eddc15da0421970fa365e63b0f Mon Sep 17 00:00:00 2001 From: Krasotin Pavel Date: Fri, 19 Dec 2025 17:13:46 +0300 Subject: [PATCH 1/5] nexus.vlandb added nexus.rul edited to use new patch logic --- annet/rulebook/nexus/vlandb.py | 176 +++++++++++++++++++++++++++++++ annet/rulebook/texts/nexus.order | 30 +++++- annet/rulebook/texts/nexus.rul | 14 ++- 3 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 annet/rulebook/nexus/vlandb.py diff --git a/annet/rulebook/nexus/vlandb.py b/annet/rulebook/nexus/vlandb.py new file mode 100644 index 00000000..43ff00ca --- /dev/null +++ b/annet/rulebook/nexus/vlandb.py @@ -0,0 +1,176 @@ +import re +from typing import Any, Dict, Iterator, Optional, Set, Tuple + +from annet.annlib.lib import cisco_collapse_vlandb as collapse_vlandb +from annet.annlib.lib import cisco_expand_vlandb as expand_vlandb +from annet.annlib.types import Op + + +# Constants +NEXUS_SWITCHPORT_VLAN_CHUNK: int = 64 +NEXUS_DEFAULT_RANGE: range = range(1, 4095) # 4094 inclusive + + +def swtrunk( + rule: Dict[str, Any], key: Tuple, diff: Dict[str, Any], **_ +) -> Iterator[Tuple[bool, str, Optional[list]]]: + """ + Patch logic for Cisco Nexus `switchport trunk allowed vlan` command. + Processes VLAN configuration changes and yields commands to apply. + """ + yield from _process_vlandb(rule, key, diff, NEXUS_SWITCHPORT_VLAN_CHUNK) + + +def _process_vlandb( + rule: Dict[str, Any], key: Tuple, diff: Dict[str, Any], chunk_size: int +) -> Iterator[Tuple[bool, str, Optional[list]]]: + """ + Core logic for processing VLAN database changes. + + Args: + rule: Configuration rule dictionary + key: Rule identifier tuple + diff: Dictionary containing ADDED, REMOVED, and AFFECTED operations + chunk_size: Maximum number of VLAN ranges per command + + Yields: + Tuples of (should_add: bool, command: str, children: Optional[list]) + """ + # Early exit if no changes + if not diff[Op.ADDED] and not diff[Op.REMOVED]: + return + + # Process affected blocks with modified content + for affected in diff[Op.AFFECTED]: + yield (True, affected["row"], affected["children"]) + + # Parse added and removed VLAN configurations + pref_added, vlans_new, new_blocks = _parse_vlancfg_actions(diff[Op.ADDED]) + pref_removed, vlans_old, old_blocks = _parse_vlancfg_actions(diff[Op.REMOVED]) + + # Handle case where no existing configuration exists + if not diff[Op.REMOVED]: + vlans_old = set(NEXUS_DEFAULT_RANGE) + + # Use removed prefix if added prefix is not available + if not pref_added: + pref_added = pref_removed + + # Handle "none" configuration special case + if len(diff[Op.ADDED]) == 1 and not vlans_new: + yield (True, f"{pref_added} none", None) + return + + # Process blocks where VLAN remains but content changed + removed_blocks_keys = set(old_blocks.keys()) - set(new_blocks.keys()) + for vlan_id in removed_blocks_keys & vlans_new: + yield (True, f"{pref_added} {vlan_id}", old_blocks[vlan_id]) + + # Calculate VLANs to add and remove + vlans_to_remove = vlans_old - vlans_new + vlans_to_add = vlans_new - vlans_old + + # Generate remove commands in chunks + if vlans_to_remove: + collapsed_remove = collapse_vlandb(vlans_to_remove) + for chunk in _chunked(collapsed_remove, chunk_size): + yield (False, f"{pref_added} remove {','.join(chunk)}", None) + + # Generate add commands in chunks + if vlans_to_add: + collapsed_add = collapse_vlandb(vlans_to_add) + for chunk in _chunked(collapsed_add, chunk_size): + yield (True, f"{pref_added} add {','.join(chunk)}", None) + + # Process new VLAN blocks + for vlan_id, block in new_blocks.items(): + yield (True, f"{pref_added} {vlan_id}", block) + + +def _chunked(items: list, chunk_size: int) -> Iterator[list[str]]: + """ + Split a list into chunks of specified size. + + Args: + items: List to split + chunk_size: Maximum size of each chunk + + Yields: + Chunks of the original list + """ + for i in range(0, len(items), chunk_size): + yield items[i : i + chunk_size] + + +def _parse_vlancfg_actions( + actions: list[Dict[str, Any]], +) -> Tuple[Optional[str], Set[int], Dict[str, Any]]: + """ + Parse VLAN configuration actions to extract prefix, VLAN IDs, and blocks. + + Args: + actions: List of action dictionaries with 'row' and 'children' keys + + Returns: + Tuple of (prefix, vlan_ids, blocks) + """ + prefix: Optional[str] = None + vlan_ids: Set[int] = set() + blocks: Dict[str, Any] = {} + + for action in actions: + current_prefix, vlan_set = _parse_vlancfg(action["row"]) + + # Use the first encountered prefix + if prefix is None: + prefix = current_prefix + + # Handle VLAN blocks with children + if action["children"]: + if len(vlan_set) != 1: + raise ValueError( + f"VLAN block must contain exactly one VLAN ID: {action['row']}" + ) + vlan_id = next(iter(vlan_set)) + blocks[str(vlan_id)] = action["children"] + + vlan_ids.update(vlan_set) + + return prefix, vlan_ids, blocks + + +def _parse_vlancfg(row: str) -> Tuple[str, Set[int]]: + """ + Parse Cisco VLAN configuration string into prefix and VLAN IDs. + + Args: + row: Configuration string (e.g., "switchport trunk allowed vlan 1-10,20") + + Returns: + Tuple of (command_prefix, set_of_vlan_ids) + + Raises: + ValueError: If the row cannot be parsed + """ + # Normalize spaces around commas + normalized_row = re.sub(r",\s+", ",", row) + words = normalized_row.split() + + # Handle "none" configuration + if words[-1] == "none": + prefix = " ".join(words[:-1]) + return prefix, set() + + # Validate VLAN configuration format + if not re.match(r"[\d,-]+$", words[-1]): + raise ValueError(f"Unable to parse VLAN configuration row: {row}") + + # Extract prefix and VLAN configuration + prefix_words = words[:-2] if words[-2] == "add" else words[:-1] + prefix = " ".join(prefix_words) + vlan_config = words[-1] + + # Expand VLAN ranges to individual IDs + vlan_ids = expand_vlandb(vlan_config) + + return prefix, vlan_ids diff --git a/annet/rulebook/texts/nexus.order b/annet/rulebook/texts/nexus.order index e9243544..47d75b70 100644 --- a/annet/rulebook/texts/nexus.order +++ b/annet/rulebook/texts/nexus.order @@ -3,6 +3,8 @@ # - Если команда начинается с undo и прописан параметр %order_reverse - команда считается # обратной, но занимает место между прямыми там, где указано. +# Сначала было имя +hostname # Фичи должны быть включены прежде всего feature # За ним сервисы @@ -13,7 +15,9 @@ interface breakout no password strength-check username tacacs-server -aaa +radius-server + +aaa group server ip access-list ipv6 access-list @@ -26,6 +30,8 @@ control-plane no policy-map %order_reverse no class-map %order_reverse +bfd + snmp-server source-interface snmp-server user snmp-server host @@ -42,6 +48,7 @@ ntp distribute ntp server ntp commit +aaa vlan vlan group @@ -60,11 +67,19 @@ ipv6 dhcp relay vrf context +vpc + +lldp + interface */Vlan\d+/ +interface */port-channel\d+/ +interface nve interface * - no switchport + no switchport %order_reverse switchport - switchport access vlan * + switchport mode + switchport access * + switchport trunk * encapsulation vrf member ip @@ -73,13 +88,18 @@ interface * ipv6 nd ~ channel-group + no shutdown %order_reverse interface */\S+\.\d+/ -# удалять eth-trunk можно только после того, как вычистим member interfaces -undo interface */port-channel\d+/ %order_reverse +# удалять port-channel можно только после того, как вычистим member interfaces +no interface */port-channel\d+/ %order_reverse router bgp address-family template neighbor + +evpn + +logging \ No newline at end of file diff --git a/annet/rulebook/texts/nexus.rul b/annet/rulebook/texts/nexus.rul index 4586b72f..2ec17ce5 100644 --- a/annet/rulebook/texts/nexus.rul +++ b/annet/rulebook/texts/nexus.rul @@ -16,7 +16,7 @@ ip community-list standard ~ !vrf context management vrf context * -vlan group * vlan-list %logic=cisco.vlandb.simple +# vlan group * vlan-list %logic=cisco.vlandb.simple vlan */[^\d].*/ vlan %logic=cisco.vlandb.simple name @@ -48,6 +48,18 @@ interface */(Vlan|Ethernet.*\.|port-channel.*\.?)\d+$/ %diff_logic=nexus.iface.d ipv6 nd ~ %logic=cisco.misc.no_ipv6_nd_suppress_ra mtu +# Port-Channel +interface */\w*port-channel[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff + switchport mode + switchport trunk native vlan + switchport access vlan + switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk + vrf member + ipv6 link-local + ipv6 address * + mtu + storm-control * level + # Physical interface */\w*Ethernet[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff switchport mode From 1dced79aa06a2cc93d58729d43093d7b452264e6 Mon Sep 17 00:00:00 2001 From: Krasotin Pavel Date: Tue, 23 Dec 2025 12:49:25 +0300 Subject: [PATCH 2/5] Add `no shutdown` to allowed commands on port-channel memebrs POrt-channle and ethernet merged inf .rul file --- annet/rulebook/nexus/iface.py | 48 ++++++++++++++++++---------------- annet/rulebook/texts/nexus.rul | 18 +++---------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/annet/rulebook/nexus/iface.py b/annet/rulebook/nexus/iface.py index fb86d98c..4b29841c 100644 --- a/annet/rulebook/nexus/iface.py +++ b/annet/rulebook/nexus/iface.py @@ -1,7 +1,6 @@ from annet.annlib.lib import uniq from annet.annlib.rulebook import common from annet.annlib.types import Op - from annet.rulebook.cisco.iface import is_in_channel, is_ip_cmd, is_vpn_cmd @@ -16,7 +15,9 @@ def diff(old, new, diff_pre, _pops=(Op.AFFECTED,)): iface_new = new.get(iface_row, {}) iface_pre = diff_pre[iface_row]["subtree"] vpn_changed = False - for (op, cmd, _, _) in common.default_diff(iface_old, iface_new, iface_pre, _pops): + for op, cmd, _, _ in common.default_diff( + iface_old, iface_new, iface_pre, _pops + ): if op in {Op.ADDED, Op.REMOVED}: vpn_changed |= is_vpn_cmd(cmd) break @@ -35,6 +36,7 @@ def diff(old, new, diff_pre, _pops=(Op.AFFECTED,)): return ret + # === # Вырезает все команды не разрешенные @@ -50,23 +52,27 @@ def _filter_channel_members(tree): def _is_allowed_on_channel(cmd_line): - return cmd_line.startswith(( - "channel-group", - "cdp", - "description", - "inherit", - "ip port", - "ipv6 port", - "mac port", - "lacp", - "switchport host", - "switchport", - "shutdown", - "rate-limit cpu", - "snmp trap link-status", - "mtu", - "macsec", # NOCDEV-9008 - )) + return cmd_line.startswith( + ( + "channel-group", + "cdp", + "description", + "inherit", + "ip port", + "ipv6 port", + "mac port", + "lacp", + "switchport host", + "switchport", + "shutdown", + "no shutdown", + "rate-limit cpu", + "snmp trap link-status", + "mtu", + "macsec", # NOCDEV-9008 + ) + ) + # === @@ -87,6 +93,4 @@ def _is_allowed_on_old_lag_memeber(cmd_line): """ Эти команды принудительно добавим на интерфейс после удаления его из lag """ - return cmd_line.startswith(( - "mtu", - )) + return cmd_line.startswith(("mtu",)) diff --git a/annet/rulebook/texts/nexus.rul b/annet/rulebook/texts/nexus.rul index 2ec17ce5..b4e0ecc0 100644 --- a/annet/rulebook/texts/nexus.rul +++ b/annet/rulebook/texts/nexus.rul @@ -48,31 +48,19 @@ interface */(Vlan|Ethernet.*\.|port-channel.*\.?)\d+$/ %diff_logic=nexus.iface.d ipv6 nd ~ %logic=cisco.misc.no_ipv6_nd_suppress_ra mtu -# Port-Channel -interface */\w*port-channel[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff +# Port-Channel/Ethernet +interface */\w*(Ethernet|port-channel)[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff switchport mode switchport trunk native vlan switchport access vlan switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk + spanning-tree link-type vrf member ipv6 link-local ipv6 address * mtu storm-control * level -# Physical -interface */\w*Ethernet[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff - switchport mode - switchport trunk native vlan - switchport access vlan - switchport trunk allowed vlan %logic=cisco.vlandb.swtrunk - vrf member - ipv6 link-local - ipv6 address * - channel-group - mtu - storm-control * level - router bgp * router-id vrf * From 62a485eca223edfb8b8a9d92f8d081c8285d9989 Mon Sep 17 00:00:00 2001 From: Krasotin Pavel Date: Tue, 23 Dec 2025 19:09:33 +0300 Subject: [PATCH 3/5] Change order of commands in patch of `tests/annet/test_patch/nexus_lag_member_remove.yaml` --- annet/rulebook/texts/nexus.rul | 18 +++++++++++++++--- .../test_patch/nexus_lag_member_remove.yaml | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/annet/rulebook/texts/nexus.rul b/annet/rulebook/texts/nexus.rul index b4e0ecc0..9aa76f85 100644 --- a/annet/rulebook/texts/nexus.rul +++ b/annet/rulebook/texts/nexus.rul @@ -48,19 +48,31 @@ interface */(Vlan|Ethernet.*\.|port-channel.*\.?)\d+$/ %diff_logic=nexus.iface.d ipv6 nd ~ %logic=cisco.misc.no_ipv6_nd_suppress_ra mtu -# Port-Channel/Ethernet -interface */\w*(Ethernet|port-channel)[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff +# Port-Channel +interface */\w*port-channel[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff switchport mode switchport trunk native vlan switchport access vlan switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk - spanning-tree link-type vrf member ipv6 link-local ipv6 address * mtu storm-control * level +# Ethernet +interface */\w*Ethernet[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff + switchport mode + switchport trunk native vlan + switchport access vlan + switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk + vrf member + ipv6 link-local + ipv6 address * + channel-group + mtu + storm-control * level + router bgp * router-id vrf * diff --git a/tests/annet/test_patch/nexus_lag_member_remove.yaml b/tests/annet/test_patch/nexus_lag_member_remove.yaml index 4ce3f23c..eace87d3 100644 --- a/tests/annet/test_patch/nexus_lag_member_remove.yaml +++ b/tests/annet/test_patch/nexus_lag_member_remove.yaml @@ -30,7 +30,6 @@ service-policy type qos input CLASSIFIER patch: | conf t - no interface port-channel101 interface Ethernet1/9/1 no channel-group no ip redirects @@ -43,5 +42,6 @@ no shutdown service-policy type qos input CLASSIFIER exit + no interface port-channel101 exit copy running-config startup-config From c3cf701df392c699c97258f773dfd90e5bd961bb Mon Sep 17 00:00:00 2001 From: Krasotin Pavel Date: Tue, 23 Dec 2025 19:22:05 +0300 Subject: [PATCH 4/5] Optimizing nexus.rul --- annet/rulebook/texts/nexus.rul | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/annet/rulebook/texts/nexus.rul b/annet/rulebook/texts/nexus.rul index 9aa76f85..a85dd0d9 100644 --- a/annet/rulebook/texts/nexus.rul +++ b/annet/rulebook/texts/nexus.rul @@ -16,7 +16,7 @@ ip community-list standard ~ !vrf context management vrf context * -# vlan group * vlan-list %logic=cisco.vlandb.simple +vlan group * vlan-list %logic=cisco.vlandb.simple vlan */[^\d].*/ vlan %logic=cisco.vlandb.simple name @@ -48,8 +48,8 @@ interface */(Vlan|Ethernet.*\.|port-channel.*\.?)\d+$/ %diff_logic=nexus.iface.d ipv6 nd ~ %logic=cisco.misc.no_ipv6_nd_suppress_ra mtu -# Port-Channel -interface */\w*port-channel[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff +# Port-Channel/Ethernet +interface */\w*(Ethernet|port-channel)[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff switchport mode switchport trunk native vlan switchport access vlan @@ -58,19 +58,7 @@ interface */\w*port-channel[0-9\/]+$/ %logic=common.permanent %diff_logic=ne ipv6 link-local ipv6 address * mtu - storm-control * level - -# Ethernet -interface */\w*Ethernet[0-9\/]+$/ %logic=common.permanent %diff_logic=nexus.iface.diff - switchport mode - switchport trunk native vlan - switchport access vlan - switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk - vrf member - ipv6 link-local - ipv6 address * channel-group - mtu storm-control * level router bgp * From fa02dbedcabfc78b81971b0ef7cb6711c322c764 Mon Sep 17 00:00:00 2001 From: Krasotin Pavel Date: Tue, 23 Dec 2025 19:47:16 +0300 Subject: [PATCH 5/5] Return original formatting of `annet/rulebook/nexus/iface.py` --- annet/rulebook/nexus/iface.py | 48 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/annet/rulebook/nexus/iface.py b/annet/rulebook/nexus/iface.py index 4b29841c..00bdee64 100644 --- a/annet/rulebook/nexus/iface.py +++ b/annet/rulebook/nexus/iface.py @@ -15,9 +15,7 @@ def diff(old, new, diff_pre, _pops=(Op.AFFECTED,)): iface_new = new.get(iface_row, {}) iface_pre = diff_pre[iface_row]["subtree"] vpn_changed = False - for op, cmd, _, _ in common.default_diff( - iface_old, iface_new, iface_pre, _pops - ): + for (op, cmd, _, _) in common.default_diff(iface_old, iface_new, iface_pre, _pops): if op in {Op.ADDED, Op.REMOVED}: vpn_changed |= is_vpn_cmd(cmd) break @@ -36,7 +34,6 @@ def diff(old, new, diff_pre, _pops=(Op.AFFECTED,)): return ret - # === # Вырезает все команды не разрешенные @@ -52,27 +49,24 @@ def _filter_channel_members(tree): def _is_allowed_on_channel(cmd_line): - return cmd_line.startswith( - ( - "channel-group", - "cdp", - "description", - "inherit", - "ip port", - "ipv6 port", - "mac port", - "lacp", - "switchport host", - "switchport", - "shutdown", - "no shutdown", - "rate-limit cpu", - "snmp trap link-status", - "mtu", - "macsec", # NOCDEV-9008 - ) - ) - + return cmd_line.startswith(( + "channel-group", + "cdp", + "description", + "inherit", + "ip port", + "ipv6 port", + "mac port", + "lacp", + "switchport host", + "switchport", + "shutdown", + "no shutdown", + "rate-limit cpu", + "snmp trap link-status", + "mtu", + "macsec", # NOCDEV-9008 + )) # === @@ -93,4 +87,6 @@ def _is_allowed_on_old_lag_memeber(cmd_line): """ Эти команды принудительно добавим на интерфейс после удаления его из lag """ - return cmd_line.startswith(("mtu",)) + return cmd_line.startswith(( + "mtu", + )) \ No newline at end of file