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.
- annet/__init__.py +61 -0
- annet/adapters/__init__.py +0 -0
- annet/adapters/netbox/__init__.py +0 -0
- annet/adapters/netbox/common/__init__.py +0 -0
- annet/adapters/netbox/common/client.py +87 -0
- annet/adapters/netbox/common/manufacturer.py +62 -0
- annet/adapters/netbox/common/models.py +105 -0
- annet/adapters/netbox/common/query.py +23 -0
- annet/adapters/netbox/common/status_client.py +25 -0
- annet/adapters/netbox/common/storage_opts.py +14 -0
- annet/adapters/netbox/provider.py +34 -0
- annet/adapters/netbox/v24/__init__.py +0 -0
- annet/adapters/netbox/v24/api_models.py +73 -0
- annet/adapters/netbox/v24/client.py +59 -0
- annet/adapters/netbox/v24/storage.py +196 -0
- annet/adapters/netbox/v37/__init__.py +0 -0
- annet/adapters/netbox/v37/api_models.py +38 -0
- annet/adapters/netbox/v37/client.py +62 -0
- annet/adapters/netbox/v37/storage.py +149 -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 +116 -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 +826 -0
- annet/argparse.py +415 -0
- annet/cli.py +237 -0
- annet/cli_args.py +503 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +77 -0
- annet/deploy.py +536 -0
- annet/diff.py +84 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +865 -0
- annet/generators/__init__.py +435 -0
- annet/generators/base.py +136 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/generators/entire.py +97 -0
- annet/generators/exceptions.py +10 -0
- annet/generators/jsonfragment.py +125 -0
- annet/generators/partial.py +119 -0
- annet/generators/perf.py +79 -0
- annet/generators/ref.py +15 -0
- annet/generators/result.py +127 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +167 -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 +125 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +227 -0
- annet-0.0.dist-info/AUTHORS +21 -0
- annet-0.0.dist-info/LICENSE +21 -0
- annet-0.0.dist-info/METADATA +26 -0
- annet-0.0.dist-info/RECORD +137 -0
- annet-0.0.dist-info/WHEEL +5 -0
- annet-0.0.dist-info/entry_points.txt +5 -0
- annet-0.0.dist-info/top_level.txt +2 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +53 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
from annet.rulebook.common import default, default_diff
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def user(key, diff, **_):
|
|
9
|
+
check_for_remove = True
|
|
10
|
+
added = []
|
|
11
|
+
for add in diff[Op.ADDED]:
|
|
12
|
+
added.append((True, add["row"], None))
|
|
13
|
+
if add["row"].startswith("local-user %s password" % key[0]):
|
|
14
|
+
check_for_remove = False
|
|
15
|
+
if check_for_remove:
|
|
16
|
+
for rem in diff[Op.REMOVED]:
|
|
17
|
+
# Обрабатывать удаление только пароля, если меняется что-то другое, можно просто накатить без удаления
|
|
18
|
+
if rem["row"].startswith("local-user %s password" % key[0]):
|
|
19
|
+
yield (False, "undo local-user %s" % key[0], None)
|
|
20
|
+
return
|
|
21
|
+
yield from added
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def domain(rule, key, diff, **_):
|
|
25
|
+
"""
|
|
26
|
+
При удалении метода для accounting|authorization|authentication
|
|
27
|
+
не нужно указывать сам метод, поэтому откидываем последний ключ.
|
|
28
|
+
"""
|
|
29
|
+
if diff[Op.REMOVED]:
|
|
30
|
+
yield (False, rule["reverse"].format(key[0], ""), None)
|
|
31
|
+
else:
|
|
32
|
+
yield from default(rule, key, diff)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def local_user_diff(old, new, diff_pre, **kwargs):
|
|
36
|
+
diff = default_diff(old, new, diff_pre, **kwargs)
|
|
37
|
+
filtered_diff = []
|
|
38
|
+
# Группируем команды local-user по пользователю
|
|
39
|
+
# и назначению (mode будет "password", "service-type", etc.)
|
|
40
|
+
# {("username", "mode"): {op: diff_item}}}
|
|
41
|
+
grouped = defaultdict(dict)
|
|
42
|
+
for diff_item in diff:
|
|
43
|
+
username, mode = _local_user_row_key(diff_item.row)
|
|
44
|
+
if username and mode:
|
|
45
|
+
grouped[(username, mode)][diff_item.op] = diff_item
|
|
46
|
+
|
|
47
|
+
filtered_diff = []
|
|
48
|
+
for diff_item in diff:
|
|
49
|
+
username, mode = _local_user_row_key(diff_item.row)
|
|
50
|
+
if username and mode:
|
|
51
|
+
ops = set(grouped[(username, mode)])
|
|
52
|
+
# NOCDEVDUTY-1786 делаем так чтобы в генераторе не требовалось точно попасть в порядок service-type
|
|
53
|
+
# у хуавей порядок аргументов в данном месте меняется в зависимости от версии софта
|
|
54
|
+
# при этом команда принимается в любом виде, меняется отображение в конфиге, вводить ее можно как угодно
|
|
55
|
+
# поэтому если команды local-user * service-type ... совпадают с точностью до перестановки то ничего не правим
|
|
56
|
+
if mode == "service-type" and ops == {Op.ADDED, Op.REMOVED}:
|
|
57
|
+
added = set(grouped[(username, mode)][Op.ADDED].row.split())
|
|
58
|
+
removed = set(grouped[(username, mode)][Op.REMOVED].row.split())
|
|
59
|
+
if added == removed:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
filtered_diff.append(diff_item)
|
|
63
|
+
|
|
64
|
+
return filtered_diff
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _local_user_row_key(row):
|
|
68
|
+
username, mode = None, None
|
|
69
|
+
splitted_row = row.split()
|
|
70
|
+
# Ожидаемый формат команды 'local-user <username> <mode> ...'
|
|
71
|
+
if splitted_row and splitted_row[0] == "local-user":
|
|
72
|
+
if len(splitted_row) >= 3:
|
|
73
|
+
username = splitted_row[1]
|
|
74
|
+
mode = splitted_row[2]
|
|
75
|
+
return username, mode
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
from annet.rulebook import common
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def undo_commit(rule, key, diff, **_):
|
|
9
|
+
# Huawei не даёт снести конфигурацию bgp и написать заново одним коммитом. Говорит:
|
|
10
|
+
# Invalid configuration. BGP is under undo.
|
|
11
|
+
# при попытке создать новую после удаления
|
|
12
|
+
if diff[Op.REMOVED]:
|
|
13
|
+
rule["force_commit"] = True
|
|
14
|
+
yield (False, rule["reverse"], None)
|
|
15
|
+
# commit нужен под undo bgp
|
|
16
|
+
rule["force_commit"] = False
|
|
17
|
+
yield from common.default(rule, key, diff)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def peer(rule, key, diff, **_): # pylint: disable=unused-argument
|
|
21
|
+
"""
|
|
22
|
+
Особенность peer-команд в том, что
|
|
23
|
+
peer IP as-number N
|
|
24
|
+
является основной командой, и отменить её можно только через
|
|
25
|
+
undo peer IP
|
|
26
|
+
, то есть полностью удалив все настройки пира.
|
|
27
|
+
|
|
28
|
+
При этом, as-number может выставляться и для группы:
|
|
29
|
+
group SPINES
|
|
30
|
+
peer SPINES as-number 13238
|
|
31
|
+
в таком случае игнорим, позволяем удалить эту настройку поскольку она не дефайнит группу
|
|
32
|
+
undo peer SPINES as-number
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
assert not diff[Op.AFFECTED], "Peer commands could not contain subcommands"
|
|
36
|
+
for action in sorted(diff[Op.REMOVED], key=lambda act: "as-number" not in act["row"].split()):
|
|
37
|
+
tokens = action["row"].split()
|
|
38
|
+
(_, addr_or_group_name, param, *__) = tokens
|
|
39
|
+
if param == "as-number":
|
|
40
|
+
if _is_ip_addr(addr_or_group_name):
|
|
41
|
+
yield (False, "undo peer {}".format(*key), None)
|
|
42
|
+
else:
|
|
43
|
+
# мы не можем делать common.default потому что правило определено как peer * а не peer * *
|
|
44
|
+
# таким образом дефолтное поведение тут будет "undo peer PEERGROUP" что не то что мы хотим
|
|
45
|
+
yield (False, "undo peer {} as-number".format(*key), None)
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
if param in ["connect-interface", "ebgp-max-hop", "local-as", "substitute-as", "password", "preferred-value"]:
|
|
49
|
+
yield (False, "undo " + " ".join(tokens[:3]), None)
|
|
50
|
+
else:
|
|
51
|
+
yield (False, "undo " + action["row"], None)
|
|
52
|
+
|
|
53
|
+
for action in sorted(diff[Op.ADDED], key=lambda act: "as-number" not in act["row"]):
|
|
54
|
+
yield (True, action["row"], None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bfd(rule, key, diff, **_):
|
|
58
|
+
"""
|
|
59
|
+
[*vla-1x1-bgp]undo peer SPINE1 bfd min-tx-interval 500 min-rx-interval 500 detect-multiplier 4
|
|
60
|
+
│Error: Unrecognized command found at '^' position.
|
|
61
|
+
|
|
62
|
+
[*vla-1x1-bgp]undo peer SPINE1 bfd min-rx-interval
|
|
63
|
+
[~vla-1x1-bgp]undo peer SPINE1 bfd min-tx-interval
|
|
64
|
+
[*vla-1x1-bgp]undo peer SPINE1 bfd detect-multiplier
|
|
65
|
+
"""
|
|
66
|
+
if diff[Op.REMOVED]:
|
|
67
|
+
assert len(diff[Op.REMOVED]) <= 1 and len(diff[Op.ADDED]) <= 1
|
|
68
|
+
new_params = set()
|
|
69
|
+
if diff[Op.ADDED]:
|
|
70
|
+
new_params = set(_bfd_params_used(diff[Op.ADDED][0]["row"]))
|
|
71
|
+
for token in _bfd_params_used(diff[Op.REMOVED][0]["row"]):
|
|
72
|
+
if token not in new_params:
|
|
73
|
+
yield (False, rule["reverse"].format(*key) + " " + token, None)
|
|
74
|
+
diff[Op.REMOVED] = []
|
|
75
|
+
if diff[Op.ADDED]:
|
|
76
|
+
yield from common.default(rule, key, diff, **_)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_ip_addr(addr_or_string):
|
|
80
|
+
ret = None
|
|
81
|
+
for af in (socket.AF_INET6, socket.AF_INET):
|
|
82
|
+
try:
|
|
83
|
+
ret = socket.inet_pton(af, addr_or_string)
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
else:
|
|
87
|
+
break
|
|
88
|
+
return bool(ret)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _bfd_params_used(row):
|
|
92
|
+
prev = None
|
|
93
|
+
for token in row.split():
|
|
94
|
+
if prev and token.isnumeric():
|
|
95
|
+
if prev and token.isnumeric():
|
|
96
|
+
yield prev
|
|
97
|
+
prev = token
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
from annet.rulebook import common
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def permanent(rule, key, diff, **kwargs): # pylint: disable=redefined-outer-name
|
|
9
|
+
ifname = key[0]
|
|
10
|
+
if re.match(r"(Eth-Trunk|Vlanif|Vbdif|Loop[Bb]ack|Tunnel|.*\.\d+)", ifname):
|
|
11
|
+
# эти интерфейсы можно удалять
|
|
12
|
+
yield from common.default(rule, key, diff, **kwargs)
|
|
13
|
+
else:
|
|
14
|
+
yield from common.permanent(rule, key, diff, **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# [NOCDEV-2180] Хуавей просит переввести ip конфигурацию после изменения vrf
|
|
18
|
+
def binding_change(old, new, diff_pre, _pops=(Op.AFFECTED,)):
|
|
19
|
+
ret = common.default_diff(old, new, diff_pre, _pops)
|
|
20
|
+
vpn_changed = False
|
|
21
|
+
for (op, cmd, _, _) in ret:
|
|
22
|
+
if op in {Op.ADDED, Op.REMOVED}:
|
|
23
|
+
vpn_changed |= _is_vpn_cmd(cmd)
|
|
24
|
+
if vpn_changed:
|
|
25
|
+
for cmd in list(old.keys()):
|
|
26
|
+
if not _is_vpn_cmd(cmd):
|
|
27
|
+
del old[cmd]
|
|
28
|
+
ret = common.default_diff(old, new, diff_pre, _pops)
|
|
29
|
+
return ret
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_vpn_cmd(cmd):
|
|
33
|
+
return cmd.startswith("ip binding vpn-instance")
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import re
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
from annet.annlib.types import Op
|
|
6
|
+
from contextlog import get_logger
|
|
7
|
+
|
|
8
|
+
from annet.rulebook import common
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VRPVersion(namedtuple("VRPVersionBase", ["V", "R", "C", "SPC"])):
|
|
12
|
+
ANY = object()
|
|
13
|
+
ATTR_NAMES = ["V", "R", "C", "SPC"]
|
|
14
|
+
|
|
15
|
+
def __eq__(self, other):
|
|
16
|
+
if not isinstance(other, type(self)):
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
for attr_name in self.ATTR_NAMES:
|
|
20
|
+
self_attr = getattr(self, attr_name)
|
|
21
|
+
if self_attr is self.ANY:
|
|
22
|
+
continue
|
|
23
|
+
other_attr = getattr(other, attr_name)
|
|
24
|
+
if other_attr is self.ANY:
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
if self_attr != other_attr:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def __ne__(self, other):
|
|
33
|
+
return not self == other
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_version(version: str) -> VRPVersion:
|
|
37
|
+
# CP - Cold Patch
|
|
38
|
+
# HP - Hot Patch
|
|
39
|
+
if not version:
|
|
40
|
+
# FIXME: возможно, если в RT нет данных, надо спрашивать у самого устройства?
|
|
41
|
+
version = "VRP V200R002C50SPC800"
|
|
42
|
+
get_logger().warning("SW version not set, falling back to %r", version)
|
|
43
|
+
res = re.match(r"(?:VRP )?V(?P<v>\d+)R(?P<r>\d+)C(?P<c>\d+)(SPC(?P<spc>\d+))?(?P<opt>T)?", version)
|
|
44
|
+
assert res is not None, f"can't parse version '{version}'"
|
|
45
|
+
m = res.groupdict() # pylint: disable=invalid-name
|
|
46
|
+
return VRPVersion(int(m["v"]), int(m["r"]), int(m["c"] or 0), int(m["spc"] or 0))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =====
|
|
50
|
+
def rp_node(rule, key, diff, **_):
|
|
51
|
+
# route-policy NAME ACTION node NUM
|
|
52
|
+
(rp_name, node_id) = key
|
|
53
|
+
if diff[Op.REMOVED]:
|
|
54
|
+
if diff[Op.ADDED]:
|
|
55
|
+
sub_diff = {Op.AFFECTED: [], Op.ADDED: [], Op.REMOVED: [], Op.MOVED: [], Op.UNCHANGED: []}
|
|
56
|
+
sub_diff[Op.AFFECTED] = diff[Op.REMOVED]
|
|
57
|
+
yield from common.default(rule, key, sub_diff)
|
|
58
|
+
else:
|
|
59
|
+
yield (False, "undo route-policy %s node %s" % (rp_name, node_id), None)
|
|
60
|
+
|
|
61
|
+
if diff[Op.AFFECTED] or diff[Op.ADDED]:
|
|
62
|
+
yield from common.default(rule, key, diff)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def undo_redo(rule, key, diff, **_):
|
|
66
|
+
yield from common.undo_redo(rule, key, diff, **_)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def prefix_list(rule, key, diff, **kwargs):
|
|
70
|
+
# для того чтобы опредилить полностью ли изменяется
|
|
71
|
+
# префикс лист в рулбуке huawei.rul описан ключ (family, name)
|
|
72
|
+
# однако с точки зрения команды каждый индекс - отдельная команда
|
|
73
|
+
# поэтому мы группируем их по индексу тут и передаем в common
|
|
74
|
+
diff_by_index = {}
|
|
75
|
+
for op, rows in diff.items():
|
|
76
|
+
for row in rows:
|
|
77
|
+
# ожидаемый формат команды префикс-листа
|
|
78
|
+
# ip ip-prefix PRFX_CT_LU_ALLOWED_ROUTES index 15 ..
|
|
79
|
+
# ip ipv6-prefix PFXS_SPECIALv6 index 20 ..
|
|
80
|
+
_ip, _family, _name, _index, index, *_ = row["row"].split()
|
|
81
|
+
if index not in diff_by_index:
|
|
82
|
+
sub_diff = {op: [] for op in diff.keys()}
|
|
83
|
+
diff_by_index[index] = sub_diff
|
|
84
|
+
diff_by_index[index][op].append(row)
|
|
85
|
+
|
|
86
|
+
family, name = key
|
|
87
|
+
if family not in {"ip", "ipv6"}:
|
|
88
|
+
raise NotImplementedError("Unknown family '%s'" % family)
|
|
89
|
+
if diff[Op.ADDED] or diff[Op.REMOVED] or diff[Op.MOVED]:
|
|
90
|
+
# поскольку исходно у нас в ключе правила нет индекса
|
|
91
|
+
# нужно добавить его туда иначе undo-правило будет без оного
|
|
92
|
+
indexed_rule = copy.deepcopy(rule)
|
|
93
|
+
indexed_rule["reverse"] = "undo ip {}-prefix {} index {}"
|
|
94
|
+
|
|
95
|
+
# stub_index референсится в рулбуке huawei.order чтобы обеспечить
|
|
96
|
+
# добавление/удаление стаба в первую/последнюю очередь
|
|
97
|
+
stub, stub_index = "", 99999999
|
|
98
|
+
|
|
99
|
+
# если мы только добавляем новые команды (например создаем) в префик-лист
|
|
100
|
+
# либо удаляем/двигаем но при этом у нас есть не изменяемые части
|
|
101
|
+
# хуавей не будет считать лист удаляемым и стаб-правило не нужно
|
|
102
|
+
if (diff[Op.REMOVED] or diff[Op.MOVED]) and not diff[Op.UNCHANGED]:
|
|
103
|
+
stub = "deny 0.0.0.0 32" if family == "ip" else "deny :: 128"
|
|
104
|
+
if stub:
|
|
105
|
+
yield (True, f"ip {family}-prefix {name} index {stub_index} {stub}", None)
|
|
106
|
+
for index, sub_diff in diff_by_index.items():
|
|
107
|
+
yield from common.undo_redo(indexed_rule, (family, name, index), sub_diff, **kwargs)
|
|
108
|
+
if stub:
|
|
109
|
+
yield (False, f"undo ip {family}-prefix {name} index {stub_index}", None)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def static(rule, key, diff, **_):
|
|
113
|
+
"""
|
|
114
|
+
Для отката статического маршрута фактически необходимо передавать почти все аргументы,
|
|
115
|
+
кроме различных track ...
|
|
116
|
+
При этом, аргументов может быть разное количество - опциональный VRF, опциональный интерфейс.
|
|
117
|
+
Поэтому мы не парсим саму команду, а только удаляем ненужные аргументы.
|
|
118
|
+
"""
|
|
119
|
+
if diff[Op.REMOVED]:
|
|
120
|
+
param = key[0]
|
|
121
|
+
idx = param.find(" track")
|
|
122
|
+
if idx > 0:
|
|
123
|
+
key = (param[0:idx],)
|
|
124
|
+
idx = param.find(" description")
|
|
125
|
+
if idx > 0:
|
|
126
|
+
key = (param[0:idx],)
|
|
127
|
+
idx = param.find(" preference")
|
|
128
|
+
if idx > 0:
|
|
129
|
+
key = (param[0:idx],)
|
|
130
|
+
yield from common.default(rule, key, diff)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def undo_trust(rule, key, diff, hw, **_):
|
|
134
|
+
"""на CE свитчах команда undo trust; на S undo trust *"""
|
|
135
|
+
if diff[Op.REMOVED]:
|
|
136
|
+
if hw.Quidway and not hw.S6700:
|
|
137
|
+
yield False, "undo trust %s" % key, None
|
|
138
|
+
else:
|
|
139
|
+
yield False, "undo trust", None
|
|
140
|
+
else:
|
|
141
|
+
yield from common.default(rule, key, diff)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def port_queue(rule, key, diff, **_):
|
|
145
|
+
"""
|
|
146
|
+
Для отката конфигурации port-queue на интерфейсе требуется только частичное указание параметров.
|
|
147
|
+
Пример отключения/включения:
|
|
148
|
+
interface 100GE0/1/33
|
|
149
|
+
undo port-queue af3 wfq outbound
|
|
150
|
+
port-queue af3 wfq weight 30 port-wred WRED outbound
|
|
151
|
+
|
|
152
|
+
По сути на убрать все параметры между 'wfq' и 'outbound'
|
|
153
|
+
NOC-19414
|
|
154
|
+
"""
|
|
155
|
+
if diff[Op.REMOVED]:
|
|
156
|
+
param = key[0]
|
|
157
|
+
idx = param.find("weight")
|
|
158
|
+
if idx > 0:
|
|
159
|
+
key = (param[0:idx] + "outbound",)
|
|
160
|
+
yield from common.default(rule, key, diff)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def netstream_undo(rule, key, diff, **_):
|
|
164
|
+
if diff[Op.REMOVED]:
|
|
165
|
+
# The only part we need is the last keyword: inbound or outbound
|
|
166
|
+
# Unfortunately, key is a tuple so we cast it to a list and back
|
|
167
|
+
key = list(key)
|
|
168
|
+
key[1] = key[1].split(" ")[-1]
|
|
169
|
+
key = tuple(key)
|
|
170
|
+
yield from common.default(rule, key, diff)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def old_snmp_iface_trap_undo(rule, key, diff, hw, **_):
|
|
174
|
+
# хитрая логика для старый хуавеев
|
|
175
|
+
# тут вместо полной команды с undo нужно сгенерить не полную строку
|
|
176
|
+
if diff[Op.REMOVED]:
|
|
177
|
+
if hw.Quidway:
|
|
178
|
+
yield False, "undo mac-address trap notification", None
|
|
179
|
+
else:
|
|
180
|
+
yield False, "undo mac-address trap notification learn", None
|
|
181
|
+
else:
|
|
182
|
+
yield from common.default(rule, key, diff)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def stelnet(rule, key, diff, **_):
|
|
186
|
+
# не заменяем строки stelnet ipv4 server enable и stelnet ipv6 server enable на stelnet server enable
|
|
187
|
+
# чтобы не дергать SSH
|
|
188
|
+
if diff[Op.REMOVED] and diff[Op.ADDED]:
|
|
189
|
+
removed = {x["row"] for x in diff[Op.REMOVED]}
|
|
190
|
+
added = {x["row"] for x in diff[Op.ADDED]}
|
|
191
|
+
if removed == {"stelnet ipv4 server enable", "stelnet ipv6 server enable"} and added == {"stelnet server enable"}:
|
|
192
|
+
return
|
|
193
|
+
yield from common.default(rule, key, diff)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def snmpagent_sysinfo_version(rule, key, diff, hw, **_):
|
|
197
|
+
if hw.Huawei.CE and (diff[Op.ADDED] or diff[Op.REMOVED]):
|
|
198
|
+
assert len(diff[Op.AFFECTED]) == 0, "WTF? Affected not empty: %r" % (diff[Op.AFFECTED])
|
|
199
|
+
versions = set(["v1", "v2c", "v3"])
|
|
200
|
+
|
|
201
|
+
result = set()
|
|
202
|
+
for op in [Op.REMOVED, Op.ADDED]:
|
|
203
|
+
for action in diff[op]:
|
|
204
|
+
args = action["row"].split()[3:]
|
|
205
|
+
assert len(args) > 0, "Empty op %r: %r" % (op, action["row"])
|
|
206
|
+
|
|
207
|
+
if args[-1] == "disable":
|
|
208
|
+
args = args[:-1]
|
|
209
|
+
disable = True
|
|
210
|
+
else:
|
|
211
|
+
disable = False
|
|
212
|
+
if "all" in args:
|
|
213
|
+
args = versions
|
|
214
|
+
else:
|
|
215
|
+
assert len(set(args).difference(versions)) == 0, "Incorrect args: %r" % (args)
|
|
216
|
+
|
|
217
|
+
if (op == Op.REMOVED and disable) or (op == Op.ADDED and not disable):
|
|
218
|
+
result.update(args)
|
|
219
|
+
else:
|
|
220
|
+
result.difference_update(args)
|
|
221
|
+
|
|
222
|
+
if result == versions:
|
|
223
|
+
yield (True, "snmp-agent sys-info version all", None)
|
|
224
|
+
else:
|
|
225
|
+
yield (False, "snmp-agent sys-info version all disable", None)
|
|
226
|
+
yield (True, "snmp-agent sys-info version %s" % (" ".join(result)), None)
|
|
227
|
+
else:
|
|
228
|
+
yield from common.default(rule, key, diff)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def vty_acl_undo(rule, key, diff, **_):
|
|
232
|
+
if diff[Op.REMOVED]:
|
|
233
|
+
chunks = key[0].split()
|
|
234
|
+
result_chunks = ["undo acl"]
|
|
235
|
+
if len(chunks) == 3 and chunks[0] == "ipv6":
|
|
236
|
+
result_chunks.append("ipv6")
|
|
237
|
+
result_chunks.append(chunks[-1])
|
|
238
|
+
yield False, " ".join(result_chunks), None
|
|
239
|
+
else:
|
|
240
|
+
yield from common.default(rule, key, diff)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def port_split(rule, key, diff, **_):
|
|
244
|
+
# pylint: disable=unused-argument
|
|
245
|
+
def _port_split(old, new, old_row, new_row):
|
|
246
|
+
removed = set(old).difference(new)
|
|
247
|
+
added = set(new).difference(old)
|
|
248
|
+
if old and new:
|
|
249
|
+
for ifname in removed:
|
|
250
|
+
yield (False, "undo port split dimension interface " + ifname, None)
|
|
251
|
+
for ifname in added:
|
|
252
|
+
yield (True, "port split dimension interface " + ifname, None)
|
|
253
|
+
elif old and not new:
|
|
254
|
+
yield (False, "undo " + old_row, None)
|
|
255
|
+
elif new and not old:
|
|
256
|
+
yield (True, new_row, None)
|
|
257
|
+
|
|
258
|
+
def _row_slot(row):
|
|
259
|
+
res = ""
|
|
260
|
+
for ch in row:
|
|
261
|
+
if ch == "/":
|
|
262
|
+
break
|
|
263
|
+
res = res + ch if ch.isnumeric() else ""
|
|
264
|
+
return int(res) if res else 0
|
|
265
|
+
|
|
266
|
+
old_by_slot = {_row_slot(x["row"]): x["row"] for x in diff[Op.REMOVED]}
|
|
267
|
+
new_by_slot = {_row_slot(x["row"]): x["row"] for x in diff[Op.ADDED]}
|
|
268
|
+
for slot in set(old_by_slot.keys()).union(new_by_slot.keys()):
|
|
269
|
+
old_row = old_by_slot[slot] if slot in old_by_slot else ""
|
|
270
|
+
new_row = new_by_slot[slot] if slot in new_by_slot else ""
|
|
271
|
+
old = _expand_portsplit(old_row)
|
|
272
|
+
new = _expand_portsplit(new_row)
|
|
273
|
+
yield from _port_split(old, new, old_row, new_row)
|
|
274
|
+
if old_by_slot or new_by_slot:
|
|
275
|
+
yield (True, "port split refresh", None)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _expand_portsplit(row):
|
|
279
|
+
expanded = []
|
|
280
|
+
row_parts = row.split()
|
|
281
|
+
for (index, part) in enumerate(row_parts):
|
|
282
|
+
if part == "to":
|
|
283
|
+
iface_base = "/".join(row_parts[index - 1].split("/")[:-1])
|
|
284
|
+
left = int(row_parts[index - 1].split("/")[-1])
|
|
285
|
+
right = int(row_parts[index + 1].split("/")[-1])
|
|
286
|
+
for i in range(left + 1, right):
|
|
287
|
+
expanded.append(iface_base + "/" + str(i))
|
|
288
|
+
else:
|
|
289
|
+
expanded.append(part)
|
|
290
|
+
return expanded
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def classifier(rule, key, diff, **_):
|
|
294
|
+
# если type меняется нужно сначала удалить все if-match
|
|
295
|
+
# а после этого пересоздать classifier
|
|
296
|
+
if diff[Op.ADDED] and diff[Op.REMOVED]:
|
|
297
|
+
yield (True, diff[Op.REMOVED][0]["row"], diff[Op.REMOVED][0]["children"])
|
|
298
|
+
yield from common.default(rule, key, diff)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def undo_children(rule, key, diff, **_):
|
|
302
|
+
def removed_count(subdiff):
|
|
303
|
+
ret = 0
|
|
304
|
+
for child in subdiff["children"].values():
|
|
305
|
+
for child_diff in child["items"].values():
|
|
306
|
+
ret += len(child_diff[Op.REMOVED])
|
|
307
|
+
return ret
|
|
308
|
+
|
|
309
|
+
def common_default(op, subdiff):
|
|
310
|
+
newdiff = {Op.ADDED: [], Op.REMOVED: [], Op.MOVED: [], Op.AFFECTED: [], Op.UNCHANGED: []}
|
|
311
|
+
newdiff[op] = [subdiff]
|
|
312
|
+
yield from common.default(rule, key, newdiff)
|
|
313
|
+
|
|
314
|
+
# Приходится самим говорить undo поскольку мы притворяемся одним блоком
|
|
315
|
+
for subdiff in diff[Op.REMOVED]:
|
|
316
|
+
# Сначала нужно удалить все group-member'ы
|
|
317
|
+
if diff[Op.REMOVED][0]["children"]:
|
|
318
|
+
yield (True, diff[Op.REMOVED][0]["row"], diff[Op.REMOVED][0]["children"])
|
|
319
|
+
yield False, "undo " + subdiff["row"], None
|
|
320
|
+
# Сначала разбираем affected, там внутри могут быть undo
|
|
321
|
+
for subdiff in sorted(diff[Op.AFFECTED], key=removed_count, reverse=True):
|
|
322
|
+
yield from common_default(Op.AFFECTED, subdiff)
|
|
323
|
+
for subdiff in diff[Op.ADDED]:
|
|
324
|
+
yield from common_default(Op.ADDED, subdiff)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def clear_instead_undo(rule, key, diff, **_):
|
|
328
|
+
# Для ряда конфигурационных строк возникает вечный diff, поскольку в конфиге строка либо явно включена,
|
|
329
|
+
# либо явно выключена. Если она не описана в генераторе, т.е. мы полагаемся на дефолт, то используя clear
|
|
330
|
+
# вместо undo мы возвращаем конфиг в дефолтное состояние.
|
|
331
|
+
# NOC-20102 @gslv 11-02-2022
|
|
332
|
+
if diff[Op.REMOVED]:
|
|
333
|
+
if diff[Op.REMOVED][0]["row"].endswith(" disable"):
|
|
334
|
+
cmd = diff[Op.REMOVED][0]["row"].replace(" disable", "")
|
|
335
|
+
yield (True, "clear " + cmd, False)
|
|
336
|
+
else:
|
|
337
|
+
yield from common.default(rule, key, diff)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from annet.annlib.lib import huawei_collapse_vlandb as collapse_vlandb
|
|
2
|
+
from annet.annlib.lib import huawei_expand_vlandb as expand_vlandb
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
from annet.rulebook import common
|
|
6
|
+
from annet.rulebook.common import DiffItem
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# =====
|
|
10
|
+
def single(rule, key, diff, **_):
|
|
11
|
+
yield from _process_vlandb(rule, key, diff, False, False, None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def multi(rule, key, diff, **_):
|
|
15
|
+
yield from _process_vlandb(rule, key, diff, True, False, 10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def multi_all(rule, key, diff, **_):
|
|
19
|
+
yield from _process_vlandb(rule, key, diff, True, True, 10)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def vlan_diff(old, new, diff_pre, _pops):
|
|
23
|
+
batch_new = set() # vlan batch ... vlan ids
|
|
24
|
+
for row in new:
|
|
25
|
+
prefix, vlans = _parse_vlancfg(row)
|
|
26
|
+
if prefix == "vlan batch":
|
|
27
|
+
batch_new.update(vlans)
|
|
28
|
+
ret = []
|
|
29
|
+
for item in common.default_diff(old, new, diff_pre, _pops):
|
|
30
|
+
prefix, vlan_ids = _parse_vlancfg(item.row)
|
|
31
|
+
# если влан был объявлен глобально и при этом остается в батче
|
|
32
|
+
# команда undo vlan ... будет пытаться полностью выпилить его с устройства
|
|
33
|
+
# и из батча тоже. при этом делать undo vlan ... ; vlan batch ... не выход
|
|
34
|
+
# поскольку для удаления cli требует удалить все vlanif"ы и проч
|
|
35
|
+
if prefix == "vlan" and item.op == Op.REMOVED and batch_new.intersection(vlan_ids):
|
|
36
|
+
result_item = DiffItem(Op.AFFECTED, item.row, item.children, item.diff_pre)
|
|
37
|
+
# если влан объявлен глобально и одновременно с этим в батче
|
|
38
|
+
# и при этом в блоке глобального объявления нет никаких опций
|
|
39
|
+
# не добавляем его он будет висеть зазря - таким образом мы сохраним
|
|
40
|
+
# симметрию с предыдущей логикой оба инварианта будут выдавать пустой патч
|
|
41
|
+
elif prefix == "vlan" and batch_new.intersection(vlan_ids) and not item.children:
|
|
42
|
+
result_item = None
|
|
43
|
+
# vlan batch и остальное мы не трогаем
|
|
44
|
+
else:
|
|
45
|
+
result_item = item
|
|
46
|
+
if result_item:
|
|
47
|
+
ret.append(result_item)
|
|
48
|
+
return ret
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# =====
|
|
52
|
+
def _process_vlandb(rule, key, diff, multi, multi_all, multi_chunk): # pylint: disable=unused-argument,redefined-outer-name
|
|
53
|
+
assert len(diff[Op.AFFECTED]) == 0, "WTF? Affected signle: %r" % (diff[Op.AFFECTED])
|
|
54
|
+
if not multi:
|
|
55
|
+
for op in (Op.ADDED, Op.REMOVED):
|
|
56
|
+
assert 0 <= len(diff[op]) <= 1, "Too many actions: %r" % (diff)
|
|
57
|
+
|
|
58
|
+
if diff[Op.REMOVED] and not diff[Op.ADDED]: # Removed
|
|
59
|
+
if multi and multi_all:
|
|
60
|
+
yield (False, rule["reverse"].format(*key) + " all", None)
|
|
61
|
+
return
|
|
62
|
+
elif not multi and not multi_all:
|
|
63
|
+
yield (False, rule["reverse"].format(*key), None)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
(prefix_add, new) = _parse_vlancfg_actions(diff[Op.ADDED])
|
|
67
|
+
(prefix_del, old) = _parse_vlancfg_actions(diff[Op.REMOVED])
|
|
68
|
+
removed = old.difference(new)
|
|
69
|
+
added = new.difference(old)
|
|
70
|
+
|
|
71
|
+
if removed:
|
|
72
|
+
collapsed = collapse_vlandb(removed)
|
|
73
|
+
for chunk in (_chunked(collapsed, multi_chunk) if multi else [collapsed]):
|
|
74
|
+
yield (False, "undo %s %s" % (prefix_del, " ".join(chunk)), None)
|
|
75
|
+
|
|
76
|
+
if added:
|
|
77
|
+
collapsed = collapse_vlandb(added)
|
|
78
|
+
for chunk in (_chunked(collapsed, multi_chunk) if multi else [collapsed]):
|
|
79
|
+
yield (True, "%s %s" % (prefix_add, " ".join(chunk)), None)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _chunked(items, size):
|
|
83
|
+
for offset in range(0, len(items), size):
|
|
84
|
+
yield items[offset:offset + size]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_vlancfg_actions(actions):
|
|
88
|
+
prefix = None
|
|
89
|
+
vlandb = set()
|
|
90
|
+
for action in actions:
|
|
91
|
+
(prefix, part) = _parse_vlancfg(action["row"])
|
|
92
|
+
vlandb.update(part)
|
|
93
|
+
return (prefix, vlandb)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_vlancfg(row):
|
|
97
|
+
parts = row.split()
|
|
98
|
+
assert len(parts) > 0, row
|
|
99
|
+
index = None
|
|
100
|
+
for (index, item) in reversed(list(enumerate(parts))):
|
|
101
|
+
if not (item.isdigit() or item == "to"):
|
|
102
|
+
break
|
|
103
|
+
prefix = " ".join(parts[:index + 1])
|
|
104
|
+
vlandb = expand_vlandb(" ".join(parts[index + 1:]))
|
|
105
|
+
return (prefix, vlandb)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _find_new_vlans(root_pre):
|
|
109
|
+
ret = set()
|
|
110
|
+
for (rule, pre) in root_pre.items():
|
|
111
|
+
if not rule.startswith("vlan batch"):
|
|
112
|
+
continue
|
|
113
|
+
new = _parse_vlancfg_actions(pre["items"][tuple()][Op.ADDED])[1]
|
|
114
|
+
ret.update(new)
|
|
115
|
+
return ret
|