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,196 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from . import patching, tabparser
|
|
6
|
+
from .diff import diff_ops, ops_sign
|
|
7
|
+
from .rbparser import acl
|
|
8
|
+
|
|
9
|
+
UnifiedInputConfig = str # Конфиг классических сетевых устройств
|
|
10
|
+
FileInputConfig = typing.Dict[str, typing.Any] # Конфиг вайтбоксов и серверов
|
|
11
|
+
InputConfig = typing.Union[UnifiedInputConfig, FileInputConfig]
|
|
12
|
+
|
|
13
|
+
Acl = typing.Dict[str, typing.Any]
|
|
14
|
+
|
|
15
|
+
UnifiedConfigTree = typing.OrderedDict[str, typing.Any]
|
|
16
|
+
FileConfigTree = typing.Dict[str, typing.Any]
|
|
17
|
+
ConfigTree = typing.Union[UnifiedConfigTree, FileConfigTree]
|
|
18
|
+
|
|
19
|
+
DiffTree = typing.OrderedDict[str, typing.Any]
|
|
20
|
+
UnifiedDiff = typing.List[typing.Tuple[str, str, typing.List, typing.Optional[int]]]
|
|
21
|
+
FileConfigDiff = typing.Dict[str, typing.Any]
|
|
22
|
+
Diff = typing.Union[UnifiedDiff, FileConfigDiff]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_acl(text: str, vendor: str) -> Acl:
|
|
26
|
+
return acl.compile_acl_text(text, vendor)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def filter_config(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: InputConfig) -> InputConfig:
|
|
30
|
+
if isinstance(input_config, str):
|
|
31
|
+
config: ConfigTree = tabparser.parse_to_tree(input_config, fmtr.split)
|
|
32
|
+
config = patching.apply_acl(config, acl, fatal_acl=False)
|
|
33
|
+
config = fmtr.join(config)
|
|
34
|
+
else:
|
|
35
|
+
config = typing.cast(input_config, FileConfigTree)
|
|
36
|
+
config = apply_acl_fileconfig(input_config, acl)
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def filter_diff(acl: Acl, fmtr: tabparser.CommonFormatter, input_config: InputConfig) -> InputConfig:
|
|
41
|
+
if isinstance(input_config, str):
|
|
42
|
+
input_config = shift_op(input_config)
|
|
43
|
+
diff_tee: DiffTree = tabparser.parse_to_tree(input_config, fmtr.split)
|
|
44
|
+
diff: Diff = tree_to_diff(diff_tee)
|
|
45
|
+
diff = patching.apply_acl_diff(diff, acl)
|
|
46
|
+
config = fmtr.join(diff_to_tree(diff))
|
|
47
|
+
config = unshift_op(config)
|
|
48
|
+
config = config.rstrip()
|
|
49
|
+
else:
|
|
50
|
+
config = typing.cast(input_config, FileConfigTree)
|
|
51
|
+
config = apply_acl_fileconfig(input_config, acl)
|
|
52
|
+
return config
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def filter_patch(acl: Acl, fmtr: tabparser.CommonFormatter, text: str) -> str:
|
|
56
|
+
return filter_config(acl, fmtr, text)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# NOCDEV-6378 на патч для Juniper/Nokia нельзя просто так наложить filter_acl
|
|
60
|
+
def filter_patch_jun_nokia(diff_filtered: InputConfig, fmtr: tabparser.CommonFormatter, text: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Накладываем ACL на патчи для Juniper/Nokia
|
|
63
|
+
|
|
64
|
+
Поскольку в патче уже потерена иерархия команд - они развернуты в строки типа
|
|
65
|
+
Нужна дополнительная информация о изначальном конфиге, которую можно подсмотреть в дифе
|
|
66
|
+
set interface et-0/0/0 unit ....
|
|
67
|
+
delete interface et-0/0/0 unit ....
|
|
68
|
+
/configure port 1/1/c17/1 ...
|
|
69
|
+
/configure delete port 1/1/c18/1 ...
|
|
70
|
+
"""
|
|
71
|
+
diff_tree_stripped: DiffTree = tabparser.parse_to_tree(strip_op(diff_filtered), fmtr.split)
|
|
72
|
+
_tree_expand_lists_nokia_jun(diff_tree_stripped)
|
|
73
|
+
patch_lines_passed = []
|
|
74
|
+
for patch_line in text.split("\n"):
|
|
75
|
+
patch_parts = [x for x in patch_line.split(" ") if x]
|
|
76
|
+
diff_current = diff_tree_stripped
|
|
77
|
+
# strip set|delete|/configure
|
|
78
|
+
while patch_parts and patch_parts[0] in {"/configure", "set", "delete"}:
|
|
79
|
+
patch_parts = patch_parts[1:]
|
|
80
|
+
while patch_parts and diff_current:
|
|
81
|
+
for i in range(len(patch_parts), -1, -1):
|
|
82
|
+
key = " ".join(patch_parts[:i])
|
|
83
|
+
# consume parts and go down in diff hierarchy
|
|
84
|
+
if key in diff_current:
|
|
85
|
+
patch_parts = patch_parts[i:]
|
|
86
|
+
diff_current = diff_current[key]
|
|
87
|
+
break
|
|
88
|
+
# no progress has been made
|
|
89
|
+
else:
|
|
90
|
+
break
|
|
91
|
+
if not patch_parts:
|
|
92
|
+
patch_lines_passed.append(patch_line)
|
|
93
|
+
return "\n".join(patch_lines_passed)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply_acl_fileconfig(config, rules):
|
|
97
|
+
passed = {}
|
|
98
|
+
for (filename, filecontent) in config.items():
|
|
99
|
+
(match, _) = patching.match_row_to_acl(filename, rules)
|
|
100
|
+
if match:
|
|
101
|
+
if not (match["is_reverse"] and match["attrs"]["cant_delete"]):
|
|
102
|
+
passed[filename] = filecontent
|
|
103
|
+
return passed
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_op(line: str) -> typing.Tuple[str, str, str]:
|
|
107
|
+
op = " "
|
|
108
|
+
indent = ""
|
|
109
|
+
opidx = -1
|
|
110
|
+
rowstart = 0
|
|
111
|
+
for rowstart in range(len(line)):
|
|
112
|
+
if line[rowstart] not in diff_ops:
|
|
113
|
+
break
|
|
114
|
+
for opidx in range(rowstart):
|
|
115
|
+
if line[opidx] != " ":
|
|
116
|
+
break
|
|
117
|
+
if opidx >= 0:
|
|
118
|
+
op = line[opidx]
|
|
119
|
+
indent = line[:opidx] + line[opidx + 1:rowstart]
|
|
120
|
+
if op != " ":
|
|
121
|
+
indent = indent + " "
|
|
122
|
+
return op, indent, line[rowstart:]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def shift_op(text: str) -> str:
|
|
126
|
+
ret = ""
|
|
127
|
+
for line in text.split("\n"):
|
|
128
|
+
op, indent, line = get_op(line)
|
|
129
|
+
ret += indent + op + line + "\n"
|
|
130
|
+
return ret
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def unshift_op(text: str) -> str:
|
|
134
|
+
ret = ""
|
|
135
|
+
for line in text.split("\n"):
|
|
136
|
+
op, indent, line = get_op(line)
|
|
137
|
+
ret += op + indent + line + "\n"
|
|
138
|
+
return ret
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def strip_op(text: str) -> str:
|
|
142
|
+
ret: str = ""
|
|
143
|
+
for line in text.split("\n"):
|
|
144
|
+
op, indent, line = get_op(line)
|
|
145
|
+
if op != " ":
|
|
146
|
+
indent = indent[1:]
|
|
147
|
+
ret += indent + line + "\n"
|
|
148
|
+
return ret
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def tree_to_diff(diff_tree: ConfigTree) -> Diff:
|
|
152
|
+
ret = []
|
|
153
|
+
for row, v in diff_tree.items():
|
|
154
|
+
op, _, row = get_op(row)
|
|
155
|
+
diff_op = diff_ops[op]
|
|
156
|
+
children = []
|
|
157
|
+
d_match = None
|
|
158
|
+
if isinstance(v, dict):
|
|
159
|
+
children = tree_to_diff(v)
|
|
160
|
+
ret.append((diff_op, row, children, d_match))
|
|
161
|
+
return ret
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def diff_to_tree(diff: Diff) -> ConfigTree:
|
|
165
|
+
ret = collections.OrderedDict()
|
|
166
|
+
for diff_op, row, children, _ in diff:
|
|
167
|
+
row = ops_sign[diff_op] + row
|
|
168
|
+
ret[row] = diff_to_tree(children)
|
|
169
|
+
return ret
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _tree_expand_lists_nokia_jun(diff_tree: DiffTree):
|
|
173
|
+
"""
|
|
174
|
+
Раскрываем списки Nokia/Juniper в отдельные элементы
|
|
175
|
+
{command: {"[a, b, c]": {}}} -> {command a: {}, command b: {}, command c: {}}
|
|
176
|
+
|
|
177
|
+
В неупорядоченном множестве префиксов также стираем ';' на конце - их не бывает в патче
|
|
178
|
+
{prefix-list: {"2a02::/64;": {}, "2a03::/64;": {}}} -> {prefix-list: {"2a02::/64": {}, "2a03::/64": {}}}
|
|
179
|
+
"""
|
|
180
|
+
process: typing.List[DiffTree] = [diff_tree]
|
|
181
|
+
list_regexp = re.compile(r"^(.*)\s+\[(.+)\]$")
|
|
182
|
+
while process:
|
|
183
|
+
tree, process = process[0], process[1:]
|
|
184
|
+
for cmd in list(tree.keys()):
|
|
185
|
+
children = tree[cmd]
|
|
186
|
+
matches = list_regexp.search(cmd)
|
|
187
|
+
normalized_cmd = cmd.rstrip(";")
|
|
188
|
+
if matches:
|
|
189
|
+
for c in matches.group(2).split(" "):
|
|
190
|
+
if c.strip():
|
|
191
|
+
tree[" ".join([matches.group(1), c])] = children
|
|
192
|
+
if normalized_cmd != cmd:
|
|
193
|
+
del tree[cmd]
|
|
194
|
+
tree[normalized_cmd] = children
|
|
195
|
+
if isinstance(children, dict):
|
|
196
|
+
process.append(children)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Support JSON patch (RFC 6902) and JSON Pointer (RFC 6901)."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import jsonpatch
|
|
8
|
+
import jsonpointer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_json(data: Any, stable: bool = False) -> str:
|
|
12
|
+
"""Serialize to json."""
|
|
13
|
+
return json.dumps(data, indent=4, ensure_ascii=False, sort_keys=not stable) + "\n"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def apply_json_fragment(
|
|
17
|
+
old: Dict[str, Any],
|
|
18
|
+
new_fragment: Dict[str, Any],
|
|
19
|
+
acl: List[str],
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Replace parts of the old document with 'new_fragment' using ACL restrictions.
|
|
23
|
+
"""
|
|
24
|
+
full_new_config = copy.deepcopy(old)
|
|
25
|
+
for acl_item in acl:
|
|
26
|
+
pointer = jsonpointer.JsonPointer(acl_item)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
new_value = pointer.get(new_fragment)
|
|
30
|
+
except jsonpointer.JsonPointerException:
|
|
31
|
+
# no value found in new_fragment by the pointer, skip the ACL item
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
_ensure_pointer_exists(full_new_config, pointer)
|
|
35
|
+
pointer.set(full_new_config, new_value)
|
|
36
|
+
|
|
37
|
+
return full_new_config
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ensure_pointer_exists(doc: Dict[str, Any], pointer: jsonpointer.JsonPointer) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Ensure that document has all pointer parts (if possible).
|
|
43
|
+
|
|
44
|
+
This is workaround for errors of type:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
jsonpointer.JsonPointerException: member 'MY_PART' not found in {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See for details: https://github.com/stefankoegl/python-json-pointer/issues/41
|
|
51
|
+
"""
|
|
52
|
+
parts_except_the_last = pointer.get_parts()[:-1]
|
|
53
|
+
doc_pointer: Dict[str, Any] = doc
|
|
54
|
+
for part in parts_except_the_last:
|
|
55
|
+
if part not in doc_pointer:
|
|
56
|
+
# create an empty object by the pointer part
|
|
57
|
+
doc_pointer[part] = {}
|
|
58
|
+
|
|
59
|
+
if isinstance(doc_pointer, dict):
|
|
60
|
+
# follow the pointer to delve deeper
|
|
61
|
+
doc_pointer = doc_pointer[part]
|
|
62
|
+
else:
|
|
63
|
+
# not a dict - cannot delve deeper
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def make_patch(old: Dict[str, Any], new: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
68
|
+
"""Generate a JSON patch by comparing the old document with the new one."""
|
|
69
|
+
return jsonpatch.make_patch(old, new).patch
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def apply_patch(content: Optional[bytes], patch_bytes: bytes) -> bytes:
|
|
73
|
+
"""
|
|
74
|
+
Apply JSON patch to file contents.
|
|
75
|
+
|
|
76
|
+
If content is None it is considered that the file does not exist.
|
|
77
|
+
"""
|
|
78
|
+
old_doc: Any
|
|
79
|
+
if content is not None:
|
|
80
|
+
old_doc = json.loads(content)
|
|
81
|
+
else:
|
|
82
|
+
old_doc = None
|
|
83
|
+
|
|
84
|
+
patch_data = json.loads(patch_bytes)
|
|
85
|
+
patch = jsonpatch.JsonPatch(patch_data)
|
|
86
|
+
new_doc = patch.apply(old_doc)
|
|
87
|
+
|
|
88
|
+
new_contents = format_json(new_doc, stable=True).encode()
|
|
89
|
+
return new_contents
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def apply_acl_filters(content: Dict[str, Any], filters: List[str]) -> Dict[str, Any]:
|
|
93
|
+
result = {}
|
|
94
|
+
for f in filters:
|
|
95
|
+
filter_text = f.strip()
|
|
96
|
+
if not filter_text:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
pointer = jsonpointer.JsonPointer(filter_text)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
part = pointer.get(copy.deepcopy(content))
|
|
103
|
+
|
|
104
|
+
sub_tree = result
|
|
105
|
+
for i in pointer.get_parts():
|
|
106
|
+
if i not in sub_tree:
|
|
107
|
+
sub_tree[i] = {}
|
|
108
|
+
sub_tree = sub_tree[i]
|
|
109
|
+
|
|
110
|
+
patch = jsonpatch.JsonPatch([{"op": "add", "path": filter_text, "value": part}])
|
|
111
|
+
result = patch.apply(result)
|
|
112
|
+
except jsonpointer.JsonPointerException:
|
|
113
|
+
# no value found in content by the pointer, skip the ACL item
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
return result
|