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
111 changes: 111 additions & 0 deletions annet/rulebook/routeros/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
Custom diff logic for RouterOS user commands.
"""

import re
from collections import OrderedDict as odict
from annet.types import Op
from annet.annlib.rulebook.common import DiffItem, call_diff_logic


def extract_comment(line: str) -> str:
"""Extract comment value from RouterOS user add command."""
match = re.search(r'comment=["\']?([^"\'\s]+)["\']?', line)
return match.group(1) if match else ""


def normalize_user_line(line: str) -> str:
"""Normalize user line by removing password parameter."""
normalized = re.sub(r'\s+password=["\']?[^"\'\s]+["\']?', '', line)
return normalized.strip()


def diff(old: odict, new: odict, diff_pre: odict, _pops: tuple[Op, ...] = (Op.AFFECTED,)) -> list[DiffItem]:
"""Custom diff logic for RouterOS user commands."""
diff_indexed = []

# Create normalized mappings
old_normalized = {}
new_normalized = {}

for row in old:
normalized = normalize_user_line(row)
comment = extract_comment(row)
old_normalized[normalized] = (row, comment)

for row in new:
normalized = normalize_user_line(row)
comment = extract_comment(row)
new_normalized[normalized] = (row, comment)

# Find removed users
for index, (normalized, (original_row, _)) in enumerate(old_normalized.items()):
if normalized not in new_normalized:
children = call_diff_logic(
diff_pre[original_row]["subtree"],
old[original_row],
odict(),
_pops + (Op.REMOVED,)
)
diff_indexed.append((index, DiffItem(
op=Op.REMOVED,
row=original_row,
children=children,
diff_pre=diff_pre[original_row]["match"],
)))

old_indexes = {normalized: index for index, normalized in enumerate(old_normalized)}

# Process users in new config
for normalized, (new_row, new_comment) in new_normalized.items():
if normalized in old_normalized:
old_row, old_comment = old_normalized[normalized]

if old_comment == new_comment:
# Password unchanged - AFFECTED
children = call_diff_logic(
diff_pre[new_row]["subtree"],
old.get(old_row, {}),
new[new_row],
_pops + (Op.AFFECTED,)
)
index = old_indexes.get(normalized, len(old_normalized))
diff_indexed.append((index, DiffItem(
op=Op.AFFECTED,
row=old_row,
children=children,
diff_pre=diff_pre[new_row]["match"],
)))
else:
# Password changed - MOVED
children = call_diff_logic(
diff_pre[new_row]["subtree"],
old.get(old_row, {}),
new[new_row],
_pops + (Op.MOVED,)
)
index = old_indexes.get(normalized, len(old_normalized))
diff_indexed.append((index, DiffItem(
op=Op.MOVED,
row=new_row,
children=children,
diff_pre=diff_pre[new_row]["match"],
)))
else:
# New user - ADDED
children = call_diff_logic(
diff_pre[new_row]["subtree"],
odict(),
new[new_row],
_pops + (Op.ADDED,)
)
index = len(old_normalized)
diff_indexed.append((index, DiffItem(
op=Op.ADDED,
row=new_row,
children=children,
diff_pre=diff_pre[new_row]["match"],
)))

diff_indexed.sort(key=lambda x: x[0])
return [item for _, item in diff_indexed]
1 change: 1 addition & 0 deletions annet/rulebook/texts/routeros.rul
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@


user
add ~ %diff_logic=routeros.user.diff
~
group
add ~
Expand Down
17 changes: 17 additions & 0 deletions tests/annet/test_patch/routeros_user_diff.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
- vendor: routeros
before: |
/user
add address="" comment="oldhash123" disabled=no group=full name=admin password=oldpass
add address="" comment="hash456" disabled=no group=full name=user1 password=pass1
add address="" comment="hash789" disabled=no group=read name=user2 password=pass2
add address="" comment="hashremove" disabled=no group=write name=toremove password=oldpass
after: |
/user
add address="" comment="oldhash123" disabled=no group=full name=admin password=oldpass
add address="" comment="newhash456" disabled=no group=full name=user1 password=newpass1
add address="" comment="hash789" disabled=no group=read name=user2 password=pass2
add address="" comment="hashnew" disabled=no group=full name=newuser password=newpass
patch: |
/user remove [ find address="" comment="hashremove" disabled=no group=write name=toremove ]
/user add address="" comment="newhash456" disabled=no group=full name=user1 password=newpass1
/user add address="" comment="hashnew" disabled=no group=full name=newuser password=newpass