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
annet/lib.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Awaitable, Optional
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from annet.annlib.lib import ( # pylint: disable=unused-import
|
|
11
|
+
ContextOrderedDict,
|
|
12
|
+
HuaweiNumBlock,
|
|
13
|
+
LMSMatcher,
|
|
14
|
+
add_annotation,
|
|
15
|
+
catch_ctrl_c,
|
|
16
|
+
cisco_collapse_vlandb,
|
|
17
|
+
cisco_expand_vlandb,
|
|
18
|
+
find_exc_in_stack,
|
|
19
|
+
find_modules,
|
|
20
|
+
first,
|
|
21
|
+
flatten,
|
|
22
|
+
huawei_collapse_vlandb,
|
|
23
|
+
huawei_expand_vlandb,
|
|
24
|
+
huawei_iface_ranges,
|
|
25
|
+
is_relative,
|
|
26
|
+
jinja_render,
|
|
27
|
+
jun_activate,
|
|
28
|
+
jun_is_inactive,
|
|
29
|
+
juniper_fmt_prefix_lists_acl,
|
|
30
|
+
juniper_port_split,
|
|
31
|
+
make_ip4_mask,
|
|
32
|
+
mako_render,
|
|
33
|
+
merge_dicts,
|
|
34
|
+
percentile,
|
|
35
|
+
uniq,
|
|
36
|
+
)
|
|
37
|
+
from contextlog import get_logger
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_TEMPLATE_CONTEXT_PATH: Optional[str] = None
|
|
41
|
+
_DEFAULT_CONTEXT_PATH: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_template_context_path() -> str:
|
|
45
|
+
if _TEMPLATE_CONTEXT_PATH is None:
|
|
46
|
+
set_template_context_path(str(Path(sys.modules["annet"].__file__).parent / "configs/context.yml"))
|
|
47
|
+
return _TEMPLATE_CONTEXT_PATH
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def set_template_context_path(path: str) -> None:
|
|
51
|
+
global _TEMPLATE_CONTEXT_PATH # pylint: disable=global-statement
|
|
52
|
+
_TEMPLATE_CONTEXT_PATH = path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_default_context_path() -> str:
|
|
56
|
+
if _DEFAULT_CONTEXT_PATH is None:
|
|
57
|
+
set_default_context_path("~/.annet/context.yml")
|
|
58
|
+
return _DEFAULT_CONTEXT_PATH
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_default_context_path(path: str) -> None:
|
|
62
|
+
global _DEFAULT_CONTEXT_PATH # pylint: disable=global-statement
|
|
63
|
+
_DEFAULT_CONTEXT_PATH = path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@lru_cache(maxsize=1)
|
|
67
|
+
def _get_template_context():
|
|
68
|
+
with open(get_template_context_path()) as f:
|
|
69
|
+
return yaml.safe_load(f)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_context_path(touch: Optional[bool] = False) -> str:
|
|
73
|
+
path = Path(os.getenv("ANN_CONTEXT_CONFIG_PATH", get_default_context_path())).expanduser().absolute()
|
|
74
|
+
if not path.exists():
|
|
75
|
+
src = get_template_context_path()
|
|
76
|
+
if not touch:
|
|
77
|
+
return str(src)
|
|
78
|
+
try:
|
|
79
|
+
# populate path with default configuration
|
|
80
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
shutil.copy(src, path)
|
|
82
|
+
except shutil.SameFileError:
|
|
83
|
+
pass
|
|
84
|
+
return str(path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_context() -> dict:
|
|
88
|
+
with open(get_context_path()) as f:
|
|
89
|
+
raw = yaml.safe_load(f)
|
|
90
|
+
_fill_in_default_generator_modules(raw)
|
|
91
|
+
context_name = os.getenv("ANN_SELECTED_CONTEXT", raw["selected_context"])
|
|
92
|
+
res = {k: raw[k][v] for k, v in raw["context"][context_name].items()}
|
|
93
|
+
if "ANN_GENERATORS_CONTEXT" in os.environ: # an undocumented hack to maintain backwards compatibility; TODO: remove
|
|
94
|
+
res["generators"] = raw["generators"][os.getenv("ANN_GENERATORS_CONTEXT")]
|
|
95
|
+
return res
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@lru_cache(maxsize=1)
|
|
99
|
+
def _warn_no_generators_in_context():
|
|
100
|
+
get_logger().warning(
|
|
101
|
+
"Older version of the context configuration found. Getting generators references from the template context"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _fill_in_default_generator_modules(raw: dict) -> bool:
|
|
106
|
+
"""Backwards compatibility hack to add existing generators refs to context"""
|
|
107
|
+
if "generators" not in raw:
|
|
108
|
+
_warn_no_generators_in_context()
|
|
109
|
+
raw["generators"] = _get_template_context()["generators"]
|
|
110
|
+
for dst_context in raw["context"].values():
|
|
111
|
+
dst_context["generators"] = "default"
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def repair_context_file() -> None:
|
|
117
|
+
path = get_context_path()
|
|
118
|
+
with open(path) as f:
|
|
119
|
+
data = yaml.safe_load(f)
|
|
120
|
+
if _fill_in_default_generator_modules(data):
|
|
121
|
+
with open(path, "w") as f:
|
|
122
|
+
yaml.dump(data, f, sort_keys=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def do_async(coro: Awaitable):
|
|
126
|
+
loop = asyncio.get_event_loop()
|
|
127
|
+
res = loop.run_until_complete(coro)
|
|
128
|
+
return res
|
annet/output.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import os
|
|
3
|
+
import posixpath
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Optional, Tuple, Type, Dict
|
|
6
|
+
|
|
7
|
+
import colorama
|
|
8
|
+
from annet.annlib.output import ( # pylint: disable=unused-import
|
|
9
|
+
LABEL_NEW_PREFIX,
|
|
10
|
+
OutputWriter,
|
|
11
|
+
TextArgs,
|
|
12
|
+
capture_output,
|
|
13
|
+
dir_or_file_output,
|
|
14
|
+
format_file_diff,
|
|
15
|
+
print_as_json,
|
|
16
|
+
print_as_yaml,
|
|
17
|
+
print_err_label,
|
|
18
|
+
print_label,
|
|
19
|
+
)
|
|
20
|
+
from contextlog import get_logger
|
|
21
|
+
|
|
22
|
+
from annet.cli_args import FileOutOptions, QueryOptions
|
|
23
|
+
from annet.connectors import Connector
|
|
24
|
+
from annet.storage import Device, storage_connector
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _DriverConnector(Connector["OutputDriver"]):
|
|
28
|
+
name = "OutputDriver"
|
|
29
|
+
ep_name = "output"
|
|
30
|
+
|
|
31
|
+
def _get_default(self) -> Type["OutputDriver"]:
|
|
32
|
+
return OutputDriverBasic
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
output_driver_connector = _DriverConnector()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OutputDriver(abc.ABC):
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
def write_output(self, arg_out: FileOutOptions, items, query_result_count=1) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abc.abstractmethod
|
|
44
|
+
def format_fails(self, fail, fqdns: Optional[Dict[int, str]] = None) -> Tuple[str, str]:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abc.abstractmethod
|
|
48
|
+
def cfg_file_names(self, device: Device) -> List[str]:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abc.abstractmethod
|
|
52
|
+
def entire_config_dest_path(self, device: Device, config_path: str) -> str:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OutputDriverBasic(OutputDriver):
|
|
57
|
+
def write_output(self, arg_out: FileOutOptions, items, query_result_count=1):
|
|
58
|
+
"""
|
|
59
|
+
пишет результаты генерации в файл или директорию :dest
|
|
60
|
+
:dest - это директория в случаях:
|
|
61
|
+
- заканчивается на "/"
|
|
62
|
+
- существует директория с таким именем
|
|
63
|
+
- более одного устройства в результате запроса
|
|
64
|
+
- есть entire-генераторы (определяется по типу первого результата, если устройство одно)
|
|
65
|
+
"""
|
|
66
|
+
logger = get_logger()
|
|
67
|
+
|
|
68
|
+
items_iter = iter(items)
|
|
69
|
+
try:
|
|
70
|
+
first_result = next(items_iter)
|
|
71
|
+
except StopIteration:
|
|
72
|
+
# нет результатов, ничего не пишем и не создаём
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
def _reassemble_items():
|
|
76
|
+
yield first_result
|
|
77
|
+
yield from items_iter
|
|
78
|
+
|
|
79
|
+
dest = arg_out.dest
|
|
80
|
+
dir_mode = dir_or_file_output(dest, query_result_count, suggest_dir=(os.sep in first_result[0]))
|
|
81
|
+
_reassembled_items = list(_reassemble_items())
|
|
82
|
+
|
|
83
|
+
for output_no, (label, output, is_fail) in enumerate(_reassembled_items):
|
|
84
|
+
writer = output if isinstance(output, OutputWriter) else OutputWriter(output)
|
|
85
|
+
label = os.path.normpath(label)
|
|
86
|
+
label_color = colorama.Back.RED if is_fail else colorama.Back.GREEN
|
|
87
|
+
if dest is None:
|
|
88
|
+
if hasattr(arg_out, "format") and arg_out.format == "json":
|
|
89
|
+
if output_no > 0:
|
|
90
|
+
sys.stdout.write(",")
|
|
91
|
+
elif output_no == 0:
|
|
92
|
+
sys.stdout.write("{")
|
|
93
|
+
sys.stdout.write('"%s": ' % label)
|
|
94
|
+
writer.write(sys.stdout)
|
|
95
|
+
if output_no == len(_reassembled_items) - 1:
|
|
96
|
+
sys.stdout.write("}")
|
|
97
|
+
else:
|
|
98
|
+
if not arg_out.no_label:
|
|
99
|
+
print_label(label, back_color=label_color)
|
|
100
|
+
writer.write(sys.stdout)
|
|
101
|
+
elif dir_mode:
|
|
102
|
+
if label.startswith(LABEL_NEW_PREFIX):
|
|
103
|
+
label = label[len(LABEL_NEW_PREFIX):]
|
|
104
|
+
if label.startswith(os.sep):
|
|
105
|
+
# just in case.
|
|
106
|
+
label = label.lstrip(os.sep)
|
|
107
|
+
if os.sep not in label:
|
|
108
|
+
# vendor config
|
|
109
|
+
label = os.path.basename(label)
|
|
110
|
+
else:
|
|
111
|
+
# entire generated file
|
|
112
|
+
parts = label.split(os.sep)
|
|
113
|
+
hostname = parts[0]
|
|
114
|
+
label = os.sep.join(parts[1:])
|
|
115
|
+
if not arg_out.expand_path:
|
|
116
|
+
label = os.path.basename(label)
|
|
117
|
+
if query_result_count > 1:
|
|
118
|
+
label = os.path.join(hostname, label)
|
|
119
|
+
file_dest = os.path.join(dest, "errors") if is_fail else dest
|
|
120
|
+
out_file = os.path.normpath(os.path.join(file_dest, label))
|
|
121
|
+
logger.info("writing '%s'", out_file)
|
|
122
|
+
dirname = os.path.dirname(out_file)
|
|
123
|
+
if not os.path.exists(dirname):
|
|
124
|
+
os.makedirs(dirname)
|
|
125
|
+
with open(out_file, "w") as file:
|
|
126
|
+
writer.write(file)
|
|
127
|
+
else:
|
|
128
|
+
logger.info("writing '%s'", dest)
|
|
129
|
+
with open(dest, "w") as file:
|
|
130
|
+
writer.write(file)
|
|
131
|
+
|
|
132
|
+
def format_fails(self, fail, fqdns: Optional[Dict[int, str]] = None):
|
|
133
|
+
ret = []
|
|
134
|
+
fqdns = fqdns or {}
|
|
135
|
+
for (assignment, exc) in fail.items():
|
|
136
|
+
label = assignment
|
|
137
|
+
if assignment in fqdns:
|
|
138
|
+
label = fqdns[assignment]
|
|
139
|
+
elif isinstance(assignment, tuple):
|
|
140
|
+
label = assignment[0]
|
|
141
|
+
else:
|
|
142
|
+
ValueError("Failed to parse failed assignment %r" % assignment)
|
|
143
|
+
ret.append((label, getattr(exc, "formatted_output", f"{repr(exc)} (formatted_output is absent)"), True))
|
|
144
|
+
return ret
|
|
145
|
+
|
|
146
|
+
def cfg_file_names(self, device: Device) -> List[str]:
|
|
147
|
+
return [f"{device.hostname}.cfg"]
|
|
148
|
+
|
|
149
|
+
def entire_config_dest_path(self, device, config_path: str) -> str:
|
|
150
|
+
"""Формирует путь к конфигу в директории destname.
|
|
151
|
+
|
|
152
|
+
Например, для устройства с hostname `my-device`:
|
|
153
|
+
```
|
|
154
|
+
>>> device.entire_config_dest_path("/etc/frr/frr.conf")
|
|
155
|
+
'my-device.cfg/etc/frr/frr.conf'
|
|
156
|
+
>>>
|
|
157
|
+
```
|
|
158
|
+
"""
|
|
159
|
+
# NOTE: с полученным `config_path` работаем через `posixpath`, а не через `os.path`, потому что
|
|
160
|
+
# entire-путь POSIX-специфичный; но в конце формируем путь через `os.path` для текущей платформы
|
|
161
|
+
if not posixpath.abspath(config_path):
|
|
162
|
+
raise RuntimeError(f"Want absolute config path, but relative received: {config_path}")
|
|
163
|
+
cfg_files = self.cfg_file_names(device)
|
|
164
|
+
# NOTE: получаем путь без "/" в начале, например, "etc/frr/frr.conf"
|
|
165
|
+
relative_config_path = posixpath.relpath(config_path, "/")
|
|
166
|
+
dest_config_path_parts = [cfg_files[0]] + relative_config_path.split(posixpath.sep)
|
|
167
|
+
return os.path.join(*dest_config_path_parts)
|