diff --git a/annet/rulebook/routeros/user.py b/annet/rulebook/routeros/user.py new file mode 100644 index 00000000..d14afd0b --- /dev/null +++ b/annet/rulebook/routeros/user.py @@ -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] diff --git a/annet/rulebook/texts/routeros.rul b/annet/rulebook/texts/routeros.rul index bd1b7b89..b70f5bcf 100644 --- a/annet/rulebook/texts/routeros.rul +++ b/annet/rulebook/texts/routeros.rul @@ -13,6 +13,7 @@ user + add ~ %diff_logic=routeros.user.diff ~ group add ~ diff --git a/tests/annet/test_patch/routeros_user_diff.yaml b/tests/annet/test_patch/routeros_user_diff.yaml new file mode 100644 index 00000000..e41f522c --- /dev/null +++ b/tests/annet/test_patch/routeros_user_diff.yaml @@ -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