From eaaff562a2fb27dd4557cd93759c8682ea262d8b Mon Sep 17 00:00:00 2001 From: vadvolo Date: Tue, 2 Dec 2025 23:16:25 +0000 Subject: [PATCH 1/3] [feat] add custom diff logic to mt user --- annet/rulebook/routeros/user_diff.py | 111 +++++++++++++++++++++++ annet/rulebook/texts/routeros.rul | 1 + annet/rulebook/texts/routeros.rul.backup | 45 +++++++++ 3 files changed, 157 insertions(+) create mode 100644 annet/rulebook/routeros/user_diff.py create mode 100644 annet/rulebook/texts/routeros.rul.backup diff --git a/annet/rulebook/routeros/user_diff.py b/annet/rulebook/routeros/user_diff.py new file mode 100644 index 00000000..62ac31a5 --- /dev/null +++ b/annet/rulebook/routeros/user_diff.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 user_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..208efe15 100644 --- a/annet/rulebook/texts/routeros.rul +++ b/annet/rulebook/texts/routeros.rul @@ -13,6 +13,7 @@ user + add ~ %diff_logic=routeros.user_diff.user_diff ~ group add ~ diff --git a/annet/rulebook/texts/routeros.rul.backup b/annet/rulebook/texts/routeros.rul.backup new file mode 100644 index 00000000..bd1b7b89 --- /dev/null +++ b/annet/rulebook/texts/routeros.rul.backup @@ -0,0 +1,45 @@ +# Операторы: +# * Один аргумент в undo +# ~ Несколько аргументов (минимум один) в undo +# +# Параметры: +# %global Команда действует на любом уровне ниже +# %logic=... Кастомная функция обработки правила +# %diff_logic=... Кастомная функция построения диффа. +# данная функция работает для подблоков (в отличие от %logic) +# %comment=... Добавить коммент после строки который будет видно с опцией patch --add-comments +# Сделано в основном для того чтобы генерировать специальные команды для наливки +# ----- + + +user + ~ + group + add ~ + +file + print file=* + set * %logic=routeros.file.change + +system + logging + action + set ~ + add ~ + set ~ + add ~ + +interface + gre + add + +interface + list + member + add + +ip + address + add ~ + +~ %global From d83c5542cebc030361bf760bec4014df4e0aa6d5 Mon Sep 17 00:00:00 2001 From: vadvolo Date: Tue, 2 Dec 2025 23:17:33 +0000 Subject: [PATCH 2/3] [feat] add custom diff logic to mt user --- annet/rulebook/texts/routeros.rul.backup | 45 ------------------------ 1 file changed, 45 deletions(-) delete mode 100644 annet/rulebook/texts/routeros.rul.backup diff --git a/annet/rulebook/texts/routeros.rul.backup b/annet/rulebook/texts/routeros.rul.backup deleted file mode 100644 index bd1b7b89..00000000 --- a/annet/rulebook/texts/routeros.rul.backup +++ /dev/null @@ -1,45 +0,0 @@ -# Операторы: -# * Один аргумент в undo -# ~ Несколько аргументов (минимум один) в undo -# -# Параметры: -# %global Команда действует на любом уровне ниже -# %logic=... Кастомная функция обработки правила -# %diff_logic=... Кастомная функция построения диффа. -# данная функция работает для подблоков (в отличие от %logic) -# %comment=... Добавить коммент после строки который будет видно с опцией patch --add-comments -# Сделано в основном для того чтобы генерировать специальные команды для наливки -# ----- - - -user - ~ - group - add ~ - -file - print file=* - set * %logic=routeros.file.change - -system - logging - action - set ~ - add ~ - set ~ - add ~ - -interface - gre - add - -interface - list - member - add - -ip - address - add ~ - -~ %global From 1526489b4a0c6653a28969f83132f45fd91a8cfd Mon Sep 17 00:00:00 2001 From: vadvolo Date: Mon, 29 Dec 2025 13:02:58 +0000 Subject: [PATCH 3/3] update function naming and add tests --- .../rulebook/routeros/{user_diff.py => user.py} | 2 +- annet/rulebook/texts/routeros.rul | 2 +- tests/annet/test_patch/routeros_user_diff.yaml | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) rename annet/rulebook/routeros/{user_diff.py => user.py} (97%) create mode 100644 tests/annet/test_patch/routeros_user_diff.yaml diff --git a/annet/rulebook/routeros/user_diff.py b/annet/rulebook/routeros/user.py similarity index 97% rename from annet/rulebook/routeros/user_diff.py rename to annet/rulebook/routeros/user.py index 62ac31a5..d14afd0b 100644 --- a/annet/rulebook/routeros/user_diff.py +++ b/annet/rulebook/routeros/user.py @@ -20,7 +20,7 @@ def normalize_user_line(line: str) -> str: return normalized.strip() -def user_diff(old: odict, new: odict, diff_pre: odict, _pops: tuple[Op, ...] = (Op.AFFECTED,)) -> list[DiffItem]: +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 = [] diff --git a/annet/rulebook/texts/routeros.rul b/annet/rulebook/texts/routeros.rul index 208efe15..b70f5bcf 100644 --- a/annet/rulebook/texts/routeros.rul +++ b/annet/rulebook/texts/routeros.rul @@ -13,7 +13,7 @@ user - add ~ %diff_logic=routeros.user_diff.user_diff + 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