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,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