annet 0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of annet might be problematic. Click here for more details.

Files changed (137) hide show
  1. annet/__init__.py +61 -0
  2. annet/adapters/__init__.py +0 -0
  3. annet/adapters/netbox/__init__.py +0 -0
  4. annet/adapters/netbox/common/__init__.py +0 -0
  5. annet/adapters/netbox/common/client.py +87 -0
  6. annet/adapters/netbox/common/manufacturer.py +62 -0
  7. annet/adapters/netbox/common/models.py +105 -0
  8. annet/adapters/netbox/common/query.py +23 -0
  9. annet/adapters/netbox/common/status_client.py +25 -0
  10. annet/adapters/netbox/common/storage_opts.py +14 -0
  11. annet/adapters/netbox/provider.py +34 -0
  12. annet/adapters/netbox/v24/__init__.py +0 -0
  13. annet/adapters/netbox/v24/api_models.py +73 -0
  14. annet/adapters/netbox/v24/client.py +59 -0
  15. annet/adapters/netbox/v24/storage.py +196 -0
  16. annet/adapters/netbox/v37/__init__.py +0 -0
  17. annet/adapters/netbox/v37/api_models.py +38 -0
  18. annet/adapters/netbox/v37/client.py +62 -0
  19. annet/adapters/netbox/v37/storage.py +149 -0
  20. annet/annet.py +25 -0
  21. annet/annlib/__init__.py +7 -0
  22. annet/annlib/command.py +49 -0
  23. annet/annlib/diff.py +158 -0
  24. annet/annlib/errors.py +8 -0
  25. annet/annlib/filter_acl.py +196 -0
  26. annet/annlib/jsontools.py +116 -0
  27. annet/annlib/lib.py +495 -0
  28. annet/annlib/netdev/__init__.py +0 -0
  29. annet/annlib/netdev/db.py +62 -0
  30. annet/annlib/netdev/devdb/__init__.py +28 -0
  31. annet/annlib/netdev/devdb/data/devdb.json +137 -0
  32. annet/annlib/netdev/views/__init__.py +0 -0
  33. annet/annlib/netdev/views/dump.py +121 -0
  34. annet/annlib/netdev/views/hardware.py +112 -0
  35. annet/annlib/output.py +246 -0
  36. annet/annlib/patching.py +533 -0
  37. annet/annlib/rbparser/__init__.py +0 -0
  38. annet/annlib/rbparser/acl.py +120 -0
  39. annet/annlib/rbparser/deploying.py +55 -0
  40. annet/annlib/rbparser/ordering.py +52 -0
  41. annet/annlib/rbparser/platform.py +51 -0
  42. annet/annlib/rbparser/syntax.py +115 -0
  43. annet/annlib/rulebook/__init__.py +0 -0
  44. annet/annlib/rulebook/common.py +350 -0
  45. annet/annlib/tabparser.py +648 -0
  46. annet/annlib/types.py +35 -0
  47. annet/api/__init__.py +826 -0
  48. annet/argparse.py +415 -0
  49. annet/cli.py +237 -0
  50. annet/cli_args.py +503 -0
  51. annet/configs/context.yml +18 -0
  52. annet/configs/logging.yaml +39 -0
  53. annet/connectors.py +77 -0
  54. annet/deploy.py +536 -0
  55. annet/diff.py +84 -0
  56. annet/executor.py +551 -0
  57. annet/filtering.py +40 -0
  58. annet/gen.py +865 -0
  59. annet/generators/__init__.py +435 -0
  60. annet/generators/base.py +136 -0
  61. annet/generators/common/__init__.py +0 -0
  62. annet/generators/common/initial.py +33 -0
  63. annet/generators/entire.py +97 -0
  64. annet/generators/exceptions.py +10 -0
  65. annet/generators/jsonfragment.py +125 -0
  66. annet/generators/partial.py +119 -0
  67. annet/generators/perf.py +79 -0
  68. annet/generators/ref.py +15 -0
  69. annet/generators/result.py +127 -0
  70. annet/hardware.py +45 -0
  71. annet/implicit.py +139 -0
  72. annet/lib.py +128 -0
  73. annet/output.py +167 -0
  74. annet/parallel.py +448 -0
  75. annet/patching.py +25 -0
  76. annet/reference.py +148 -0
  77. annet/rulebook/__init__.py +114 -0
  78. annet/rulebook/arista/__init__.py +0 -0
  79. annet/rulebook/arista/iface.py +16 -0
  80. annet/rulebook/aruba/__init__.py +16 -0
  81. annet/rulebook/aruba/ap_env.py +146 -0
  82. annet/rulebook/aruba/misc.py +8 -0
  83. annet/rulebook/cisco/__init__.py +0 -0
  84. annet/rulebook/cisco/iface.py +68 -0
  85. annet/rulebook/cisco/misc.py +57 -0
  86. annet/rulebook/cisco/vlandb.py +90 -0
  87. annet/rulebook/common.py +19 -0
  88. annet/rulebook/deploying.py +87 -0
  89. annet/rulebook/huawei/__init__.py +0 -0
  90. annet/rulebook/huawei/aaa.py +75 -0
  91. annet/rulebook/huawei/bgp.py +97 -0
  92. annet/rulebook/huawei/iface.py +33 -0
  93. annet/rulebook/huawei/misc.py +337 -0
  94. annet/rulebook/huawei/vlandb.py +115 -0
  95. annet/rulebook/juniper/__init__.py +107 -0
  96. annet/rulebook/nexus/__init__.py +0 -0
  97. annet/rulebook/nexus/iface.py +92 -0
  98. annet/rulebook/patching.py +143 -0
  99. annet/rulebook/ribbon/__init__.py +12 -0
  100. annet/rulebook/texts/arista.deploy +20 -0
  101. annet/rulebook/texts/arista.order +125 -0
  102. annet/rulebook/texts/arista.rul +59 -0
  103. annet/rulebook/texts/aruba.deploy +20 -0
  104. annet/rulebook/texts/aruba.order +83 -0
  105. annet/rulebook/texts/aruba.rul +87 -0
  106. annet/rulebook/texts/cisco.deploy +27 -0
  107. annet/rulebook/texts/cisco.order +82 -0
  108. annet/rulebook/texts/cisco.rul +105 -0
  109. annet/rulebook/texts/huawei.deploy +188 -0
  110. annet/rulebook/texts/huawei.order +388 -0
  111. annet/rulebook/texts/huawei.rul +471 -0
  112. annet/rulebook/texts/juniper.rul +120 -0
  113. annet/rulebook/texts/nexus.deploy +24 -0
  114. annet/rulebook/texts/nexus.order +85 -0
  115. annet/rulebook/texts/nexus.rul +83 -0
  116. annet/rulebook/texts/nokia.rul +31 -0
  117. annet/rulebook/texts/pc.order +5 -0
  118. annet/rulebook/texts/pc.rul +9 -0
  119. annet/rulebook/texts/ribbon.deploy +22 -0
  120. annet/rulebook/texts/ribbon.rul +77 -0
  121. annet/rulebook/texts/routeros.order +38 -0
  122. annet/rulebook/texts/routeros.rul +45 -0
  123. annet/storage.py +125 -0
  124. annet/tabparser.py +36 -0
  125. annet/text_term_format.py +95 -0
  126. annet/tracing.py +170 -0
  127. annet/types.py +227 -0
  128. annet-0.0.dist-info/AUTHORS +21 -0
  129. annet-0.0.dist-info/LICENSE +21 -0
  130. annet-0.0.dist-info/METADATA +26 -0
  131. annet-0.0.dist-info/RECORD +137 -0
  132. annet-0.0.dist-info/WHEEL +5 -0
  133. annet-0.0.dist-info/entry_points.txt +5 -0
  134. annet-0.0.dist-info/top_level.txt +2 -0
  135. annet_generators/__init__.py +0 -0
  136. annet_generators/example/__init__.py +12 -0
  137. annet_generators/example/lldp.py +53 -0
