annet 0.1__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.
- annet/__init__.py +61 -0
- annet/annet.py +25 -0
- annet/annlib/__init__.py +7 -0
- annet/annlib/command.py +49 -0
- annet/annlib/diff.py +158 -0
- annet/annlib/errors.py +8 -0
- annet/annlib/filter_acl.py +196 -0
- annet/annlib/jsontools.py +89 -0
- annet/annlib/lib.py +495 -0
- annet/annlib/netdev/__init__.py +0 -0
- annet/annlib/netdev/db.py +62 -0
- annet/annlib/netdev/devdb/__init__.py +28 -0
- annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet/annlib/netdev/views/__init__.py +0 -0
- annet/annlib/netdev/views/dump.py +121 -0
- annet/annlib/netdev/views/hardware.py +112 -0
- annet/annlib/output.py +246 -0
- annet/annlib/patching.py +533 -0
- annet/annlib/rbparser/__init__.py +0 -0
- annet/annlib/rbparser/acl.py +120 -0
- annet/annlib/rbparser/deploying.py +55 -0
- annet/annlib/rbparser/ordering.py +52 -0
- annet/annlib/rbparser/platform.py +51 -0
- annet/annlib/rbparser/syntax.py +115 -0
- annet/annlib/rulebook/__init__.py +0 -0
- annet/annlib/rulebook/common.py +350 -0
- annet/annlib/tabparser.py +648 -0
- annet/annlib/types.py +35 -0
- annet/api/__init__.py +807 -0
- annet/argparse.py +415 -0
- annet/cli.py +192 -0
- annet/cli_args.py +493 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +64 -0
- annet/deploy.py +441 -0
- annet/diff.py +85 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +828 -0
- annet/generators/__init__.py +987 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +170 -0
- annet/parallel.py +448 -0
- annet/patching.py +25 -0
- annet/reference.py +148 -0
- annet/rulebook/__init__.py +114 -0
- annet/rulebook/arista/__init__.py +0 -0
- annet/rulebook/arista/iface.py +16 -0
- annet/rulebook/aruba/__init__.py +16 -0
- annet/rulebook/aruba/ap_env.py +146 -0
- annet/rulebook/aruba/misc.py +8 -0
- annet/rulebook/cisco/__init__.py +0 -0
- annet/rulebook/cisco/iface.py +68 -0
- annet/rulebook/cisco/misc.py +57 -0
- annet/rulebook/cisco/vlandb.py +90 -0
- annet/rulebook/common.py +19 -0
- annet/rulebook/deploying.py +87 -0
- annet/rulebook/huawei/__init__.py +0 -0
- annet/rulebook/huawei/aaa.py +75 -0
- annet/rulebook/huawei/bgp.py +97 -0
- annet/rulebook/huawei/iface.py +33 -0
- annet/rulebook/huawei/misc.py +337 -0
- annet/rulebook/huawei/vlandb.py +115 -0
- annet/rulebook/juniper/__init__.py +107 -0
- annet/rulebook/nexus/__init__.py +0 -0
- annet/rulebook/nexus/iface.py +92 -0
- annet/rulebook/patching.py +143 -0
- annet/rulebook/ribbon/__init__.py +12 -0
- annet/rulebook/texts/arista.deploy +20 -0
- annet/rulebook/texts/arista.order +125 -0
- annet/rulebook/texts/arista.rul +59 -0
- annet/rulebook/texts/aruba.deploy +20 -0
- annet/rulebook/texts/aruba.order +83 -0
- annet/rulebook/texts/aruba.rul +87 -0
- annet/rulebook/texts/cisco.deploy +27 -0
- annet/rulebook/texts/cisco.order +82 -0
- annet/rulebook/texts/cisco.rul +105 -0
- annet/rulebook/texts/huawei.deploy +188 -0
- annet/rulebook/texts/huawei.order +388 -0
- annet/rulebook/texts/huawei.rul +471 -0
- annet/rulebook/texts/juniper.rul +120 -0
- annet/rulebook/texts/nexus.deploy +24 -0
- annet/rulebook/texts/nexus.order +85 -0
- annet/rulebook/texts/nexus.rul +83 -0
- annet/rulebook/texts/nokia.rul +31 -0
- annet/rulebook/texts/pc.order +5 -0
- annet/rulebook/texts/pc.rul +9 -0
- annet/rulebook/texts/ribbon.deploy +22 -0
- annet/rulebook/texts/ribbon.rul +77 -0
- annet/rulebook/texts/routeros.order +38 -0
- annet/rulebook/texts/routeros.rul +45 -0
- annet/storage.py +121 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +223 -0
- annet-0.1.dist-info/AUTHORS +21 -0
- annet-0.1.dist-info/LICENSE +21 -0
- annet-0.1.dist-info/METADATA +24 -0
- annet-0.1.dist-info/RECORD +113 -0
- annet-0.1.dist-info/WHEEL +5 -0
- annet-0.1.dist-info/entry_points.txt +6 -0
- annet-0.1.dist-info/top_level.txt +3 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +52 -0
- annet_nbexport/__init__.py +220 -0
- annet_nbexport/main.py +46 -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
|