Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions annet/rulebook/nexus/iface.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -62,6 +61,7 @@ def _is_allowed_on_channel(cmd_line):
"switchport host",
"switchport",
"shutdown",
"no shutdown",
"rate-limit cpu",
"snmp trap link-status",
"mtu",
Expand Down Expand Up @@ -89,4 +89,4 @@ def _is_allowed_on_old_lag_memeber(cmd_line):
"""
return cmd_line.startswith((
"mtu",
))
))
176 changes: 176 additions & 0 deletions annet/rulebook/nexus/vlandb.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 25 additions & 5 deletions annet/rulebook/texts/nexus.order
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# - Если команда начинается с undo и прописан параметр %order_reverse - команда считается
# обратной, но занимает место между прямыми там, где указано.

# Сначала было имя
hostname
# Фичи должны быть включены прежде всего
feature
# За ним сервисы
Expand All @@ -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
Expand All @@ -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
Expand All @@ -42,6 +48,7 @@ ntp distribute
ntp server
ntp commit

aaa

vlan
vlan group
Expand All @@ -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
Expand All @@ -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
8 changes: 4 additions & 4 deletions annet/rulebook/texts/nexus.rul
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ interface */(Vlan|Ethernet.*\.|port-channel.*\.?)\d+$/ %diff_logic=nexus.iface.d
ipv6 nd ~ %logic=cisco.misc.no_ipv6_nd_suppress_ra
mtu

# Physical
interface */\w*Ethernet[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=cisco.vlandb.swtrunk
switchport trunk allowed vlan %logic=nexus.vlandb.swtrunk
vrf member
ipv6 link-local
ipv6 address *
channel-group
mtu
channel-group
storm-control * level

router bgp *
Expand Down
2 changes: 1 addition & 1 deletion tests/annet/test_patch/nexus_lag_member_remove.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,5 +42,6 @@
no shutdown
service-policy type qos input CLASSIFIER
exit
no interface port-channel101
exit
copy running-config startup-config
Loading