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,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Dict,
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Set,
|
|
10
|
+
Union,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from annet.storage import Device, Storage
|
|
14
|
+
from .base import TreeGenerator, _filter_str
|
|
15
|
+
from .exceptions import NotSupportedDevice
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JSONFragment(TreeGenerator):
|
|
19
|
+
"""Generates parts of JSON config file."""
|
|
20
|
+
|
|
21
|
+
TYPE = "JSON_FRAGMENT"
|
|
22
|
+
TAGS: List[str] = []
|
|
23
|
+
|
|
24
|
+
def __init__(self, storage: Storage):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.storage = storage
|
|
27
|
+
self._json_config: Dict[str, Any] = {}
|
|
28
|
+
self._config_pointer: List[str] = []
|
|
29
|
+
|
|
30
|
+
# if two generators edit same file, commands from generator with greater `reload_prio` will be used
|
|
31
|
+
if not hasattr(self, "reload_prio"):
|
|
32
|
+
self.reload_prio = 100
|
|
33
|
+
|
|
34
|
+
def supports_device(self, device: Device):
|
|
35
|
+
return bool(self.path(device))
|
|
36
|
+
|
|
37
|
+
def path(self, device: Device) -> Optional[str]:
|
|
38
|
+
raise NotImplementedError("Required PATH for JSON_FRAGMENT generator")
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get_aliases(cls) -> Set[str]:
|
|
42
|
+
return {cls.__name__, *cls.TAGS}
|
|
43
|
+
|
|
44
|
+
def acl(self, device: Device) -> Union[str, List[str]]:
|
|
45
|
+
"""
|
|
46
|
+
Restrict the generator to a specified ACL using JSON Pointer syntax.
|
|
47
|
+
|
|
48
|
+
Expected ACL to be a list of strings, but a single string is also allowed.
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
|
|
51
|
+
|
|
52
|
+
def acl_safe(self, device: Device) -> Union[str, List[str]]:
|
|
53
|
+
"""
|
|
54
|
+
Restrict the generator to a specified safe ACL using JSON Pointer syntax.
|
|
55
|
+
|
|
56
|
+
Expected ACL to be a list of strings, but a single string is also allowed.
|
|
57
|
+
"""
|
|
58
|
+
raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
|
|
59
|
+
|
|
60
|
+
def run(self, device: Device):
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def get_reload_cmds(self, device: Device) -> str:
|
|
64
|
+
ret = self.reload(device) or ""
|
|
65
|
+
return ret
|
|
66
|
+
|
|
67
|
+
def reload(self, device) -> Optional[str]:
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
@contextlib.contextmanager
|
|
71
|
+
def block(self, *tokens, indent=None): # pylint: disable=unused-argument
|
|
72
|
+
block_str = "".join(map(_filter_str, tokens))
|
|
73
|
+
self._config_pointer.append(block_str)
|
|
74
|
+
try:
|
|
75
|
+
yield
|
|
76
|
+
finally:
|
|
77
|
+
self._config_pointer.pop()
|
|
78
|
+
|
|
79
|
+
@contextlib.contextmanager
|
|
80
|
+
def block_piped(self, *tokens, indent=None): # pylint: disable=unused-argument
|
|
81
|
+
block_str = "|".join(map(_filter_str, tokens))
|
|
82
|
+
self._config_pointer.append(block_str)
|
|
83
|
+
try:
|
|
84
|
+
yield
|
|
85
|
+
finally:
|
|
86
|
+
self._config_pointer.pop()
|
|
87
|
+
|
|
88
|
+
def __call__(self, device: Device, annotate: bool = False):
|
|
89
|
+
for cfg_fragment in self.run(device):
|
|
90
|
+
self._set_or_replace_dict(self._config_pointer, cfg_fragment)
|
|
91
|
+
return self._json_config
|
|
92
|
+
|
|
93
|
+
def _set_or_replace_dict(self, pointer, value):
|
|
94
|
+
if not pointer:
|
|
95
|
+
if self._json_config == {}:
|
|
96
|
+
self._json_config = value
|
|
97
|
+
else:
|
|
98
|
+
self._json_config = [self._json_config, value]
|
|
99
|
+
else:
|
|
100
|
+
self._set_dict(self._json_config, pointer, value)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def _to_str(cls, value: Any) -> str:
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
return value
|
|
106
|
+
elif isinstance(value, list):
|
|
107
|
+
return [cls._to_str(x) for x in value]
|
|
108
|
+
elif isinstance(value, dict):
|
|
109
|
+
for k, v in value.items():
|
|
110
|
+
value[k] = cls._to_str(v)
|
|
111
|
+
return value
|
|
112
|
+
return str(value)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _set_dict(cls, cfg, pointer, value):
|
|
116
|
+
# pointer has at least one key
|
|
117
|
+
if len(pointer) == 1:
|
|
118
|
+
if pointer[0] in cfg:
|
|
119
|
+
cfg[pointer[0]] = [cfg[pointer[0]], cls._to_str(value)]
|
|
120
|
+
else:
|
|
121
|
+
cfg[pointer[0]] = cls._to_str(value)
|
|
122
|
+
else:
|
|
123
|
+
if pointer[0] not in cfg:
|
|
124
|
+
cfg[pointer[0]] = {}
|
|
125
|
+
cls._set_dict(cfg[pointer[0]], pointer[1:], cls._to_str(value))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import (
|
|
5
|
+
Iterable,
|
|
6
|
+
List,
|
|
7
|
+
Set,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from annet.lib import (
|
|
12
|
+
add_annotation,
|
|
13
|
+
flatten,
|
|
14
|
+
)
|
|
15
|
+
from .base import TreeGenerator, _filter_str
|
|
16
|
+
from .exceptions import NotSupportedDevice
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PartialGenerator(TreeGenerator):
|
|
20
|
+
TYPE = "PARTIAL"
|
|
21
|
+
TAGS: List[str] = []
|
|
22
|
+
|
|
23
|
+
def __init__(self, storage):
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.storage = storage
|
|
26
|
+
self._annotate = False
|
|
27
|
+
self._running_gen = None
|
|
28
|
+
self._annotations = []
|
|
29
|
+
self._annotation_module = self.__class__.__module__ or ""
|
|
30
|
+
|
|
31
|
+
def supports_device(self, device) -> bool:
|
|
32
|
+
if self.__class__.run is PartialGenerator.run:
|
|
33
|
+
return bool(self._get_vendor_func(device.hw.vendor, "run"))
|
|
34
|
+
else:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
def acl(self, device):
|
|
38
|
+
acl_func = self._get_vendor_func(device.hw.vendor, "acl")
|
|
39
|
+
if acl_func:
|
|
40
|
+
return acl_func(device)
|
|
41
|
+
|
|
42
|
+
def acl_safe(self, device):
|
|
43
|
+
acl_func = self._get_vendor_func(device.hw.vendor, "acl_safe")
|
|
44
|
+
if acl_func:
|
|
45
|
+
return acl_func(device)
|
|
46
|
+
|
|
47
|
+
def run(self, device) -> Iterable[Union[str, tuple]]:
|
|
48
|
+
run_func = self._get_vendor_func(device.hw.vendor, "run")
|
|
49
|
+
if run_func:
|
|
50
|
+
return run_func(device)
|
|
51
|
+
|
|
52
|
+
def get_user_runner(self, device):
|
|
53
|
+
if self.__class__.run is not PartialGenerator.run:
|
|
54
|
+
return self.run
|
|
55
|
+
return self._get_vendor_func(device.hw.vendor, "run")
|
|
56
|
+
|
|
57
|
+
def _get_vendor_func(self, vendor: str, func_name: str):
|
|
58
|
+
attr_name = f"{func_name}_{vendor}"
|
|
59
|
+
if hasattr(self, attr_name):
|
|
60
|
+
return getattr(self, attr_name)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# =====
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_aliases(cls) -> Set[str]:
|
|
67
|
+
return {cls.__name__, *cls.TAGS}
|
|
68
|
+
|
|
69
|
+
def __call__(self, device, annotate=False):
|
|
70
|
+
self._indents = []
|
|
71
|
+
self._rows = []
|
|
72
|
+
self._running_gen = self.run(device)
|
|
73
|
+
self._annotate = annotate
|
|
74
|
+
|
|
75
|
+
if annotate and self.__class__.__module__:
|
|
76
|
+
self._annotation_module = ".".join(
|
|
77
|
+
self.__class__.__module__.split(".")[-2:])
|
|
78
|
+
|
|
79
|
+
for text in self._running_gen:
|
|
80
|
+
if isinstance(text, tuple):
|
|
81
|
+
text = " ".join(map(_filter_str, flatten(text)))
|
|
82
|
+
else:
|
|
83
|
+
text = _filter_str(text)
|
|
84
|
+
self._append_text(text)
|
|
85
|
+
|
|
86
|
+
for row in self._rows:
|
|
87
|
+
assert re.search(r"\bNone\b", row) is None, \
|
|
88
|
+
"Found 'None' in yield result: %s" % (row)
|
|
89
|
+
if annotate:
|
|
90
|
+
generated_rows = (
|
|
91
|
+
add_annotation(x, y)
|
|
92
|
+
for (x, y) in zip(self._rows, self._annotations)
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
generated_rows = self._rows
|
|
96
|
+
return "\n".join(generated_rows) + "\n"
|
|
97
|
+
|
|
98
|
+
def _append_text(self, text):
|
|
99
|
+
def annotation_cb(row):
|
|
100
|
+
annotation = "%s:%d" % self.get_running_line()
|
|
101
|
+
self._annotations.append(annotation)
|
|
102
|
+
return row
|
|
103
|
+
|
|
104
|
+
self._append_text_cb(
|
|
105
|
+
text,
|
|
106
|
+
annotation_cb if self._annotate else None
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get_running_line(self):
|
|
110
|
+
if not self._running_gen or not self._running_gen.gi_frame:
|
|
111
|
+
return (repr(self._running_gen), -1)
|
|
112
|
+
return self._annotation_module, self._running_gen.gi_frame.f_lineno
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def literal(cls, item):
|
|
116
|
+
return '"{}"'.format(item)
|
|
117
|
+
|
|
118
|
+
def __repr__(self):
|
|
119
|
+
return "<%s>" % self.__class__.__name__
|
annet/generators/perf.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Union,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from annet import tracing
|
|
10
|
+
from annet.tracing import tracing_connector
|
|
11
|
+
from annet.types import (
|
|
12
|
+
GeneratorPartialRunArgs,
|
|
13
|
+
GeneratorPerf,
|
|
14
|
+
)
|
|
15
|
+
from .entire import Entire
|
|
16
|
+
from .partial import PartialGenerator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GeneratorPerfMesurer:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
gen: Union[PartialGenerator, Entire],
|
|
23
|
+
run_args: Optional[GeneratorPartialRunArgs] = None,
|
|
24
|
+
trace_min_duration: tracing.MinDurationT = None
|
|
25
|
+
):
|
|
26
|
+
self._gen = gen
|
|
27
|
+
self._run_args = run_args
|
|
28
|
+
|
|
29
|
+
self._start_time: float = 0.0
|
|
30
|
+
self._span_ctx = None
|
|
31
|
+
self._span = None
|
|
32
|
+
self._trace_min_duration = trace_min_duration
|
|
33
|
+
|
|
34
|
+
self.last_result: Optional[GeneratorPerf] = None
|
|
35
|
+
|
|
36
|
+
def start(self) -> None:
|
|
37
|
+
self.last_result = None
|
|
38
|
+
|
|
39
|
+
self._span_ctx = tracing_connector.get().start_as_current_span(
|
|
40
|
+
"gen:call",
|
|
41
|
+
tracer_name=self._gen.__class__.__module__,
|
|
42
|
+
min_duration=self._trace_min_duration,
|
|
43
|
+
)
|
|
44
|
+
self._span = self._span_ctx.__enter__() # pylint: disable=unnecessary-dunder-call
|
|
45
|
+
|
|
46
|
+
if self._span:
|
|
47
|
+
self._span.set_attributes(
|
|
48
|
+
{"generator.class": self._gen.__class__.__name__})
|
|
49
|
+
if self._run_args:
|
|
50
|
+
tracing_connector.get().set_device_attributes(
|
|
51
|
+
self._span, self._run_args.device,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self._start_time = time.monotonic()
|
|
55
|
+
|
|
56
|
+
def finish(self, exc_type=None, exc_val=None,
|
|
57
|
+
exc_tb=None) -> GeneratorPerf:
|
|
58
|
+
total = time.monotonic() - self._start_time
|
|
59
|
+
self._span_ctx.__exit__(exc_type, exc_val, exc_tb)
|
|
60
|
+
|
|
61
|
+
meta = {}
|
|
62
|
+
if tracing_connector.get().enabled:
|
|
63
|
+
span_context = self._span.get_span_context()
|
|
64
|
+
meta = {
|
|
65
|
+
"span": {
|
|
66
|
+
"trace_id": str(span_context.trace_id),
|
|
67
|
+
"span_id": str(span_context.span_id),
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
self.last_result = GeneratorPerf(total=total, rt=total, meta=meta)
|
|
72
|
+
return self.last_result
|
|
73
|
+
|
|
74
|
+
def __enter__(self):
|
|
75
|
+
self.start()
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
79
|
+
self.finish(exc_type, exc_val, exc_tb)
|
annet/generators/ref.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .partial import PartialGenerator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RefGenerator(PartialGenerator):
|
|
5
|
+
def __init__(self, storage, groups=None):
|
|
6
|
+
super().__init__(storage)
|
|
7
|
+
self.groups = groups
|
|
8
|
+
|
|
9
|
+
def ref(self, device):
|
|
10
|
+
if hasattr(self, "ref_" + device.hw.vendor):
|
|
11
|
+
return getattr(self, "ref_" + device.hw.vendor)(device)
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
def with_groups(self, groups):
|
|
15
|
+
return type(self)(self.storage, groups)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from collections import OrderedDict as odict
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Dict,
|
|
8
|
+
Optional,
|
|
9
|
+
Tuple, Callable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from annet.annlib import jsontools
|
|
13
|
+
from annet.lib import (
|
|
14
|
+
merge_dicts,
|
|
15
|
+
)
|
|
16
|
+
from annet.reference import RefMatcher, RefTracker
|
|
17
|
+
from annet.types import (
|
|
18
|
+
GeneratorEntireResult,
|
|
19
|
+
GeneratorJSONFragmentResult,
|
|
20
|
+
GeneratorPartialResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _combine_acl_text(
|
|
25
|
+
partial_results: Dict[str, GeneratorPartialResult],
|
|
26
|
+
acl_getter: Callable[[GeneratorPartialResult], str]
|
|
27
|
+
) -> str:
|
|
28
|
+
acl_text = ""
|
|
29
|
+
for gr in partial_results.values():
|
|
30
|
+
for line in textwrap.dedent(acl_getter(gr)).split("\n"):
|
|
31
|
+
if line and not line.isspace():
|
|
32
|
+
acl_text += line.rstrip()
|
|
33
|
+
acl_text += fr" %generator_names={gr.name}"
|
|
34
|
+
acl_text += "\n"
|
|
35
|
+
return acl_text
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RunGeneratorResult:
|
|
39
|
+
"""
|
|
40
|
+
Результат запуска run_partial_generators/run_file_generators
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.partial_results: Dict[str, GeneratorPartialResult] = {}
|
|
45
|
+
self.entire_results: Dict[str, GeneratorEntireResult] = {}
|
|
46
|
+
self.json_fragment_results: Dict[str, GeneratorJSONFragmentResult] = {}
|
|
47
|
+
self.ref_track: RefTracker = RefTracker()
|
|
48
|
+
self.ref_matcher: RefMatcher = RefMatcher()
|
|
49
|
+
|
|
50
|
+
def add_partial(self, result: GeneratorPartialResult):
|
|
51
|
+
self.partial_results[result.name] = result
|
|
52
|
+
|
|
53
|
+
def add_entire(self, result: GeneratorEntireResult) -> None:
|
|
54
|
+
# Если есть несколько генераторов на один файл, выбрать тот, что с большим приоритетом
|
|
55
|
+
if result.path:
|
|
56
|
+
if result.path not in self.entire_results or \
|
|
57
|
+
result.prio > self.entire_results[result.path].prio:
|
|
58
|
+
self.entire_results[result.path] = result
|
|
59
|
+
|
|
60
|
+
def add_json_fragment(self, result: GeneratorJSONFragmentResult) -> None:
|
|
61
|
+
self.json_fragment_results[result.name] = result
|
|
62
|
+
|
|
63
|
+
def config_tree(self, safe: bool = False) -> Dict[str, Any]: # OrderedDict
|
|
64
|
+
tree = odict()
|
|
65
|
+
for gr in self.partial_results.values():
|
|
66
|
+
config = gr.safe_config if safe else gr.config
|
|
67
|
+
tree = merge_dicts(tree, config)
|
|
68
|
+
return tree
|
|
69
|
+
|
|
70
|
+
def new_files(self, safe: bool = False) -> Dict[str, Tuple[str, str]]:
|
|
71
|
+
files = {}
|
|
72
|
+
for gr in self.entire_results.values():
|
|
73
|
+
if not safe or gr.is_safe:
|
|
74
|
+
files[gr.path] = (gr.output, gr.reload)
|
|
75
|
+
return files
|
|
76
|
+
|
|
77
|
+
def acl_text(self) -> str:
|
|
78
|
+
return _combine_acl_text(self.partial_results, lambda gr: gr.acl)
|
|
79
|
+
|
|
80
|
+
def acl_safe_text(self) -> str:
|
|
81
|
+
return _combine_acl_text(self.partial_results, lambda gr: gr.acl_safe)
|
|
82
|
+
|
|
83
|
+
def new_json_fragment_files(
|
|
84
|
+
self,
|
|
85
|
+
old_files: Dict[str, Optional[str]],
|
|
86
|
+
safe: bool = False,
|
|
87
|
+
) -> Dict[str, Tuple[Any, Optional[str]]]:
|
|
88
|
+
# TODO: safe
|
|
89
|
+
files: Dict[str, Tuple[Any, Optional[str]]] = {}
|
|
90
|
+
reload_prios: Dict[str, int] = {}
|
|
91
|
+
for generator_result in self.json_fragment_results.values():
|
|
92
|
+
filepath = generator_result.path
|
|
93
|
+
if filepath not in files:
|
|
94
|
+
if old_files.get(filepath) is not None:
|
|
95
|
+
files[filepath] = (old_files[filepath], None)
|
|
96
|
+
else:
|
|
97
|
+
files[filepath] = ({}, None)
|
|
98
|
+
result_acl = generator_result.acl
|
|
99
|
+
if safe:
|
|
100
|
+
result_acl = generator_result.acl_safe
|
|
101
|
+
previous_config: Dict[str, Any] = files[filepath][0]
|
|
102
|
+
new_fragment = generator_result.config
|
|
103
|
+
new_config = jsontools.apply_json_fragment(
|
|
104
|
+
previous_config,
|
|
105
|
+
new_fragment,
|
|
106
|
+
result_acl,
|
|
107
|
+
)
|
|
108
|
+
if filepath in reload_prios and \
|
|
109
|
+
reload_prios[filepath] > generator_result.reload_prio:
|
|
110
|
+
_, reload_cmd = files[filepath]
|
|
111
|
+
else:
|
|
112
|
+
reload_cmd = generator_result.reload
|
|
113
|
+
reload_prios[filepath] = generator_result.reload_prio
|
|
114
|
+
files[filepath] = (new_config, reload_cmd)
|
|
115
|
+
return files
|
|
116
|
+
|
|
117
|
+
def perf_mesures(self) -> Dict[str, Dict[str, int]]:
|
|
118
|
+
mesures = {}
|
|
119
|
+
for gr in self.partial_results.values():
|
|
120
|
+
mesures[gr.name] = {"total": gr.perf.total,
|
|
121
|
+
"rt": gr.perf.rt,
|
|
122
|
+
"meta": gr.perf.meta}
|
|
123
|
+
for gr in self.entire_results.values():
|
|
124
|
+
mesures[gr.name] = {"total": gr.perf.total,
|
|
125
|
+
"rt": gr.perf.rt,
|
|
126
|
+
"meta": gr.perf.meta}
|
|
127
|
+
return mesures
|
annet/hardware.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from annet.annlib.netdev.views.hardware import HardwareView, hw_to_vendor
|
|
5
|
+
|
|
6
|
+
from annet.connectors import Connector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from annet.annlib.netdev.views.hardware import vendor_to_hw
|
|
11
|
+
except ImportError:
|
|
12
|
+
from netdev.views.hardware import vendor_to_hw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _HardwareConnector(Connector["HarwareProvider"]):
|
|
16
|
+
name = "Hardware"
|
|
17
|
+
ep_name = "hardware"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
hardware_connector = _HardwareConnector()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HarwareProvider(abc.ABC):
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def make_hw(self, hw_model: str, sw_version: str) -> Any:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def vendor_to_hw(self, vendor: str) -> Any:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abc.abstractmethod
|
|
33
|
+
def hw_to_vendor(self, hw: Any) -> str:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AnnetHardwareProvider(HarwareProvider):
|
|
38
|
+
def make_hw(self, hw_model: str, sw_version: str) -> HardwareView:
|
|
39
|
+
return HardwareView(hw_model, sw_version)
|
|
40
|
+
|
|
41
|
+
def vendor_to_hw(self, vendor: str) -> HardwareView:
|
|
42
|
+
return vendor_to_hw(vendor)
|
|
43
|
+
|
|
44
|
+
def hw_to_vendor(self, hw: HardwareView) -> str:
|
|
45
|
+
return hw_to_vendor(hw)
|
annet/implicit.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from collections import OrderedDict as odict
|
|
2
|
+
|
|
3
|
+
from annet.annlib.rbparser import syntax
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def config(config_tree, rules):
|
|
7
|
+
implicit_config_tree = odict()
|
|
8
|
+
for (row, rule) in rules.items():
|
|
9
|
+
matched_lines = [line for line in config_tree.keys() if rule["regexp"].match(line)]
|
|
10
|
+
if rule["type"] != "ignore":
|
|
11
|
+
if not any(matched_lines) and row not in config_tree:
|
|
12
|
+
implicit_config_tree[row] = odict()
|
|
13
|
+
for line in matched_lines:
|
|
14
|
+
implicit_config_tree[line] = config(config_tree[line], rule["children"])
|
|
15
|
+
return implicit_config_tree
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compile_rules(device):
|
|
19
|
+
return compile_tree(_implicit_tree(device))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compile_tree(tree):
|
|
23
|
+
rules = odict()
|
|
24
|
+
for (_, attrs) in tree.items():
|
|
25
|
+
rule = {
|
|
26
|
+
"type": attrs["type"],
|
|
27
|
+
"children": compile_tree(attrs["children"]) if attrs.get("children") else odict(),
|
|
28
|
+
"regexp": syntax.compile_row_regexp(attrs["row"]),
|
|
29
|
+
}
|
|
30
|
+
rules[attrs["row"]] = rule
|
|
31
|
+
return rules
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _implicit_tree(device):
|
|
35
|
+
text = ""
|
|
36
|
+
if device.hw.Huawei:
|
|
37
|
+
if device.hw.Huawei.CE:
|
|
38
|
+
text = """
|
|
39
|
+
stp mode mstp
|
|
40
|
+
stp enable
|
|
41
|
+
undo ntp server disable
|
|
42
|
+
undo telnet server disable
|
|
43
|
+
undo dhcp enable
|
|
44
|
+
!user-interface con *
|
|
45
|
+
user privilege level 3
|
|
46
|
+
!user-interface vty ~
|
|
47
|
+
protocol inbound all
|
|
48
|
+
netconf
|
|
49
|
+
"""
|
|
50
|
+
elif device.hw.Huawei.NE:
|
|
51
|
+
text = """
|
|
52
|
+
!bgp *
|
|
53
|
+
!ipv4-family unicast
|
|
54
|
+
undo synchronization
|
|
55
|
+
!ipv6-family unicast
|
|
56
|
+
undo synchronization
|
|
57
|
+
!user-interface con *
|
|
58
|
+
user privilege level 3
|
|
59
|
+
!user-interface vty ~
|
|
60
|
+
protocol inbound all
|
|
61
|
+
aaa
|
|
62
|
+
undo user-password complexity-check
|
|
63
|
+
netconf
|
|
64
|
+
"""
|
|
65
|
+
else:
|
|
66
|
+
text = """
|
|
67
|
+
stp mode mstp
|
|
68
|
+
!interface X?GigabitEthernet*
|
|
69
|
+
bpdu enable
|
|
70
|
+
netconf
|
|
71
|
+
"""
|
|
72
|
+
elif device.hw.Arista:
|
|
73
|
+
# эта часть конфигурации будет не видна в конфиге, если она включена с таким набором полей:
|
|
74
|
+
text = r"""
|
|
75
|
+
ip load-sharing trident fields ipv6 destination-port source-ip ingress-interface destination-ip source-port flow-label
|
|
76
|
+
ip load-sharing trident fields ip source-ip source-port destination-ip destination-port ingress-interface
|
|
77
|
+
"""
|
|
78
|
+
elif device.hw.Nexus:
|
|
79
|
+
text = r"""
|
|
80
|
+
# часть конфигурации скрытая если включена
|
|
81
|
+
snmp-server enable traps link linkDown
|
|
82
|
+
snmp-server enable traps link linkUp
|
|
83
|
+
"""
|
|
84
|
+
if device.hw.Nexus.N3x.N3432 \
|
|
85
|
+
or (device.hw.Nexus.N9x.N9500 and "spine1" in device.tags) \
|
|
86
|
+
or device.hw.Nexus.N9x.N9316 or device.hw.Cisco.Nexus.N9x.N9364:
|
|
87
|
+
text += r"""
|
|
88
|
+
# SVI
|
|
89
|
+
!interface Vlan*
|
|
90
|
+
!shutdown
|
|
91
|
+
!interface mgmt[0-9]*
|
|
92
|
+
no shutdown
|
|
93
|
+
mtu 1500
|
|
94
|
+
!interface Ethernet1*
|
|
95
|
+
no shutdown
|
|
96
|
+
# Лупбеки
|
|
97
|
+
!interface */Loopback[0-9.]+/
|
|
98
|
+
no shutdown
|
|
99
|
+
# Агрегаты
|
|
100
|
+
!interface */port-channel[0-9.]+/
|
|
101
|
+
no shutdown
|
|
102
|
+
# BGP
|
|
103
|
+
!router bgp *
|
|
104
|
+
!neighbor *
|
|
105
|
+
no shutdown
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
elif device.hw.Nexus.N3x:
|
|
109
|
+
# У нексуса сложные взаимоотношения с
|
|
110
|
+
# shutdown командой вездe
|
|
111
|
+
# в данный момент поведение проверенно для Cisco Nexus 3132Q 6.0(2)U6(7)
|
|
112
|
+
text += r"""
|
|
113
|
+
# SVI
|
|
114
|
+
!interface Vlan*
|
|
115
|
+
!shutdown
|
|
116
|
+
!interface mgmt[0-9]*
|
|
117
|
+
no shutdown
|
|
118
|
+
# Физические НЕ сплитованные интерфейсы и subif'ы
|
|
119
|
+
!interface */Ethernet1\/[0-9.]*/
|
|
120
|
+
no shutdown
|
|
121
|
+
# Физические НЕ сплитованные интерфейсы и subif'ы
|
|
122
|
+
!interface */Ethernet1\/[0-9]+\/[0-9.]+/
|
|
123
|
+
# только explicit
|
|
124
|
+
# Лупбеки
|
|
125
|
+
!interface */Loopback[0-9.]+/
|
|
126
|
+
no shutdown
|
|
127
|
+
# Агрегаты
|
|
128
|
+
!interface */port-channel[0-9.]+/
|
|
129
|
+
no shutdown
|
|
130
|
+
# BGP
|
|
131
|
+
!router bgp *
|
|
132
|
+
!neighbor *
|
|
133
|
+
no shutdown
|
|
134
|
+
"""
|
|
135
|
+
return parse_text(text)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_text(text):
|
|
139
|
+
return syntax.parse_text(text, {})
|