@@ -0,0 +1,55 @@
1
+ import functools
2
+ import re
3
+ from collections import OrderedDict as odict
4
+ from collections import namedtuple
5
+
6
+ Answer = namedtuple("Answer", ("text", "send_nl"))
7
+
8
+
9
+ def compile_messages(tree):
10
+ ignore = []
11
+ dialogs = odict()
12
+ for attrs in tree.values():
13
+ if attrs["type"] == "normal":
14
+ match = re.match(r"^ignore:(.+)$", attrs["row"])
15
+ if match:
16
+ ignore.append(MakeMessageMatcher(match.group(1)))
17
+ continue
18
+
19
+ match = re.match(r"^dialog:(.+):::(.+)$", attrs["row"])
20
+ if match:
21
+ dialogs[MakeMessageMatcher(match.group(1))] = Answer(
22
+ text=match.group(2).strip(),
23
+ send_nl=attrs["params"]["send_nl"],
24
+ )
25
+ return (ignore, dialogs)
26
+
27
+
28
+ class MakeMessageMatcher:
29
+ def __init__(self, text):
30
+ text = text.strip()
31
+ self._text = text
32
+ if text.startswith("/") and text.endswith("/"):
33
+ regexp = re.compile(text[1:-1].strip(), flags=re.I)
34
+ self._fn = regexp.match
35
+ else:
36
+ self._fn = (lambda arg: _simplify_text(text) in _simplify_text(arg))
37
+
38
+ def __str__(self):
39
+ return "%s(%r)" % (self.__class__.__name__, self._text)
40
+
41
+ __repr__ = __str__
42
+
43
+ def __call__(self, intext):
44
+ return self._fn(intext)
45
+
46
+ def __eq__(self, other):
47
+ return type(other) is type(self) and self._text == other._text # pylint: disable=protected-access
48
+
49
+ def __hash__(self):
50
+ return hash("%s_%s" % (self.__class__.__name__, self._text))
51
+
52
+
53
+ @functools.lru_cache()
54
+ def _simplify_text(text):
55
+ return re.sub(r"\s", "", text).lower()
@@ -0,0 +1,52 @@
1
+ import functools
2
+ import re
3
+ from collections import OrderedDict as odict
4
+
5
+ from valkit.common import valid_bool
6
+
7
+ from . import platform, syntax
8
+
9
+
10
+ # =====
11
+ @functools.lru_cache()
12
+ def compile_ordering_text(text, vendor):
13
+ return _compile_ordering(
14
+ tree=syntax.parse_text(text, params_scheme={
15
+ "order_reverse": {
16
+ "validator": valid_bool,
17
+ "default": False,
18
+ },
19
+ }),
20
+ reverse_prefix=platform.VENDOR_REVERSES[vendor],
21
+ )
22
+
23
+
24
+ def decompile_ordering_rulebook(rb) -> str:
25
+ def _decompile_ordering_text(rb, level):
26
+ indent = " "
27
+ for attrs in rb.values():
28
+ yield indent * level + attrs["attrs"]["raw_rule"]
29
+ yield from _decompile_ordering_text(attrs["children"], level + 1)
30
+ return "\n".join(_decompile_ordering_text(rb, 0))
31
+
32
+
33
+ # =====
34
+ def _compile_ordering(tree, reverse_prefix):
35
+ ordering = odict()
36
+ for (rule_id, attrs) in tree.items():
37
+ if attrs["type"] == "normal":
38
+ ordering[rule_id] = {
39
+ "attrs": {
40
+ "direct_regexp": syntax.compile_row_regexp(attrs["row"]),
41
+ "reverse_regexp": (
42
+ syntax.compile_row_regexp(reverse_prefix + " " + attrs["row"])
43
+ if not attrs["row"].startswith(reverse_prefix + " ") else
44
+ syntax.compile_row_regexp(re.sub(r"^%s\s+" % (reverse_prefix), "", attrs["row"]))
45
+ ),
46
+ "order_reverse": attrs["params"]["order_reverse"],
47
+ "raw_rule": attrs["raw_rule"],
48
+ "context": attrs["context"],
49
+ },
50
+ "children": _compile_ordering(attrs["children"], reverse_prefix),
51
+ }
52
+ return ordering
@@ -0,0 +1,51 @@
1
+ VENDOR_REVERSES = {
2
+ "huawei": "undo",
3
+ "cisco": "no",
4
+ "nexus": "no",
5
+ "juniper": "delete",
6
+ "arista": "no",
7
+ "nokia": "delete",
8
+ "routeros": "remove",
9
+ "aruba": "no",
10
+ "pc": "-",
11
+ "ribbon": "delete",
12
+ }
13
+
14
+ VENDOR_DIFF = {
15
+ "huawei": "common.default_diff",
16
+ "cisco": "common.default_diff",
17
+ "nexus": "common.default_diff",
18
+ "juniper": "juniper.default_diff",
19
+ "arista": "common.default_diff",
20
+ "nokia": "juniper.default_diff",
21
+ "routeros": "common.default_diff",
22
+ "aruba": "aruba.default_diff",
23
+ "pc": "common.default_diff",
24
+ "ribbon": "ribbon.default_diff",
25
+ }
26
+
27
+ VENDOR_DIFF_ORDERED = {
28
+ "huawei": "common.ordered_diff",
29
+ "cisco": "common.ordered_diff",
30
+ "nexus": "common.ordered_diff",
31
+ "juniper": "juniper.ordered_diff",
32
+ "arista": "common.ordered_diff",
33
+ "nokia": "juniper.ordered_diff",
34
+ "routeros": "common.ordered_diff",
35
+ "aruba": "common.ordered_diff",
36
+ "pc": "common.ordered_diff",
37
+ "ribbon": "ribbon.default_diff",
38
+ }
39
+
40
+ VENDOR_EXIT = {
41
+ "huawei": "quit",
42
+ "cisco": "exit",
43
+ "nexus": "exit",
44
+ "arista": "exit",
45
+ "juniper": "",
46
+ "nokia": "",
47
+ "routeros": "",
48
+ "aruba": "exit",
49
+ "pc": "",
50
+ "ribbon": "exit",
51
+ }
@@ -0,0 +1,115 @@
1
+ import functools
2
+ import re
3
+ from collections import OrderedDict as odict
4
+
5
+ from annet.annlib import lib, tabparser
6
+
7
+
8
+ # =====
9
+ def parse_text(text, params_scheme):
10
+ return _parse_tree_with_params(tabparser.parse_to_tree(text, _split_rows, ["#"]), params_scheme)
11
+
12
+
13
+ @functools.lru_cache()
14
+ def compile_row_regexp(row, flags=0):
15
+ if "(?i)" in row:
16
+ row = row.replace("(?i)", "")
17
+ flags |= re.IGNORECASE
18
+
19
+ if "*" in row:
20
+ row = re.sub(r"\(([^\?])", r"(?:\1", row) # Все дефолтные группы превратить в non-captured
21
+ row = re.sub(r"\*/(\S+)/", r"(\1)", row) # */{regex_one_word}/ -> ({regex_one_word})
22
+ row = re.sub(r"(^|\s)\*", r"\1([^\\s]+)", row)
23
+
24
+ # Заменяем <someting> на named-группы
25
+ row = re.sub(r"<(\w+)>", r"(?P<\1>\\w+)", row)
26
+
27
+ if row.endswith("~"):
28
+ # We determine the most specific regex for the row at matching in match_row_to_acls
29
+ row = row[:-1] + "(.+)"
30
+ elif row.endswith("..."):
31
+ row = row[:-3]
32
+ elif "~/" in row:
33
+ # ~/{regex}/ -> {regex}, () не нужны поскольку уже (?:) - non-captured
34
+ row = re.sub(r"~/([^/]+)/", r"\1", row)
35
+ else:
36
+ row += r"(?:\s|$)"
37
+ row = re.sub(r"\s+", r"\\s+", row)
38
+ return re.compile("^" + row, flags=flags)
39
+
40
+
41
+ # =====
42
+ def _split_rows(text):
43
+ for row in re.split(r"\n(?!\s*%(?!context))", text):
44
+ yield row.replace("\n", " ")
45
+
46
+
47
+ def _parse_tree_with_params(raw_tree, scheme, context=None):
48
+ tree = odict()
49
+ if context is None:
50
+ context = {}
51
+ for (raw_rule, children) in raw_tree.items():
52
+ (row, params) = _parse_raw_rule(raw_rule, scheme)
53
+ row_type = "normal"
54
+ if row.startswith("!"):
55
+ row = row[1:].strip()
56
+ if len(row) == 0:
57
+ continue
58
+ row_type = "ignore"
59
+ elif row.startswith(r"%context="):
60
+ context = _parse_context(context, row)
61
+ continue
62
+ tree[raw_rule] = {
63
+ "row": row,
64
+ "type": row_type,
65
+ "params": params,
66
+ "children": _parse_tree_with_params(children, scheme, context.copy()),
67
+ "raw_rule": raw_rule,
68
+ "context": context.copy(),
69
+ }
70
+ return tree
71
+
72
+
73
+ def _parse_raw_rule(raw_rule, scheme):
74
+ try:
75
+ index = raw_rule.index("%")
76
+ params = {
77
+ key: (value if len(value) != 0 else "1")
78
+ for (key, value) in re.findall(r"\s%([a-zA-Z_]\w*)(?:=([^\s]*))?", raw_rule)
79
+ }
80
+ if params:
81
+ raw_rule = raw_rule[:index].strip()
82
+ except ValueError:
83
+ params = {}
84
+
85
+ row = re.sub(r"\s+", " ", raw_rule.strip())
86
+ params = _fill_and_validate(params, scheme, raw_rule)
87
+ return (row, params)
88
+
89
+
90
+ def _fill_and_validate(params, scheme, raw_rule):
91
+ return {
92
+ key: (attrs["validator"](params[key]) if key in params else (
93
+ attrs["default"](raw_rule) if callable(attrs["default"]) else attrs["default"]
94
+ ))
95
+ for (key, attrs) in scheme.items()
96
+ }
97
+
98
+
99
+ def match_context(ifcontext, context):
100
+ if not ifcontext:
101
+ return True
102
+ for ifcontext_value in ifcontext:
103
+ name, value = ifcontext_value.split(":")
104
+ if name in context:
105
+ if context[name] == value:
106
+ return True
107
+ return False
108
+
109
+
110
+ def _parse_context(context, row):
111
+ keyword = r"%context="
112
+ if not row.startswith(keyword):
113
+ raise ValueError(row)
114
+ name, value = row[len(keyword):].strip().split(":")
115
+ return lib.merge_dicts(context, {name: value})
File without changes
@@ -0,0 +1,350 @@
1
+ import os
2
+ import typing
3
+ from collections import OrderedDict as odict
4
+
5
+ from annet.annlib.command import CommandList, Command
6
+
7
+ from annet.types import Op
8
+
9
+
10
+ # =====
11
+ def default(rule, key, diff, **_):
12
+ r"""
13
+ Функция default() обеспичвает базовую логику обработки всех правил. Ее можно заменить с помощью
14
+ параметра %logic в текстовом рулбуке. Она вызывается для каждой команды с уникальным ключом и
15
+ должна возвратить сгенерированный текст патча на основе предоставленного диффа, и, при необходимости,
16
+ вызвать обработку дочерних правил/данных.
17
+
18
+ Первым аргументом (rule) она принимает словарь с правилом:
19
+ {
20
+ # Однострочная команда, не блок, не имеет чилдов
21
+ "logic": <function default at 0x7fe22ea83510>, # Функция для обработки правила
22
+ "provides": [], # Макросы, реализуемые этим правилом
23
+ "requires": [], # Макросы, требуемые для правила
24
+
25
+ # Регулярка для разбора строки
26
+ "regexp": re.compile(r"^snmp-agent\s+sys-info\s+([^\s]+).*$"),
27
+
28
+ # Шаблон для отмены команды (в качестве аргументов следует использовать ключ)
29
+ "reverse": "undo snmp-agent sys-info {}",
30
+ }
31
+
32
+ Вторым аргументом (key) идет tuple, состоящий из ключа, распаршенного из строчки с помощью regexp:
33
+ ("contact",) # Пример для разбора строки "snmp-agent sys-info contact"
34
+
35
+ В третий аргумент передается словарь с диффом:
36
+ {
37
+ # Команды/блоки, добавленные в новой конфигурации
38
+ Op.ADDED: [{"children": None, "row": "undo snmp-agent sys-info version all"}],
39
+
40
+ # Бывает только в блоках, содержит изменившихся чилдов внутри блоков
41
+ Op.AFFECTED: [],
42
+
43
+ # Удаленные команды/блоки
44
+ Op.REMOVED: [{"children": None, "row": "undo snmp-agent sys-info version v3"}],
45
+
46
+ # Команды которые никак не изменились (но иногда нужны для других команд)
47
+ Op.UNCHANGED: [{"children": None, "row": "snmp all-interfaces"}]
48
+ }
49
+ """
50
+ for op in [Op.ADDED, Op.REMOVED, Op.AFFECTED, Op.MOVED]:
51
+ # Дефолтная функция генерации патчей считает, что не бывает команд с одинаковыми
52
+ # ключами и разным значением. при этом unchanged мы так не проверяем поскольку
53
+ # такие случаи возможны когда у нас подмешиваются implicit команды
54
+ assert 0 <= len(diff[op]) <= 1, "Too many %s actions for rows %r" % (op, [x["row"] for x in diff[op]])
55
+ if diff[Op.AFFECTED]:
56
+ # При изменении блока нужно вызвать обработку чилдов
57
+ yield (True, diff[Op.AFFECTED][0]["row"], diff[Op.AFFECTED][0]["children"])
58
+ elif diff[Op.ADDED] or diff[Op.MOVED]:
59
+ key = Op.ADDED if diff.get(Op.ADDED) else Op.MOVED
60
+ # При модификации строки удаление нас не интересует, добавление проходит как affected
61
+ yield (True, diff[key][0]["row"], diff[key][0]["children"])
62
+ elif diff[Op.REMOVED]:
63
+ # При удалении или перемещеннии блока просто снести строку
64
+ yield (False, rule["reverse"].format(*key), None)
65
+
66
+
67
+ def ordered(rule, key, diff, **kwargs):
68
+ if diff[Op.MOVED]:
69
+ # Сносим top-level блок
70
+ yield (False, rule["reverse"].format(*key), None)
71
+ # Дальше Op.MOVED будут пересозданы заново в новом порядке
72
+ # FIXME вообще-то следовало бы удалять REMOVED из чайлдов
73
+ # поскольку блок уже очищен и пересоздается заново
74
+ yield from default(rule, key, diff, **kwargs)
75
+
76
+
77
+ def rewrite(rule, key, diff, **kwargs):
78
+ # Переписывает блок игнорируя предыдущее его состояние
79
+ if not diff[Op.REMOVED]:
80
+ yield from default(rule, key, diff, **kwargs)
81
+
82
+
83
+ def permanent(rule, key, diff, **kwargs):
84
+ # Данный блок не подлежат удалению
85
+ if diff[Op.REMOVED]:
86
+ # Если он отдельный - просто игнорируем
87
+ if not diff[Op.REMOVED][0]["children"]:
88
+ return
89
+ # Если у него есть потомки - сделаем их affected
90
+ diff[Op.AFFECTED] += diff[Op.REMOVED]
91
+ diff[Op.REMOVED] = []
92
+ yield from default(rule, key, diff, **kwargs)
93
+
94
+
95
+ def ignore_changes(rule, key, diff, **kwargs):
96
+ """
97
+ logic-функция, которая удаляет или добавляет строки, но не меняет одну на другую.
98
+ """
99
+ if diff[Op.ADDED] and diff[Op.REMOVED]:
100
+ pass
101
+ else:
102
+ yield from default(rule, key, diff, **kwargs)
103
+
104
+
105
+ def undo_redo(rule, key, diff, **_):
106
+ """
107
+ Если команда отменяется через undo key, но не может быть заменена через
108
+ key value, а требует сначала undo key, а уж потом - key value,
109
+ этот хелпер делает именно так: сначала undo key, потом - key value
110
+ """
111
+ if not (diff[Op.ADDED] and diff[Op.REMOVED] and not diff[Op.AFFECTED]):
112
+ yield from default(rule, key, diff)
113
+ else:
114
+ for side in [Op.REMOVED, Op.ADDED]:
115
+ new_diff = {op: [] for op in diff.keys()}
116
+ new_diff[side] = diff[side]
117
+ yield from default(rule, key, new_diff)
118
+
119
+
120
+ def default_instead_undo(rule, key, diff, **_):
121
+ # Для ряда конфигурационных строк возникает вечный diff, поскольку в конфиге строка либо явно включена,
122
+ # либо явно выключена. Если она не описана в генераторе, т.е. мы полагаемся на дефолт, то используя default
123
+ # вместо "no ..." мы возвращаем конфиг в дефолтное состояние.
124
+ # NOC-20503 @lesnix 11-08-2022
125
+ if diff[Op.REMOVED]:
126
+ rule["reverse"] = rule["reverse"].replace("no", "default")
127
+ yield from default(rule, key, diff)
128
+
129
+
130
+ # =====
131
+ class DiffItem(typing.NamedTuple):
132
+ op: str
133
+ row: str
134
+ children: typing.List["DiffItem"]
135
+ diff_pre: typing.Dict[str, typing.Any]
136
+
137
+
138
+ def default_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
139
+ diff = base_diff(old, new, diff_pre, _pops, moved_to_affected=True)
140
+ return diff
141
+
142
+
143
+ def ordered_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
144
+ diff = base_diff(old, new, diff_pre, _pops, moved_to_affected=False)
145
+ return diff
146
+
147
+
148
+ def rewrite_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
149
+ def iter_diff(
150
+ diff: typing.List[DiffItem],
151
+ ) -> typing.Iterable[typing.Tuple[int, typing.List[DiffItem]]]:
152
+ queue = [diff]
153
+ while queue:
154
+ items, queue = queue[0], queue[1:]
155
+ for i, item in enumerate(items):
156
+ yield i, items
157
+ queue.append(item.children)
158
+
159
+ # оставляем маркер чтобы увидеть что мы - старший rewrite
160
+ rewrite_marker = "rewrite"
161
+ rewrite_tail = (rewrite_marker, _pops[-1])
162
+ _pops = _pops + rewrite_tail
163
+ diff = base_diff(old, new, diff_pre, _pops, moved_to_affected=False)
164
+ # если мы rewrite верхнего уровня, и в поддереве все Op.AFFECTED
165
+ # то есть не было совершенно никаких изменений, удаляем его из дифа
166
+ if rewrite_marker not in _pops[:-len(rewrite_tail)]:
167
+ if all(its[i].op == Op.AFFECTED for i, its in iter_diff(diff)):
168
+ diff.clear()
169
+ else:
170
+ for i, items in iter_diff(diff):
171
+ if items[i].op == Op.AFFECTED:
172
+ items[i] = items[i]._replace(op=Op.MOVED)
173
+ return diff
174
+
175
+
176
+ def multiline_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
177
+ """
178
+ Особая логика diff'a для хуавейных мультилайнов.
179
+ Она трактует все дочерние элементы %multiline-команды как
180
+ одну общую команду, покидывая внутрь тот Op который был
181
+ определен на вернем уровне
182
+ """
183
+ def process_multiline(op, tree):
184
+ for (row, children) in tree.items():
185
+ yield (op, row, list(process_multiline(op, children)), None)
186
+ ret = []
187
+ for item in default_diff(old, new, diff_pre, _pops):
188
+ if old.get(item.row, {}) == new.get(item.row, {}):
189
+ continue
190
+ op, tree = Op.ADDED, new
191
+ if item.op == Op.REMOVED:
192
+ op, tree = Op.REMOVED, old
193
+ children = list(process_multiline(op, tree[item.row]))
194
+ ret.append(DiffItem(item.op, item.row, children, item.diff_pre))
195
+ return ret
196
+
197
+
198
+ def base_diff(old, new, diff_pre, pops, moved_to_affected=False) -> typing.List[DiffItem]:
199
+ diff_indexed: typing.List[typing.Tuple[int, DiffItem]] = []
200
+ old = _ignore_case(diff_pre, old)
201
+ new = _ignore_case(diff_pre, new)
202
+
203
+ for (index, row) in enumerate(old):
204
+ if row not in new:
205
+ children = call_diff_logic(diff_pre[row]["subtree"], old[row], {}, pops + (Op.REMOVED,))
206
+ diff_indexed.append((index, DiffItem(
207
+ op=Op.REMOVED,
208
+ row=row,
209
+ children=children,
210
+ diff_pre=diff_pre[row]["match"],
211
+ )))
212
+ old_indexes = {row: index for (index, row) in enumerate(old)}
213
+ block_in_disorder = False
214
+ parent_op = pops[-1]
215
+ for (index, row) in enumerate(new):
216
+ if row not in old:
217
+ block_in_disorder = True
218
+ op = Op.ADDED
219
+ elif block_in_disorder or index != old_indexes[row]:
220
+ block_in_disorder = True
221
+ op = (Op.MOVED if not moved_to_affected else parent_op)
222
+ else:
223
+ op = parent_op
224
+ children = call_diff_logic(diff_pre[row]["subtree"], old.get(row, {}), new[row], pops + (op,))
225
+ diff_indexed.append((index, DiffItem(
226
+ op=op,
227
+ row=row,
228
+ children=children,
229
+ diff_pre=diff_pre[row]["match"],
230
+ )))
231
+ diff_indexed.sort()
232
+ return [x[1] for x in diff_indexed]
233
+
234
+
235
+ def call_diff_logic(diff_pre, old, new, pops=(Op.AFFECTED,)):
236
+ """
237
+ Группируем команды в старом и новом конфиге согласно выставленным
238
+ в рулбуке атрибутам %diff_logic и вызывает их поочереди согласно
239
+ порядку команд в old и new, предпочитая old (т.е. сначала удаления)
240
+ """
241
+ diff_logics = odict()
242
+ for row in old:
243
+ logic = diff_pre[row]["match"]["attrs"]["diff_logic"]
244
+ if logic not in diff_logics:
245
+ diff_logics[logic] = (odict(), odict())
246
+ diff_logics[logic][0][row] = old[row]
247
+ for row in new:
248
+ logic = diff_pre[row]["match"]["attrs"]["diff_logic"]
249
+ if logic not in diff_logics:
250
+ diff_logics[logic] = (odict(), odict())
251
+ diff_logics[logic][1][row] = new[row]
252
+ ret = []
253
+ for logic, (old, new) in diff_logics.items():
254
+ ret.extend(logic(old=old, new=new, diff_pre=diff_pre, _pops=pops))
255
+ return ret
256
+
257
+
258
+ def _ignore_case(diff_pre, cfg):
259
+ has_ignore_case = False
260
+ for row in diff_pre:
261
+ if diff_pre[row]["match"]["attrs"]["ignore_case"]:
262
+ has_ignore_case = True
263
+ if not has_ignore_case:
264
+ return cfg
265
+
266
+ ret = cfg.__class__()
267
+ for row in cfg:
268
+ new_row = row
269
+ if diff_pre[row]["match"]["attrs"]["ignore_case"]:
270
+ new_row = row.lower()
271
+ ret[new_row] = cfg[row]
272
+ diff_pre[new_row] = diff_pre[row]
273
+ return ret
274
+
275
+
276
+ # ====
277
+
278
+ class ApplyItem(typing.NamedTuple):
279
+ before: CommandList
280
+ after: CommandList
281
+
282
+
283
+ def apply(hw, do_commit, do_finalize, **_):
284
+ before, after = CommandList(), CommandList()
285
+ if hw.Huawei:
286
+ before.add_cmd(Command("system-view"))
287
+ if do_commit and (hw.Huawei.CE or hw.Huawei.NE):
288
+ after.add_cmd(Command("commit"))
289
+ after.add_cmd(Command("q"))
290
+ if do_finalize:
291
+ after.add_cmd(Command("save", timeout=20))
292
+ elif hw.Arista:
293
+ before.add_cmd(Command("conf s"))
294
+ if do_commit:
295
+ after.add_cmd(Command("commit"))
296
+ else:
297
+ after.add_cmd(Command("abort")) # просто exit оставит висеть configure session
298
+ if do_finalize:
299
+ after.add_cmd(Command("write memory"))
300
+ elif hw.ASR or hw.XRV:
301
+ # коммит сам сохраняет изменения в стартап do_finalize не применим
302
+ before.add_cmd(Command("configure exclusive"))
303
+ if do_commit:
304
+ after.add_cmd(Command("commit"))
305
+ after.add_cmd(Command("exit"))
306
+ elif hw.Cisco or hw.Nexus:
307
+ # классический ios и nxos не умеет коммиты
308
+ before.add_cmd(Command("conf t"))
309
+ after.add_cmd(Command("exit"))
310
+ if do_finalize:
311
+ after.add_cmd(Command("copy running-config startup-config", timeout=40))
312
+ elif hw.Juniper:
313
+ # коммит сам сохраняет изменения в стартап do_finalize не применим
314
+ before.add_cmd(Command("configure exclusive"))
315
+ if do_commit:
316
+ after.add_cmd(Command("commit", timeout=30))
317
+ after.add_cmd(Command("exit"))
318
+ elif hw.PC:
319
+ if hw.soft.startswith(("Cumulus", "SwitchDev")):
320
+ if os.environ.get("ETCKEEPER_CHECK", False):
321
+ before.add_cmd(Command("etckeeper check"))
322
+ elif hw.Nokia:
323
+ # коммит сам сохраняет изменения в стартап do_finalize не применим
324
+ before.add_cmd(Command("configure private"))
325
+ if do_commit:
326
+ after.add_cmd(Command("commit"))
327
+ elif hw.RouterOS:
328
+ # FIXME: пока не удалось победить \x1b[c после включения safe mode
329
+ # if len(cmds) > 99:
330
+ # raise Exception("RouterOS does not support more 100 actions in safe mode")
331
+ # before.add_cmd(RosDevice.SAFE_MODE)
332
+ pass
333
+ # after.add_cmd(RosDevice.SAFE_MODE)
334
+ elif hw.Aruba:
335
+ before.add_cmd(Command("conf t"))
336
+ after.add_cmd(Command("end"))
337
+ if do_commit:
338
+ after.add_cmd(Command("commit apply"))
339
+ if do_finalize:
340
+ after.add_cmd(Command("write memory"))
341
+ elif hw.Ribbon:
342
+ # коммит сам сохраняет изменения в стартап do_finalize не применим
343
+ before.add_cmd(Command("configure exclusive"))
344
+ if do_commit:
345
+ after.add_cmd(Command("commit", timeout=30))
346
+ after.add_cmd(Command("exit"))
347
+ else:
348
+ raise Exception("unknown hw %s" % hw)
349
+
350
+ return before, after